From e1e288f201c4a9c4134438a834b812758facb4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrication=20du=20Num=C3=A9rique?= Date: Wed, 4 Jun 2025 08:35:53 +0200 Subject: [PATCH] =?UTF-8?q?Mise=20=C3=A0=20jour=20=C3=A0=20partir=20de=20d?= =?UTF-8?q?ev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../batch_generate_fiches.py | 200 + IA/00 - fiches_corpus/generate_corpus.py | 96 + .../analyze_graph.py | 209 + IA/01 - corpus_rapport_factuel/check_paths.py | 146 + .../generate_template.py | 1267 + .../replace_paths.py | 147 + IA/02 - injection_fiches/README.md | 90 + IA/02 - injection_fiches/auto_ingest.py | 254 + IA/02 - injection_fiches/auto_ingest.sh | 90 + .../docker-compose.yml.example | 40 + IA/02 - injection_fiches/nettoyer_pgpt.py | 224 + IA/02 - injection_fiches/watch_directory.py | 263 + IA/02 - injection_fiches/watch_directory.sh | 94 + IA/get_regeneration_plan.py | 171 + IA/make_config.py | 141 + README.md | 106 +- app/analyse/interface.py | 6 +- app/analyse/sankey.py | 22 +- app/fiches/interface.py | 7 +- app/fiches/utils/dynamic/minerai/minerai.py | 2 + app/fiches/utils/tickets/core.py | 8 +- app/fiches/utils/tickets/creation.py | 61 +- app/fiches/utils/tickets/display.py | 1 - app/ia_nalyse/README.md | 42 + app/ia_nalyse/__init__.py | 2 + app/ia_nalyse/interface.py | 195 + app/personnalisation/import_export.py | 7 +- app/personnalisation/interface.py | 4 +- app/plan_d_action/__init__.py | 2 + app/plan_d_action/interface.py | 120 + app/plan_d_action/utils/data/__init__.py | 8 + app/plan_d_action/utils/data/config.py | 165 + .../utils/data/data_processing.py | 192 + app/plan_d_action/utils/data/data_utils.py | 67 + app/plan_d_action/utils/data/pda_interface.py | 167 + app/plan_d_action/utils/data/plan_d_action.py | 350 + app/plan_d_action/utils/interface/__init__.py | 17 + app/plan_d_action/utils/interface/config.py | 27 + app/plan_d_action/utils/interface/export.py | 33 + .../utils/interface/niveau_utils.py | 8 + app/plan_d_action/utils/interface/parser.py | 11 + .../utils/interface/selection.py | 74 + .../utils/interface/visualization.py | 11 + app/plan_d_action/utils/jobs/59479ea2.md | 11344 ++++++++ app/visualisations/graphes.py | 6 +- app/visualisations/interface.py | 12 +- assets/README.md | 9 +- assets/config.yaml | 8 +- assets/locales/en.json | 591 +- assets/locales/fr.json | 591 +- assets/styles/base.css | 311 +- batch_ia/.gitignore | 1 + batch_ia/__init__.py | 27 + batch_ia/analyse_ia.py | 75 + batch_ia/batch-fabnum-dev.service | 31 + batch_ia/batch_runner.py | 32 + batch_ia/batch_utils.py | 64 + batch_ia/nettoyer_pgpt.py | 100 + batch_ia/status.json | 1 + batch_ia/utils/__init__.py | 1 + batch_ia/utils/config.py | 93 + batch_ia/utils/files.py | 133 + batch_ia/utils/graphs.py | 591 + batch_ia/utils/ia.py | 286 + batch_ia/utils/sections.py | 770 + batch_ia/utils/sections_utils.py | 92 + components/README.md | 5 +- components/connexion.py | 5 +- components/sidebar.py | 9 +- fabnum-dev.service | 15 + fabnum.py | 24 +- pgpt/.docker/router.yml | 16 + pgpt/.gitignore | 8 + pgpt/Dockerfile.ollama | 51 + pgpt/docker-compose.yaml | 121 + pgpt/private_gpt/__init__.py | 27 + pgpt/private_gpt/__main__.py | 11 + pgpt/private_gpt/components/__init__.py | 0 .../components/embedding/__init__.py | 0 .../components/embedding/custom/__init__.py | 0 .../components/embedding/custom/sagemaker.py | 82 + .../embedding/embedding_component.py | 167 + .../private_gpt/components/ingest/__init__.py | 0 .../components/ingest/ingest_component.py | 517 + .../components/ingest/ingest_helper.py | 111 + pgpt/private_gpt/components/llm/__init__.py | 1 + .../components/llm/custom/__init__.py | 0 .../components/llm/custom/sagemaker.py | 276 + .../components/llm/llm_component.py | 225 + .../components/llm/prompt_helper.py | 310 + .../components/node_store/__init__.py | 0 .../node_store/node_store_component.py | 68 + .../components/vector_store/__init__.py | 0 .../components/vector_store/batched_chroma.py | 106 + .../vector_store/vector_store_component.py | 217 + pgpt/private_gpt/constants.py | 3 + pgpt/private_gpt/di.py | 19 + pgpt/private_gpt/launcher.py | 69 + pgpt/private_gpt/main.py | 6 + pgpt/private_gpt/open_ai/__init__.py | 1 + .../open_ai/extensions/__init__.py | 1 + .../open_ai/extensions/context_filter.py | 7 + pgpt/private_gpt/open_ai/openai_models.py | 122 + pgpt/private_gpt/paths.py | 18 + pgpt/private_gpt/server/__init__.py | 1 + pgpt/private_gpt/server/chat/__init__.py | 0 pgpt/private_gpt/server/chat/chat_router.py | 115 + pgpt/private_gpt/server/chat/chat_service.py | 217 + pgpt/private_gpt/server/chunks/__init__.py | 0 .../server/chunks/chunks_router.py | 55 + .../server/chunks/chunks_service.py | 125 + .../server/completions/__init__.py | 1 + .../server/completions/completions_router.py | 92 + .../private_gpt/server/embeddings/__init__.py | 0 .../server/embeddings/embeddings_router.py | 35 + .../server/embeddings/embeddings_service.py | 30 + pgpt/private_gpt/server/health/__init__.py | 0 .../server/health/health_router.py | 17 + pgpt/private_gpt/server/ingest/__init__.py | 0 .../server/ingest/ingest_router.py | 104 + .../server/ingest/ingest_service.py | 125 + .../server/ingest/ingest_watcher.py | 45 + pgpt/private_gpt/server/ingest/model.py | 32 + .../server/recipes/summarize/__init__.py | 0 .../recipes/summarize/summarize_router.py | 86 + .../recipes/summarize/summarize_service.py | 182 + pgpt/private_gpt/server/utils/__init__.py | 0 pgpt/private_gpt/server/utils/auth.py | 69 + pgpt/private_gpt/settings/__init__.py | 1 + pgpt/private_gpt/settings/settings.py | 638 + pgpt/private_gpt/settings/settings_loader.py | 57 + pgpt/private_gpt/settings/yaml.py | 41 + pgpt/private_gpt/ui/__init__.py | 1 + pgpt/private_gpt/ui/avatar-bot.ico | Bin 0 -> 15406 bytes pgpt/private_gpt/ui/images.py | 1 + pgpt/private_gpt/ui/ui.py | 596 + pgpt/private_gpt/utils/__init__.py | 1 + pgpt/private_gpt/utils/eta.py | 122 + pgpt/private_gpt/utils/ollama.py | 95 + pgpt/private_gpt/utils/retry.py | 31 + pgpt/private_gpt/utils/typing.py | 5 + pgpt/pyproject.toml | 197 + pgpt/settings-docker.yaml | 42 + pgpt/settings-ollama.yaml | 31 + pgpt/settings.yaml | 158 + pgpt/start-private-gpt-custom.sh | 96 + pgpt/version.txt | 1 + requirements.txt | 21 +- schema.txt | 22302 +++++----------- scripts/auto_ingest.py | 280 + scripts/config.yml | 3 + scripts/generer_analyse.py | 574 + scripts/gestion_schema/beautify.py | 428 + scripts/gestion_schema/ihh.py | 147 + utils/graph_utils.py | 22 +- utils/visualisation.py | 6 +- utils/widgets.py | 49 + 157 files changed, 33380 insertions(+), 16831 deletions(-) create mode 100644 IA/00 - fiches_corpus/batch_generate_fiches.py create mode 100644 IA/00 - fiches_corpus/generate_corpus.py create mode 100644 IA/01 - corpus_rapport_factuel/analyze_graph.py create mode 100644 IA/01 - corpus_rapport_factuel/check_paths.py create mode 100644 IA/01 - corpus_rapport_factuel/generate_template.py create mode 100644 IA/01 - corpus_rapport_factuel/replace_paths.py create mode 100644 IA/02 - injection_fiches/README.md create mode 100755 IA/02 - injection_fiches/auto_ingest.py create mode 100755 IA/02 - injection_fiches/auto_ingest.sh create mode 100644 IA/02 - injection_fiches/docker-compose.yml.example create mode 100644 IA/02 - injection_fiches/nettoyer_pgpt.py create mode 100755 IA/02 - injection_fiches/watch_directory.py create mode 100755 IA/02 - injection_fiches/watch_directory.sh create mode 100644 IA/get_regeneration_plan.py create mode 100644 IA/make_config.py create mode 100644 app/ia_nalyse/README.md create mode 100644 app/ia_nalyse/__init__.py create mode 100644 app/ia_nalyse/interface.py create mode 100644 app/plan_d_action/__init__.py create mode 100644 app/plan_d_action/interface.py create mode 100644 app/plan_d_action/utils/data/__init__.py create mode 100644 app/plan_d_action/utils/data/config.py create mode 100644 app/plan_d_action/utils/data/data_processing.py create mode 100644 app/plan_d_action/utils/data/data_utils.py create mode 100644 app/plan_d_action/utils/data/pda_interface.py create mode 100644 app/plan_d_action/utils/data/plan_d_action.py create mode 100644 app/plan_d_action/utils/interface/__init__.py create mode 100644 app/plan_d_action/utils/interface/config.py create mode 100644 app/plan_d_action/utils/interface/export.py create mode 100644 app/plan_d_action/utils/interface/niveau_utils.py create mode 100644 app/plan_d_action/utils/interface/parser.py create mode 100644 app/plan_d_action/utils/interface/selection.py create mode 100644 app/plan_d_action/utils/interface/visualization.py create mode 100644 app/plan_d_action/utils/jobs/59479ea2.md create mode 100644 batch_ia/.gitignore create mode 100644 batch_ia/__init__.py create mode 100644 batch_ia/analyse_ia.py create mode 100644 batch_ia/batch-fabnum-dev.service create mode 100644 batch_ia/batch_runner.py create mode 100644 batch_ia/batch_utils.py create mode 100644 batch_ia/nettoyer_pgpt.py create mode 100644 batch_ia/status.json create mode 100644 batch_ia/utils/__init__.py create mode 100644 batch_ia/utils/config.py create mode 100644 batch_ia/utils/files.py create mode 100644 batch_ia/utils/graphs.py create mode 100644 batch_ia/utils/ia.py create mode 100644 batch_ia/utils/sections.py create mode 100644 batch_ia/utils/sections_utils.py create mode 100644 fabnum-dev.service create mode 100644 pgpt/.docker/router.yml create mode 100644 pgpt/.gitignore create mode 100644 pgpt/Dockerfile.ollama create mode 100644 pgpt/docker-compose.yaml create mode 100644 pgpt/private_gpt/__init__.py create mode 100644 pgpt/private_gpt/__main__.py create mode 100644 pgpt/private_gpt/components/__init__.py create mode 100644 pgpt/private_gpt/components/embedding/__init__.py create mode 100644 pgpt/private_gpt/components/embedding/custom/__init__.py create mode 100644 pgpt/private_gpt/components/embedding/custom/sagemaker.py create mode 100644 pgpt/private_gpt/components/embedding/embedding_component.py create mode 100644 pgpt/private_gpt/components/ingest/__init__.py create mode 100644 pgpt/private_gpt/components/ingest/ingest_component.py create mode 100644 pgpt/private_gpt/components/ingest/ingest_helper.py create mode 100644 pgpt/private_gpt/components/llm/__init__.py create mode 100644 pgpt/private_gpt/components/llm/custom/__init__.py create mode 100644 pgpt/private_gpt/components/llm/custom/sagemaker.py create mode 100644 pgpt/private_gpt/components/llm/llm_component.py create mode 100644 pgpt/private_gpt/components/llm/prompt_helper.py create mode 100644 pgpt/private_gpt/components/node_store/__init__.py create mode 100644 pgpt/private_gpt/components/node_store/node_store_component.py create mode 100644 pgpt/private_gpt/components/vector_store/__init__.py create mode 100644 pgpt/private_gpt/components/vector_store/batched_chroma.py create mode 100644 pgpt/private_gpt/components/vector_store/vector_store_component.py create mode 100644 pgpt/private_gpt/constants.py create mode 100644 pgpt/private_gpt/di.py create mode 100644 pgpt/private_gpt/launcher.py create mode 100644 pgpt/private_gpt/main.py create mode 100644 pgpt/private_gpt/open_ai/__init__.py create mode 100644 pgpt/private_gpt/open_ai/extensions/__init__.py create mode 100644 pgpt/private_gpt/open_ai/extensions/context_filter.py create mode 100644 pgpt/private_gpt/open_ai/openai_models.py create mode 100644 pgpt/private_gpt/paths.py create mode 100644 pgpt/private_gpt/server/__init__.py create mode 100644 pgpt/private_gpt/server/chat/__init__.py create mode 100644 pgpt/private_gpt/server/chat/chat_router.py create mode 100644 pgpt/private_gpt/server/chat/chat_service.py create mode 100644 pgpt/private_gpt/server/chunks/__init__.py create mode 100644 pgpt/private_gpt/server/chunks/chunks_router.py create mode 100644 pgpt/private_gpt/server/chunks/chunks_service.py create mode 100644 pgpt/private_gpt/server/completions/__init__.py create mode 100644 pgpt/private_gpt/server/completions/completions_router.py create mode 100644 pgpt/private_gpt/server/embeddings/__init__.py create mode 100644 pgpt/private_gpt/server/embeddings/embeddings_router.py create mode 100644 pgpt/private_gpt/server/embeddings/embeddings_service.py create mode 100644 pgpt/private_gpt/server/health/__init__.py create mode 100644 pgpt/private_gpt/server/health/health_router.py create mode 100644 pgpt/private_gpt/server/ingest/__init__.py create mode 100644 pgpt/private_gpt/server/ingest/ingest_router.py create mode 100644 pgpt/private_gpt/server/ingest/ingest_service.py create mode 100644 pgpt/private_gpt/server/ingest/ingest_watcher.py create mode 100644 pgpt/private_gpt/server/ingest/model.py create mode 100644 pgpt/private_gpt/server/recipes/summarize/__init__.py create mode 100644 pgpt/private_gpt/server/recipes/summarize/summarize_router.py create mode 100644 pgpt/private_gpt/server/recipes/summarize/summarize_service.py create mode 100644 pgpt/private_gpt/server/utils/__init__.py create mode 100644 pgpt/private_gpt/server/utils/auth.py create mode 100644 pgpt/private_gpt/settings/__init__.py create mode 100644 pgpt/private_gpt/settings/settings.py create mode 100644 pgpt/private_gpt/settings/settings_loader.py create mode 100644 pgpt/private_gpt/settings/yaml.py create mode 100644 pgpt/private_gpt/ui/__init__.py create mode 100644 pgpt/private_gpt/ui/avatar-bot.ico create mode 100644 pgpt/private_gpt/ui/images.py create mode 100644 pgpt/private_gpt/ui/ui.py create mode 100644 pgpt/private_gpt/utils/__init__.py create mode 100644 pgpt/private_gpt/utils/eta.py create mode 100644 pgpt/private_gpt/utils/ollama.py create mode 100644 pgpt/private_gpt/utils/retry.py create mode 100644 pgpt/private_gpt/utils/typing.py create mode 100644 pgpt/pyproject.toml create mode 100644 pgpt/settings-docker.yaml create mode 100644 pgpt/settings-ollama.yaml create mode 100644 pgpt/settings.yaml create mode 100755 pgpt/start-private-gpt-custom.sh create mode 100644 pgpt/version.txt create mode 100644 scripts/auto_ingest.py create mode 100644 scripts/config.yml create mode 100644 scripts/generer_analyse.py create mode 100644 scripts/gestion_schema/beautify.py create mode 100644 scripts/gestion_schema/ihh.py create mode 100644 utils/widgets.py diff --git a/IA/00 - fiches_corpus/batch_generate_fiches.py b/IA/00 - fiches_corpus/batch_generate_fiches.py new file mode 100644 index 0000000..b271589 --- /dev/null +++ b/IA/00 - fiches_corpus/batch_generate_fiches.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +import os +import re +import yaml +import requests +import argparse +import logging + +# Import des fonctions de génération +from app.fiches.generer import ( + generer_fiche +) +from app.fiches.utils.fiche_utils import load_seuils +from utils.gitea import charger_arborescence_fiches +from config import GITEA_TOKEN, FICHES_CRITICITE + +# Configuration du logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler("batch_generation.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +def get_fiche_type(md_source): + """Extrait le type de fiche depuis le frontmatter YAML.""" + front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md_source) + if not front_match: + return "autre" + + context = yaml.safe_load(front_match.group(1)) + return context.get("type_fiche", "autre") + +def get_indice_court(md_source): + """Extrait l'indice court depuis le frontmatter YAML.""" + front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md_source) + if not front_match: + return None + + context = yaml.safe_load(front_match.group(1)) + return context.get("indice_court") + +def batch_generate_fiches(output_dir="", force_regenerate=False): + """Génère toutes les fiches en batch en suivant un ordre de priorité.""" + # Assurer que les répertoires de sortie existent + if output_dir: + os.makedirs(output_dir, exist_ok=True) + + # Toujours créer les répertoires nécessaires + os.makedirs(os.path.join("Fiches"), exist_ok=True) + os.makedirs(os.path.join("HTML"), exist_ok=True) + os.makedirs(os.path.join("static", "Fiches"), exist_ok=True) + + # Charger les seuils + try: + seuils = load_seuils("assets/config.yaml") + logger.info("Seuils chargés avec succès") + except Exception as e: + logger.error(f"Erreur lors du chargement des seuils: {e}") + return + + # Charger l'arborescence des fiches + try: + arborescence = charger_arborescence_fiches() + logger.info(f"Arborescence chargée: {len(arborescence)} dossiers trouvés") + except Exception as e: + logger.error(f"Erreur lors du chargement de l'arborescence: {e}") + return + + # Créer une liste de toutes les fiches avec leurs informations + toutes_fiches = [] + for dossier, fiches in arborescence.items(): + for fiche in fiches: + toutes_fiches.append({ + "dossier": dossier, + "nom": fiche["nom"], + "download_url": fiche["download_url"], + "type": fiche.get("type", "autre") + }) + + logger.info(f"Total de {len(toutes_fiches)} fiches à traiter") + + # Organisation des fiches par type pour respecter l'ordre de priorité + fiches_par_type = { + "criticite": [], + "assemblage": [], + "fabrication": [], + "minerai": [], + "autre": [] + } + + # Télécharger et catégoriser les fiches + headers = {"Authorization": f"token {GITEA_TOKEN}"} + + # Création des listes spécifiques pour faciliter le traitement ordonné + fiches_criticite = [] + autres_fiches = [] + + # Première étape : identifier les fiches de criticité + logger.info("Identification des fiches de criticité...") + for fiche in toutes_fiches: + # Vérifier si c'est une fiche de criticité par deux méthodes + est_criticite = False + + # Méthode 1: vérifier par le nom du fichier + if fiche["nom"] in FICHES_CRITICITE: + est_criticite = True + logger.info(f"Fiche de criticité identifiée par nom: {fiche['nom']}") + + # Méthode 2: vérifier par le dossier + elif fiche["dossier"] == "Criticités": + est_criticite = True + logger.info(f"Fiche de criticité identifiée par dossier: {fiche['nom']}") + + if est_criticite: + fiches_criticite.append(fiche) + else: + autres_fiches.append(fiche) + + # Traiter d'abord les fiches de criticité + logger.info(f"Traitement prioritaire des fiches de criticité ({len(fiches_criticite)} fiches)") + criticite_count = 0 + + for fiche in fiches_criticite: + try: + # Télécharger la fiche de criticité + reponse = requests.get(fiche["download_url"], headers=headers) + reponse.raise_for_status() + md_source = reponse.text + + # Générer immédiatement la fiche de criticité + logger.info(f"Génération prioritaire de la fiche de criticité: {fiche['dossier']}/{fiche['nom']}") + generer_fiche(md_source, fiche["dossier"], fiche["nom"], seuils) + + # Ajouter à la liste pour le décompte + fiches_par_type["criticite"].append(fiche) + criticite_count += 1 + except Exception as e: + logger.error(f"Erreur lors de la génération de la fiche de criticité {fiche['nom']}: {e}") + + logger.info(f"{criticite_count} fiches de criticité générées") + + # Maintenant catégoriser et traiter les fiches restantes (non-criticité) + for fiche in autres_fiches: + try: + # Télécharger le contenu pour déterminer le type + reponse = requests.get(fiche["download_url"], headers=headers) + reponse.raise_for_status() + md_source = reponse.text + + # Déterminer le type + type_fiche = get_fiche_type(md_source) + if type_fiche in fiches_par_type: + fiches_par_type[type_fiche].append({**fiche, "md_source": md_source}) + else: + fiches_par_type["autre"].append({**fiche, "md_source": md_source}) + + except Exception as e: + logger.error(f"Erreur lors du traitement de {fiche['nom']}: {e}") + + # Ordre de traitement pour les fiches restantes + ordre_types = ["assemblage", "fabrication", "minerai", "autre"] + + # Générer les fiches restantes dans l'ordre spécifié + for type_fiche in ordre_types: + logger.info(f"Traitement des fiches de type '{type_fiche}' ({len(fiches_par_type[type_fiche])} fiches)") + + for fiche in fiches_par_type[type_fiche]: + try: + md_source = fiche["md_source"] + dossier = fiche["dossier"] + nom_fichier = fiche["nom"] + + # Vérifier si la génération est nécessaire + html_path = os.path.join("HTML", dossier, os.path.splitext(nom_fichier)[0] + ".html") + if force_regenerate or not os.path.exists(html_path): + logger.info(f"Génération de {dossier}/{nom_fichier}") + generer_fiche(md_source, dossier, nom_fichier, seuils) + else: + logger.info(f"Ignoré (déjà généré): {dossier}/{nom_fichier}") + + except Exception as e: + logger.error(f"Erreur lors de la génération de {fiche['nom']}: {e}") + + logger.info("Génération batch terminée") + +def main(): + parser = argparse.ArgumentParser(description="Générateur batch de fiches") + parser.add_argument("--output", "-o", default="", help="Répertoire de sortie (laissez vide pour utiliser les dossiers de l'application)") + parser.add_argument("--force", "-f", action="store_true", help="Forcer la régénération de toutes les fiches") + args = parser.parse_args() + + logger.info(f"Démarrage de la génération batch (output: {args.output if args.output else 'dossiers par défaut'}, force: {args.force})") + batch_generate_fiches(output_dir=args.output, force_regenerate=args.force) + +if __name__ == "__main__": + main() diff --git a/IA/00 - fiches_corpus/generate_corpus.py b/IA/00 - fiches_corpus/generate_corpus.py new file mode 100644 index 0000000..db0c407 --- /dev/null +++ b/IA/00 - fiches_corpus/generate_corpus.py @@ -0,0 +1,96 @@ +import os +import re +import shutil +from pathlib import Path + +EXCLUDE_DIRS = {"Local"} +MAX_SECTION_LENGTH = 1200 # non utilisé ici car découpe selon présence de ### + +def slugify(text): + return re.sub(r'\W+', '-', text.strip()).strip('-').lower() + +def split_markdown_sections_refined(content): + lines = content.splitlines() + sections = [] + header_level_2 = None + section_lines = [] + subsections = [] + current_subsection = None + inside_section = False + + for line in lines: + if line.startswith("## "): + if header_level_2: + if current_subsection: + subsections.append(current_subsection) + sections.append((header_level_2, section_lines, subsections)) + section_lines, subsections = [], [] + current_subsection = None + header_level_2 = line[3:].strip() + inside_section = True + elif line.startswith("### ") and inside_section: + if current_subsection: + subsections.append(current_subsection) + current_subsection = (line[4:].strip(), []) + elif inside_section: + if current_subsection: + current_subsection[1].append(line) + else: + section_lines.append(line) + + if header_level_2: + if current_subsection: + subsections.append(current_subsection) + sections.append((header_level_2, section_lines, subsections)) + return sections + +def process_markdown_file(md_path, rel_output_dir): + with open(md_path, encoding="utf-8") as f: + content = f.read() + sections = split_markdown_sections_refined(content) + + for idx, (sec_title, sec_lines, subsections) in enumerate(sections): + base_name = f"{idx:02d}-{slugify(sec_title)}" + if subsections: + sec_dir = rel_output_dir / base_name + sec_dir.mkdir(parents=True, exist_ok=True) + with open(sec_dir / "_intro.md", "w", encoding="utf-8") as f_out: + f_out.write(f"## {sec_title}\n") + f_out.write("\n".join(sec_lines).strip()) + for sub_idx, (sub_title, sub_lines) in enumerate(subsections): + sub_name = f"{sub_idx:02d}-{slugify(sub_title)}.md" + with open(sec_dir / sub_name, "w", encoding="utf-8") as f_out: + f_out.write(f"### {sub_title}\n") + f_out.write("\n".join(sub_lines).strip()) + else: + with open(rel_output_dir / f"{base_name}.md", "w", encoding="utf-8") as f_out: + f_out.write(f"## {sec_title}\n") + f_out.write("\n".join(sec_lines).strip()) + +def build_corpus_structure(): + BASE_DIR = Path(__file__).resolve().parent.parent.parent + print(BASE_DIR) + SOURCE_DIR = BASE_DIR / "Fiches" + DEST_DIR = BASE_DIR / "Corpus" + + if DEST_DIR.exists(): + shutil.rmtree(DEST_DIR) + DEST_DIR.mkdir(parents=True, exist_ok=True) + + for root, _, files in os.walk(SOURCE_DIR): + rel_path = Path(root).relative_to(SOURCE_DIR) + if any(part in EXCLUDE_DIRS for part in rel_path.parts): + continue + for file in files: + if not file.endswith(".md") or ".md." in file: + continue + input_file = Path(root) / file + subdir = rel_path + filename_no_ext = Path(file).stem + output_dir = DEST_DIR / subdir / filename_no_ext + output_dir.mkdir(parents=True, exist_ok=True) + process_markdown_file(input_file, output_dir) + +if __name__ == "__main__": + build_corpus_structure() + print("✅ Corpus généré avec succès dans le dossier 'Corpus/'") diff --git a/IA/01 - corpus_rapport_factuel/analyze_graph.py b/IA/01 - corpus_rapport_factuel/analyze_graph.py new file mode 100644 index 0000000..108142c --- /dev/null +++ b/IA/01 - corpus_rapport_factuel/analyze_graph.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script d'analyse de la structure du graphe DOT pour comprendre +comment intégrer l'ISG dans le générateur de template. +""" + +import os +from pathlib import Path +from networkx.drawing.nx_agraph import read_dot + +# Chemins +BASE_DIR = Path(__file__).resolve().parent +GRAPH_PATH = BASE_DIR / "graphe.dot" + +def analyze_graph_structure(dot_path): + """Analyse la structure du graphe et affiche ses caractéristiques.""" + print(f"Analyse du fichier: {dot_path}") + + # Lire le graphe + G = read_dot(dot_path) + + # Informations de base + print(f"Nombre total de nœuds: {len(G.nodes())}") + print(f"Nombre total d'arêtes: {len(G.edges())}") + + # Analyse des attributs des nœuds + node_attrs = {} + for node, attrs in G.nodes(data=True): + for key in attrs: + if key not in node_attrs: + node_attrs[key] = set() + node_attrs[key].add(attrs[key]) + + print("\nAttributs des nœuds:") + for attr, values in node_attrs.items(): + print(f"- {attr}: {len(values)} valeurs différentes") + if len(values) < 20: # Afficher seulement si le nombre de valeurs est raisonnable + print(f" Valeurs: {', '.join(sorted(values))}") + + # Analyse des niveaux (si l'attribut existe) + if 'level' in node_attrs: + print("\nAnalyse par niveau:") + levels = {} + for node, attrs in G.nodes(data=True): + if 'level' in attrs: + level = attrs['level'] + if level not in levels: + levels[level] = [] + levels[level].append(node) + + for level, nodes in sorted(levels.items()): + print(f"- Niveau {level}: {len(nodes)} nœuds") + # Afficher quelques exemples + if len(nodes) < 5: + print(f" Exemples: {', '.join(nodes)}") + else: + print(f" Exemples: {', '.join(nodes[:3])}... (et {len(nodes)-3} autres)") + + # Analyse des attributs ISG + print("\nRecherche des attributs ISG:") + isg_nodes = [] + for node, attrs in G.nodes(data=True): + if 'isg' in attrs: + isg_nodes.append((node, attrs['isg'])) + + if isg_nodes: + print(f"- {len(isg_nodes)} nœuds avec attribut ISG") + print(" Exemples:") + for node, isg in isg_nodes[:5]: + print(f" - {node}: ISG = {isg}") + else: + print("- Aucun nœud avec attribut ISG trouvé") + + # Analyse des connexions pour les nœuds critiques (IHH) + print("\nAnalyse des nœuds avec IHH:") + ihh_nodes = [] + for node, attrs in G.nodes(data=True): + if 'ihh_pays' in attrs or 'ihh_acteurs' in attrs: + ihh_value_pays = attrs.get('ihh_pays', 'N/A') + ihh_value_acteurs = attrs.get('ihh_acteurs', 'N/A') + ihh_nodes.append((node, ihh_value_pays, ihh_value_acteurs)) + + if ihh_nodes: + print(f"- {len(ihh_nodes)} nœuds avec attributs IHH") + print(" Exemples:") + for node, ihh_pays, ihh_acteurs in ihh_nodes[:5]: + print(f" - {node}: IHH pays = {ihh_pays}, IHH acteurs = {ihh_acteurs}") + + # Analyser les connexions de ce nœud + print(f" Connexions sortantes:") + out_edges = list(G.out_edges(node)) + if out_edges: + for i, (_, target) in enumerate(out_edges[:3]): + print(f" - Vers {target}") + if len(out_edges) > 3: + print(f" - ... et {len(out_edges)-3} autres") + else: + print(" - Aucune connexion sortante") + + print(f" Connexions entrantes:") + in_edges = list(G.in_edges(node)) + if in_edges: + for i, (source, _) in enumerate(in_edges[:3]): + print(f" - Depuis {source}") + if len(in_edges) > 3: + print(f" - ... et {len(in_edges)-3} autres") + else: + print(" - Aucune connexion entrante") + else: + print("- Aucun nœud avec attributs IHH trouvé") + + # Vérifier si un nœud a un attribut de niveau 99 (ISG supposé) + print("\nRecherche des nœuds de niveau 99 (ISG):") + level_99_nodes = [] + for node, attrs in G.nodes(data=True): + if attrs.get('level') == '99': + level_99_nodes.append(node) + + if level_99_nodes: + print(f"- {len(level_99_nodes)} nœuds de niveau 99") + print(" Exemples:") + for node in level_99_nodes[:5]: + print(f" - {node}") + # Analyser les connexions de ce nœud + print(f" Connexions entrantes:") + in_edges = list(G.in_edges(node)) + if in_edges: + for i, (source, _) in enumerate(in_edges[:3]): + print(f" - Depuis {source}") + if len(in_edges) > 3: + print(f" - ... et {len(in_edges)-3} autres") + else: + print(" - Aucune connexion entrante") + else: + print("- Aucun nœud de niveau 99 trouvé") + +def check_isg_paths(dot_path): + """Vérifie les chemins entre les nœuds critiques (IHH) et les nœuds ISG.""" + print("\nAnalyse des chemins entre nœuds IHH et nœuds ISG:") + + # Lire le graphe + G = read_dot(dot_path) + + # Identifier les nœuds avec IHH + ihh_nodes = [] + for node, attrs in G.nodes(data=True): + ihh_pays = attrs.get('ihh_pays', 0) + ihh_acteurs = attrs.get('ihh_acteurs', 0) + try: + ihh_pays = float(ihh_pays) + ihh_acteurs = float(ihh_acteurs) + if ihh_pays > 25 or ihh_acteurs > 25: # Seuil critique + ihh_nodes.append(node) + except (ValueError, TypeError): + pass + + if not ihh_nodes: + print("- Aucun nœud IHH critique trouvé") + return + + print(f"- {len(ihh_nodes)} nœuds IHH critiques identifiés") + + # Pour chaque nœud IHH critique, chercher des chemins vers des nœuds ISG + for node in ihh_nodes[:5]: # Limiter à 5 exemples + print(f"\n Analyse des chemins pour {node}:") + + # Analyser les voisins directs + successors = list(G.successors(node)) + print(f" - {len(successors)} successeurs directs") + + if successors: + for succ in successors[:3]: + print(f" - Vers {succ}") + + # Vérifier les attributs de ce successeur + succ_attrs = G.nodes[succ] + print(f" Attributs: {', '.join(f'{k}={v}' for k, v in succ_attrs.items() if k in ['level', 'isg'])}") + + # Chercher les successeurs de niveau 2 + succ2 = list(G.successors(succ)) + print(f" {len(succ2)} successeurs de niveau 2") + + if succ2: + for s2 in succ2[:2]: + print(f" - Vers {s2}") + s2_attrs = G.nodes[s2] + print(f" Attributs: {', '.join(f'{k}={v}' for k, v in s2_attrs.items() if k in ['level', 'isg'])}") + + # Chercher encore plus loin si nécessaire + succ3 = list(G.successors(s2)) + print(f" {len(succ3)} successeurs de niveau 3") + + if succ3: + for s3 in succ3[:2]: + print(f" - Vers {s3}") + s3_attrs = G.nodes[s3] + print(f" Attributs: {', '.join(f'{k}={v}' for k, v in s3_attrs.items() if k in ['level', 'isg'])}") + +def main(): + """Fonction principale.""" + print("=== Analyse de la structure du graphe ===") + analyze_graph_structure(GRAPH_PATH) + + print("\n=== Analyse des chemins entre nœuds critiques et ISG ===") + check_isg_paths(GRAPH_PATH) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/IA/01 - corpus_rapport_factuel/check_paths.py b/IA/01 - corpus_rapport_factuel/check_paths.py new file mode 100644 index 0000000..a0a14c9 --- /dev/null +++ b/IA/01 - corpus_rapport_factuel/check_paths.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import re +import sys +from collections import defaultdict + +def extract_paths(file_path): + """Extrait tous les chemins du fichier rapport_template.md""" + paths = [] + try: + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + # Extraire les lignes qui commencent par "Corpus/" + if line.strip().startswith("Corpus/"): + paths.append(line.strip()) + except Exception as e: + print(f"Erreur lors de la lecture du fichier {file_path}: {e}") + sys.exit(1) + + return paths + +def check_paths(paths, base_dir): + """Vérifie si les chemins existent dans le système de fichiers""" + results = { + "existing": [], + "missing": [], + "problematic": [] # Chemins qui pourraient nécessiter des corrections + } + + for path in paths: + # Vérifier si le chemin est absolu ou relatif + abs_path = os.path.join(base_dir, path) + + if os.path.exists(abs_path): + results["existing"].append(path) + else: + # Essayer de détecter des problèmes potentiels + problem_detected = False + + # Vérifier les chemins avec "Fiche minerai" ou "Fiche fabrication" + if "Fiche minerai" in path or "Fiche fabrication" in path: + # Problème courant: mauvaise casse ou absence du mot "minerai" + path_lower = path.lower() + if "minerai" not in path_lower and "/minerai/" in path_lower: + corrected_path = path.replace("/Fiche ", "/Fiche minerai ") + if os.path.exists(os.path.join(base_dir, corrected_path)): + results["problematic"].append((path, corrected_path, "Mot 'minerai' manquant")) + problem_detected = True + + # Vérifier les chemins SSD + if "SSD25" in path: + corrected_path = path.replace("SSD25", "SSD 2.5") + if os.path.exists(os.path.join(base_dir, corrected_path)): + results["problematic"].append((path, corrected_path, "Format 'SSD25' au lieu de 'SSD 2.5'")) + problem_detected = True + + # Si aucun problème spécifique n'a été détecté, marquer comme manquant + if not problem_detected: + results["missing"].append(path) + + return results + +def find_similar_paths(missing_path, base_dir): + """Essaie de trouver des chemins similaires pour aider à diagnostiquer le problème""" + missing_parts = missing_path.split('/') + similar_paths = [] + + # Rechercher dans les sous-répertoires correspondants + search_dir = os.path.join(base_dir, *missing_parts[:-1]) + if os.path.exists(search_dir): + for file in os.listdir(search_dir): + if file.endswith('.md'): + similar_path = os.path.join(search_dir, file).replace(base_dir + '/', '') + similar_paths.append(similar_path) + + # Si aucun chemin similaire n'est trouvé, remonter d'un niveau + if not similar_paths and len(missing_parts) > 2: + parent_dir = os.path.join(base_dir, *missing_parts[:-2]) + if os.path.exists(parent_dir): + for dir_name in os.listdir(parent_dir): + if dir_name.lower() in missing_parts[-2].lower(): + dir_path = os.path.join(parent_dir, dir_name) + if os.path.isdir(dir_path): + for file in os.listdir(dir_path): + if file.endswith('.md'): + similar_path = os.path.join(dir_path, file).replace(base_dir + '/', '') + similar_paths.append(similar_path) + + return similar_paths + +def main(): + # Vérifier que nous sommes dans le bon répertoire + script_dir = os.path.dirname(os.path.abspath(__file__)) + base_dir = script_dir + + # Chemin vers le rapport_template.md + template_path = os.path.join(base_dir, "Corpus", "rapport_template.md") + + if not os.path.exists(template_path): + print(f"Erreur: Le fichier {template_path} n'existe pas.") + sys.exit(1) + + print("=== Vérification des chemins dans rapport_template.md ===") + + # Extraire les chemins + paths = extract_paths(template_path) + print(f"Nombre total de chemins trouvés: {len(paths)}") + + # Vérifier les chemins + results = check_paths(paths, base_dir) + + # Afficher les résultats + print("\n=== Résultats ===") + print(f"Chemins existants: {len(results['existing'])}") + print(f"Chemins manquants: {len(results['missing'])}") + print(f"Chemins problématiques: {len(results['problematic'])}") + + # Afficher les chemins manquants + if results["missing"]: + print("\n=== Chemins manquants ===") + for path in results["missing"]: + print(f"- {path}") + similar = find_similar_paths(path, base_dir) + if similar: + print(" Chemins similaires trouvés:") + for sim_path in similar[:3]: # Limiter à 3 suggestions + print(f" * {sim_path}") + + # Afficher les chemins problématiques avec suggestions + if results["problematic"]: + print("\n=== Chemins problématiques ===") + for orig, corrected, reason in results["problematic"]: + print(f"- {orig}") + print(f" Suggestion: {corrected}") + print(f" Raison: {reason}") + + # Résumé + if not results["missing"] and not results["problematic"]: + print("\nTous les chemins dans le rapport sont valides !") + else: + print("\nDes chemins problématiques ont été détectés. Veuillez corriger les erreurs.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/IA/01 - corpus_rapport_factuel/generate_template.py b/IA/01 - corpus_rapport_factuel/generate_template.py new file mode 100644 index 0000000..d1696b5 --- /dev/null +++ b/IA/01 - corpus_rapport_factuel/generate_template.py @@ -0,0 +1,1267 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script de génération de template de rapport IHH/ISG. +Ce script lit un fichier DOT, identifie les éléments Orange ou Rouge, +et génère un template avec les chemins d'accès aux sections à récupérer. +""" + +import os +import re +import glob +import yaml +import argparse +import unicodedata +import networkx as nx +from pathlib import Path +from networkx.drawing.nx_agraph import read_dot + +# Chemins de base +BASE_DIR = Path(__file__).resolve().parent +BASE_DIR = BASE_DIR / ".." +CORPUS_DIR = BASE_DIR / "Corpus" +GRAPH_PATH = BASE_DIR / "graphe.dot" +CONFIG_PATH = BASE_DIR / "assets/config.yaml" +OUTPUT_PATH = CORPUS_DIR / "rapport_template.md" + +# Chemins pour les indices et documents de référence +# Chemins utilisés pour les références vers les fiches techniques +IHH_ROOT_PATH = 'Criticités/Fiche technique IHH' +ISG_ROOT_PATH = 'Criticités/Fiche technique ISG' +IVC_ROOT_PATH = 'Criticités/Fiche technique IVC' +ICS_ROOT_PATH = 'Criticités/Fiche technique ICS' +# Utiliser simplement le chemin relatif pour accéder aux fichiers +CRIT_PATH = 'Criticités' + +def load_config(): + """Charge les seuils depuis le fichier de configuration""" + try: + with open(CONFIG_PATH, 'r') as file: + config = yaml.safe_load(file) + return config.get('seuils', {}) + except Exception as e: + print(f"Erreur lors du chargement de la configuration: {e}") + # Valeurs par défaut si le fichier est inaccessible + return { + 'ISG': { + 'vert': {'max': 40}, + 'orange': {'min': 40, 'max': 70}, + 'rouge': {'min': 70} + }, + 'IVC': { + 'vert': {'max': 5}, + 'orange': {'min': 5, 'max': 15}, + 'rouge': {'min': 15} + }, + 'IHH': { + 'vert': {'max': 15}, + 'orange': {'min': 15, 'max': 25}, + 'rouge': {'min': 25} + }, + 'ICS': { + 'vert': {'max': 0.30}, + 'orange': {'min': 0.30, 'max': 0.60}, + 'rouge': {'min': 0.60} + } + } + +def determine_threshold_color(value, index_type): + """Détermine la couleur du seuil en fonction de la valeur et du type d'indice""" + seuils = load_config().get(index_type, {}) + + if not seuils: + return None + + # Vérifier d'abord le seuil rouge (priorité la plus haute) + if 'rouge' in seuils and 'min' in seuils['rouge'] and value >= seuils['rouge']['min']: + return 'rouge' + + # Ensuite le seuil orange + if 'orange' in seuils and 'min' in seuils['orange'] and 'max' in seuils['orange']: + if value >= seuils['orange']['min'] and value < seuils['orange']['max']: + return 'orange' + + # Par défaut, c'est vert + return 'vert' + +# Définition des types de ressources et leurs opérations associées +RESOURCE_TYPES = { + "produit": {"folder": "Assemblage", "operation": "assemblage"}, + "composant": {"folder": "Fabrication", "operation": "fabrication"}, + "minerai": {"folder": "Minerai", "operations": ["extraction", "traitement"]} +} + +def parse_graph(dot_path, filter_level='orange'): + """Analyse le fichier DOT et extrait les éléments avec criticité Orange ou Rouge. + + Args: + dot_path: Chemin vers le fichier graphe.dot + filter_level: Niveau de seuil à utiliser ('orange' ou 'rouge') + """ + G = read_dot(dot_path) + elements = [] + + # Charger les seuils depuis la configuration + seuils = load_config() + + # Définir les seuils pour IHH, ISG, et IVC (Orange et Rouge) + ihh_seuil_orange = seuils.get('IHH', {}).get('orange', {}).get('min', 15) + ihh_seuil_rouge = seuils.get('IHH', {}).get('rouge', {}).get('min', 25) + isg_seuil_orange = seuils.get('ISG', {}).get('orange', {}).get('min', 40) + isg_seuil_rouge = seuils.get('ISG', {}).get('rouge', {}).get('min', 70) + ivc_seuil_orange = seuils.get('IVC', {}).get('orange', {}).get('min', 5) + ivc_seuil_rouge = seuils.get('IVC', {}).get('rouge', {}).get('min', 15) + + # Sélectionner les seuils en fonction du niveau de filtrage + if filter_level == 'rouge': + ihh_seuil = ihh_seuil_rouge + isg_seuil = isg_seuil_rouge + ivc_seuil = ivc_seuil_rouge + else: # 'orange' par défaut + ihh_seuil = ihh_seuil_orange + isg_seuil = isg_seuil_orange + ivc_seuil = ivc_seuil_orange + + # Traiter les nœuds + for node, attrs in G.nodes(data=True): + if '_' not in node: + continue + + op, res = node.split('_', 1) + + # Récupérer les valeurs IHH si disponibles + ihh_pays = attrs.get('ihh_pays', 0) + ihh_acteurs = attrs.get('ihh_acteurs', 0) + + try: + ihh_pays = float(ihh_pays) + ihh_acteurs = float(ihh_acteurs) + except (ValueError, TypeError): + ihh_pays = 0 + ihh_acteurs = 0 + + # Récupérer la valeur ISG si disponible + isg = attrs.get('isg', 0) + try: + isg = float(isg) + except (ValueError, TypeError): + isg = 0 + + # Récupérer la valeur IVC si disponible + ivc = attrs.get('ivc', 0) + try: + ivc = float(ivc) + except (ValueError, TypeError): + ivc = 0 + + # Déterminer si l'élément est critique selon le seuil configuré + ihh_critique = ihh_pays > ihh_seuil or ihh_acteurs > ihh_seuil + isg_critique = isg > isg_seuil + ivc_critique = ivc > ivc_seuil + + if ihh_critique or isg_critique or ivc_critique: + elements.append({ + 'operation': op, + 'resource': res, + 'ihh_pays': ihh_pays, + 'ihh_acteurs': ihh_acteurs, + 'isg': isg, + 'ivc': ivc, + 'ihh_critique': ihh_critique, + 'isg_critique': isg_critique, + 'ivc_critique': ivc_critique, + 'node_id': node + }) + + # Traiter les arêtes pour détecter les ICS critiques + for u, v, attrs in G.edges(data=True): + if 'ics' in attrs: + ics_value = attrs.get('ics', 0) + try: + ics_value = float(ics_value) + except (ValueError, TypeError): + ics_value = 0 + + ics_seuil_orange = seuils.get('ICS', {}).get('orange', {}).get('min', 0.30) + ics_seuil_rouge = seuils.get('ICS', {}).get('rouge', {}).get('min', 0.60) + + # Sélectionner le seuil ICS en fonction du niveau de filtrage + ics_seuil = ics_seuil_rouge if filter_level == 'rouge' else ics_seuil_orange + + ics_critique = ics_value >= ics_seuil + + if ics_critique: + # Ajouter l'ICS critique au nœud de destination (minerai) + # Trouver d'abord si le nœud de destination existe déjà dans elements + dest_node = v + dest_element = next((e for e in elements if e['node_id'] == dest_node), None) + + if dest_element: + # Mettre à jour l'élément existant + dest_element['ics_critique'] = True + dest_element['ics_value'] = ics_value + else: + # Si le nœud destination n'est pas déjà dans elements (pas critique pour d'autres raisons) + # Créer une entrée s'il s'agit d'un format operation_resource + if '_' in dest_node: + op, res = dest_node.split('_', 1) + elements.append({ + 'operation': op, + 'resource': res, + 'node_id': dest_node, + 'ics_critique': True, + 'ics_value': ics_value + }) + + return elements + +def analyze_geopolitical_stability(dot_path, filter_levels=None): + """Analyse la stabilité géopolitique à partir du graphe. + + Args: + dot_path: Chemin vers le fichier graphe.dot + filter_levels: Liste de niveaux de criticité à considérer ('orange', 'rouge') + Si None, prend les deux niveaux par défaut + """ + G = read_dot(dot_path) + + # Si aucun niveau n'est spécifié, on considère orange et rouge + if filter_levels is None: + filter_levels = ['orange', 'rouge'] + elif isinstance(filter_levels, str): + filter_levels = [filter_levels] + + print(f"Analyse ISG du fichier {dot_path} avec filtres {', '.join(filter_levels)}") + + # Charger les seuils depuis la configuration + seuils = load_config().get('ISG', {}) + + # Déterminer le seuil en fonction des niveaux demandés + isg_threshold = None + if 'orange' in filter_levels and 'rouge' not in filter_levels: + isg_threshold = seuils.get('orange', {}).get('min', 40) + print(f"Niveau orange: ISG >= {isg_threshold}") + elif 'rouge' in filter_levels and 'orange' not in filter_levels: + isg_threshold = seuils.get('rouge', {}).get('min', 70) + print(f"Niveau rouge: ISG >= {isg_threshold}") + else: # Les deux niveaux ou cas par défaut + isg_threshold = seuils.get('orange', {}).get('min', 40) + print(f"Niveaux orange et rouge: ISG >= {isg_threshold}") + print(f"Seuil ISG critique: {isg_threshold}") + results = {} + + # Trouver d'abord tous les nœuds géographiques avec ISG + geo_isg = {} + for node, attrs in G.nodes(data=True): + if 'isg' in attrs: + pays_name = node.split('_')[0] if '_' in node else node + isg_value = float(attrs['isg']) + geo_isg[pays_name] = isg_value + print(f"Nœud géographique avec ISG trouvé: {node}, ISG: {isg_value}") + + # Pour chaque nœud d'opération + operations_count = 0 + for node, attrs in G.nodes(data=True): + if '_' in node and ('ihh_pays' in attrs or 'ihh_acteurs' in attrs): + operations_count += 1 + parts = node.split('_') + op, res = parts[0], '_'.join(parts[1:]) if len(parts) > 1 else parts[0] + operation_key = f"{op}_{res}" + print(f"Analyse de l'opération: {operation_key}") + results[operation_key] = { + 'pays_critiques': [], + 'acteurs_critiques': [] + } + + # Chercher les pays liés directement à cette opération + for pays_node in G.successors(node): + # Vérifier que c'est un nœud de pays valide (contient "_" et son premier segment est dans geo_isg) + # et n'est pas un nœud géographique + pays_prefix = pays_node.split('_')[0] if '_' in pays_node else pays_node + if '_' in pays_node and 'geographique' not in pays_node and pays_prefix in geo_isg: + pays_name = pays_prefix + # Extraire le pourcentage de l'attribut label de l'arête + edge_label = G.edges[node, pays_node].get('label', '0%') + pays_part = int(edge_label.strip('%')) if edge_label.strip('%').isdigit() else 0 + + # Chercher le nœud géographique correspondant + geo_node = f"{pays_name}_geographique" + isg_value = geo_isg.get(pays_name, 0) + + print(f" - Pays trouvé: {pays_name}, ISG: {isg_value}, Part: {pays_part}%, seuil: {isg_threshold}") + + if isg_value >= isg_threshold: + results[operation_key]['pays_critiques'].append({ + 'nom': pays_name, + 'isg': isg_value, + 'part': pays_part + }) + print(f" => Pays critique ajouté: {pays_name}, ISG: {isg_value}") + + # Chercher les acteurs liés à ce pays + for acteur_node in G.successors(pays_node): + # Vérifier que c'est bien un nœud d'acteur (format: NomActeur_Pays_Operation) + if '_' in acteur_node: + acteur_parts = acteur_node.split("_") + if len(acteur_parts) >= 3 and acteur_parts[1] == pays_name: + acteur_name = acteur_parts[0] + + # Ignorer si l'acteur a le même nom que le pays + if acteur_name.lower() != pays_name.lower(): + edge_label = G.edges[pays_node, acteur_node].get('label', '0%') + acteur_part = int(edge_label.strip('%')) if edge_label.strip('%').isdigit() else 0 + + # Utiliser l'ISG du pays pour l'acteur + if isg_value >= isg_threshold: + results[operation_key]['acteurs_critiques'].append({ + 'nom': acteur_name, + 'pays': pays_name, + 'isg': isg_value, + 'part': acteur_part + }) + print(f" => Acteur critique ajouté: {acteur_name}, ISG: {isg_value}") + + # Résumé de l'analyse + total_critical_countries = sum(len(data['pays_critiques']) for data in results.values()) + total_critical_actors = sum(len(data['acteurs_critiques']) for data in results.values()) + print(f"Analyse ISG terminée: {operations_count} opérations analysées") + print(f"Résultats: {total_critical_countries} pays critiques, {total_critical_actors} acteurs critiques") + return results + +# Function removed as no longer needed with the new approach + +def find_real_path(base_dir, resource_type, resource_name, file_pattern, G=None): + """Recherche le chemin réel d'un fichier/dossier.""" + # Obtenir le label du graphe si disponible + resource_label = resource_name + if G: + for node, attrs in G.nodes(data=True): + if node == resource_name and 'label' in attrs: + resource_label = attrs['label'] + break + + resource_name_lower = resource_name.lower() + resource_label_lower = resource_label.lower() + + # Créer des variantes du label pour les cas spéciaux comme "SSD 2.5 pouces" -> "SSD 2.5" + label_variants = [resource_label, resource_label_lower] + + # Ajouter une variante sans "pouces" ou autre suffixe + if " " in resource_label: + # Pour "SSD 2.5 pouces", ajouter "SSD 2.5" + # Pour "Carte mère", ajouter "Carte" + parts = resource_label.split(" ") + if len(parts) >= 2: + short_label = " ".join(parts[:2]) # Prendre les deux premiers mots + label_variants.append(short_label) + label_variants.append(short_label.lower()) + + # Version avec première lettre majuscule + capitalized_label = resource_label_lower.title() + label_variants.append(capitalized_label) + + short_capitalized = short_label.title() + label_variants.append(short_capitalized) + + # Cas spécial pour les principaux assembleurs/fabricants + is_principaux_pattern = "principaux" in file_pattern.lower() + + if is_principaux_pattern: + # Rechercher les fichiers des principaux assembleurs/fabricants + if resource_type == "produit": + patterns = [] + for variant in label_variants: + patterns.extend([ + f"Assemblage/Fiche assemblage {variant}/??-principaux-assembleurs.md", + f"Assemblage/Fiche*{variant}*/??-principaux-assembleurs.md", + f"**/*{variant}*/*principaux*assembleurs*.md" + ]) + # Ajouter les patterns avec le nom du nœud aussi + patterns.extend([ + f"Assemblage/Fiche assemblage {resource_name}/??-principaux-assembleurs.md", + f"Assemblage/Fiche assemblage {resource_name_lower}/??-principaux-assembleurs.md", + f"Assemblage/Fiche*{resource_name}*/??-principaux-assembleurs.md", + f"Assemblage/Fiche*{resource_name_lower}*/??-principaux-assembleurs.md", + f"**/*{resource_name}*/*principaux*assembleurs*.md", + f"**/*{resource_name_lower}*/*principaux*assembleurs*.md" + ]) + elif resource_type == "composant": + patterns = [] + for variant in label_variants: + patterns.extend([ + f"Fabrication/Fiche fabrication {variant}/??-principaux-fabricants.md", + f"Fabrication/Fiche*{variant}*/??-principaux-fabricants.md", + f"**/*{variant}*/*principaux*fabricants*.md" + ]) + # Ajouter les patterns avec le nom du nœud aussi + patterns.extend([ + f"Fabrication/Fiche fabrication {resource_name}/??-principaux-fabricants.md", + f"Fabrication/Fiche fabrication {resource_name_lower}/??-principaux-fabricants.md", + f"Fabrication/Fiche*{resource_name}*/??-principaux-fabricants.md", + f"Fabrication/Fiche*{resource_name_lower}*/??-principaux-fabricants.md", + f"**/*{resource_name}*/*principaux*fabricants*.md", + f"**/*{resource_name_lower}*/*principaux*fabricants*.md" + ]) + elif resource_type == "minerai": + operation = file_pattern.split("-")[-1] if "-" in file_pattern else "" + patterns = [] + for variant in label_variants: + # S'assurer qu'il n'y a pas de tiret à la fin du nom du minerai + clean_variant = variant.rstrip(" -") + patterns.extend([ + f"Minerai/Fiche minerai {clean_variant}/??-principaux-producteurs-{operation}.md", + f"Minerai/Fiche {clean_variant}/??-principaux-producteurs-{operation}.md", + f"Minerai/Fiche*minerai*{clean_variant}*/??-principaux-producteurs*.md", + f"Minerai/Fiche*{clean_variant}*/??-principaux-producteurs*.md", + f"**/*minerai*{clean_variant}*/*principaux*producteurs*.md", + f"**/*{clean_variant}*/*principaux*producteurs*.md" + ]) + # Ajouter les patterns avec le nom du nœud aussi + clean_resource = resource_name.rstrip(" -") + clean_resource_lower = resource_name_lower.rstrip(" -") + patterns.extend([ + f"Minerai/Fiche minerai {clean_resource}/??-principaux-producteurs-{operation}.md", + f"Minerai/Fiche {clean_resource}/??-principaux-producteurs-{operation}.md", + f"Minerai/Fiche minerai {clean_resource_lower}/??-principaux-producteurs-{operation}.md", + f"Minerai/Fiche {clean_resource_lower}/??-principaux-producteurs-{operation}.md", + f"Minerai/Fiche*minerai*{clean_resource}*/??-principaux-producteurs*.md", + f"Minerai/Fiche*{clean_resource}*/??-principaux-producteurs*.md", + f"Minerai/Fiche*minerai*{clean_resource_lower}*/??-principaux-producteurs*.md", + f"Minerai/Fiche*{clean_resource_lower}*/??-principaux-producteurs*.md", + f"**/*minerai*{clean_resource}*/*principaux*producteurs*.md", + f"**/*{clean_resource}*/*principaux*producteurs*.md", + f"**/*minerai*{clean_resource_lower}*/*principaux*producteurs*.md", + f"**/*{clean_resource_lower}*/*principaux*producteurs*.md" + ]) + else: + # Pour les IHH, privilégier la recherche dans le répertoire centralisé + if resource_type == "produit": + patterns = [] + for variant in label_variants: + patterns.extend([ + f"{IHH_ROOT_PATH}/*-assemblage-{variant}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{variant}/*-indice-de-herfindahl-hirschmann.md" + ]) + # Ajouter les patterns avec le nom du nœud aussi + patterns.extend([ + f"{IHH_ROOT_PATH}/*-assemblage-{resource_name}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-assemblage-{resource_name_lower}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{resource_name}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{resource_name_lower}/*-indice-de-herfindahl-hirschmann.md" + ]) + elif resource_type == "composant": + patterns = [] + for variant in label_variants: + patterns.extend([ + f"{IHH_ROOT_PATH}/*-fabrication-{variant}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{variant}/*-indice-de-herfindahl-hirschmann.md" + ]) + # Ajouter les patterns avec le nom du nœud aussi + patterns.extend([ + f"{IHH_ROOT_PATH}/*-fabrication-{resource_name}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-fabrication-{resource_name_lower}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{resource_name}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{resource_name_lower}/*-indice-de-herfindahl-hirschmann.md" + ]) + elif resource_type == "minerai": + operation = file_pattern.split("-")[-1] if "-" in file_pattern else "" + if operation: + patterns = [] + for variant in label_variants: + patterns.extend([ + # Nouvelle structure + f"{IHH_ROOT_PATH}/*-opérations-{variant}/*-indice-de-herfindahl-hirschmann-{operation}.md", + # Ancienne structure + f"{IHH_ROOT_PATH}/*-{operation}-{variant}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{variant}*/*-{operation}*.md" + ]) + # Ajouter les patterns avec le nom du nœud aussi + patterns.extend([ + # Nouvelle structure + f"{IHH_ROOT_PATH}/*-opérations-{resource_name}/*-indice-de-herfindahl-hirschmann-{operation}.md", + f"{IHH_ROOT_PATH}/*-opérations-{resource_name_lower}/*-indice-de-herfindahl-hirschmann-{operation}.md", + # Ancienne structure + f"{IHH_ROOT_PATH}/*-{operation}-{resource_name}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{operation}-{resource_name_lower}/*-indice-de-herfindahl-hirschmann.md", + f"{IHH_ROOT_PATH}/*-{resource_name}*/*-{operation}*.md", + f"{IHH_ROOT_PATH}/*-{resource_name_lower}*/*-{operation}*.md" + ]) + else: + patterns = [] + for variant in label_variants: + patterns.extend([ + f"{IHH_ROOT_PATH}/*-opérations-{variant}/*.md", + f"{IHH_ROOT_PATH}/*-{variant}*/*.md" + ]) + # Ajouter les patterns avec le nom du nœud aussi + patterns.extend([ + f"{IHH_ROOT_PATH}/*-opérations-{resource_name}/*.md", + f"{IHH_ROOT_PATH}/*-opérations-{resource_name_lower}/*.md", + f"{IHH_ROOT_PATH}/*-{resource_name}*/*.md", + f"{IHH_ROOT_PATH}/*-{resource_name_lower}*/*.md" + ]) + + # Chercher dans les patterns + for pattern in patterns: + matches = glob.glob(os.path.join(base_dir, pattern), recursive=True) + if matches: + return os.path.relpath(matches[0], base_dir) + + # Si aucun chemin réel n'est trouvé, construire un chemin générique dans la structure appropriée + # Utiliser le label pour le chemin par défaut, et choisir la meilleure variante + best_label = resource_label + # Si le label contient "pouces" ou d'autres termes descriptifs longs, utiliser une version plus courte + if " " in resource_label and len(resource_label.split(" ")) > 2: + parts = resource_label.split(" ") + best_label = " ".join(parts[:2]) # Prendre les deux premiers mots + + if resource_type == "produit": + if is_principaux_pattern: + return f"Assemblage/Fiche assemblage {best_label}/02-principaux-assembleurs.md" + else: + # Numéros arbitraires mais dans le format correct + return f"{IHH_ROOT_PATH}/10-assemblage-{best_label}/00-indice-de-herfindahl-hirschmann.md" + elif resource_type == "composant": + if is_principaux_pattern: + return f"Fabrication/Fiche fabrication {best_label}/02-principaux-fabricants.md" + else: + # Numéros arbitraires mais dans le format correct + return f"{IHH_ROOT_PATH}/20-fabrication-{best_label}/00-indice-de-herfindahl-hirschmann.md" + elif resource_type == "minerai": + operation = file_pattern.split("-")[-1] if "-" in file_pattern else "" + if is_principaux_pattern: + # Choisir le numéro du préfixe en fonction de l'opération + prefix_num = "03" if operation == "extraction" else "05" if operation == "traitement" else "02" + # S'assurer que le nom du minerai est correctement formaté (sans tiret à la fin et en minuscules) + clean_label = best_label.rstrip(" -").lower() + return f"Minerai/Fiche minerai {clean_label}/{prefix_num}-principaux-producteurs-{operation}.md" + else: + # Numéros arbitraires mais dans le format correct + return f"{IHH_ROOT_PATH}/30-{operation}-{best_label}/00-indice-de-herfindahl-hirschmann.md" + + return "" + +def categorize_elements(elements): + """Catégorise les éléments en produits finaux, composants et minerais.""" + produits = [] + composants = [] + minerais = [] + + for element in elements: + resource = element['resource'].lower() + + if resource in ['serveur', 'smartphone', 'ordinateur', 'véhicule électrique']: + produits.append(element) + elif resource in ['boitier', 'cartemere', 'ssd25', 'ssdm2', 'batterie', 'puce', 'cpu', 'mémoire', 'écran', 'disquedur', 'connecteurs', 'connectivite']: + composants.append(element) + else: + minerais.append(element) + + # On regroupe d'abord les minerais par ressource + minerais_par_ressource = {} + for m in minerais: + resource = m['resource'] + if resource not in minerais_par_ressource: + minerais_par_ressource[resource] = [] + minerais_par_ressource[resource].append(m) + + # Reconstruire la liste des minerais, groupés par ressource et triés alphabétiquement + minerais = [] + for resource in sorted(minerais_par_ressource.keys(), key=str.lower): + minerais.extend(minerais_par_ressource[resource]) + + # Tri alphabétique des produits et composants + produits.sort(key=lambda x: x['resource'].lower()) + composants.sort(key=lambda x: x['resource'].lower()) + + return produits, composants, minerais + +def generate_template(elements, isg_data=None, G=None): + """Génère le template de rapport avec les chemins d'accès aux sections.""" + template = [] + + # Corps principal + template.append("# Évaluation des vulnérabilités critiques") + template.append("") + template.append("## Introduction") + template.append("Ce rapport vise à évaluer les vulnérabilités systémiques associées à la chaîne de valeur représentée dans le graphe fourni.") + template.append("") + + # Éléments factuels + template.append("## Éléments factuels") + template.append("Cette section présente les données clés collectées automatiquement à partir du graphe et du corpus documentaire.") + template.append("") + + # Catégoriser les éléments + produits, composants, minerais = categorize_elements(elements) + + # Obtenir les labels des éléments à partir du graphe + resource_labels = {} + if G: + for node, attrs in G.nodes(data=True): + if 'label' in attrs: + resource_labels[node] = attrs['label'] + + # Obtenir les labels des éléments à partir du graphe + resource_labels = {} + if G: + for node, attrs in G.nodes(data=True): + if 'label' in attrs: + resource_labels[node] = attrs['label'] + + # Traiter les produits finaux + for produit in produits: + resource = produit['resource'] + # Utiliser le label du graphe si disponible + display_name = resource_labels.get(resource, resource) + template.append(f"### {display_name}") + template.append("") + + if produit['ihh_critique']: + template.append(f"#### Assemblage {display_name}") + template.append("") + # Déterminer la couleur du seuil IHH + ihh_pays_color = determine_threshold_color(produit['ihh_pays'], 'IHH') + ihh_acteurs_color = determine_threshold_color(produit['ihh_acteurs'], 'IHH') + ihh_pays_color_text = f" - seuil {ihh_pays_color.capitalize()}" if ihh_pays_color else "" + ihh_acteurs_color_text = f" - seuil {ihh_acteurs_color.capitalize()}" if ihh_acteurs_color else "" + + template.append("##### Concentration") + template.append(f"- IHH pays : {produit['ihh_pays']}{ihh_pays_color_text}") + template.append(f"- IHH acteurs : {produit['ihh_acteurs']}{ihh_acteurs_color_text}") + template.append("- Principaux pays producteurs : ...") + template.append("- Principaux acteurs : ...") + template.append("") + + # Vérifier si nous avons des données ISG pour cette opération + if isg_data: + operation_key = f"{produit['operation']}_{produit['resource']}" + print(f"Vérification ISG pour produit: {operation_key}") + if operation_key in isg_data: + print(f" - Trouvé dans isg_data: pays={len(isg_data[operation_key]['pays_critiques'])}, acteurs={len(isg_data[operation_key]['acteurs_critiques'])}") + if operation_key in isg_data and (isg_data[operation_key]['pays_critiques'] or isg_data[operation_key]['acteurs_critiques']): + template.append("#### Stabilité Géopolitique") + print(f" => Section ISG ajoutée pour {operation_key}") + + if isg_data[operation_key]['pays_critiques']: + template.append("**Pays à risque critique** :") + for pays in isg_data[operation_key]['pays_critiques']: + isg_color = determine_threshold_color(pays['isg'], 'ISG') + isg_color_text = f" - seuil {isg_color.capitalize()}" if isg_color else "" + template.append(f"- {pays['nom']} (ISG: {pays['isg']}{isg_color_text}) - Production de {pays['part']}% des ressources") + template.append("") + + if isg_data[operation_key]['acteurs_critiques']: + template.append("**Acteurs à risque critique** :") + for acteur in isg_data[operation_key]['acteurs_critiques']: + isg_color = determine_threshold_color(acteur['isg'], 'ISG') + isg_color_text = f" - seuil {isg_color.capitalize()}" if isg_color else "" + template.append(f"- {acteur['nom']} ({acteur['pays']}, ISG: {acteur['isg']}{isg_color_text}) - Contrôle de {acteur['part']}% du marché") + template.append("") + elif produit['isg_critique']: + template.append("#### Stabilité Géopolitique") + template.append("- Analyse des risques géopolitiques non disponible") + template.append("") + + # Traiter les composants + for composant in composants: + resource = composant['resource'] + # Utiliser le label du graphe si disponible + display_name = resource_labels.get(resource, resource) + template.append(f"### {display_name}") + template.append("") + + if composant['ihh_critique']: + template.append(f"#### Fabrication {display_name}") + template.append("") + # Déterminer la couleur du seuil IHH + ihh_pays_color = determine_threshold_color(composant['ihh_pays'], 'IHH') + ihh_acteurs_color = determine_threshold_color(composant['ihh_acteurs'], 'IHH') + ihh_pays_color_text = f" - seuil {ihh_pays_color.capitalize()}" if ihh_pays_color else "" + ihh_acteurs_color_text = f" - seuil {ihh_acteurs_color.capitalize()}" if ihh_acteurs_color else "" + + template.append("##### Concentration") + template.append(f"- IHH pays : {composant['ihh_pays']}{ihh_pays_color_text}") + template.append(f"- IHH acteurs : {composant['ihh_acteurs']}{ihh_acteurs_color_text}") + template.append("- Principaux pays producteurs : ...") + template.append("- Principaux fabricants : ...") + template.append("") + + # Vérifier si nous avons des données ISG pour cette opération + if isg_data: + operation_key = f"{composant['operation']}_{composant['resource']}" + print(f"Vérification ISG pour composant: {operation_key}") + if operation_key in isg_data: + print(f" - Trouvé dans isg_data: pays={len(isg_data[operation_key]['pays_critiques'])}, acteurs={len(isg_data[operation_key]['acteurs_critiques'])}") + if operation_key in isg_data and (isg_data[operation_key]['pays_critiques'] or isg_data[operation_key]['acteurs_critiques']): + template.append("#### Stabilité Géopolitique") + print(f" => Section ISG ajoutée pour {operation_key}") + + if isg_data[operation_key]['pays_critiques']: + template.append("**Pays à risque critique** :") + for pays in isg_data[operation_key]['pays_critiques']: + isg_color = determine_threshold_color(pays['isg'], 'ISG') + isg_color_text = f" - seuil {isg_color.capitalize()}" if isg_color else "" + template.append(f"- {pays['nom']} (ISG: {pays['isg']}{isg_color_text}) - Production de {pays['part']}% des ressources") + template.append("") + + if isg_data[operation_key]['acteurs_critiques']: + template.append("**Acteurs à risque critique** :") + for acteur in isg_data[operation_key]['acteurs_critiques']: + isg_color = determine_threshold_color(acteur['isg'], 'ISG') + isg_color_text = f" - seuil {isg_color.capitalize()}" if isg_color else "" + template.append(f"- {acteur['nom']} ({acteur['pays']}, ISG: {acteur['isg']}{isg_color_text}) - Contrôle de {acteur['part']}% du marché") + template.append("") + elif composant['isg_critique']: + template.append("#### Stabilité Géopolitique") + template.append("- Analyse des risques géopolitiques non disponible") + template.append("") + + # Regrouper les minerais par nom de ressource pour le tri alphabétique + minerais_par_ressource = {} + for minerai in minerais: + resource = minerai['resource'] + if resource not in minerais_par_ressource: + minerais_par_ressource[resource] = [] + minerais_par_ressource[resource].append(minerai) + + # Traiter les ressources dans l'ordre alphabétique + for resource in sorted(minerais_par_ressource.keys(), key=str.lower): + # Utiliser le label du graphe si disponible + display_name = resource_labels.get(resource, resource) + template.append(f"### {display_name}") + template.append("") + + # Pour chaque ressource, traiter ses opérations + minerais_resource = minerais_par_ressource[resource] + + # Identifier les types d'opérations disponibles pour cette ressource + operations_disponibles = {'Extraction': None, 'Traitement': None} + for m in minerais_resource: + if m['operation'] in operations_disponibles: + operations_disponibles[m['operation']] = m + + # Traiter les opérations dans un ordre spécifique (Extraction puis Traitement) + operations_ordre = ['Extraction', 'Traitement'] + + for operation in operations_ordre: + m = operations_disponibles.get(operation) + if m and m['ihh_critique']: + # Utiliser le label du graphe si disponible + display_name = resource_labels.get(resource, resource) + template.append(f"#### {operation} {display_name}") + template.append("") + # Déterminer la couleur du seuil IHH + ihh_pays_color = determine_threshold_color(m['ihh_pays'], 'IHH') + ihh_acteurs_color = determine_threshold_color(m['ihh_acteurs'], 'IHH') + ihh_pays_color_text = f" - seuil {ihh_pays_color.capitalize()}" if ihh_pays_color else "" + ihh_acteurs_color_text = f" - seuil {ihh_acteurs_color.capitalize()}" if ihh_acteurs_color else "" + + template.append("##### Concentration") + template.append(f"- IHH pays : {m['ihh_pays']}{ihh_pays_color_text}") + template.append(f"- IHH acteurs : {m['ihh_acteurs']}{ihh_acteurs_color_text}") + template.append("- Principaux pays producteurs : ...") + template.append("- Principaux acteurs : ...") + template.append("") + + # Vérifier si nous avons des données ISG pour cette opération + if isg_data: + operation_key = f"{m['operation']}_{resource}" + print(f"Vérification ISG pour minerai: {operation_key}") + if operation_key in isg_data: + print(f" - Trouvé dans isg_data: pays={len(isg_data[operation_key]['pays_critiques'])}, acteurs={len(isg_data[operation_key]['acteurs_critiques'])}") + if operation_key in isg_data and (isg_data[operation_key]['pays_critiques'] or isg_data[operation_key]['acteurs_critiques']): + template.append("#### Stabilité Géopolitique") + print(f" => Section ISG ajoutée pour {operation_key}") + + if isg_data[operation_key]['pays_critiques']: + template.append("**Pays à risque critique** :") + for pays in isg_data[operation_key]['pays_critiques']: + isg_color = determine_threshold_color(pays['isg'], 'ISG') + isg_color_text = f" - seuil {isg_color.capitalize()}" if isg_color else "" + template.append(f"- {pays['nom']} (ISG: {pays['isg']}{isg_color_text}) - Production de {pays['part']}% des ressources") + template.append("") + + if isg_data[operation_key]['acteurs_critiques']: + template.append("**Acteurs à risque critique** :") + for acteur in isg_data[operation_key]['acteurs_critiques']: + isg_color = determine_threshold_color(acteur['isg'], 'ISG') + isg_color_text = f" - seuil {isg_color.capitalize()}" if isg_color else "" + template.append(f"- {acteur['nom']} ({acteur['pays']}, ISG: {acteur['isg']}{isg_color_text}) - Contrôle de {acteur['part']}% du marché") + template.append("") + elif m['isg_critique']: + template.append("#### Stabilité Géopolitique") + template.append("- Analyse des risques géopolitiques non disponible") + template.append("") + + if minerai['ihh_critique']: + # Déterminer la couleur du seuil IHH + ihh_color = determine_threshold_color(minerai['ihh_pays'], 'IHH') + ihh_color_text = f" - seuil {ihh_color.capitalize()}" if ihh_color else "" + + template.append("#### Substituabilité") + # Ajouter la section ICS pour les minerais + ics_path = find_ics_path(resource) + if ics_path: + template.append(f"Corpus/{ics_path}") + else: + template.append("[résumé]") + template.append("") + template.append("#### Concurrence") + + # Rechercher l'IVC pour ce minerai + ivc_info = find_ivc_path(resource) + + if ivc_info: + template.append(f"IVC: {ivc_info['ivc_value']} - seuil {ivc_info['threshold'].capitalize()}") + template.append("") + # Ajouter le lien vers les secteurs concurrents + secteurs_path = f"{ivc_info['path']}/01-secteurs-concurrents.md" + if os.path.exists(os.path.join(CORPUS_DIR, secteurs_path)): + template.append(f"Corpus/{secteurs_path}") + else: + template.append("[Information sur les secteurs concurrents non disponible]") + else: + template.append("[résumé]") + + template.append("") + + # Annexes + template.append("## Annexes") + template.append("Contenus documentaires complets :") + template.append("") + + # Indices documentaires + try: + has_ihh = any(element['ihh_critique'] for element in elements) + except: + has_ihh = False + try: + has_isg = any(element['isg_critique'] for element in elements) + except: + has_isg = False + try: + has_ivc = any(element['ivc_critique'] for element in elements) + except: + has_ivc = False + try: + has_ics = any(element['ics_critique'] for element in elements) + except: + has_ics = False + + if has_ihh: + template.append("### Indice Herfindahl-Hirschmann (IHH)") + template.append("") + template.append("#### Contexte et objectif") + template.append(f"Corpus/{IHH_ROOT_PATH}/00-contexte-et-objectif.md") + template.append("") + template.append("#### Mode de calcul") + template.append(f"Corpus/{IHH_ROOT_PATH}/01-mode-de-calcul/_intro.md") + template.append("") + template.append("#### Seuils") + template.append(f"Corpus/{IHH_ROOT_PATH}/01-mode-de-calcul/00-seuils-d-interprétation.md") + template.append("") + + if has_isg: + template.append("### Indice de Stabilité Géopolitique (ISG)") + template.append("") + template.append("#### Contexte et objectif") + template.append(f"Corpus/{ISG_ROOT_PATH}/00-contexte-et-objectif.md") + template.append("") + template.append("#### Mode de calcul") + template.append(f"Corpus/{ISG_ROOT_PATH}/01-mode-de-calcul/_intro.md") + template.append("") + template.append("#### Seuils") + template.append(f"Corpus/{ISG_ROOT_PATH}/01-mode-de-calcul/04-seuils-d-interprétation.md") + template.append("") + + if has_ivc: + template.append("### Indice de Vulnérabilité Concurrentielle (IVC)") + template.append("") + template.append("#### Contexte et objectif") + template.append(f"Corpus/{IVC_ROOT_PATH}/00-contexte-et-objectif.md") + template.append("") + template.append("#### Mode de calcul") + template.append(f"Corpus/{IVC_ROOT_PATH}/01-mode-de-calcul/_intro.md") + template.append("") + template.append("#### Paramètres") + template.append(f"Corpus/{IVC_ROOT_PATH}/01-mode-de-calcul/00-paramètres.md") + template.append("") + template.append("#### Seuils") + template.append(f"Corpus/{IVC_ROOT_PATH}/01-mode-de-calcul/01-seuils-d-interprétation.md") + template.append("") + + if has_ics: + template.append("### Indice de Criticité de Substituabilité (ICS)") + template.append("") + template.append("#### Contexte et objectif") + template.append(f"Corpus/{ICS_ROOT_PATH}/00-contexte-et-objectif.md") + template.append("") + template.append("#### Mode de calcul") + template.append(f"Corpus/{ICS_ROOT_PATH}/01-mode-de-calcul/_intro.md") + template.append("") + template.append("#### Paramètres") + template.append(f"Corpus/{ICS_ROOT_PATH}/01-mode-de-calcul/00-paramètres.md") + template.append("") + template.append("#### Seuils") + template.append(f"Corpus/{ICS_ROOT_PATH}/01-mode-de-calcul/01-seuils-d-interprétation.md") + template.append("") + + # Détails des éléments critiques + for produit in produits: + resource = produit['resource'] + # Utiliser le label du graphe si disponible + display_name = resource_labels.get(resource, resource) + template.append(f"### {display_name}") + template.append("") + + if produit['ihh_critique']: + template.append("#### Indice de Herfindahl-Hirschmann") + ihh_path = find_real_path(CORPUS_DIR, "produit", resource, "indice-de-herfindahl-hirschmann", G) + template.append(f"Corpus/{ihh_path}") + template.append("") + template.append("#### Principaux assembleurs") + assembleurs_path = find_real_path(CORPUS_DIR, "produit", resource, "principaux-assembleurs", G) + template.append(f"Corpus/{assembleurs_path}") + template.append("") + + for composant in composants: + resource = composant['resource'] + # Utiliser le label du graphe si disponible + display_name = resource_labels.get(resource, resource) + template.append(f"### {display_name}") + template.append("") + + if composant['ihh_critique']: + template.append("#### Indice de Herfindahl-Hirschmann") + ihh_path = find_real_path(CORPUS_DIR, "composant", resource, "indice-de-herfindahl-hirschmann", G) + template.append(f"Corpus/{ihh_path}") + template.append("") + template.append("#### Principaux fabricants") + fabricants_path = find_real_path(CORPUS_DIR, "composant", resource, "principaux-fabricants", G) + template.append(f"Corpus/{fabricants_path}") + template.append("") + + # Détails des minerais - on traite Extraction puis Traitement + # Créer un dictionnaire pour regrouper les entrées par ressource + minerais_par_ressource = {} + for minerai in minerais: + resource = minerai['resource'] + if resource not in minerais_par_ressource: + minerais_par_ressource[resource] = [] + minerais_par_ressource[resource].append(minerai) + + # Parcourir les ressources dans l'ordre alphabétique + for resource in sorted(minerais_par_ressource.keys(), key=str.lower): + # Utiliser le label du graphe si disponible + display_name = resource_labels.get(resource, resource) + template.append(f"### {display_name}") + template.append("") + + # Identifier les types d'opérations disponibles pour cette ressource + operations_disponibles = {'Extraction': None, 'Traitement': None} + for m in minerais_par_ressource[resource]: + if m['operation'] in operations_disponibles: + operations_disponibles[m['operation']] = m + + # Traiter les opérations dans un ordre spécifique (Extraction puis Traitement) + operations_ordre = ['Extraction', 'Traitement'] + + for operation in operations_ordre: + m = operations_disponibles.get(operation) + if m and m['ihh_critique']: + # Utiliser le label du graphe si disponible + display_name = resource_labels.get(resource, resource) + template.append(f"#### Indice de Herfindahl-Hirschmann - {operation}") + ihh_path = find_real_path(CORPUS_DIR, "minerai", resource, f"indice-de-herfindahl-{operation.lower()}", G) + if ihh_path: + template.append(f"Corpus/{ihh_path}") + else: + # Chercher d'abord avec la nouvelle structure + new_pattern = f"{IHH_ROOT_PATH}/*-opérations-{resource.lower()}/*-indice-de-herfindahl-hirschmann-{operation.lower()}.md" + matches = glob.glob(os.path.join(CORPUS_DIR, new_pattern)) + if matches: + template.append(f"Corpus/{os.path.relpath(matches[0], CORPUS_DIR)}") + else: + # Utiliser l'ancienne structure + template.append(f"Corpus/{IHH_ROOT_PATH}/30-{operation.lower()}-{resource}/00-indice-de-herfindahl-hirschmann.md") + template.append("") + + template.append(f"##### Principaux producteurs - {operation}") + producteurs_path = find_real_path(CORPUS_DIR, "minerai", resource, f"principaux-producteurs-{operation.lower()}", G) + if producteurs_path: + template.append(f"Corpus/{producteurs_path}") + else: + template.append(f"Corpus/Minerai/Fiche {resource}/02-principaux-producteurs-{operation.lower()}.md") + template.append("") + + # Ajouter la section IVC pour les minerais + ivc_info = find_ivc_path(resource) + if ivc_info: + template.append("#### Indice de Vulnérabilité de Concurrence (IVC)") + + # Ajouter les liens vers les différents fichiers de la fiche IVC + ivc_sections = [ + "03-répartition-des-usages.md", + "04-tendance.md", + "05-procédés-alternatifs.md", + "06-réserves.md" + ] + + for section in ivc_sections: + section_path = f"{ivc_info['path']}/{section}" + if os.path.exists(os.path.join(CORPUS_DIR, section_path)): + template.append(f"Corpus/{section_path}") + + template.append("") + + # Ajouter la section ICS pour les minerais + if G: + ics_components = find_ics_component_paths(resource, G) + if ics_components: + template.append("#### Indice de Criticité de Substituabilité (ICS)") + template.append("") + + for component_name, info in ics_components.items(): + template.append(f"##### {info['label']}") + template.append("") + for file_path in info['files']: + template.append(f"Corpus/{file_path}") + template.append("") + + return "\n".join(template) + +def find_ivc_path(resource): + """Trouve le chemin de la fiche IVC pour un minerai donné. + + Args: + resource: Le nom du minerai (par exemple 'Cuivre', 'Antimoine') + + Returns: + Un dictionnaire contenant les informations IVC ou None si non trouvé + """ + # Normaliser le nom de la ressource en minuscules + resource_lower = resource.lower() + + # Chercher dans le répertoire Criticités avec caractères accentués + ivc_base_dir = "Criticités" + ivc_dir = CORPUS_DIR / ivc_base_dir / "Fiche technique IVC" + + if not os.path.exists(ivc_dir): + print(f"Répertoire IVC introuvable: {ivc_dir}") + return None + + # Utiliser glob pattern pour chercher les dossiers IVC contenant le nom du minerai + # Format attendu dans les dossiers: "XX-minerai-ivc-VALEUR-vulnérabilité-SEUIL" + search_pattern = os.path.join(ivc_dir, f"*{resource_lower}*ivc*") + matching_dirs = [] + + # Vérifier d'abord avec le nom exact du minerai + for item_path in glob.glob(search_pattern, recursive=False): + if os.path.isdir(item_path): + item = os.path.basename(item_path) + matching_dirs.append((item, item_path)) + + # Si aucune correspondance exacte, essayer avec une recherche plus générale + if not matching_dirs: + for item_path in glob.glob(os.path.join(ivc_dir, "*ivc*"), recursive=False): + if os.path.isdir(item_path): + item = os.path.basename(item_path) + if resource_lower in item.lower(): + matching_dirs.append((item, item_path)) + + # Traiter les correspondances trouvées + for item, item_path in matching_dirs: + try: + # Format attendu: "XX-minerai-ivc-VALEUR-vulnérabilité-SEUIL" + parts = item.split('-') + ivc_index = parts.index('ivc') + ivc_value = int(parts[ivc_index + 1]) + + # Déterminer le seuil + threshold = determine_threshold_color(ivc_value, 'IVC') + + relative_path = os.path.join(ivc_base_dir, "Fiche technique IVC", item) + return { + 'path': relative_path, + 'ivc_value': ivc_value, + 'threshold': threshold + } + except (ValueError, IndexError) as e: + print(f"Erreur lors de l'analyse du chemin IVC {item}: {e}") + continue + + print(f"Aucune fiche IVC trouvée pour {resource}") + return None + +def write_template(template, output_path): + """Écrit le template dans un fichier.""" + with open(output_path, 'w', encoding='utf-8') as f: + f.write(template) + print(f"✅ Template généré : {output_path}") + +def find_ics_path(resource): + """Trouve le chemin de la fiche ICS pour un minerai donné.""" + resource_lower = resource.lower() + # Utiliser le chemin relatif pour éviter les problèmes d'encodage + ics_dir = os.path.join(CORPUS_DIR, CRIT_PATH, "Fiche technique ICS") + + if not os.path.exists(ics_dir): + print(f"Répertoire ICS introuvable: {ics_dir}") + return None + + # Rechercher un fichier qui contient le nom du minerai + for item in os.listdir(ics_dir): + item_path = os.path.join(ics_dir, item) + # Recherche à la fois par nom exact et par inclusion dans le nom du fichier + if os.path.isfile(item_path) and (resource_lower in item.lower() or + item.lower().endswith(f"-{resource_lower}.md")): + return os.path.join(ICS_ROOT_PATH, item) + + print(f"Aucune fiche ICS trouvée pour {resource}") + return None + +def find_ics_component_paths(resource, G): + """Trouve les sous-répertoires ICS pour les relations entre composants et un minerai.""" + resource_lower = resource.lower() + ics_dir = os.path.join(CORPUS_DIR, CRIT_PATH, "Fiche technique ICS") + component_info = {} + + if not os.path.exists(ics_dir): + print(f"Répertoire ICS introuvable: {ics_dir}") + return component_info + + # Rechercher des répertoires correspondant au format attendu + for item in os.listdir(ics_dir): + item_path = os.path.join(ics_dir, item) + if os.path.isdir(item_path): + # Format attendu: XX-composant-minerai-coefficient-Y-YY + parts = item.lower().split('-') + if len(parts) >= 4 and resource_lower in parts: + # Trouver le composant en cherchant dans les parties avant 'coefficient' + component_idx = -1 + for i, part in enumerate(parts): + if part == "coefficient": + component_idx = i - 2 # Le composant est 2 segments avant "coefficient" + break + + if component_idx >= 0 and component_idx < len(parts): + component_name = parts[component_idx] + # Rechercher le label du composant dans le graphe et vérifier qu'il existe + label, exists_in_graph = get_component_label(G, component_name) + + # Ne continuer que si le composant existe dans le graphe + if exists_in_graph: + # Préparer la liste des fichiers + files = [] + + # D'abord vérifier si _intro.md existe et l'ajouter en premier + intro_file = os.path.join(item_path, "_intro.md") + if os.path.exists(intro_file): + files.append(os.path.join(ICS_ROOT_PATH, item, "_intro.md")) + + # Ensuite ajouter les fichiers importants dans l'ordre + important_files = ['faisabilité-technique', 'délai-d-implémentation', 'impact-coût'] + for prefix in important_files: + for file in sorted(os.listdir(item_path)): + if file.endswith('.md') and prefix in file.lower(): + files.append(os.path.join(ICS_ROOT_PATH, item, file)) + + # Ne pas ajouter les fichiers de sources ou autres non nécessaires + excluded_files = ["sources.md", "03-sources.md"] + for file in sorted(os.listdir(item_path)): + file_path = os.path.join(ICS_ROOT_PATH, item, file) + if (file.endswith('.md') and file_path not in files and + not any(excluded in file.lower() for excluded in excluded_files) and + file != "_intro.md"): # Ne pas ajouter _intro.md à nouveau + files.append(file_path) + + component_info[component_name] = { + 'label': label, + 'files': files + } + + return component_info + +def get_component_label(G, component_name): + """Récupère le label d'un composant à partir du graphe. + + Retourne: + Un tuple (label, exists_in_graph) où: + - label est le nom formaté du composant + - exists_in_graph est un booléen indiquant si le composant existe dans le graphe + """ + # D'abord, essayer de trouver une correspondance exacte + for node, attrs in G.nodes(data=True): + if node.lower() == component_name.lower(): + if 'label' in attrs: + return attrs['label'], True + else: + return component_name.capitalize(), True + + # Si pas de correspondance exacte, chercher un nœud qui contient le nom du composant + for node, attrs in G.nodes(data=True): + if component_name.lower() in node.lower(): + if 'label' in attrs: + return attrs['label'], True + else: + return component_name.capitalize(), True + + # Si toujours pas trouvé, le composant n'existe pas dans le graphe + return component_name.capitalize(), False + +def main(): + """Fonction principale.""" + # Analyser les arguments de la ligne de commande + parser = argparse.ArgumentParser(description='Génère un rapport de vulnérabilités') + parser.add_argument('--filter', choices=['all', 'orange', 'rouge'], default='all', + help='Niveau de criticité pour l\'ISG: "all" (orange+rouge), "orange" ou "rouge"') + args = parser.parse_args() + + # Déterminer les niveaux de filtrage + filter_levels = None + if args.filter == 'orange': + filter_levels = ['orange'] + elif args.filter == 'rouge': + filter_levels = ['rouge'] + else: # 'all' + filter_levels = ['orange', 'rouge'] + + print("=== Début de la génération du template ===") + elements = parse_graph(GRAPH_PATH, filter_level=args.filter) + print(f"Éléments extraits du graphe: {len(elements)}") + + # Analyser la stabilité géopolitique + print("\n=== Analyse de la stabilité géopolitique ===") + isg_data = analyze_geopolitical_stability(GRAPH_PATH, filter_levels=filter_levels) + + # Vérifier les données ISG + print("\n=== Résumé des données ISG ===") + found_isg_data = False + for op_key, data in isg_data.items(): + if data['pays_critiques'] or data['acteurs_critiques']: + found_isg_data = True + print(f"Opération {op_key}:") + if data['pays_critiques']: + print(f" - {len(data['pays_critiques'])} pays critiques") + for pays in data['pays_critiques']: + print(f" * {pays['nom']} (ISG: {pays['isg']}, part: {pays['part']}%)") + if data['acteurs_critiques']: + print(f" - {len(data['acteurs_critiques'])} acteurs critiques") + for acteur in data['acteurs_critiques']: + print(f" * {acteur['nom']} ({acteur['pays']}, ISG: {acteur['isg']}, part: {acteur['part']}%)") + + if not found_isg_data: + print("Aucune donnée ISG critique trouvée.") + + # Charger le graphe pour obtenir les labels des composants + G = read_dot(GRAPH_PATH) + + # Générer le template avec les données ISG et le graphe + print("\n=== Génération du template ===") + template = generate_template(elements, isg_data, G) + write_template(template, OUTPUT_PATH) + print("=== Fin de la génération du template ===") + +if __name__ == "__main__": + main() diff --git a/IA/01 - corpus_rapport_factuel/replace_paths.py b/IA/01 - corpus_rapport_factuel/replace_paths.py new file mode 100644 index 0000000..cc3bfb2 --- /dev/null +++ b/IA/01 - corpus_rapport_factuel/replace_paths.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script pour remplacer les références de chemins dans le rapport par le contenu des fichiers. +Ajuste automatiquement les niveaux de titres pour maintenir la hiérarchie. +""" + +import os +import re +from pathlib import Path + +# Chemins de base +BASE_DIR = Path(__file__).resolve().parent +BASE_DIR = BASE_DIR / ".." +CORPUS_DIR = BASE_DIR / "Corpus" +INPUT_PATH = CORPUS_DIR / "rapport_template.md" +OUTPUT_PATH = CORPUS_DIR / "rapport_final.md" + +def determine_heading_level(line): + """Détermine le niveau de titre d'une ligne.""" + match = re.match(r'^(#+)\s+', line) + if match: + return len(match.group(1)) + return 0 + +def determine_parent_level(lines, current_index): + """Détermine le niveau de titre parent pour une ligne donnée.""" + # Remonter dans les lignes précédentes pour trouver le titre parent + for i in range(current_index - 1, -1, -1): + level = determine_heading_level(lines[i]) + if level > 0: + return level + return 0 + +def adjust_heading_levels(content, parent_level, is_intro_file=False, is_ivc_section=False): + """Ajuste les niveaux de titres dans le contenu pour s'adapter à la hiérarchie.""" + lines = content.split('\n') + + # Si le contenu est vide, retourner une chaîne vide + if not lines: + return "" + + # Déterminer le niveau minimum de titre dans le contenu original + min_level = 10 + for line in lines: + level = determine_heading_level(line) + if level > 0 and level < min_level: + min_level = level + + # Si aucun titre trouvé, simplement supprimer la première ligne si nécessaire + if min_level == 10: + if not is_intro_file and not is_ivc_section and lines: + return '\n'.join(lines[1:]) + return content + + # Traitement spécial pour les fichiers IVC et intro + if is_ivc_section or is_intro_file: + adjusted_lines = [] + # Pour les fichiers IVC ou intro, on garde toutes les lignes mais on ajuste les niveaux des titres + for line in lines: + level = determine_heading_level(line) + if level > 0: + # Nouveau niveau = niveau parent + 1 + (niveau actuel - min_level) + new_level = parent_level + 1 + (level - min_level) + # S'assurer que le niveau ne dépasse pas 6 (limite en markdown) + new_level = min(new_level, 6) + line = re.sub(r'^#+\s+', '#' * new_level + ' ', line) + adjusted_lines.append(line) + else: + # Pour les fichiers standards, on supprime la première ligne + lines = lines[1:] + adjusted_lines = [] + # Ajuster les niveaux de titres pour les lignes restantes + for line in lines: + level = determine_heading_level(line) + if level > 0: + # Nouveau niveau = niveau parent + 1 + (niveau actuel - min_level) + new_level = parent_level + 1 + (level - min_level) + # S'assurer que le niveau ne dépasse pas 6 (limite en markdown) + new_level = min(new_level, 6) + line = re.sub(r'^#+\s+', '#' * new_level + ' ', line) + adjusted_lines.append(line) + + return '\n'.join(adjusted_lines) + +def process_report(): + """Traite le rapport pour remplacer les chemins par le contenu.""" + if not os.path.exists(INPUT_PATH): + print(f"Fichier d'entrée introuvable: {INPUT_PATH}") + return + + # Lire le rapport + with open(INPUT_PATH, 'r', encoding='utf-8') as f: + lines = f.readlines() + + output_lines = [] + i = 0 + while i < len(lines): + line = lines[i].strip() + + # Vérifier si la ligne est un chemin + if line.startswith('Corpus/'): + path = line + full_path = BASE_DIR / path + + # Déterminer le niveau de titre parent + parent_level = determine_parent_level(lines, i) + + try: + # Lire le contenu du fichier + if os.path.exists(full_path): + # Vérifier si c'est un fichier _intro.md + is_intro_file = os.path.basename(full_path) == "_intro.md" + + # Vérifier si c'est une section d'Indice de Vulnérabilité de Concurrence + is_ivc_section = "Vulnérabilité de Concurrence" in line or "/ivc-" in path.lower() or "/fiche technique ivc/" in path.lower() + + with open(full_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Ajuster les niveaux de titres + adjusted_content = adjust_heading_levels(content, parent_level, is_intro_file, is_ivc_section) + + # Ajouter le contenu ajusté + output_lines.append(f"") + output_lines.append(adjusted_content) + output_lines.append(f"") + else: + output_lines.append(f"") + output_lines.append(line) + except Exception as e: + output_lines.append(f"") + output_lines.append(line) + else: + # Conserver la ligne telle quelle + output_lines.append(line) + + i += 1 + + # Écrire le rapport final + with open(OUTPUT_PATH, 'w', encoding='utf-8') as f: + f.write('\n'.join(output_lines)) + + print(f"Rapport final généré: {OUTPUT_PATH}") + +if __name__ == "__main__": + process_report() diff --git a/IA/02 - injection_fiches/README.md b/IA/02 - injection_fiches/README.md new file mode 100644 index 0000000..3779639 --- /dev/null +++ b/IA/02 - injection_fiches/README.md @@ -0,0 +1,90 @@ +# Script d'Injection Automatique pour PrivateGPT + +Ce script permet d'automatiser l'injection de documents dans PrivateGPT à partir d'un répertoire local. Au lieu d'utiliser l'interface utilisateur pour télécharger les fichiers un par un, vous pouvez injecter un dossier entier en une seule commande. + +## Prérequis + +- Python 3.7 ou supérieur +- PrivateGPT installé et fonctionnel sous Docker +- Accès à l'API REST de PrivateGPT (port 8001 par défaut) + +## Installation + +1. Clonez ce dépôt ou téléchargez les fichiers dans un dossier + +2. Installez les dépendances nécessaires : +```bash +pip install requests +``` + +## Utilisation + +Le script s'utilise en ligne de commande avec différentes options : + +```bash +python auto_ingest.py -d REPERTOIRE [-u URL] [-r] [-t THREADS] [--retry RETRY] [--retry-delay RETRY_DELAY] [--timeout TIMEOUT] [--extensions EXT1 EXT2 ...] +``` + +### Options + +- `-d, --directory` : Chemin du répertoire contenant les fichiers à injecter (obligatoire) +- `-u, --url` : URL de l'API PrivateGPT (défaut: http://localhost:8001) +- `-r, --recursive` : Parcourir récursivement les sous-répertoires +- `-t, --threads` : Nombre de threads pour les injections parallèles (défaut: 5) +- `--retry` : Nombre de tentatives en cas d'échec (défaut: 3) +- `--retry-delay` : Délai entre les tentatives en secondes (défaut: 5) +- `--timeout` : Délai d'attente pour chaque requête en secondes (défaut: 300) +- `--extensions` : Liste d'extensions spécifiques à injecter (ex: pdf txt) + +## Exemples d'utilisation + +### Injection simple d'un répertoire + +```bash +python auto_ingest.py -d /chemin/vers/documents +``` + +### Injection récursive avec extensions spécifiques + +```bash +python auto_ingest.py -d /chemin/vers/documents -r --extensions pdf docx txt +``` + +### Injection avec paramètres avancés + +```bash +python auto_ingest.py -d /chemin/vers/documents -r -t 10 --timeout 600 --retry 5 +``` + +## Formats de fichiers supportés + +Par défaut, le script reconnaît et traite les formats suivants : +- PDF (.pdf) +- Documents texte (.txt, .md) +- Documents Microsoft Office (.doc, .docx, .ppt, .pptx, .xls, .xlsx) +- CSV (.csv) +- EPUB (.epub) +- HTML (.html, .htm) + +## Résolution des problèmes + +### Erreur de connexion + +Si vous obtenez des erreurs de connexion, vérifiez que : +1. PrivateGPT est bien en cours d'exécution +2. L'URL est correcte (par défaut: http://localhost:8001) +3. Le port 8001 est accessible et n'est pas bloqué par un pare-feu + +### Erreurs d'injection + +- Si un fichier spécifique ne peut pas être injecté, vérifiez qu'il est d'un format supporté par PrivateGPT +- Pour les fichiers volumineux, vous pouvez augmenter la valeur de `--timeout` +- En cas d'erreurs répétées, augmentez les valeurs de `--retry` et `--retry-delay` + +## Logs + +Le script génère des logs dans : +- La console (stdout) +- Un fichier pgpt_auto_ingest.log dans le répertoire courant + +Ces logs contiennent des informations détaillées sur le processus d'injection. \ No newline at end of file diff --git a/IA/02 - injection_fiches/auto_ingest.py b/IA/02 - injection_fiches/auto_ingest.py new file mode 100755 index 0000000..be0abac --- /dev/null +++ b/IA/02 - injection_fiches/auto_ingest.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script d'injection automatique de documents pour PrivateGPT + +Ce script parcourt un répertoire spécifié et injecte tous les fichiers +compatibles dans PrivateGPT via son API REST. +""" + +import os +import sys +import time +import argparse +import logging +import requests +from pathlib import Path +from typing import List, Dict, Tuple, Set +from concurrent.futures import ThreadPoolExecutor, as_completed + +# Configuration du logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler("pgpt_auto_ingest.log") + ] +) +logger = logging.getLogger(__name__) + +# Extensions de fichiers couramment supportées par PrivateGPT +SUPPORTED_EXTENSIONS = { + '.pdf', '.txt', '.md', '.doc', '.docx', '.ppt', '.pptx', + '.xls', '.xlsx', '.csv', '.epub', '.html', '.htm' +} + +def parse_arguments(): + """Parse les arguments de ligne de commande.""" + parser = argparse.ArgumentParser( + description="Injecte automatiquement tous les fichiers d'un répertoire dans PrivateGPT" + ) + parser.add_argument( + "-d", "--directory", + type=str, + required=True, + help="Chemin du répertoire contenant les fichiers à injecter" + ) + parser.add_argument( + "-u", "--url", + type=str, + default="http://localhost:8001", + help="URL de l'API PrivateGPT (défaut: http://localhost:8001)" + ) + parser.add_argument( + "-r", "--recursive", + action="store_true", + help="Parcourir récursivement les sous-répertoires" + ) + parser.add_argument( + "-t", "--threads", + type=int, + default=5, + help="Nombre de threads pour les injections parallèles (défaut: 5)" + ) + parser.add_argument( + "--retry", + type=int, + default=3, + help="Nombre de tentatives en cas d'échec (défaut: 3)" + ) + parser.add_argument( + "--retry-delay", + type=int, + default=5, + help="Délai entre les tentatives en secondes (défaut: 5)" + ) + parser.add_argument( + "--timeout", + type=int, + default=300, + help="Délai d'attente pour chaque requête en secondes (défaut: 300)" + ) + parser.add_argument( + "--extensions", + nargs="+", + help="Liste d'extensions spécifiques à injecter (ex: .pdf .txt)" + ) + + return parser.parse_args() + +def find_files(directory: str, recursive: bool = False, + extensions: Set[str] = SUPPORTED_EXTENSIONS) -> List[Path]: + """ + Trouve tous les fichiers avec les extensions spécifiées dans le répertoire. + + Args: + directory: Répertoire à scanner + recursive: Si True, parcourt aussi les sous-répertoires + extensions: Ensemble d'extensions de fichier à inclure + + Returns: + Liste des chemins de fichiers trouvés + """ + directory_path = Path(directory) + + if not directory_path.exists() or not directory_path.is_dir(): + logger.error(f"Le répertoire {directory} n'existe pas ou n'est pas un répertoire.") + return [] + + files = [] + + if recursive: + # Parcours récursif + for root, _, filenames in os.walk(directory): + for filename in filenames: + file_path = Path(root) / filename + if file_path.suffix.lower() in extensions: + files.append(file_path) + else: + # Parcours non récursif + for file_path in directory_path.iterdir(): + if file_path.is_file() and file_path.suffix.lower() in extensions: + files.append(file_path) + + return files + +def ingest_file(file_path: Path, pgpt_url: str, timeout: int, + retry_count: int, retry_delay: int) -> Tuple[Path, bool, str]: + """ + Injecte un fichier dans PrivateGPT. + + Args: + file_path: Chemin du fichier à injecter + pgpt_url: URL de base de l'API PrivateGPT + timeout: Délai d'attente pour la requête + retry_count: Nombre de tentatives en cas d'échec + retry_delay: Délai entre les tentatives en secondes + + Returns: + Tuple contenant (chemin_fichier, succès, message) + """ + ingest_url = f"{pgpt_url}/v1/ingest/file" + + for attempt in range(retry_count): + try: + logger.info(f"Injection de {file_path} (tentative {attempt + 1}/{retry_count})") + + with open(file_path, 'rb') as file: + files = {'file': (file_path.name, file, 'application/octet-stream')} + response = requests.post(ingest_url, files=files, timeout=timeout) + + if response.status_code == 200: + result = response.json() + doc_ids = result.get('document_ids', []) + logger.info(f"Succès! {file_path} -> {len(doc_ids)} documents créés") + return file_path, True, f"{len(doc_ids)} documents créés" + else: + error_msg = f"Erreur HTTP {response.status_code}: {response.text}" + logger.warning(error_msg) + if attempt < retry_count - 1: + logger.info(f"Nouvelle tentative dans {retry_delay} secondes...") + time.sleep(retry_delay) + else: + return file_path, False, error_msg + + except Exception as e: + error_msg = f"Exception: {str(e)}" + logger.warning(error_msg) + if attempt < retry_count - 1: + logger.info(f"Nouvelle tentative dans {retry_delay} secondes...") + time.sleep(retry_delay) + else: + return file_path, False, error_msg + + return file_path, False, "Nombre maximum de tentatives atteint" + +def main(): + """Fonction principale.""" + args = parse_arguments() + + # Préparation des extensions si spécifiées + extensions = set(args.extensions) if args.extensions else SUPPORTED_EXTENSIONS + # Assurer que les extensions commencent par un point + extensions = {ext if ext.startswith('.') else f'.{ext}' for ext in extensions} + + logger.info(f"Démarrage de l'injection automatique depuis {args.directory}") + logger.info(f"URL PrivateGPT: {args.url}") + logger.info(f"Mode récursif: {args.recursive}") + logger.info(f"Extensions: {', '.join(extensions)}") + + # Trouver les fichiers + files = find_files(args.directory, args.recursive, extensions) + total_files = len(files) + + if total_files == 0: + logger.warning(f"Aucun fichier trouvé avec les extensions {', '.join(extensions)} dans {args.directory}") + return + + logger.info(f"Trouvé {total_files} fichiers à injecter") + + # Statistiques + successful = 0 + failed = 0 + failed_files = [] + + # Injection des fichiers en parallèle + with ThreadPoolExecutor(max_workers=args.threads) as executor: + futures = { + executor.submit( + ingest_file, + file_path, + args.url, + args.timeout, + args.retry, + args.retry_delay + ): file_path for file_path in files + } + + for future in as_completed(futures): + file_path, success, message = future.result() + if success: + successful += 1 + else: + failed += 1 + failed_files.append((file_path, message)) + + # Afficher la progression + progress = (successful + failed) / total_files * 100 + logger.info(f"Progression: {progress:.1f}% ({successful + failed}/{total_files})") + + # Rapport final + logger.info("="*50) + logger.info("RAPPORT D'INJECTION") + logger.info("="*50) + logger.info(f"Total des fichiers: {total_files}") + logger.info(f"Succès: {successful}") + logger.info(f"Échecs: {failed}") + + if failed > 0: + logger.info("\nDétails des échecs:") + for file_path, message in failed_files: + logger.info(f"- {file_path}: {message}") + + logger.info("="*50) + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + logger.info("\nInterruption par l'utilisateur. Arrêt du processus.") + sys.exit(1) + except Exception as e: + logger.error(f"Erreur non gérée: {str(e)}", exc_info=True) + sys.exit(1) diff --git a/IA/02 - injection_fiches/auto_ingest.sh b/IA/02 - injection_fiches/auto_ingest.sh new file mode 100755 index 0000000..8355e1c --- /dev/null +++ b/IA/02 - injection_fiches/auto_ingest.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Script d'exécution pour auto_ingest.py + +# Vérification des dépendances +check_dependencies() { + # Vérifier Python + if ! command -v python3 &> /dev/null; then + echo "Erreur: Python 3 n'est pas installé ou n'est pas dans le PATH" + exit 1 + fi + + # Vérifier pip et requests + if ! python3 -c "import requests" &> /dev/null; then + echo "Installation de la bibliothèque requests..." + pip3 install requests + if [ $? -ne 0 ]; then + echo "Erreur: Impossible d'installer la bibliothèque requests" + echo "Exécutez manuellement: pip3 install requests" + exit 1 + fi + fi +} + +# Fonction d'aide +show_help() { + echo "Script d'injection automatique pour PrivateGPT" + echo "" + echo "Usage: $0 [options] -d RÉPERTOIRE" + echo "" + echo "Options:" + echo " -d, --directory RÉPERTOIRE Répertoire contenant les fichiers à injecter (obligatoire)" + echo " -u, --url URL URL de l'API PrivateGPT (défaut: http://localhost:8001)" + echo " -r, --recursive Parcourir récursivement les sous-répertoires" + echo " -t, --threads N Nombre de threads pour les injections parallèles (défaut: 5)" + echo " --retry N Nombre de tentatives en cas d'échec (défaut: 3)" + echo " --retry-delay N Délai entre les tentatives en secondes (défaut: 5)" + echo " --timeout N Délai d'attente pour chaque requête en secondes (défaut: 300)" + echo " --extensions EXT1 EXT2 ... Liste d'extensions spécifiques à injecter" + echo " -h, --help Afficher cette aide" + echo "" + echo "Exemple: $0 -d /documents -r --extensions pdf docx" +} + +# Vérifier si aucun argument n'est fourni +if [ $# -eq 0 ]; then + show_help + exit 1 +fi + +# Vérifier l'argument d'aide +for arg in "$@"; do + if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then + show_help + exit 0 + fi +done + +# Vérifier la présence de l'argument obligatoire (-d ou --directory) +directory_specified=false +for ((i=1; i<=$#; i++)); do + if [ "${!i}" = "-d" ] || [ "${!i}" = "--directory" ]; then + directory_specified=true + break + fi +done + +if [ "$directory_specified" = false ]; then + echo "Erreur: L'option -d/--directory est obligatoire" + echo "Utilisez -h ou --help pour afficher l'aide" + exit 1 +fi + +# Vérifier les dépendances +check_dependencies + +# Chemin au script Python +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON_SCRIPT="${SCRIPT_DIR}/auto_ingest.py" + +# Vérifier que le script Python existe +if [ ! -f "$PYTHON_SCRIPT" ]; then + echo "Erreur: Le script auto_ingest.py n'existe pas dans $SCRIPT_DIR" + exit 1 +fi + +# Rendre le script Python exécutable +chmod +x "$PYTHON_SCRIPT" + +# Exécuter le script Python avec tous les arguments +python3 "$PYTHON_SCRIPT" "$@" \ No newline at end of file diff --git a/IA/02 - injection_fiches/docker-compose.yml.example b/IA/02 - injection_fiches/docker-compose.yml.example new file mode 100644 index 0000000..7c570d9 --- /dev/null +++ b/IA/02 - injection_fiches/docker-compose.yml.example @@ -0,0 +1,40 @@ +version: '3.8' + +services: + privategpt: + image: ghcr.io/zylon-ai/private-gpt:latest + container_name: privategpt + ports: + - "8001:8001" + environment: + - PGPT_PROFILES=local + # Décommentez et modifiez ces variables si vous voulez utiliser un modèle différent + # - PGPT_SETTINGS_LLMS_DEFAULT__MODEL=/models/custom-model.gguf + # - PGPT_SETTINGS_EMBEDDING_DEFAULT__MODEL=/models/custom-embedding-model + volumes: + # Volume persistant pour les données + - privategpt-data:/app/local_data + # Montage du répertoire d'auto-injection + - ./documents_to_ingest:/app/documents_to_ingest + # Montage des modèles personnalisés (décommentez si nécessaire) + # - ./custom_models:/app/models + restart: unless-stopped + # Décommentez ces lignes si vous avez un GPU NVIDIA + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + +volumes: + privategpt-data: + name: privategpt-data + +# Instructions d'utilisation: +# 1. Copiez ce fichier sous le nom "docker-compose.yml" +# 2. Créez un répertoire "documents_to_ingest" à côté du fichier docker-compose.yml +# 3. Placez vos documents à injecter dans ce répertoire +# 4. Lancez avec la commande: docker-compose up -d +# 5. Utilisez le script d'injection: ./auto_ingest.sh -d documents_to_ingest -u http://localhost:8001 \ No newline at end of file diff --git a/IA/02 - injection_fiches/nettoyer_pgpt.py b/IA/02 - injection_fiches/nettoyer_pgpt.py new file mode 100644 index 0000000..3487b62 --- /dev/null +++ b/IA/02 - injection_fiches/nettoyer_pgpt.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +""" +Script de nettoyage pour PrivateGPT + +Ce script permet de lister et supprimer les documents ingérés dans PrivateGPT. +Options: +- Lister tous les documents +- Supprimer des documents par préfixe (ex: "temp_section_") +- Supprimer des documents par motif +- Supprimer tous les documents + +Usage: + python nettoyer_pgpt.py --list + python nettoyer_pgpt.py --delete-prefix "temp_section_" + python nettoyer_pgpt.py --delete-pattern "rapport_.*\.md" + python nettoyer_pgpt.py --delete-all +""" + +import argparse +import json +import re +import requests +import sys +import time +import uuid +from typing import List, Dict, Any, Optional + +# Configuration de l'API PrivateGPT +PGPT_URL = "http://127.0.0.1:8001" +API_URL = f"{PGPT_URL}/v1" + + +def check_api_availability() -> bool: + """Vérifie si l'API PrivateGPT est disponible""" + try: + response = requests.get(f"{PGPT_URL}/health") + if response.status_code == 200: + print("✅ API PrivateGPT disponible") + return True + else: + print(f"❌ L'API PrivateGPT a retourné le code d'état {response.status_code}") + return False + except requests.RequestException as e: + print(f"❌ Erreur de connexion à l'API PrivateGPT: {e}") + return False + + +def list_documents() -> List[Dict[str, Any]]: + """Liste tous les documents ingérés et renvoie la liste des métadonnées""" + try: + # Récupérer la liste des documents + response = requests.get(f"{API_URL}/ingest/list") + response.raise_for_status() + data = response.json() + + # Format de réponse OpenAI + if "data" in data: + documents = data.get("data", []) + # Format alternatif + else: + documents = data.get("documents", []) + + # Construire une liste normalisée des documents + normalized_docs = [] + for doc in documents: + doc_id = doc.get("doc_id") or doc.get("id") + metadata = doc.get("doc_metadata", {}) + filename = metadata.get("file_name") or metadata.get("filename", "Inconnu") + + normalized_docs.append({ + "id": doc_id, + "filename": filename, + "metadata": metadata + }) + + return normalized_docs + + except Exception as e: + print(f"❌ Erreur lors de la récupération des documents: {e}") + return [] + + +def print_documents(documents: List[Dict[str, Any]]) -> None: + """Affiche la liste des documents de façon lisible""" + if not documents: + print("📋 Aucun document trouvé dans PrivateGPT") + return + + print(f"📋 {len(documents)} documents trouvés dans PrivateGPT:") + + # Regrouper par nom de fichier pour un affichage plus compact + files_grouped = {} + for doc in documents: + filename = doc["filename"] + if filename not in files_grouped: + files_grouped[filename] = [] + files_grouped[filename].append(doc["id"]) + + # Afficher les résultats groupés + for i, (filename, ids) in enumerate(files_grouped.items(), 1): + print(f"{i}. {filename} ({len(ids)} chunks)") + if args.verbose: + for j, doc_id in enumerate(ids, 1): + print(f" {j}. ID: {doc_id}") + + +def delete_document(doc_id: str) -> bool: + """Supprime un document par son ID""" + try: + response = requests.delete(f"{API_URL}/ingest/{doc_id}") + if response.status_code == 200: + return True + else: + print(f"⚠️ Échec de la suppression de l'ID {doc_id}: Code {response.status_code}") + return False + except Exception as e: + print(f"❌ Erreur lors de la suppression de l'ID {doc_id}: {e}") + return False + + +def delete_documents_by_criteria(documents: List[Dict[str, Any]], + prefix: Optional[str] = None, + pattern: Optional[str] = None, + delete_all: bool = False) -> int: + """ + Supprime des documents selon différents critères + Retourne le nombre de documents supprimés + """ + if not documents: + print("❌ Aucun document à supprimer") + return 0 + + if not (prefix or pattern or delete_all): + print("❌ Aucun critère de suppression spécifié") + return 0 + + # Comptage des suppressions réussies + success_count = 0 + + # Filtrer les documents à supprimer + docs_to_delete = [] + + if delete_all: + docs_to_delete = documents + print(f"🗑️ Suppression de tous les documents ({len(documents)} chunks)...") + elif prefix: + docs_to_delete = [doc for doc in documents if doc["filename"].startswith(prefix)] + print(f"🗑️ Suppression des documents dont le nom commence par '{prefix}' ({len(docs_to_delete)} chunks)...") + elif pattern: + try: + regex = re.compile(pattern) + docs_to_delete = [doc for doc in documents if regex.search(doc["filename"])] + print(f"🗑️ Suppression des documents correspondant au motif '{pattern}' ({len(docs_to_delete)} chunks)...") + except re.error as e: + print(f"❌ Expression régulière invalide: {e}") + return 0 + + # Demander confirmation si beaucoup de documents + if len(docs_to_delete) > 5 and not args.force: + confirm = input(f"⚠️ Vous êtes sur le point de supprimer {len(docs_to_delete)} chunks. Confirmer ? (o/N) ") + if confirm.lower() != 'o': + print("❌ Opération annulée") + return 0 + + # Supprimer les documents + for doc in docs_to_delete: + if delete_document(doc["id"]): + success_count += 1 + if args.verbose: + print(f"✅ Document supprimé: {doc['filename']} (ID: {doc['id']})") + + # Petite pause pour éviter de surcharger l'API + time.sleep(0.1) + + print(f"✅ {success_count}/{len(docs_to_delete)} documents supprimés avec succès") + return success_count + + +def generate_unique_prefix() -> str: + """Génère un préfixe unique basé sur un UUID pour différencier les fichiers temporaires""" + unique_id = str(uuid.uuid4())[:8] # Prendre les 8 premiers caractères de l'UUID + return f"temp_{unique_id}_" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Utilitaire de nettoyage pour PrivateGPT") + + # Options principales + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--list", action="store_true", help="Lister tous les documents ingérés") + group.add_argument("--delete-prefix", type=str, help="Supprimer les documents dont le nom commence par PREFIX") + group.add_argument("--delete-pattern", type=str, help="Supprimer les documents dont le nom correspond au motif PATTERN (regex)") + group.add_argument("--delete-all", action="store_true", help="Supprimer tous les documents (⚠️ DANGER)") + group.add_argument("--generate-prefix", action="store_true", help="Générer un préfixe unique pour les fichiers temporaires") + + # Options additionnelles + parser.add_argument("--force", action="store_true", help="Ne pas demander de confirmation") + parser.add_argument("--verbose", action="store_true", help="Afficher plus de détails") + + args = parser.parse_args() + + # Vérifier la disponibilité de l'API + if not check_api_availability(): + sys.exit(1) + + # Générer un préfixe unique + if args.generate_prefix: + unique_prefix = generate_unique_prefix() + print(f"🔑 Préfixe unique généré: {unique_prefix}") + print(f"Utilisez ce préfixe pour les fichiers temporaires de votre script.") + sys.exit(0) + + # Récupérer la liste des documents + documents = list_documents() + + # Traiter selon l'option choisie + if args.list: + print_documents(documents) + elif args.delete_prefix: + delete_documents_by_criteria(documents, prefix=args.delete_prefix) + elif args.delete_pattern: + delete_documents_by_criteria(documents, pattern=args.delete_pattern) + elif args.delete_all: + delete_documents_by_criteria(documents, delete_all=True) \ No newline at end of file diff --git a/IA/02 - injection_fiches/watch_directory.py b/IA/02 - injection_fiches/watch_directory.py new file mode 100755 index 0000000..860139d --- /dev/null +++ b/IA/02 - injection_fiches/watch_directory.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script de surveillance de répertoire pour l'injection automatique dans PrivateGPT + +Ce script surveille un répertoire et injecte automatiquement les nouveaux fichiers +dans PrivateGPT dès qu'ils sont ajoutés ou modifiés. +""" + +import os +import sys +import time +import logging +import argparse +import subprocess +from pathlib import Path +from typing import Set, Dict, List +from datetime import datetime + +try: + from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler, FileCreatedEvent, FileModifiedEvent +except ImportError: + print("Bibliothèque 'watchdog' non installée. Installation en cours...") + subprocess.run([sys.executable, "-m", "pip", "install", "watchdog"]) + from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler, FileCreatedEvent, FileModifiedEvent + +# Configuration du logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler("pgpt_watch_directory.log") + ] +) +logger = logging.getLogger(__name__) + +# Extensions de fichiers couramment supportées par PrivateGPT +SUPPORTED_EXTENSIONS = { + '.pdf', '.txt', '.md', '.doc', '.docx', '.ppt', '.pptx', + '.xls', '.xlsx', '.csv', '.epub', '.html', '.htm' +} + +class DocumentHandler(FileSystemEventHandler): + """Gestionnaire d'événements pour les fichiers de documents.""" + + def __init__(self, watch_dir: str, ingest_script: str, pgpt_url: str, + extensions: Set[str], delay: int = 5): + """ + Initialise le gestionnaire d'événements. + + Args: + watch_dir: Répertoire à surveiller + ingest_script: Chemin vers le script d'injection + pgpt_url: URL de l'API PrivateGPT + extensions: Extensions de fichiers à traiter + delay: Délai en secondes à attendre avant le traitement (évite de traiter des fichiers partiellement écrits) + """ + self.watch_dir = os.path.abspath(watch_dir) + self.ingest_script = os.path.abspath(ingest_script) + self.pgpt_url = pgpt_url + self.extensions = extensions + self.delay = delay + + # Queue pour les fichiers en attente de traitement + self.pending_files: Dict[str, float] = {} + + # Vérifier que le script d'injection existe + if not os.path.exists(self.ingest_script): + logger.error(f"Le script d'injection {self.ingest_script} n'existe pas!") + raise FileNotFoundError(f"Script d'injection introuvable: {self.ingest_script}") + + def on_created(self, event): + """Appelé lorsqu'un fichier est créé.""" + if not event.is_directory: + self._handle_file_event(event) + + def on_modified(self, event): + """Appelé lorsqu'un fichier est modifié.""" + if not event.is_directory: + self._handle_file_event(event) + + def _handle_file_event(self, event): + """Traite un événement de fichier (création ou modification).""" + file_path = event.src_path + file_ext = os.path.splitext(file_path)[1].lower() + + # Ignorer les fichiers non supportés + if file_ext not in self.extensions: + return + + # Ignorer les fichiers temporaires et cachés + file_name = os.path.basename(file_path) + if file_name.startswith('.') or file_name.startswith('~') or file_name.endswith('.tmp'): + return + + # Ajouter à la queue avec l'horodatage actuel + self.pending_files[file_path] = time.time() + logger.info(f"Fichier détecté: {file_path} (en attente pendant {self.delay} secondes)") + + def process_pending_files(self): + """Traite les fichiers en attente qui ont dépassé le délai d'attente.""" + current_time = time.time() + files_to_process: List[str] = [] + + # Identifier les fichiers prêts à être traités + for file_path, timestamp in list(self.pending_files.items()): + if current_time - timestamp >= self.delay: + if os.path.exists(file_path): # Vérifier que le fichier existe toujours + files_to_process.append(file_path) + self.pending_files.pop(file_path) + + # Traiter les fichiers par lot + if files_to_process: + self._ingest_files(files_to_process) + + def _ingest_files(self, files: List[str]): + """ + Injecte une liste de fichiers en utilisant le script d'injection. + + Args: + files: Liste des chemins de fichiers à injecter + """ + try: + # Créer un répertoire temporaire pour stocker la liste des fichiers + temp_dir = os.path.join(os.path.dirname(self.ingest_script), "temp") + os.makedirs(temp_dir, exist_ok=True) + + # Créer un fichier de liste + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + list_file = os.path.join(temp_dir, f"files_to_ingest_{timestamp}.txt") + + with open(list_file, "w") as f: + for file_path in files: + f.write(f"{file_path}\n") + + # Construire la commande pour le script d'injection + for file_path in files: + file_dir = os.path.dirname(file_path) + logger.info(f"Injection de {file_path}...") + + cmd = [ + sys.executable, + self.ingest_script, + "-d", file_dir, + "-u", self.pgpt_url, + "--extensions", os.path.splitext(file_path)[1][1:] # Extension sans le point + ] + + # Exécuter la commande + process = subprocess.run( + cmd, + capture_output=True, + text=True + ) + + if process.returncode == 0: + logger.info(f"Injection réussie de {file_path}") + else: + logger.error(f"Échec de l'injection de {file_path}: {process.stderr}") + + except Exception as e: + logger.error(f"Erreur lors de l'injection des fichiers: {str(e)}") + +def parse_arguments(): + """Parse les arguments de ligne de commande.""" + parser = argparse.ArgumentParser( + description="Surveille un répertoire et injecte automatiquement les nouveaux fichiers dans PrivateGPT" + ) + parser.add_argument( + "-d", "--directory", + type=str, + required=True, + help="Chemin du répertoire à surveiller" + ) + parser.add_argument( + "-s", "--script", + type=str, + default=None, + help="Chemin vers le script auto_ingest.py (par défaut: détection automatique)" + ) + parser.add_argument( + "-u", "--url", + type=str, + default="http://localhost:8001", + help="URL de l'API PrivateGPT (défaut: http://localhost:8001)" + ) + parser.add_argument( + "--delay", + type=int, + default=5, + help="Délai en secondes avant de traiter un nouveau fichier (défaut: 5)" + ) + parser.add_argument( + "--extensions", + nargs="+", + help="Liste d'extensions spécifiques à surveiller (ex: pdf txt)" + ) + + return parser.parse_args() + +def main(): + """Fonction principale.""" + args = parse_arguments() + + # Préparation des extensions si spécifiées + extensions = set(args.extensions) if args.extensions else SUPPORTED_EXTENSIONS + # Assurer que les extensions commencent par un point + extensions = {ext if ext.startswith('.') else f'.{ext}' for ext in extensions} + + # Déterminer le chemin du script d'injection + if args.script: + ingest_script = args.script + else: + # Utiliser le script auto_ingest.py dans le même répertoire que ce script + ingest_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "auto_ingest.py") + + # Créer le répertoire de surveillance s'il n'existe pas + watch_dir = os.path.abspath(args.directory) + if not os.path.exists(watch_dir): + logger.info(f"Création du répertoire de surveillance: {watch_dir}") + os.makedirs(watch_dir, exist_ok=True) + + logger.info(f"Démarrage de la surveillance de {watch_dir}") + logger.info(f"URL PrivateGPT: {args.url}") + logger.info(f"Extensions surveillées: {', '.join(extensions)}") + logger.info(f"Délai de traitement: {args.delay} secondes") + + # Initialiser le gestionnaire et l'observateur + event_handler = DocumentHandler( + watch_dir=watch_dir, + ingest_script=ingest_script, + pgpt_url=args.url, + extensions=extensions, + delay=args.delay + ) + + observer = Observer() + observer.schedule(event_handler, path=watch_dir, recursive=True) + observer.start() + + try: + logger.info("Surveillance en cours... (Ctrl+C pour quitter)") + + while True: + # Traiter les fichiers en attente + event_handler.process_pending_files() + time.sleep(1) + + except KeyboardInterrupt: + logger.info("\nInterruption par l'utilisateur. Arrêt de la surveillance.") + observer.stop() + + observer.join() + +if __name__ == "__main__": + try: + main() + except Exception as e: + logger.error(f"Erreur non gérée: {str(e)}", exc_info=True) + sys.exit(1) \ No newline at end of file diff --git a/IA/02 - injection_fiches/watch_directory.sh b/IA/02 - injection_fiches/watch_directory.sh new file mode 100755 index 0000000..15c12d6 --- /dev/null +++ b/IA/02 - injection_fiches/watch_directory.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Script d'exécution pour watch_directory.py + +# Vérification des dépendances +check_dependencies() { + # Vérifier Python + if ! command -v python3 &> /dev/null; then + echo "Erreur: Python 3 n'est pas installé ou n'est pas dans le PATH" + exit 1 + fi + + # Vérifier les bibliothèques Python requises + python3 -c " +try: + import watchdog +except ImportError: + print('Installation de la bibliothèque watchdog...') + import subprocess, sys + subprocess.run([sys.executable, '-m', 'pip', 'install', 'watchdog']) +" || { + echo "Erreur: Impossible d'installer les dépendances" + exit 1 + } +} + +# Fonction d'aide +show_help() { + echo "Script de surveillance de répertoire pour PrivateGPT" + echo "" + echo "Usage: $0 [options] -d RÉPERTOIRE" + echo "" + echo "Options:" + echo " -d, --directory RÉPERTOIRE Répertoire à surveiller (obligatoire)" + echo " -s, --script CHEMIN Chemin vers le script auto_ingest.py (facultatif)" + echo " -u, --url URL URL de l'API PrivateGPT (défaut: http://localhost:8001)" + echo " --delay N Délai en secondes avant de traiter un nouveau fichier (défaut: 5)" + echo " --extensions EXT1 EXT2 ... Liste d'extensions spécifiques à surveiller" + echo " -h, --help Afficher cette aide" + echo "" + echo "Exemple: $0 -d /documents/à/surveiller --extensions pdf docx" +} + +# Vérifier si aucun argument n'est fourni +if [ $# -eq 0 ]; then + show_help + exit 1 +fi + +# Vérifier l'argument d'aide +for arg in "$@"; do + if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then + show_help + exit 0 + fi +done + +# Vérifier la présence de l'argument obligatoire (-d ou --directory) +directory_specified=false +for ((i=1; i<=$#; i++)); do + if [ "${!i}" = "-d" ] || [ "${!i}" = "--directory" ]; then + directory_specified=true + break + fi +done + +if [ "$directory_specified" = false ]; then + echo "Erreur: L'option -d/--directory est obligatoire" + echo "Utilisez -h ou --help pour afficher l'aide" + exit 1 +fi + +# Vérifier les dépendances +check_dependencies + +# Chemin au script Python +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON_SCRIPT="${SCRIPT_DIR}/watch_directory.py" + +# Vérifier que le script Python existe +if [ ! -f "$PYTHON_SCRIPT" ]; then + echo "Erreur: Le script watch_directory.py n'existe pas dans $SCRIPT_DIR" + exit 1 +fi + +# Rendre le script Python exécutable +chmod +x "$PYTHON_SCRIPT" + +# Message d'information sur l'arrêt +echo "Démarrage de la surveillance..." +echo "Appuyez sur Ctrl+C pour arrêter la surveillance" +echo "" + +# Exécuter le script Python avec tous les arguments +python3 "$PYTHON_SCRIPT" "$@" \ No newline at end of file diff --git a/IA/get_regeneration_plan.py b/IA/get_regeneration_plan.py new file mode 100644 index 0000000..20f94d6 --- /dev/null +++ b/IA/get_regeneration_plan.py @@ -0,0 +1,171 @@ +from datetime import datetime +from collections import defaultdict, deque +import os +import sys +from datetime import timezone + +sys.path.append(os.path.dirname(os.path.dirname(__file__))) + +# À adapter dans ton environnement +from config import GITEA_URL, ORGANISATION, DEPOT_FICHES, ENV +from utils.gitea import recuperer_date_dernier_commit +from IA.make_config import MAKE # MAKE doit être importé depuis un fichier de config + +def get_mtime(path): + try: + return datetime.fromtimestamp(os.path.getmtime(path), tz=timezone.utc) + except FileNotFoundError: + return None + +def get_commit_time(path_relative): + commits_url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/commits?path={path_relative.replace("Fiches", "Documents")}&sha={ENV}" + return recuperer_date_dernier_commit(commits_url) + +def resolve_path_from_where(where_str): + parts = where_str.split(".") + current = MAKE + path_stack = [] + + for part in parts: + if isinstance(current, dict) and part in current: + path_stack.append((part, current)) + current = current[part] + else: + return None + + if not isinstance(current, str): + return None + + for i in range(len(path_stack) - 1, -1, -1): + key, context = path_stack[i] + if "directory" in context: + directory = context["directory"] + if "fiches" in where_str: + return os.path.join("Fiches", directory, current) + else: + return os.path.join(directory, current) + + return None + +def identifier_type_fiche(path): + for type_fiche, data in MAKE["fiches"].items(): + if not isinstance(data, dict): + continue + directory = data.get("directory", "") + prefix = data.get("prefix", "") + base = os.path.join("Fiches", directory, prefix) + if path.startswith(base): + return type_fiche, data + raise ValueError("Type de fiche non reconnu") + +def doit_regenerer(fichier, doc_deps, fiche_data=None): + mtime_fichier = get_mtime(fichier) + + if fiche_data: + gitea_dep = fiche_data.get("depends_on", {}).get("gitea", {}) + if gitea_dep.get("compare") == "file2commit": + commit_time = get_commit_time(fichier) + if commit_time and mtime_fichier and commit_time > mtime_fichier: + return True + + for _, dep in doc_deps.items(): + if isinstance(dep, dict) and "where" in dep: + source_path = resolve_path_from_where(dep["where"]) + if source_path: + if dep["compare"] == "file2file": + mtime_source = get_mtime(source_path) + elif dep["compare"] == "file2commit": + mtime_source = get_commit_time(source_path) + else: + continue + if mtime_source and mtime_fichier and mtime_source > mtime_fichier: + return True + return False + +def get_regeneration_plan(fiche_path): + def build_dependency_graph_complete(path, graph=None, visited=None): + if graph is None: + graph = defaultdict(set) + if visited is None: + visited = set() + if path in visited: + return graph + visited.add(path) + + try: + _, fiche_data = identifier_type_fiche(path) + except ValueError: + return graph + + depends = fiche_data.get("depends_on", {}) + doc_deps = depends.get("document", {}) + + for _, dep_info in doc_deps.items(): + if isinstance(dep_info, dict) and "where" in dep_info: + dep_path = resolve_path_from_where(dep_info["where"]) + if dep_path: + graph[path].add(dep_path) + build_dependency_graph_complete(dep_path, graph, visited) + + if "file" in fiche_data: + for fichier in fiche_data["file"].values(): + dir_fiche = fiche_data.get("directory", "") + fichier_path = os.path.join("Fiches", dir_fiche, fichier) + if fichier_path not in graph: + graph[fichier_path] = set() + + return graph + + def topological_sort(graph): + in_degree = defaultdict(int) + for node in graph: + for dep in graph[node]: + in_degree[dep] += 1 + queue = deque([node for node in graph if in_degree[node] == 0]) + result = [] + + while queue: + node = queue.popleft() + result.append(node) + for dep in graph[node]: + in_degree[dep] -= 1 + if in_degree[dep] == 0: + queue.append(dep) + + all_nodes = set(graph.keys()).union(*graph.values()) + for node in all_nodes: + if node not in result: + result.append(node) + + return result[::-1] + + graph = build_dependency_graph_complete(fiche_path) + sorted_fiches = topological_sort(graph) + if fiche_path not in sorted_fiches: + sorted_fiches.append(fiche_path) + + + to_regen = [] + regen_flags = {} + + for fiche in sorted_fiches: + print(f"=> {fiche}") + try: + _, fiche_data = identifier_type_fiche(fiche) + except ValueError: + fiche_data = None + depends = fiche_data.get("depends_on", {}) if fiche_data else {} + doc_deps = depends.get("document", {}) if depends else {} + + doit = doit_regenerer(fiche, doc_deps, fiche_data) + if any(regen_flags.get(dep, False) for dep in graph.get(fiche, [])): + doit = True + + regen_flags[fiche] = doit + if doit: + to_regen.append(fiche) + + return to_regen + +plan = get_regeneration_plan("Fiches/Minerai/Fiche minerai antimoine.md") +print(plan) diff --git a/IA/make_config.py b/IA/make_config.py new file mode 100644 index 0000000..c957ef3 --- /dev/null +++ b/IA/make_config.py @@ -0,0 +1,141 @@ +from utils.gitea import recuperer_date_dernier_commit +# +# from config import GITEA_URL, GITEA_TOKEN, ORGANISATION, DEPOT_FICHES, DEPOT_CODE, ENV, ENV_CODE, DOT_FILE +# +#def recuperer_date_dernier_commit(url): +# headers = {"Authorization": f"token {GITEA_TOKEN}"} +# try: +# response = requests.get(url, headers=headers, timeout=10) +# response.raise_for_status() +# commits = response.json() +# if commits: +# return parser.isoparse(commits[0]["commit"]["author"]["date"]) +# except Exception as e: +# logging.error(f"Erreur récupération commit schema : {e}") +# return None +# +# path_relative = f"Documents/{dossier_choisi}/{fiche_choisie}" +# commits_url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/commits?path={path_relative}&sha={ENV}" +# +# local_mtime = datetime.fromtimestamp(os.path.getmtime(path_relative), tz=timezone.utc) +# remote_mtime = recuperer_date_dernier_commit(commit_url) + +MAKE = { + "assets": { + "directory": "assets", + "seuils": { + "depends_on": "None", + }, + "file": { + "seuils": "config.yaml" + } + }, + "fiches": { + "directory": "Fiches", + "criticites": { + "directory": "Criticités", + "préfix": "Fiche technique ", + "depends_on": { + "gitea": { + "compare": "file2commit" + }, + "document": { + "seuils": { + "where": "assets.file.seuils", + "compare": "file2file" + } + } + }, + "file": { + "ihh": "Fiche technique IHH.md", + "isg": "Fiche technique ISG.md", + "ivc": "Fiche technique IVC.md", + "ics": "Fiche technique ICS.md" + } + }, + "assemblage": { + "directory": "Assemblage", + "prefix": "Fiche assemblage ", + "depends_on": { + "gitea": { + "compare": "file2commit" + }, + "document": { + "ihh": { + "where": "fiches.criticites.file.ihh", + "compare": "file2file" + }, + "isg": { + "where": "fiches.criticites.file.isg", + "compare": "file2file" + } + } + } + }, + "fabrication": { + "directory": "Fabrication", + "prefix": "Fiche fabrication ", + "depends_on": { + "gitea": { + "compare": "file2commit" + }, + "document": { + "ihh": { + "where": "fiches.criticites.file.ihh", + "compare": "file2file" + }, + "isg": { + "where": "fiches.criticites.file.isg", + "compare": "file2file" + } + } + } + }, + "connexe": { + "directory": "Connexe", + "prefix": "Fiche assemblage ", + "depends_on": { + "gitea": { + "compare": "file2commit" + }, + "document": { + "ihh": { + "where": "fiches.criticites.file.ihh", + "compare": "file2file" + }, + "isg": { + "where": "fiches.criticites.file.isg", + "compare": "file2file" + } + } + } + }, + "minerai": { + "directory": "Minerai", + "prefix": "Fiche minerai ", + "depends_on": { + "gitea": { + "compare": "file2commit" + }, + "document": { + "ihh": { + "where": "fiches.criticites.file.ihh", + "compare": "file2file" + }, + "isg": { + "where": "fiches.criticites.file.isg", + "compare": "file2file" + }, + "ics": { + "where": "fiches.criticites.file.ics", + "compare": "file2file" + }, + "ivc": { + "where": "fiches.criticites.file.ivc", + "compare": "file2file" + } + } + } + } + } +} diff --git a/README.md b/README.md index 9c9f140..dfe646f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Le code proposé répond à la partie outillage, avec une architecture modulaire Le projet est bâti sur un backeng Gitea pour la gestion des fiches et des tickets d'évolution. (Accéder au backend)[https://fabnum-git.peccini.fr/FabNum/Fiches] -Le serveur qui héberge l'application héberge aussi le service Gitea, ce qui permet d'éliminer les temps de latence dus au réseau. +Le serveur qui héberge l'application héberge aussi le service Gitea, ce qui permet d'éliminer les temps de latence dus au réseau. Les fiches sont toutefois mise en cache localement en générant les fichiers markdown (référence), html (affichage) et pdf (download). L'application est écrite en python et utilise majoritairement streamlit. @@ -26,11 +26,14 @@ Le fichier **requirements.txt** permet d'installer tout ce qui est nécessaire p python -m venv venv source venv/bin/activate + # Installation depuis le fichier pip install -r requirements.txt + # Génération du fichier + pipreqs requirements.txt ### Environnement -Le fichier **.env.local** qui contient GITEA_TOKEN n'est pas dans le dépôt car il contient la clé pour accéder au backend. +Le fichier **.env.local** qui contient GITEA_TOKEN n'est pas dans le dépôt car il contient la clé pour accéder au backend. Il doit donc être créé ainsi que le Token. Le TOken doit permetre l'accès en lecture au dépôt et en lecture/écriture au gestionnaire des tickets. Pour l'environnement de pré-production, (https://fabnum-dev.peccini.fr)[https://fabnum-dev.peccini.fr] : @@ -52,7 +55,7 @@ Pour l'environnement de production, (https://fabnum.peccini.fr)[https://fabnum.p PORT=8501 -La différence entre les deux environnements se fait au travers de la configuration du proxy Nginx (ici celle de dev ; il suffit de changer dev en public pour l'environnement de production) : +La différence entre les deux environnements se fait au travers de la configuration du proxy Nginx (ci-après celle de dev ; il suffit de changer dev en public pour l'environnement de production) : # Ajout d'un en-tête personnalisé pour indiquer l'environnement add_header X-Environment "dev" always; @@ -106,11 +109,19 @@ Le cœur de l'application. Ce script sert de point d'entrée et d'orchestrateur Ce fichier est conçu de manière modulaire, déléguant les fonctionnalités spécifiques aux modules spécialisés, ce qui facilite la maintenance et les évolutions futures. +Dans la sidebar, l'application indique une estimation des émissions de gaz à effet de serre. Pour cela, un mécanisme est mis en place dans la configuration Nginx pour enregistrer dans le fichier : + /var/log/nginx/fabnum-dev.access.log +les informations d'octets transférés. + +Ce système est basé sur la création d'un cookie de session, utilisé ensuite pour distinguer les utilisations. + +<<<<<<<<<<<à compléter avec la configuration Nginx>>>>>>>>>>> + ## Architecture et principes de conception ### Modularité et simplification -L'application a été restructurée selon les principes suivants : +L'application a été structurée selon les principes suivants : 1. **Séparation des responsabilités** : Chaque module a une fonction bien définie 2. **Modularité** : Les fonctionnalités sont décomposées en composants réutilisables @@ -149,43 +160,56 @@ L'application est organisée de façon modulaire, avec une structure simplifiée ``` fabnum-dev/ -├── fabnum.py # Point d'entrée principal -├── config.py # Configuration et variables d'environnement -├── app/ # Modules fonctionnels principaux -│ ├── analyse/ # Module d'analyse des chaînes de dépendance -│ │ ├── interface.py # Interface utilisateur pour l'analyse -│ │ ├── sankey.py # Génération des diagrammes Sankey -│ │ └── README.md # Documentation du module -│ ├── fiches/ # Gestion et affichage des fiches -│ │ ├── interface.py # Interface utilisateur pour les fiches -│ │ ├── generer.py # Génération des fiches -│ │ ├── utils/ # Utilitaires spécifiques aux fiches -│ │ └── README.md # Documentation du module -│ ├── personnalisation/ # Personnalisation de la chaîne -│ │ ├── interface.py # Interface principale -│ │ ├── ajout.py # Ajout de produits -│ │ ├── modification.py # Modification de produits -│ │ ├── import_export.py # Import/export de configurations -│ │ └── README.md # Documentation du module -│ └── visualisations/ # Visualisations graphiques -│ ├── interface.py # Interface des visualisations -│ ├── graphes.py # Gestion des graphes à visualiser -│ └── README.md # Documentation du module -├── components/ # Composants d'interface réutilisables -│ ├── sidebar.py # Barre latérale de navigation -│ ├── header.py # En-tête de l'application -│ ├── footer.py # Pied de page -│ └── README.md # Documentation des composants -├── utils/ # Utilitaires partagés -│ ├── gitea.py # Connexion API Gitea -│ ├── graph_utils.py # Manipulation des graphes -│ └── README.md # Documentation des utilitaires -├── assets/ # Ressources statiques -│ ├── styles/ # Feuilles de style CSS -│ └── impact_co2.js # Calcul d'impact environnemental -├── .env # Configuration versionnée -├── .env.local # Configuration locale (non versionnée) -└── requirements.txt # Dépendances Python +├── fabnum.py # Point d'entrée principal +├── config.py # Configuration et variables d'environnement +├── app/ # Modules fonctionnels principaux +│ ├── analyse/ # Module d'analyse des chaînes de dépendance +│ │ ├── interface.py # Interface utilisateur pour l'analyse +│ │ ├── sankey.py # Génération des diagrammes Sankey +│ │ └── README.md # Documentation du module +│ ├── fiches/ # Gestion et affichage des fiches +│ │ ├── interface.py # Interface utilisateur pour les fiches +│ │ ├── generer.py # Génération des fiches +│ │ ├── utils/ # Utilitaires spécifiques aux fiches +│ │ ├── utils/dynamic # Gestion de la génération et affichage des fiches par type d'opération +│ │ ├── utils/tickets # Gestion de l'affichage et de la création des tickets +│ │ ├── utils/fiches_utils.py # Outils de gestion et rendu des fiches +│ │ └── README.md # Documentation du module +│ ├── personnalisation/ # Personnalisation de la chaîne +│ │ ├── interface.py # Interface principale +│ │ ├── ajout.py # Ajout de produits +│ │ ├── modification.py # Modification de produits +│ │ ├── import_export.py # Import/export de configurations +│ │ └── README.md # Documentation du module +│ └── visualisations/ # Visualisations graphiques +│ ├── interface.py # Interface des visualisations +│ ├── graphes.py # Gestion des graphes à visualiser +│ └── README.md # Documentation du module +├── components/ # Composants d'interface réutilisables +│ ├── sidebar.py # Barre latérale de navigation +│ ├── header.py # En-tête de l'application +│ ├── footer.py # Pied de page +│ ├── connexion.py # Module de connexion à partir d'un token Gitea +│ └── README.md # Documentation des composants +├── utils/ # Utilitaires partagés +│ ├── gitea.py # Connexion API Gitea +│ ├── graph_utils.py # Manipulation des graphes +│ ├── translations.py # Module de gestion de l'internationalisation +│ ├── visualisations.py # Manipulation des graphes de l'onglet Visualisations +│ └── README.md # Documentation des utilitaires +├── assets/ # Ressources statiques +│ ├── locales/ # Gestion de l'internationalisation +│ ├── styles/ # Feuilles de style CSS +│ ├── confir.yaml # Définition des seuils pour les indices +│ ├── fiches_labels.csv # Dictionnaire d'association entre les fiches et leurs labels (tickets) +│ ├── impact_co2.js # Calcul d'impact environnemental +│ ├── licence.md # Licence ajoutée à toutes les fiches +│ └── weakness.png # Icône de l'onglet dans le navigateur +├── .env # Configuration versionnée +├── .env.local # Configuration locale (non versionnée) +└── requirements.txt # Dépendances Python +├── .streamlit/ # Configuration streamlit côté serveur +│ └── config.toml # Fichier de configuration : important theme.base = light ``` Chaque module dispose de sa propre documentation détaillée dans un fichier README.md. diff --git a/app/analyse/interface.py b/app/analyse/interface.py index 093b56e..5195a03 100644 --- a/app/analyse/interface.py +++ b/app/analyse/interface.py @@ -1,5 +1,6 @@ import streamlit as st from utils.translations import _ +from utils.widgets import html_expander from .sankey import afficher_sankey @@ -123,8 +124,7 @@ def configurer_filtres_vulnerabilite(): def interface_analyse(G_temp): st.markdown(f"# {str(_('pages.analyse.title'))}") - with st.expander(str(_("pages.analyse.help")), expanded=False): - st.markdown("\n".join(_("pages.analyse.help_content"))) + html_expander(f"{str(_('pages.analyse.help'))}", content="\n".join(_("pages.analyse.help_content")), open_by_default=False, details_class="details_introduction") st.markdown("---") try: @@ -148,7 +148,7 @@ def interface_analyse(G_temp): # Lancement de l'analyse st.markdown("---") - if st.button(str(_("pages.analyse.run_analysis")), type="primary", key="analyse_lancer"): + if st.button(str(_("pages.analyse.run_analysis")), type="primary", key="analyse_lancer", icon=":material/graph_4:"): afficher_sankey( G_temp, niveau_depart=niveau_depart, diff --git a/app/analyse/sankey.py b/app/analyse/sankey.py index 5878064..4ae477d 100644 --- a/app/analyse/sankey.py +++ b/app/analyse/sankey.py @@ -37,14 +37,14 @@ def extraire_niveaux(G): logging.warning(f"Niveau non entier pour le noeud {node}: {niveau_str}") return niveaux -def extraire_criticite(G, u, v): +def extraire_ics(G, u, v): """Extrait la criticité d'un lien entre deux nœuds""" data = G.get_edge_data(u, v) if not data: return 0 if isinstance(data, dict) and all(isinstance(k, int) for k in data): - return float(data[0].get("criticite", 0)) - return float(data.get("criticite", 0)) + return float(data[0].get("ics", 0)) + return float(data.get("ics", 0)) def extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais): """Extrait les chemins selon les critères spécifiés""" @@ -102,7 +102,7 @@ def verifier_critere_ics(G, chemin, niveaux): if ((niveau_u == 1 and niveau_v == 2) or (niveau_u == 1001 and niveau_v == 1002) or - (niveau_u == 10 and niveau_v in (1000, 1001))) and extraire_criticite(G, u, v) > 0.66: + (niveau_u == 10 and niveau_v in (1000, 1001))) and extraire_ics(G, u, v) > 0.66: return True return False @@ -153,7 +153,7 @@ def filtrer_chemins_par_criteres(G, chemins, niveaux, niveau_depart, niveau_arri # Vérification des critères pour ce chemin has_ihh = filtrer_ihh and verifier_critere_ihh(G, chemin, niveaux, ihh_type) has_ivc = filtrer_ivc and verifier_critere_ivc(G, chemin, niveaux) - has_criticite = filtrer_ics and verifier_critere_ics(G, chemin, niveaux) + has_ics = filtrer_ics and verifier_critere_ics(G, chemin, niveaux) has_isg_critique = filtrer_isg and verifier_critere_isg(G, chemin, niveaux) # Appliquer la logique de filtrage @@ -161,12 +161,12 @@ def filtrer_chemins_par_criteres(G, chemins, niveaux, niveau_depart, niveau_arri keep = True if filtrer_ihh: keep = keep and has_ihh if filtrer_ivc: keep = keep and has_ivc - if filtrer_ics: keep = keep and has_criticite + if filtrer_ics: keep = keep and has_ics if filtrer_isg: keep = keep and has_isg_critique if keep: chemins_filtres.add(tuple(chemin)) elif logique_filtrage == "OU": - if has_ihh or has_ivc or has_criticite or has_isg_critique: + if has_ihh or has_ivc or has_ics or has_isg_critique: chemins_filtres.add(tuple(chemin)) # Extraction des liens après filtrage @@ -176,7 +176,7 @@ def filtrer_chemins_par_criteres(G, chemins, niveaux, niveau_depart, niveau_arri return liens_filtres, chemins_filtres -def couleur_criticite(p): +def couleur_ics(p): """Retourne la couleur en fonction du niveau de criticité""" if p <= 0.33: return "darkgreen" @@ -206,8 +206,8 @@ def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins): df_liens = pd.DataFrame(list(liens_chemins), columns=["source", "target"]) df_liens = df_liens.groupby(["source", "target"]).size().reset_index(name="value") - df_liens["criticite"] = df_liens.apply( - lambda row: extraire_criticite(G, row["source"], row["target"]), axis=1) + df_liens["ics"] = df_liens.apply( + lambda row: extraire_ics(G, row["source"], row["target"]), axis=1) df_liens["value"] = 0.1 # Ne garder que les nœuds effectivement connectés @@ -221,7 +221,7 @@ def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins): noeuds_utilises.add(n) df_liens["color"] = df_liens.apply( - lambda row: couleur_criticite(row["criticite"]) if row["criticite"] > 0 else "white", + lambda row: couleur_ics(row["ics"]) if row["ics"] > 0 else "white", axis=1 ) diff --git a/app/fiches/interface.py b/app/fiches/interface.py index 44596c3..d990df7 100644 --- a/app/fiches/interface.py +++ b/app/fiches/interface.py @@ -2,7 +2,6 @@ import streamlit as st import requests import os -import pathlib from utils.translations import _ from .utils.tickets.display import afficher_tickets_par_fiche @@ -16,11 +15,11 @@ from utils.gitea import charger_arborescence_fiches from .utils.fiche_utils import load_seuils, doit_regenerer_fiche from .generer import generer_fiche +from utils.widgets import html_expander def interface_fiches(): st.markdown(f"# {str(_('pages.fiches.title'))}") - with st.expander(str(_("pages.fiches.help")), expanded=False): - st.markdown("\n".join(_("pages.fiches.help_content"))) + html_expander(f"{str(_('pages.fiches.help'))}", content="\n".join(_("pages.fiches.help_content")), open_by_default=False, details_class="details_introduction") st.markdown("---") if "fiches_arbo" not in st.session_state: @@ -102,4 +101,4 @@ def interface_fiches(): formulaire_creation_ticket_dynamique(fiche_choisie) except Exception as e: - st.error(f"{str(_('pages.fiches.loading_error', 'Erreur lors du chargement de la fiche :'))} {e}") + st.error(f"{str(_('pages.fiches.loading_error'))} {e}") diff --git a/app/fiches/utils/dynamic/minerai/minerai.py b/app/fiches/utils/dynamic/minerai/minerai.py index 9710646..960e2c2 100644 --- a/app/fiches/utils/dynamic/minerai/minerai.py +++ b/app/fiches/utils/dynamic/minerai/minerai.py @@ -567,6 +567,8 @@ def build_minerai_sections(md: str) -> str: md, flags=re.DOTALL ) + # suppression pour le dernier minerai dans la fiche IHH + md = re.sub(r"# Tableaux de synthèse.*", "", md, flags=re.DOTALL) except Exception as e: st.error(f"Erreur lors de la génération des sections IHH: {e}") diff --git a/app/fiches/utils/tickets/core.py b/app/fiches/utils/tickets/core.py index f48b3db..d2544f6 100644 --- a/app/fiches/utils/tickets/core.py +++ b/app/fiches/utils/tickets/core.py @@ -110,10 +110,6 @@ def creer_ticket_gitea(titre, corps, labels): reponse = gitea_request("post", url, headers={"Content-Type": "application/json"}, data=json.dumps(data)) if not reponse: - return - - issue_url = reponse.json().get("html_url", "") - if issue_url: - st.success(f"{str(_('pages.fiches.tickets.created_success'))} [Voir le ticket]({issue_url})") + return False else: - st.success(str(_('pages.fiches.tickets.created'))) + return True diff --git a/app/fiches/utils/tickets/creation.py b/app/fiches/utils/tickets/creation.py index dd3ddb4..ebebd44 100644 --- a/app/fiches/utils/tickets/creation.py +++ b/app/fiches/utils/tickets/creation.py @@ -74,6 +74,8 @@ def afficher_controles_formulaire(): col1, col2 = st.columns(2) if col1.button(str(_("pages.fiches.tickets.preview"))): st.session_state.previsualiser = True + # S'assurer que l'expander reste ouvert en mode prévisualisation + st.session_state.expander_state = True if col2.button(str(_("pages.fiches.tickets.cancel"))): st.session_state.previsualiser = False st.rerun() @@ -81,6 +83,23 @@ def afficher_controles_formulaire(): def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible): """Gère la prévisualisation et la soumission du ticket.""" + # Si nous avons tenté de créer un ticket (succès ou erreur) + if st.session_state.get("ticket_cree", False) or st.session_state.get("ticket_erreur", False): + if st.session_state.get("ticket_cree", False): + st.success(str(_("pages.fiches.tickets.created_success"))) + else: + st.error(str(_("pages.fiches.tickets.creation_error"))) + + if st.button(str(_("pages.fiches.tickets.continue"))): + # Réinitialiser le formulaire et cacher l'expander + st.session_state.ticket_cree = False + st.session_state.ticket_erreur = False + st.session_state.previsualiser = False + st.session_state.expander_state = False + st.rerun() + return + + # Si nous ne sommes pas en mode prévisualisation, ne rien afficher if not st.session_state.get("previsualiser", False): return @@ -101,15 +120,31 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible): labels_ids.append(labels_existants["Backlog"]) corps = construire_corps_ticket_markdown(reponses) - creer_ticket_gitea(titre_ticket, corps, labels_ids) + resultat = creer_ticket_gitea(titre_ticket, corps, labels_ids) + # Marquer le résultat et ouvrir l'expander pour afficher le résultat + st.session_state.ticket_cree = resultat + st.session_state.ticket_erreur = not resultat st.session_state.previsualiser = False - st.success(str(_("pages.fiches.tickets.created"))) + st.session_state.expander_state = True + st.rerun() def formulaire_creation_ticket_dynamique(fiche_selectionnee): """Fonction principale pour le formulaire de création de ticket.""" - with st.expander(str(_("pages.fiches.tickets.create_new")), expanded=False): + # Initialiser l'état de l'expander si ce n'est pas déjà fait + if "expander_state" not in st.session_state: + st.session_state.expander_state = False + + with st.expander(str(_("pages.fiches.tickets.create_new")), expanded=st.session_state.expander_state): + # Initialiser les états si ce n'est pas déjà fait + if "ticket_cree" not in st.session_state: + st.session_state.ticket_cree = False + if "ticket_erreur" not in st.session_state: + st.session_state.ticket_erreur = False + if "previsualiser" not in st.session_state: + st.session_state.previsualiser = False + # Chargement et vérification du modèle contenu_modele = charger_modele_ticket() if not contenu_modele: @@ -119,11 +154,21 @@ def formulaire_creation_ticket_dynamique(fiche_selectionnee): # Traitement du modèle et génération du formulaire sections = parser_modele_ticket(contenu_modele) labels, selected_ops, cible = generer_labels(fiche_selectionnee) - reponses = creer_champs_formulaire(sections, fiche_selectionnee) - - # Gestion des contrôles et de la prévisualisation - afficher_controles_formulaire() - gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible) + + # Créer le formulaire et gérer ses états + if st.session_state.ticket_cree or st.session_state.ticket_erreur: + # Si le ticket a été créé ou a échoué, afficher le message approprié et le bouton continuer + gerer_previsualisation_et_soumission({}, labels, selected_ops, cible) + else: + # Sinon afficher le formulaire normal + reponses = creer_champs_formulaire(sections, fiche_selectionnee) + + # Afficher les contrôles uniquement si nous ne sommes pas en mode prévisualisation + if not st.session_state.previsualiser: + afficher_controles_formulaire() + + # Gérer la prévisualisation et soumission + gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible) def charger_modele_ticket(): diff --git a/app/fiches/utils/tickets/display.py b/app/fiches/utils/tickets/display.py index 454b23b..6dfc028 100644 --- a/app/fiches/utils/tickets/display.py +++ b/app/fiches/utils/tickets/display.py @@ -6,7 +6,6 @@ import re from collections import defaultdict from dateutil import parser from utils.translations import _ -from .core import rechercher_tickets_gitea def extraire_statut_par_label(ticket): diff --git a/app/ia_nalyse/README.md b/app/ia_nalyse/README.md new file mode 100644 index 0000000..141f7b1 --- /dev/null +++ b/app/ia_nalyse/README.md @@ -0,0 +1,42 @@ +# Module d'Analyse + +Ce module permet d'analyser les relations entre les différentes parties de la chaîne de fabrication du numérique. Il offre des outils pour visualiser les flux et identifier les vulnérabilités potentielles dans la chaîne d'approvisionnement. + +## Structure du module + +Le module d'analyse comprend deux composants principaux : + +- **interface.py** : Gère l'interface utilisateur pour paramétrer les analyses +- **sankey.py** : Génère les diagrammes de flux (Sankey) pour visualiser les relations entre les éléments + +## Fonctionnalités + +### Interface d'analyse +L'interface permet de : +- Sélectionner les niveaux de départ et d'arrivée pour l'analyse (produits, composants, minerais, opérations, etc.) +- Filtrer les données par minerais spécifiques +- Effectuer une sélection fine des nœuds de départ et d'arrivée +- Appliquer des filtres pour identifier les vulnérabilités : + - Filtres ICS (criticité pour un composant) + - Filtres IVC (criticité par rapport à la concurrence sectorielle) + - Filtres IHH (concentration géographique ou industrielle) + - Filtres ISG (instabilité des pays) +- Choisir la logique de filtrage (OU, ET) + +### Visualisation Sankey +Le module génère des diagrammes Sankey qui : +- Affichent les flux entre les différents éléments de la chaîne +- Mettent en évidence les relations de dépendance +- Permettent d'identifier visuellement les goulots d'étranglement potentiels +- Sont interactifs et permettent d'explorer la chaîne de valeur + +## Utilisation + +1. Sélectionnez un niveau de départ (ex : Produit final, Composant) +2. Choisissez un niveau d'arrivée (ex : Pays géographique, Acteur d'opération) +3. Si nécessaire, filtrez par minerais spécifiques +4. Affinez votre sélection avec des nœuds de départ et d'arrivée spécifiques +5. Appliquez les filtres de vulnérabilité souhaités +6. Lancez l'analyse pour générer le diagramme Sankey + +Le diagramme résultant permet d'identifier visuellement les relations et points de vulnérabilité dans la chaîne d'approvisionnement du numérique. \ No newline at end of file diff --git a/app/ia_nalyse/__init__.py b/app/ia_nalyse/__init__.py new file mode 100644 index 0000000..58cc0c9 --- /dev/null +++ b/app/ia_nalyse/__init__.py @@ -0,0 +1,2 @@ +# __init__.py – app/fiches +from .interface import interface_ia_nalyse diff --git a/app/ia_nalyse/interface.py b/app/ia_nalyse/interface.py new file mode 100644 index 0000000..6cc2ec7 --- /dev/null +++ b/app/ia_nalyse/interface.py @@ -0,0 +1,195 @@ +import streamlit as st +import networkx as nx +from utils.translations import _ +from utils.widgets import html_expander + +from utils.graph_utils import ( + extraire_chemins_depuis, + extraire_chemins_vers +) + +from batch_ia.batch_utils import soumettre_batch, statut_utilisateur, nettoyage_post_telechargement + +niveau_labels = { + 0: "Produit final", + 1: "Composant", + 2: "Minerai", + 10: "Opération", + 11: "Pays d'opération", + 12: "Acteur d'opération", + 99: "Pays géographique" +} + +inverse_niveau_labels = {v: k for k, v in niveau_labels.items()} + + +def preparer_graphe(G): + """Nettoie et prépare le graphe pour l'analyse.""" + niveaux_temp = { + node: int(str(attrs.get("niveau")).strip('"')) + for node, attrs in G.nodes(data=True) + if attrs.get("niveau") and str(attrs.get("niveau")).strip('"').isdigit() + } + G.remove_nodes_from([n for n in G.nodes() if n not in niveaux_temp]) + G.remove_nodes_from( + [n for n in G.nodes() if niveaux_temp.get(n) == 10 and 'Reserves' in n]) + return G, niveaux_temp + + +def selectionner_minerais(G): + """Interface pour sélectionner les minerais si nécessaire.""" + minerais_selection = None + + st.markdown(f"## {str(_('pages.ia_nalyse.select_minerals'))}") + # Tous les nœuds de niveau 2 (minerai) + minerais_nodes = sorted([ + n for n, d in G.nodes(data=True) + if d.get("niveau") and int(str(d.get("niveau")).strip('"')) == 2 + ]) + + minerais_selection = st.multiselect( + str(_("pages.ia_nalyse.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.ia_nalyse.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.ia_nalyse.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""" + if not st.session_state.get("logged_in", False) or not liens_chemins: + return + + G_export = nx.DiGraph() + for u, v in liens_chemins: + G_export.add_node(u, **G.nodes[u]) + G_export.add_node(v, **G.nodes[v]) + data = G.get_edge_data(u, v) + if isinstance(data, dict) and all(isinstance(k, int) for k in data): + G_export.add_edge(u, v, **data[0]) + elif isinstance(data, dict): + G_export.add_edge(u, v, **data) + else: + G_export.add_edge(u, v) + + return(G_export) + +def extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux): + """Extrait les liens des chemins en respectant les niveaux""" + liens = set() + for chemin in chemins: + for i in range(len(chemin) - 1): + u, v = chemin[i], chemin[i + 1] + niveau_u = niveaux.get(u, 999) + niveau_v = niveaux.get(v, 999) + if ( + (niveau_depart <= niveau_u <= niveau_arrivee or niveau_u in niveaux_speciaux) + and (niveau_depart <= niveau_v <= niveau_arrivee or niveau_v in niveaux_speciaux) + ): + liens.add((u, v)) + return liens + +def interface_ia_nalyse(G_temp): + st.markdown(f"# {str(_('pages.ia_nalyse.title'))}") + html_expander(f"{str(_('pages.ia_nalyse.help'))}", content="\n".join(_("pages.ia_nalyse.help_content")), open_by_default=False, details_class="details_introduction") + st.markdown("---") + + resultat = statut_utilisateur(st.session_state.username) + st.info(resultat["message"]) + + if resultat["statut"] is None: + # Préparation du graphe + G_temp, niveaux_temp = preparer_graphe(G_temp) + + # Sélection des niveaux + niveau_depart = 0 + niveau_arrivee = 99 + + # Sélection fine des noeuds + noeuds_depart, noeuds_arrivee = selectionner_noeuds(G_temp, niveaux_temp, niveau_depart) + + # Sélection des minerais si nécessaire + minerais = selectionner_minerais(G_temp) + + # Étape 1 : Extraction des niveaux des nœuds + niveaux = extraire_niveaux(G_temp) + + # Étape 2 : Extraction des chemins selon les critères + chemins = extraire_chemins_selon_criteres(G_temp, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais) + + niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012] + # Extraction des liens sans filtrage + liens_chemins = extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux) + + if liens_chemins: + G_final = exporter_graphe_filtre(G_temp, liens_chemins) + if st.button(str(_("pages.ia_nalyse.submit_request")), icon=":material/send:"): + soumettre_batch(st.session_state.username, G_final) + st.rerun() + else: + st.info(str(_("pages.ia_nalyse.empty_graph"))) + + elif resultat["statut"] == "terminé" and resultat["telechargement"]: + if not st.session_state.get("telechargement_confirme"): + st.download_button(str(_("buttons.download")), resultat["telechargement"], file_name="analyse.zip", icon=":material/download:") + if st.button(str(_("pages.ia_nalyse.confirm_download")), icon=":material/task_alt:"): + nettoyage_post_telechargement(st.session_state.username) + st.session_state["telechargement_confirme"] = True + st.rerun() + else: + st.success("Résultat supprimé. Vous pouvez relancer une nouvelle analyse.") + if st.button(str(_("buttons.refresh")), icon=":material/refresh:"): + st.rerun() + else: + if st.button(str(_("buttons.refresh")), icon=":material/refresh:"): + st.rerun() diff --git a/app/personnalisation/import_export.py b/app/personnalisation/import_export.py index 5d3e6bc..e45b960 100644 --- a/app/personnalisation/import_export.py +++ b/app/personnalisation/import_export.py @@ -4,7 +4,7 @@ from utils.translations import get_translation as _ def importer_exporter_graph(G): st.markdown(f"## {_('pages.personnalisation.save_restore_config')}") - if st.button(str(_("pages.personnalisation.export_config"))): + if st.button(str(_("pages.personnalisation.export_config")), icon=":material/save:"): nodes = [n for n, d in G.nodes(data=True) if d.get("personnalisation") == "oui"] edges = [(u, v) for u, v in G.edges() if u in nodes] conf = {"nodes": nodes, "edges": edges} @@ -13,7 +13,8 @@ def importer_exporter_graph(G): label=str(_("pages.personnalisation.download_json")), data=json_str, file_name="config_personnalisation.json", - mime="application/json" + mime="application/json", + icon=":material/save:" ) uploaded = st.file_uploader(str(_("pages.personnalisation.import_config")), type=["json"]) @@ -37,7 +38,7 @@ def importer_exporter_graph(G): key="restaurer_selection" ) - if st.button(str(_("pages.personnalisation.restore_selected")), type="primary"): + if st.button(str(_("pages.personnalisation.restore_selected")), type="primary", icon=":material/history:"): for node in sel_nodes: if not G.has_node(node): G.add_node(node, niveau="0", personnalisation="oui", label=node) diff --git a/app/personnalisation/interface.py b/app/personnalisation/interface.py index bcaae19..9e27d1d 100644 --- a/app/personnalisation/interface.py +++ b/app/personnalisation/interface.py @@ -5,11 +5,11 @@ from utils.translations import _ from .ajout import ajouter_produit from .modification import modifier_produit from .import_export import importer_exporter_graph +from utils.widgets import html_expander def interface_personnalisation(G): st.markdown(f"# {str(_('pages.personnalisation.title'))}") - with st.expander(str(_("pages.personnalisation.help")), expanded=False): - st.markdown("\n".join(_("pages.personnalisation.help_content"))) + html_expander(f"{str(_('pages.personnalisation.help'))}", content="\n".join(_("pages.personnalisation.help_content")), open_by_default=False, details_class="details_introduction") st.markdown("---") G = ajouter_produit(G) diff --git a/app/plan_d_action/__init__.py b/app/plan_d_action/__init__.py new file mode 100644 index 0000000..0ba06d9 --- /dev/null +++ b/app/plan_d_action/__init__.py @@ -0,0 +1,2 @@ +# __init__.py – app/fiches +from .interface import interface_plan_d_action diff --git a/app/plan_d_action/interface.py b/app/plan_d_action/interface.py new file mode 100644 index 0000000..f3cc0fd --- /dev/null +++ b/app/plan_d_action/interface.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script pour générer un rapport factorisé des vulnérabilités critiques +suivant la structure définie dans Remarques.md. +""" + +import streamlit as st +import uuid +from utils.translations import _ +from utils.widgets import html_expander +from networkx.drawing.nx_agraph import write_dot + +from batch_ia import ( + load_config, + write_report, + parse_graphs, + extract_data_from_graph, + calculate_vulnerabilities, + generate_report, +) + +from app.plan_d_action.utils.data.plan_d_action import initialiser_interface + +from app.plan_d_action.utils.interface.parser import preparer_graphe +from app.plan_d_action.utils.interface.niveau_utils import extraire_niveaux +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 +) + +inverse_niveau_labels = {v: k for k, v in niveau_labels.items()} + + +def interface_plan_d_action(G_temp): + + if "sel_prod" not in st.session_state: + st.session_state.sel_prod = None + if "sel_comp" not in st.session_state: + st.session_state.sel_comp = None + if "sel_miner" not in st.session_state: + st.session_state.sel_miner = None + + if "plan_d_action" not in st.session_state: + st.session_state["plan_d_action"] = 0 + if "g_md_done" not in st.session_state: + st.session_state["g_md_done"] = False + if "uuid" not in st.session_state: + st.session_state["uuid"] = str(uuid.uuid4())[:8] + st.session_state["G_dot"] = f"{JOBS}/{st.session_state["uuid"]}.dot" + st.session_state["G_md"] = f"{JOBS}/{st.session_state["uuid"]}.md" + + if st.session_state["plan_d_action"] == 0: + st.markdown(f"# {str(_('pages.plan_d_action.title'))}") + html_expander(f"{str(_('pages.plan_d_action.help'))}", content="\n".join(_("pages.plan_d_action.help_content")), open_by_default=False, details_class="details_introduction") + # Préparation du graphe + G_temp, niveaux_temp = preparer_graphe(G_temp) + + # Sélection des niveaux + niveau_depart = 0 + niveau_arrivee = 99 + # Sélection fine des noeuds + noeuds_depart, noeuds_arrivee = selectionner_noeuds(G_temp, niveaux_temp, niveau_depart) + # Sélection des minerais si nécessaire + if noeuds_depart: + minerais = selectionner_minerais(G_temp, noeuds_depart) + # Étape 1 : Extraction des niveaux des nœuds + niveaux = extraire_niveaux(G_temp) + # Étape 2 : Extraction des chemins selon les critères + chemins = extraire_chemins_selon_criteres(G_temp, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais) + niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012] + # Extraction des liens sans filtrage + liens_chemins = extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux) + else: + liens_chemins = None + + if liens_chemins: + G_final = exporter_graphe_filtre(G_temp, liens_chemins) + st.session_state["G_final"] = G_final + # formulaire ou sélection + if st.button(str(_("pages.plan_d_action.submit_request")), icon=":material/send:"): + # On déclenche la suite — mais on NE traite rien maintenant + st.session_state["plan_d_action"] = 1 + st.rerun() # force la réexécution immédiatement avec état mis à jour + + elif st.session_state["plan_d_action"] == 1: + st.markdown("") + # Traitement lourd une seule fois + if not st.session_state["g_md_done"]: + write_dot(st.session_state["G_final"], st.session_state["G_dot"]) + config = load_config() + graph, ref_graph = parse_graphs(st.session_state["G_dot"]) + data = extract_data_from_graph(graph, ref_graph) + results = calculate_vulnerabilities(data, config) + report, file_names = generate_report(data, results, config) + write_report(remplacer_par_badge(report), st.session_state["G_md"]) + st.session_state["g_md_done"] = True # pour ne pas re-traiter à chaque affichage + + # Affichage de l’interface Streamlit + initialiser_interface(st.session_state["G_md"]) + if (st.button("Réinitialiser", icon=":material/refresh:")): + st.session_state["plan_d_action"] = 0 + st.session_state["g_md_done"] = False + st.session_state.sel_prod = None + st.session_state.sel_comp = None + st.session_state.sel_miner = None + for f in JOBS.glob(f"*{st.session_state["uuid"]}*"): + if f.is_file(): + f.unlink() + st.rerun() diff --git a/app/plan_d_action/utils/data/__init__.py b/app/plan_d_action/utils/data/__init__.py new file mode 100644 index 0000000..b585ec8 --- /dev/null +++ b/app/plan_d_action/utils/data/__init__.py @@ -0,0 +1,8 @@ +from .config import ( + PRECONISATIONS, + INDICATEURS +) +from .data_utils import( + colorer_couleurs, + set_vulnerability +) diff --git a/app/plan_d_action/utils/data/config.py b/app/plan_d_action/utils/data/config.py new file mode 100644 index 0000000..28716a7 --- /dev/null +++ b/app/plan_d_action/utils/data/config.py @@ -0,0 +1,165 @@ +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é." + ] + } +} + +poids_operation = { + 'Extraction': 1, + 'Traitement': 1.5, + 'Assemblage': 1.5, + 'Fabrication': 2, + 'Substitution': 2 +} diff --git a/app/plan_d_action/utils/data/data_processing.py b/app/plan_d_action/utils/data/data_processing.py new file mode 100644 index 0000000..159058f --- /dev/null +++ b/app/plan_d_action/utils/data/data_processing.py @@ -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 diff --git a/app/plan_d_action/utils/data/data_utils.py b/app/plan_d_action/utils/data/data_utils.py new file mode 100644 index 0000000..444064c --- /dev/null +++ b/app/plan_d_action/utils/data/data_utils.py @@ -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 diff --git a/app/plan_d_action/utils/data/pda_interface.py b/app/plan_d_action/utils/data/pda_interface.py new file mode 100644 index 0000000..d35f495 --- /dev/null +++ b/app/plan_d_action/utils/data/pda_interface.py @@ -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") diff --git a/app/plan_d_action/utils/data/plan_d_action.py b/app/plan_d_action/utils/data/plan_d_action.py new file mode 100644 index 0000000..e045de3 --- /dev/null +++ b/app/plan_d_action/utils/data/plan_d_action.py @@ -0,0 +1,350 @@ +import streamlit as st +import matplotlib.pyplot as plt + +from app.plan_d_action.utils.data.config import ( + PRECONISATIONS, + INDICATEURS, + poids_operation +) +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 calcul_poids_chaine(poids_A, poids_F, poids_T, poids_E, poids_M): + 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"} + + return criticite_chaine, niveau_criticite, poids_total + +def analyser_chaines(chaines, produits, composants, mineraux, seuils, top_n=None): + resultats = [] + + for chaine in chaines: + sel_prod = chaine["produit"] + sel_comp = chaine["composant"] + sel_miner = chaine["minerai"] + + poids_A, *_ = set_vulnerability(produits[sel_prod]["IHH_Assemblage"], produits[sel_prod]["ISG_Assemblage"], "IHH", "ISG", seuils) + poids_F, *_ = set_vulnerability(composants[sel_comp]["IHH_Fabrication"], composants[sel_comp]["ISG_Fabrication"], "IHH", "ISG", seuils) + poids_T, *_ = set_vulnerability(mineraux[sel_miner]["IHH_Traitement"], mineraux[sel_miner]["ISG_Traitement"], "IHH", "ISG", seuils) + poids_E, *_ = set_vulnerability(mineraux[sel_miner]["IHH_Extraction"], mineraux[sel_miner]["ISG_Extraction"], "IHH", "ISG", seuils) + poids_M, *_ = set_vulnerability(mineraux[sel_miner]["ICS"], mineraux[sel_miner]["IVC"], "ICS", "IVC", seuils) + + criticite_chaine, niveau_criticite, poids_total = calcul_poids_chaine( + poids_A, poids_F, poids_T, poids_E, poids_M + ) + + resultats.append({ + "chaine": chaine, + "criticite_chaine": criticite_chaine, + "niveau_criticite": niveau_criticite, + "poids_total": poids_total + }) + + # Tri décroissant + resultats.sort(key=lambda x: x["poids_total"], reverse=True) + + # Si top_n n'est pas spécifié, tout est retourné + if top_n is None or top_n >= len(resultats): + return resultats + + # Déterminer le seuil de coupure + seuil_poids = resultats[top_n - 1]["poids_total"] + + # Inclure tous ceux dont le poids est égal au seuil + top_resultats = [r for r in resultats if r["poids_total"] >= seuil_poids] + + return top_resultats + +def tableau_de_bord(chains, produits, composants, mineraux, seuils): + col_left, col_right = st.columns([2, 3], gap="small", border=True) + with col_left: + st.markdown("**Panneau de sélection**", unsafe_allow_html=True) + + produits_disponibles = sorted({c["produit"] for c in chains}) + sel_prod = st.selectbox("Produit", produits_disponibles, index=produits_disponibles.index(st.session_state.sel_prod) if st.session_state.sel_prod else 0) + + composants_dispo = sorted({c["composant"] for c in chains if c["produit"] == sel_prod}) + sel_comp = st.selectbox("Composant", composants_dispo, index=composants_dispo.index(st.session_state.sel_comp) if st.session_state.sel_comp else 0) + + 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, index=mineraux_dispo.index(st.session_state.sel_miner) if st.session_state.sel_miner else 0) + with col_right: + top_chains = analyser_chaines(chains, produits, composants, mineraux, seuils, top_n=5) + st.markdown("**Top chaînes critiques pour sélection rapide**", unsafe_allow_html=True) + for i, entry in enumerate(top_chains): + ch = entry["chaine"] + poids = entry["poids_total"] + criticite = entry["criticite_chaine"] + if st.button(f"**{ch['produit']} <-> {ch['composant']} <-> {ch['minerai']}** : {poids:.2f} → {criticite}", key=f"select_{i}"): + st.session_state.sel_prod = ch["produit"] + st.session_state.sel_comp = ch["composant"] + st.session_state.sel_miner = ch["minerai"] + st.rerun() + + c1, c2 = st.columns([3, 2], gap="small", border=True, vertical_alignment='center') + with c1: + st.markdown("**Synthèse des criticités**", 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})") + + criticite_chaine, niveau_criticite, poids_total = calcul_poids_chaine(poids_A, poids_F, poids_T, poids_E, poids_M) + with c2: + 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=(2, 2.4)) + ax1.scatter([produits[sel_prod]["ISG_Assemblage"]], [produits[sel_prod]["IHH_Assemblage"]], label="Assemblage", s=5) + ax1.scatter([composants[sel_comp]["ISG_Fabrication"]], [composants[sel_comp]["IHH_Fabrication"]], label="Fabrication", s=5) + ax1.scatter([mineraux[sel_miner]["ISG_Extraction"]], [mineraux[sel_miner]["IHH_Extraction"]], label="Extraction", s=5) + ax1.scatter([mineraux[sel_miner]["ISG_Traitement"]], [mineraux[sel_miner]["IHH_Traitement"]], label="Traitement", 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=(0.5, -0.25), loc='upper center', fontsize=4) + plt.tight_layout() + st.pyplot(fig1) + + with col_right: + fig2, ax2 = plt.subplots(figsize=(2, 2.1)) + ax2.scatter([mineraux[sel_miner]["IVC"]], [mineraux[sel_miner]["ICS"]], color='green', s=5, label=sel_miner) + + # 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=(0.5, -0.25), loc='upper center', 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) diff --git a/app/plan_d_action/utils/interface/__init__.py b/app/plan_d_action/utils/interface/__init__.py new file mode 100644 index 0000000..142b2df --- /dev/null +++ b/app/plan_d_action/utils/interface/__init__.py @@ -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 +) diff --git a/app/plan_d_action/utils/interface/config.py b/app/plan_d_action/utils/interface/config.py new file mode 100644 index 0000000..5a0a38d --- /dev/null +++ b/app/plan_d_action/utils/interface/config.py @@ -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) diff --git a/app/plan_d_action/utils/interface/export.py b/app/plan_d_action/utils/interface/export.py new file mode 100644 index 0000000..31edd24 --- /dev/null +++ b/app/plan_d_action/utils/interface/export.py @@ -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 diff --git a/app/plan_d_action/utils/interface/niveau_utils.py b/app/plan_d_action/utils/interface/niveau_utils.py new file mode 100644 index 0000000..8cba66b --- /dev/null +++ b/app/plan_d_action/utils/interface/niveau_utils.py @@ -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 diff --git a/app/plan_d_action/utils/interface/parser.py b/app/plan_d_action/utils/interface/parser.py new file mode 100644 index 0000000..ffce8e3 --- /dev/null +++ b/app/plan_d_action/utils/interface/parser.py @@ -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 diff --git a/app/plan_d_action/utils/interface/selection.py b/app/plan_d_action/utils/interface/selection.py new file mode 100644 index 0000000..544ac37 --- /dev/null +++ b/app/plan_d_action/utils/interface/selection.py @@ -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 diff --git a/app/plan_d_action/utils/interface/visualization.py b/app/plan_d_action/utils/interface/visualization.py new file mode 100644 index 0000000..946dd5c --- /dev/null +++ b/app/plan_d_action/utils/interface/visualization.py @@ -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 diff --git a/app/plan_d_action/utils/jobs/59479ea2.md b/app/plan_d_action/utils/jobs/59479ea2.md new file mode 100644 index 0000000..a40e2f1 --- /dev/null +++ b/app/plan_d_action/utils/jobs/59479ea2.md @@ -0,0 +1,11344 @@ +# Évaluation des vulnérabilités critiques + +## Introduction + +Ce rapport analyse les vulnérabilités de la chaîne de fabrication du numérique pour : + +* les produits finaux : Procédé DUV, Procédé EUV, Station de travail portable, Télévision/Écran +* les composants : Processeur ARM, Creuset quartz, Boîtier, Écran OLED, Creuset graphite, Connecteurs, Caméra, Carte mère, Batterie, Audio, Connectivité, Mémoire RAM, SSD 2.5, Capteurs, Écran Mini et Micro LED, Écran LCD/TFT +* les minerais : Chrome, Gallium, Scandium, Hafnium, Germanium, Silicium, Phosphore, Tungstène, Antimoine, Palladium, Or, Platine, Quartz, Magnésium, Zinc, Manganèse, Titane, Cérium, Yttrium, Béryllium, Étain, Nickel, Argent, Cuivre, Holmium, Lanthane, Tantale, Graphite, Lithium, Cobalt, Praséodyme, Néodyme, Dysprosium, Terbium, Europium, Samarium, Erbium, Gadolinium + +# Chemins critiques + +## Chaînes avec risque critique + +*Ces chaînes comprennent au moins une vulnérabilité combinée élevée à critique* + +### Station de travail portable → Processeur ARM → Scandium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Scandium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 108 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 55 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 55 - :orange-badge[Orange] + +### Station de travail portable → Processeur ARM → Hafnium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Hafnium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.80 - :red-badge[Rouge] + * IVC: 126 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 40 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] + +### Station de travail portable → Processeur ARM → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Processeur ARM → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Processeur ARM → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Processeur ARM → Tungstène + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Tungstène): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 69 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Processeur ARM → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Processeur ARM → Palladium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Palladium): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 33 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Station de travail portable → Processeur ARM → Or + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 34 - :green-badge[Vert] +* Minerai (Or): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.62 - :red-badge[Rouge] + * IVC: 68 - :red-badge[Rouge] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 2 - :green-badge[Vert] + * ISG combiné: 43 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 33 - :red-badge[Rouge] + * ISG combiné: 28 - :green-badge[Vert] + +### Station de travail portable → Boîtier → Chrome + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Chrome): :green-badge[FAIBLE] + * ICS moyen: 0.39 - :orange-badge[Orange] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 24 - :orange-badge[Orange] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 23 - :orange-badge[Orange] + * ISG combiné: 55 - :orange-badge[Orange] + +### Station de travail portable → Boîtier → Magnésium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Magnésium): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 28 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 73 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Boîtier → Zinc + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Zinc): :green-badge[FAIBLE] + * ICS moyen: 0.30 - :orange-badge[Orange] + * IVC: 12 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 14 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 18 - :orange-badge[Orange] + * ISG combiné: 44 - :orange-badge[Orange] + +### Station de travail portable → Boîtier → Manganèse + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Manganèse): :green-badge[FAIBLE] + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 12 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Boîtier → Titane + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Titane): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.67 - :red-badge[Rouge] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 13 - :green-badge[Vert] + * ISG combiné: 48 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 14 - :green-badge[Vert] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Écran OLED → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 38 - :red-badge[Rouge] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Écran OLED → Cérium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 38 - :red-badge[Rouge] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Cérium): MOYENNE + * ICS moyen: 0.68 - :red-badge[Rouge] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 51 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 55 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Station de travail portable → Écran OLED → Yttrium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 38 - :red-badge[Rouge] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Yttrium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.72 - :red-badge[Rouge] + * IVC: 28 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 61 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Palladium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Palladium): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 33 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Or + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Or): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.62 - :red-badge[Rouge] + * IVC: 68 - :red-badge[Rouge] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 2 - :green-badge[Vert] + * ISG combiné: 43 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 33 - :red-badge[Rouge] + * ISG combiné: 28 - :green-badge[Vert] + +### Station de travail portable → Connecteurs → Béryllium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Béryllium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 15 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 48 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 47 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Étain + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Étain): MOYENNE + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 10 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Nickel + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Nickel): MOYENNE + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 20 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 27 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 23 - :orange-badge[Orange] + * ISG combiné: 50 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Argent + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Argent): MOYENNE + * ICS moyen: 0.61 - :red-badge[Rouge] + * IVC: 11 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 12 - :green-badge[Vert] + * ISG combiné: 53 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 9 - :green-badge[Vert] + * ISG combiné: 41 - :orange-badge[Orange] + +### Station de travail portable → Connecteurs → Cuivre + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Cuivre): MOYENNE + * ICS moyen: 0.81 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 50 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Platine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Platine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 64 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 55 - :red-badge[Rouge] + * ISG combiné: 60 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 59 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Cérium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Cérium): MOYENNE + * ICS moyen: 0.68 - :red-badge[Rouge] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 51 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 55 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Holmium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Holmium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.84 - :red-badge[Rouge] + * IVC: 30 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 52 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 39 - :red-badge[Rouge] + * ISG combiné: 52 - :orange-badge[Orange] + +### Station de travail portable → Caméra → Lanthane + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Lanthane): MOYENNE + * ICS moyen: 0.59 - :orange-badge[Orange] + * IVC: 23 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 51 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Hafnium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Hafnium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.80 - :red-badge[Rouge] + * IVC: 126 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 40 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Palladium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Palladium): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 33 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Étain + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Étain): MOYENNE + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 10 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Argent + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Argent): MOYENNE + * ICS moyen: 0.61 - :red-badge[Rouge] + * IVC: 11 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 12 - :green-badge[Vert] + * ISG combiné: 53 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 9 - :green-badge[Vert] + * ISG combiné: 41 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Cuivre + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Cuivre): MOYENNE + * ICS moyen: 0.81 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 50 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Carte mère → Tantale + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Tantale): MOYENNE + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 4 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 2 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Zinc + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Zinc): :green-badge[FAIBLE] + * ICS moyen: 0.30 - :orange-badge[Orange] + * IVC: 12 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 14 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 18 - :orange-badge[Orange] + * ISG combiné: 44 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Manganèse + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Manganèse): :green-badge[FAIBLE] + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 12 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Nickel + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Nickel): MOYENNE + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 20 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 27 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 23 - :orange-badge[Orange] + * ISG combiné: 50 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Lanthane + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Lanthane): MOYENNE + * ICS moyen: 0.59 - :orange-badge[Orange] + * IVC: 23 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 51 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Graphite + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Graphite): :green-badge[FAIBLE] + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 63 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 63 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Lithium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Lithium): MOYENNE + * ICS moyen: 0.51 - :orange-badge[Orange] + * IVC: 45 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 36 - :green-badge[Vert] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Batterie → Cobalt + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] +* Minerai (Cobalt): :green-badge[FAIBLE] + * ICS moyen: 0.57 - :orange-badge[Orange] + * IVC: 7 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 49 - :red-badge[Rouge] + * ISG combiné: 77 - :red-badge[Rouge] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 27 - :red-badge[Rouge] + * ISG combiné: 60 - :orange-badge[Orange] + +### Station de travail portable → Audio → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Audio → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Audio → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Audio → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Audio → Tungstène + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Tungstène): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 69 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Audio → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Audio → Manganèse + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Manganèse): :green-badge[FAIBLE] + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 12 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Audio → Praséodyme + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Praséodyme): MOYENNE + * ICS moyen: 0.41 - :orange-badge[Orange] + * IVC: 45 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 50 - :orange-badge[Orange] + +### Station de travail portable → Audio → Néodyme + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Néodyme): MOYENNE + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 38 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 50 - :red-badge[Rouge] + * ISG combiné: 55 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 50 - :orange-badge[Orange] + +### Station de travail portable → Audio → Dysprosium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 36 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] +* Minerai (Dysprosium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 36 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 96 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 38 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Station de travail portable → Connectivité → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Connectivité → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Connectivité → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Connectivité → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Connectivité → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Connectivité → Béryllium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Béryllium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 15 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 48 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 47 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Station de travail portable → Connectivité → Samarium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Samarium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.69 - :red-badge[Rouge] + * IVC: 35 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 57 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Station de travail portable → Connectivité → Erbium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Erbium): MOYENNE + * ICS moyen: 0.74 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 52 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 48 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] + +### Station de travail portable → Mémoire RAM → Hafnium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 56 - :red-badge[Rouge] + * ISG combiné: 35 - :green-badge[Vert] +* Minerai (Hafnium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.80 - :red-badge[Rouge] + * IVC: 126 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 40 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] + +### Station de travail portable → Mémoire RAM → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 56 - :red-badge[Rouge] + * ISG combiné: 35 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Mémoire RAM → Tantale + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 56 - :red-badge[Rouge] + * ISG combiné: 35 - :green-badge[Vert] +* Minerai (Tantale): MOYENNE + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 4 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 2 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → SSD 2.5 → Hafnium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Hafnium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.80 - :red-badge[Rouge] + * IVC: 126 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 40 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] + +### Station de travail portable → SSD 2.5 → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → SSD 2.5 → Tantale + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 37 - :red-badge[Rouge] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Tantale): MOYENNE + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 4 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 2 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Platine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Platine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 64 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 55 - :red-badge[Rouge] + * ISG combiné: 60 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 59 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Manganèse + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Manganèse): :green-badge[FAIBLE] + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 12 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 53 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Holmium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Holmium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.84 - :red-badge[Rouge] + * IVC: 30 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 52 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 39 - :red-badge[Rouge] + * ISG combiné: 52 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Samarium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Samarium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.69 - :red-badge[Rouge] + * IVC: 35 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 57 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Station de travail portable → Capteurs → Gadolinium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 17 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Gadolinium): MOYENNE + * ICS moyen: 0.74 - :red-badge[Rouge] + * IVC: 7 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 52 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 48 - :orange-badge[Orange] + +### Télévision/Écran → Boîtier → Chrome + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Chrome): :green-badge[FAIBLE] + * ICS moyen: 0.39 - :orange-badge[Orange] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 24 - :orange-badge[Orange] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 23 - :orange-badge[Orange] + * ISG combiné: 55 - :orange-badge[Orange] + +### Télévision/Écran → Boîtier → Magnésium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Magnésium): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 28 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 73 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Télévision/Écran → Boîtier → Zinc + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Zinc): :green-badge[FAIBLE] + * ICS moyen: 0.30 - :orange-badge[Orange] + * IVC: 12 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 14 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 18 - :orange-badge[Orange] + * ISG combiné: 44 - :orange-badge[Orange] + +### Télévision/Écran → Boîtier → Manganèse + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Manganèse): :green-badge[FAIBLE] + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 12 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 53 - :orange-badge[Orange] + +### Télévision/Écran → Boîtier → Titane + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Minerai (Titane): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.67 - :red-badge[Rouge] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 13 - :green-badge[Vert] + * ISG combiné: 48 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 14 - :green-badge[Vert] + * ISG combiné: 53 - :orange-badge[Orange] + +### Télévision/Écran → Écran OLED → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 38 - :red-badge[Rouge] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Télévision/Écran → Écran OLED → Cérium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 38 - :red-badge[Rouge] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Cérium): MOYENNE + * ICS moyen: 0.68 - :red-badge[Rouge] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 51 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 55 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Télévision/Écran → Écran OLED → Yttrium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 38 - :red-badge[Rouge] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Yttrium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.72 - :red-badge[Rouge] + * IVC: 28 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 61 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Palladium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Palladium): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 33 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Or + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Or): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.62 - :red-badge[Rouge] + * IVC: 68 - :red-badge[Rouge] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 2 - :green-badge[Vert] + * ISG combiné: 43 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 33 - :red-badge[Rouge] + * ISG combiné: 28 - :green-badge[Vert] + +### Télévision/Écran → Connecteurs → Béryllium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Béryllium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 15 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 48 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 47 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Étain + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Étain): MOYENNE + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 10 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Nickel + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Nickel): MOYENNE + * ICS moyen: 0.47 - :orange-badge[Orange] + * IVC: 20 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 27 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 23 - :orange-badge[Orange] + * ISG combiné: 50 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Argent + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Argent): MOYENNE + * ICS moyen: 0.61 - :red-badge[Rouge] + * IVC: 11 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 12 - :green-badge[Vert] + * ISG combiné: 53 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 9 - :green-badge[Vert] + * ISG combiné: 41 - :orange-badge[Orange] + +### Télévision/Écran → Connecteurs → Cuivre + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 25 - :red-badge[Rouge] + * ISG combiné: 41 - :orange-badge[Orange] +* Minerai (Cuivre): MOYENNE + * ICS moyen: 0.81 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 50 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Platine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Platine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 64 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 55 - :red-badge[Rouge] + * ISG combiné: 60 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 59 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Cérium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Cérium): MOYENNE + * ICS moyen: 0.68 - :red-badge[Rouge] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 51 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 55 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Holmium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Holmium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.84 - :red-badge[Rouge] + * IVC: 30 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 52 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 39 - :red-badge[Rouge] + * ISG combiné: 52 - :orange-badge[Orange] + +### Télévision/Écran → Caméra → Lanthane + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 22 - :orange-badge[Orange] + * ISG combiné: 38 - :green-badge[Vert] +* Minerai (Lanthane): MOYENNE + * ICS moyen: 0.59 - :orange-badge[Orange] + * IVC: 23 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 51 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Hafnium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Hafnium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.80 - :red-badge[Rouge] + * IVC: 126 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 40 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Palladium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Palladium): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 24 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 59 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 33 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Étain + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Étain): MOYENNE + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 10 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Argent + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Argent): MOYENNE + * ICS moyen: 0.61 - :red-badge[Rouge] + * IVC: 11 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 12 - :green-badge[Vert] + * ISG combiné: 53 - :orange-badge[Orange] +* Traitement (Traitement): :green-badge[FAIBLE] + * IHH: 9 - :green-badge[Vert] + * ISG combiné: 41 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Cuivre + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Cuivre): MOYENNE + * ICS moyen: 0.81 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): MOYENNE + * IHH: 15 - :orange-badge[Orange] + * ISG combiné: 50 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Télévision/Écran → Carte mère → Tantale + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Minerai (Tantale): MOYENNE + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 4 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 2 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Télévision/Écran → Connectivité → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Télévision/Écran → Connectivité → Germanium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Germanium): MOYENNE + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 89 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Télévision/Écran → Connectivité → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Télévision/Écran → Connectivité → Phosphore + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Phosphore): MOYENNE + * ICS moyen: 0.50 - :orange-badge[Orange] + * IVC: 32 - :orange-badge[Orange] +* Extraction (Extraction): MOYENNE + * IHH: 20 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): MOYENNE + * IHH: 19 - :orange-badge[Orange] + * ISG combiné: 54 - :orange-badge[Orange] + +### Télévision/Écran → Connectivité → Antimoine + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Antimoine): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.60 - :red-badge[Rouge] + * IVC: 27 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 41 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 43 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] + +### Télévision/Écran → Connectivité → Béryllium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Béryllium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.64 - :red-badge[Rouge] + * IVC: 15 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 48 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 47 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] + +### Télévision/Écran → Connectivité → Samarium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Samarium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.69 - :red-badge[Rouge] + * IVC: 35 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 57 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Télévision/Écran → Connectivité → Erbium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 39 - :green-badge[Vert] +* Minerai (Erbium): MOYENNE + * ICS moyen: 0.74 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 52 - :red-badge[Rouge] + * ISG combiné: 58 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 48 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] + +### Télévision/Écran → Mémoire RAM → Hafnium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 56 - :red-badge[Rouge] + * ISG combiné: 35 - :green-badge[Vert] +* Minerai (Hafnium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.80 - :red-badge[Rouge] + * IVC: 126 - :red-badge[Rouge] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 35 - :red-badge[Rouge] + * ISG combiné: 40 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 44 - :orange-badge[Orange] + +### Télévision/Écran → Mémoire RAM → Silicium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 56 - :red-badge[Rouge] + * ISG combiné: 35 - :green-badge[Vert] +* Minerai (Silicium): MOYENNE + * ICS moyen: 0.90 - :red-badge[Rouge] + * IVC: 5 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 45 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 32 - :red-badge[Rouge] + * ISG combiné: 45 - :orange-badge[Orange] + +### Télévision/Écran → Mémoire RAM → Tantale + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): MOYENNE + * IHH: 56 - :red-badge[Rouge] + * ISG combiné: 35 - :green-badge[Vert] +* Minerai (Tantale): MOYENNE + * ICS moyen: 0.70 - :red-badge[Rouge] + * IVC: 4 - :green-badge[Vert] +* Extraction (Extraction): :green-badge[FAIBLE] + * IHH: 2 - :green-badge[Vert] + * ISG combiné: 52 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 31 - :red-badge[Rouge] + * ISG combiné: 47 - :orange-badge[Orange] + +### Télévision/Écran → Écran Mini et Micro LED → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Télévision/Écran → Écran Mini et Micro LED → Yttrium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Yttrium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.72 - :red-badge[Rouge] + * IVC: 28 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 56 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 61 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Télévision/Écran → Écran Mini et Micro LED → Terbium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Terbium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.81 - :red-badge[Rouge] + * IVC: 28 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 62 - :red-badge[Rouge] + * ISG combiné: 52 - :orange-badge[Orange] + +### Télévision/Écran → Écran Mini et Micro LED → Europium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :green-badge[FAIBLE] + * IHH: 21 - :orange-badge[Orange] + * ISG combiné: 37 - :green-badge[Vert] +* Minerai (Europium): MOYENNE + * ICS moyen: 0.81 - :red-badge[Rouge] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 48 - :orange-badge[Orange] + +### Télévision/Écran → Écran LCD/TFT → Gallium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 42 - :orange-badge[Orange] +* Minerai (Gallium): :green-badge[FAIBLE] + * ICS moyen: 0.45 - :orange-badge[Orange] + * IVC: 1 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 94 - :red-badge[Rouge] + * ISG combiné: 54 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] + +### Télévision/Écran → Écran LCD/TFT → Cérium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 42 - :orange-badge[Orange] +* Minerai (Cérium): MOYENNE + * ICS moyen: 0.68 - :red-badge[Rouge] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 51 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 55 - :red-badge[Rouge] + * ISG combiné: 51 - :orange-badge[Orange] + +### Télévision/Écran → Écran LCD/TFT → Terbium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 42 - :orange-badge[Orange] +* Minerai (Terbium): :red-badge[ÉLEVÉE à CRITIQUE] + * ICS moyen: 0.81 - :red-badge[Rouge] + * IVC: 28 - :orange-badge[Orange] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 53 - :red-badge[Rouge] + * ISG combiné: 53 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 62 - :red-badge[Rouge] + * ISG combiné: 52 - :orange-badge[Orange] + +### Télévision/Écran → Écran LCD/TFT → Europium + +**Vulnérabilités identifiées:** + +* Assemblage (Assemblage): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 30 - :red-badge[Rouge] + * ISG combiné: 46 - :orange-badge[Orange] +* Fabrication (Fabrication): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 28 - :red-badge[Rouge] + * ISG combiné: 42 - :orange-badge[Orange] +* Minerai (Europium): MOYENNE + * ICS moyen: 0.81 - :red-badge[Rouge] + * IVC: 0 - :green-badge[Vert] +* Extraction (Extraction): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 34 - :red-badge[Rouge] + * ISG combiné: 49 - :orange-badge[Orange] +* Traitement (Traitement): :red-badge[ÉLEVÉE à CRITIQUE] + * IHH: 42 - :red-badge[Rouge] + * ISG combiné: 48 - :orange-badge[Orange] + +## Chaînes avec risque majeur + +*Ces chaînes comprennent au moins trois vulnérabilités combinées moyennes* + +Aucune chaîne à risque majeur identifiée. + +## Chaînes avec risque moyen + +*Ces chaînes comprennent au moins une vulnérabilité combinée moyenne* + +Aucune chaîne à risque moyen identifiée. + +## Détails des opérations + +### Procédé DUV et Assemblage + +Les scanners **DUV** (Deep Ultraviolet – 193 nm ArF immersion / 193 nm ArF sec / 248 nm KrF) couvrent les nœuds **28 nm à 7 nm** (couches critiques) et les niveaux moins exigeants. +Un ArF immersion de dernière génération (**TWINSCAN NXT:2100i**) compte environ **55 000 pièces**, pèse 115 t et coûte **90 – 140 M€**. +Les KrF modernes (**NSR‑S635E**, Nikon) se vendent autour de **45 M€**. + +Le flux d’assemblage s’effectue en 4 phases : + +1. **Pré‑intégration modules** (laser, optique, châssis) aux Pays‑Bas ou au Japon +2. **Intégration finale en salle blanche** (ASML Veldhoven, Nikon Kumagaya/Hiroshima, Canon Utsunomiya) +3. **Démontage logistique** (≈ 15–18 conteneurs) +4. **Ré‑assemblage & qualification** chez le fondeur (3–6 mois) + +| Plateforme | λ (nm) | NA max | Débit wafers/h | Commercialisation | +| :-- | :--: | :--: | :--: | :-- | +| **TWINSCAN NXT (ASML)** | 193 i | 1,35 | 275 | 2010 – | +| **NSR‑S635E (Nikon)** | 193 i | 1,35 | 250 | 2018 – | +| **FPA‑3030iR (Canon)** | 193 i | 1,35 | 240 | 2019 – | + +#### Principaux assembleurs + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Pays-Bas | ASML | Pays-Bas | 84 % | +| **Pays-Bas** | **Total** | **Pays-Bas** | **84 %** | +| Japon | Nikon | Japon | 12 % | +| Japon | Canon | Japon | 4 % | +| **Japon** | **Total** | **Japon** | **16 %** | + +_Total 2024 : ~ 240 DUV scanners (toutes longueurs d’onde) livrés, dont 90 % destinés à la Chine._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Pays-Bas | 16.0% | 29 | :green-badge[Vert] (Stable) | +| Pays-Bas | 84.0% | 28 | :green-badge[Vert] (Stable) | + +**ISG combiné: 28 - :green-badge[Vert] (Stable)** +#### Vulnérabilité combinée IHH-ISG + +* IHH: 73 - :red-badge[Rouge] (Élevée) +* ISG combiné: 28 - :green-badge[Vert] (Stable) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +### Procédé EUV et Assemblage + +Les scanners **EUV** (Extreme Ultra Violet – λ ≈ 13,5 nm) sont les équipements clés qui permettent de graver les nœuds < 7 nm. +Une machine de dernière génération (NXE:3800E) compte plus de **100 000 pièces**, pèse 180 t et coûte 220–260 M€ (EXE > 350 M€ en High-NA) ([ASML to pass tariff costs to US customers, gain three High NA EUV customers](https://www.digitimes.com/news/a20250417VL200/asml-euv-2025-earnings-demand.html), [ASML Is the Chip-Equipment Leader. Its Stock Is Poised to Bounce Back.](https://www.barrons.com/articles/asml-stock-chip-equipment-cb5b6b40?utm_source=chatgpt.com)). +Le flux d’assemblage se déroule en 4 grandes phases : + +1. **Pré-intégration modules** (source, optique, châssis) aux Pays-Bas et en Allemagne +2. **Intégration finale en salle blanche** ASML Veldhoven +3. **Démontage logistique** (≈ 35 conteneurs + 3 avions cargo) +4. **Ré-assemblage & qualification** chez le fondeur (6–9 mois) + +Les générations : + +| Plateforme | NA | Débit wafers/h | Commercialisation | +| :-- | :--: | :--: | :-- | +| **NXE** | 0,33 | 220 | 2019– | +| **EXE (High-NA)** | 0,55 | 185* | 2024– *(phase R&D)* | + +#### Principaux assembleurs + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Pays-Bas | ASML | Pays-Bas | 100 % | +| **Pays-Bas** | **Total** | **Pays-Bas** | **100 %** | + +_Total 2024 : 55 NXE livrées, 5 EXE High-NA déjà en R&D chez Intel, TSMC, Samsung_ ([ASML to pass tariff costs to US customers, gain three High NA EUV customers](https://www.digitimes.com/news/a20250417VL200/asml-euv-2025-earnings-demand.html), [Belgium's imec reports breakthroughs with new ASML chip printing machine](https://www.reuters.com/technology/belgiums-imec-reports-breakthroughs-with-new-asml-chip-printing-machine-2024-08-07/?utm_source=chatgpt.com)). + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Pays-Bas | 100.0% | 28 | :green-badge[Vert] (Stable) | + +**ISG combiné: 28 - :green-badge[Vert] (Stable)** +#### Vulnérabilité combinée IHH-ISG + +* IHH: 100 - :red-badge[Rouge] (Élevée) +* ISG combiné: 28 - :green-badge[Vert] (Stable) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +### Station de travail portable et Assemblage + +Les ordinateurs portables représentent un segment majeur du marché des périphériques informatiques, avec environ 230 millions d'unités produites annuellement dont les stations de travail portable sont la branche « puissantes ». Leur assemblage constitue un processus complexe combinant intégration électronique, mécanique de précision et optimisation thermique dans un format compact. Le processus comprend généralement l'installation de la carte mère, du processeur et du système de refroidissement, suivie du montage de l'écran, du clavier, du touchpad, de la batterie et du boîtier. Ces étapes sont complétées par des tests fonctionnels approfondis. Contrairement aux ordinateurs de bureau, les portables se caractérisent par leur haut niveau d'intégration et leur conception optimisée pour l'efficacité énergétique et la portabilité. La production est fortement concentrée en Asie, avec quelques initiatives récentes de diversification géographique pour certains modèles haut de gamme ou spécialisés (gaming, professionnels). + +#### Principaux assembleurs + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Quanta Computer | Taïwan | 24 % | +| Chine | Compal Electronics | Taïwan | 20 % | +| Chine | Foxconn | Taïwan | 13 % | +| **Chine** | **Total** | **Chine** | **56 %** | +| Taïwan | Wistron | Taïwan | 9 % | +| Taïwan | Pegatron | Taïwan | 7 % | +| **Taïwan** | **Total** | **Taïwan** | **16 %** | +| Vietnam | Compal Vietnam | Taïwan | 7 % | +| Vietnam | Foxconn Vietnam | Taïwan | 4 % | +| **Vietnam** | **Total** | **Vietnam** | **11 %** | +| Mexique | Flex | États-Unis | 3 % | +| **Mexique** | **Total** | **Mexique** | **3 %** | +| Brésil | Foxconn Brasil | Taïwan | 2 % | +| Brésil | Positivo | Brésil | 1 % | +| **Brésil** | **Total** | **Brésil** | **3 %** | +| Inde | Flex India | États-Unis | 1 % | +| Inde | Dixon Technologies | Inde | 1 % | +| **Inde** | **Total** | **Inde** | **2 %** | + +**Unité** : millions d'unités / an +**Total** : 230 + +_Note: Les capacités indiquées représentent la capacité d'assemblage annuelle en 2024-2025. La diversification géographique progresse lentement, avec un déplacement partiel de la production de la Chine vers le Vietnam, mais l'Asie reste largement dominante._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Inde | 2.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Mexique | 3.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Vietnam | 11.0% | 48 | :orange-badge[Orange] (Intermédiaire) | +| Brésil | 3.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Taiwan | 16.0% | 30 | :green-badge[Vert] (Stable) | +| Chine | 56.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire)** +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **14** | | | +| **Pays** | | | **36** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **14**, ce qui indique une **concentration faible**. Bien que **Quanta Computer (24 %)**, **Compal Electronics (20 %)**, **Foxconn (13 %)** regroupent plus de 57 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **36**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (57 %)**, **Taïwan (16 %)**, **Vietnam (11 %)**, représentant ensemble plus de 84 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 14) +- La **concentration géographique est élevée** (IHH 36) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 35 - :red-badge[Rouge] (Élevée) +* ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +### Télévision/Écran et Assemblage + +Les téléviseurs et écrans représentent un marché mondial significatif avec environ 220 millions d'unités produites annuellement. Leur assemblage constitue un processus industriel complexe qui a considérablement évolué avec la transition des technologies d'affichage, passant des écrans LCD traditionnels aux technologies OLED, MiniLED et MicroLED. Le processus d'assemblage comprend l'intégration du panneau d'affichage (composant le plus critique et coûteux), de la carte électronique principale, des systèmes audio, du rétroéclairage (pour LCD/MiniLED), des modules de connectivité et du boîtier. Contrairement à d'autres appareils électroniques, l'assemblage des téléviseurs présente des défis uniques liés à la manipulation de grands panneaux fragiles et à la gestion d'une chaîne logistique adaptée à des produits volumineux. La production est dominée par quelques acteurs majeurs, principalement concentrés en Asie, avec une spécialisation croissante selon les technologies d'affichage. + +#### Principaux assembleurs + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | TCL | Chine | 16 % | +| Chine | Hisense | Chine | 14 % | +| Chine | Foxconn | Taïwan | 11 % | +| **Chine** | **Total** | **Chine** | **41 %** | +| Corée du Sud | Samsung Electronics | Corée du Sud | 20 % | +| Corée du Sud | LG Electronics | Corée du Sud | 14 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **34 %** | +| Mexique | Samsung México | Corée du Sud | 7 % | +| Mexique | LG Electronics México | Corée du Sud | 5 % | +| **Mexique** | **Total** | **Mexique** | **12 %** | +| Pologne | LG Electronics Polska | Corée du Sud | 4 % | +| Pologne | TCL Europe | Chine | 3 % | +| **Pologne** | **Total** | **Pologne** | **7 %** | + +_Note: Les capacités indiquées représentent la capacité d'assemblage annuelle en 2024-2025. On observe une spécialisation technologique et géographique, avec la Corée du Sud dominante dans les technologies premium (OLED, MicroLED) et la Chine dans les LCD et MiniLED._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 41.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Mexique | 12.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Corée du Sud | 34.0% | 33 | :green-badge[Vert] (Stable) | +| Pologne | 7.0% | 37 | :green-badge[Vert] (Stable) | + +**ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire)** +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **13** | | | +| **Pays** | | | **30** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **13**, ce qui indique une **concentration faible**. Bien que **Samsung Electronics (20 %)**, **TCL (16 %)**, **Hisense (14 %)** regroupent plus de 50 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **30**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (41 %)**, **Corée du Sud (34 %)**, **Mexique (12 %)**, représentant ensemble plus de 87 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 13) +- La **concentration géographique est élevée** (IHH 30) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 30 - :red-badge[Rouge] (Élevée) +* ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +### Processeur ARM et Fabrication + +Les processeurs ARM représentent une famille d'architectures RISC (Reduced Instruction Set Computing) sous licence, dominante dans les appareils mobiles et l'informatique embarquée grâce à leur excellente efficacité énergétique. Contrairement aux architectures x86 (Intel/AMD) qui privilégient la performance brute, ARM optimise le rapport performance/consommation, permettant des autonomies prolongées pour les appareils sur batterie. Développée initialement par Acorn Computers puis ARM Holdings (aujourd'hui Arm Limited), cette architecture est commercialisée sous forme de licences de propriété intellectuelle. Les fabricants adaptent ensuite ces designs à leurs besoins spécifiques, créant une grande diversité d'implémentations. Les processeurs ARM modernes couvrent un large spectre de performances, des microcontrôleurs simples aux puces hautes performances comme l'Apple M3 (3nm) ou le Qualcomm Snapdragon 8 Gen 3, intégrant jusqu'à 16 cœurs et des fréquences dépassant 3.7 GHz. Cette omniprésence fait d'ARM un acteur stratégique dans l'écosystème technologique mondial, avec plus de 230 milliards de puces expédiées à ce jour. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Taïwan | TSMC | Taïwan | 54 % | +| **Taïwan** | **Total** | **Taïwan** | **54 %** | +| Corée du Sud | Samsung Foundry | Corée du Sud | 22 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **22 %** | +| États-Unis | GlobalFoundries | États-Unis | 11 % | +| États-Unis | Intel Foundry | États-Unis | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **14 %** | +| Chine | SMIC | Chine | 8 % | +| **Chine** | **Total** | **Chine** | **8 %** | +| Singapour | GlobalFoundries | États-Unis | 2 % | +| **Singapour** | **Total** | **Singapour** | **2 %** | + +**Unités** : million d'unité/an + +**Total** : 1575 + +_Note: La production est assurée par des fonderies pour le compte de concepteurs sans usines (fabless). La conception des processeurs ARM n'est pas incluse dans cette fiche._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| États-Unis | 14.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Taiwan | 54.0% | 30 | :green-badge[Vert] (Stable) | +| Corée du Sud | 22.0% | 33 | :green-badge[Vert] (Stable) | +| Singapour | 2.0% | 23 | :green-badge[Vert] (Stable) | +| Chine | 8.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 34 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **36** | +| **Pays** | | | **37** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **36**, signalant une **concentration élevée**. Le marché est largement dominé par **TSMC (54 %)**, **Samsung Foundry (22 %)**, **GlobalFoundries (11 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **37**, révélant une **concentration géographique élevée**. La répartition est dominée par **Taïwan (54 %)**, **Corée du Sud (22 %)**, **États-Unis (14 %)**, représentant ensemble plus de 90 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 36) +- La **concentration géographique est élevée** (IHH 37) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 37 - :red-badge[Rouge] (Élevée) +* ISG combiné: 34 - :green-badge[Vert] (Stable) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +### Boîtier et Fabrication + +Le boîtier constitue l'enveloppe externe des appareils électroniques, remplissant des fonctions multiples bien au-delà de la simple esthétique. Cette structure sert à la fois de protection mécanique pour les composants internes contre les chocs et l'humidité, de barrière électromagnétique, de support structurel pour l'assemblage et de dissipateur thermique. L'évolution des boîtiers s'est caractérisée par une optimisation constante du rapport résistance/poids, une miniaturisation progressive et une intégration croissante de fonctionnalités. Les matériaux utilisés varient considérablement selon l'application: alliages d'aluminium légers pour les appareils portables premium, magnésium pour les ultrabooks haute performance, plastiques renforcés pour les dispositifs grand public abordables, et acier pour les équipements industriels nécessitant robustesse et blindage électromagnétique. La conception des boîtiers est devenue une discipline complexe à l'intersection de l'ingénierie des matériaux, du design industriel et de la gestion thermique, tout en intégrant des considérations environnementales croissantes concernant la recyclabilité et l'empreinte carbone. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Foxconn | Taïwan | 28 % | +| Chine | BYD Precision | Chine | 15 % | +| Chine | Jabil Circuit | États-Unis | 9 % | +| **Chine** | **Total** | **Chine** | **52 %** | +| Taïwan | Pegatron | Taïwan | 8 % | +| Taïwan | Catcher Technology | Taïwan | 7 % | +| **Taïwan** | **Total** | **Taïwan** | **15 %** | +| Thaïlande | SVI | Thaïlande | 5 % | +| Thaïlande | Cal-Comp | Taïwan | 4 % | +| **Thaïlande** | **Total** | **Thaïlande** | **9 %** | +| Vietnam | Goertek | Chine | 7 % | +| **Vietnam** | **Total** | **Vietnam** | **7 %** | +| Mexique | Flex | États-Unis | 6 % | +| **Mexique** | **Total** | **Mexique** | **6 %** | +| Allemagne | Siemens | Allemagne | 4 % | +| **Allemagne** | **Total** | **Allemagne** | **4 %** | +| États-Unis | Apple (design) | États-Unis | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **3 %** | + +**Unités** : millions d'unités/an + +**Total** : 2500 + +_Note: Les chiffres de capacité représentent des estimations à jour (2024-2025) et incluent uniquement les boîtiers pour électronique, pas les composants structurels pour d'autres industries._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Allemagne | 4.0% | 30 | :green-badge[Vert] (Stable) | +| Thailande | 9.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Vietnam | 7.0% | 48 | :orange-badge[Orange] (Intermédiaire) | +| Mexique | 6.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Taiwan | 15.0% | 30 | :green-badge[Vert] (Stable) | +| États-Unis | 3.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 52.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | 14 | | | +| **Pays** | | | 31 | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **14**, signalant une **concentration faible**. **Foxconn (28 %)**, **BYD Precision (15 %)**, **Jabil Circuit (9 %)** représentent un peu plus de la moitié du marché. + +##### IHH par pays + +L’IHH par pays atteint **31**, révélant une **concentration géographique élevée**. **Chine (52 %)**, **Taïwan (15 %)**, **Thaïlande (9 %)** concentrent plus de 76 % des capacités. + +##### En résumé +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 14) +- La **concentration géographique est élevée** (IHH 31) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 31 - :red-badge[Rouge] (Élevée) +* ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +### Écran OLED et Fabrication + +Les écrans Mini LED représentent une évolution majeure des technologies d'affichage, positionnée entre les écrans LCD traditionnels et les OLED. Cette technologie conserve la structure de base d'un écran LCD (couche de cristaux liquides entre deux substrats) mais révolutionne le système de rétroéclairage en remplaçant les quelques dizaines de LED conventionnelles par des milliers de mini-LED, chacune mesurant généralement entre 50 et 200 microns. Cette multiplication des sources lumineuses permet d'obtenir un contrôle beaucoup plus précis de l'éclairage par zones (local dimming), améliorant significativement le contraste, la luminosité maximale et la précision des noirs. Les écrans Mini LED atteignent ainsi des performances visuelles proches des OLED tout en conservant les avantages des LCD: durée de vie prolongée, absence de risque de marquage et luminosité supérieure. Cette technologie est principalement employée dans les téléviseurs premium, les moniteurs professionnels, les ordinateurs portables haut de gamme et certains appareils mobiles. Le marché mondial des écrans Mini LED connaît une croissance rapide, estimée à plus de 35% annuellement, atteignant environ 6 milliards de dollars en 2024. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Taïwan | Epistar | Taïwan | 12 % | +| Taïwan | AU Optronics | Taïwan | 10 % | +| Taïwan | Innolux | Taïwan | 8 % | +| **Taïwan** | **Total** | **Taïwan** | **30 %** | +| Corée du Sud | Samsung Display | Corée du Sud | 14 % | +| Corée du Sud | LG Display | Corée du Sud | 11 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **25 %** | +| Chine | BOE Technology | Chine | 11 % | +| Chine | CSOT (TCL) | Chine | 9 % | +| **Chine** | **Total** | **Chine** | **20 %** | +| Japon | Sony | Japon | 5 % | +| Japon | Sharp/Foxconn | Japon | 4 % | +| **Japon** | **Total** | **Japon** | **9 %** | +| États-Unis | Apple (design) | États-Unis | 4 % | +| États-Unis | Lumileds | États-Unis | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **7 %** | +| Allemagne | OSRAM Opto | Allemagne | 4 % | +| **Allemagne** | **Total** | **Allemagne** | **4 %** | +| Vietnam | Samsung Vietnam | Corée du Sud | 3 % | +| **Vietnam** | **Total** | **Vietnam** | **3 %** | + +**Unités** : million d'unité/an + +**Total** : 81 + +_Note: Les capacités de production concernent spécifiquement les écrans Mini LED (pas LCD standard) et sont estimées pour 2024-2025. Le marché est en évolution rapide avec une augmentation notable des capacités prévue dans les prochaines années._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Corée du Sud | 54.0% | 33 | :green-badge[Vert] (Stable) | +| Chine | 28.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Taiwan | 3.0% | 30 | :green-badge[Vert] (Stable) | +| Japon | 9.0% | 29 | :green-badge[Vert] (Stable) | +| Vietnam | 2.0% | 48 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 0.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| États-Unis | 2.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 39 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **9** | | | +| **Pays** | | **21** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **9**, ce qui indique une **concentration faible**. Bien que **Samsung Display (14 %)**, **Epistar (12 %)**, **LG Display (11 %)** regroupent plus de 37 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **21**, révélant une **concentration géographique modérée**. La répartition est dominée par **Taïwan (30 %)**, **Corée du Sud (25 %)**, **Chine (20 %)**, représentant ensemble plus de 75 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 9) +- La **concentration géographique est modérée** (IHH 21) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 38 - :red-badge[Rouge] (Élevée) +* ISG combiné: 39 - :green-badge[Vert] (Stable) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +### Connecteurs et Fabrication + +Les connecteurs représentent les interfaces critiques qui assurent la continuité électrique entre différents composants et circuits des appareils électroniques. Ces éléments d'interconnexion permettent le passage des signaux électriques, de l'alimentation, et parfois des données à haute vitesse, tout en maintenant l'intégrité du signal. La diversité des connecteurs est immense, allant des microconnecteurs ultrafins pour smartphones aux robustes interfaces industrielles. La fabrication de connecteurs modernes implique une précision micrométrique, particulièrement pour les versions à haute densité pouvant intégrer plusieurs centaines de contacts sur quelques centimètres carrés. + +Les exigences de miniaturisation, de résistance aux vibrations, d'étanchéité et de durabilité (plusieurs milliers de cycles d'insertion/extraction) orientent les développements technologiques de ce secteur. L'industrie des connecteurs représente un marché mondial d'environ 80 milliards de dollars, avec une croissance annuelle stable de 5 à 7 %, stimulée par la densification des appareils électroniques et l'augmentation des débits de transmission. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Foxconn | Taïwan | 20 % | +| Chine | Luxshare Precision | Chine | 14 % | +| Chine | Lotes | Taïwan | 8 % | +| **Chine** | **Total** | **Chine** | **42 %** | +| Taïwan | Foxconn Taiwan | Taïwan | 12 % | +| Taïwan | Cheng Uei | Taïwan | 6 % | +| **Taïwan** | **Total** | **Taïwan** | **18 %** | +| Japon | Hirose Electric | Japon | 9 % | +| Japon | Japan Aviation Electronics | Japon | 5 % | +| **Japon** | **Total** | **Japon** | **14 %** | +| États-Unis | TE Connectivity | États-Unis | 6 % | +| États-Unis | Amphenol | États-Unis | 4 % | +| **États-Unis** | **Total** | **États-Unis** | **10 %** | +| Allemagne | HARTING | Allemagne | 4 % | +| Allemagne | ERNI | Allemagne | 3 % | +| **Allemagne** | **Total** | **Allemagne** | **7 %** | +| Corée du Sud | Samsung Electro-Mechanics | Corée du Sud | 5 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **5 %** | +| Suisse | Fischer Connectors | Suisse | 2 % | +| **Suisse** | **Total** | **Suisse** | **2 %** | + +*Note : Les capacités indiquées représentent la production annuelle estimée en 2024-2025, regroupée par lieu de siège social principal de l'entreprise. Si la production est mondialisée, la concentration géographique des acteurs ressort dans ces chiffres.* + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| États-Unis | 10.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 42.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Allemagne | 7.0% | 30 | :green-badge[Vert] (Stable) | +| Japon | 14.0% | 29 | :green-badge[Vert] (Stable) | +| Taiwan | 18.0% | 30 | :green-badge[Vert] (Stable) | +| Corée du Sud | 5.0% | 33 | :green-badge[Vert] (Stable) | +| Suisse | 2.0% | 22 | :green-badge[Vert] (Stable) | + +**ISG combiné: 41 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | 11 | | | +| **Pays** | | | 25 | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **11**, traduisant une **concentration faible**. + +##### IHH par pays + +L’IHH par pays atteint **25**, soulignant une **concentration géographique élevée**. + +##### En résumé +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 11) +- La **concentration géographique est élevée** (IHH 25) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 25 - :red-badge[Rouge] (Élevée) +* ISG combiné: 41 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +### Caméra et Fabrication + +La caméra représente aujourd'hui un élément critique des appareils électroniques modernes, des smartphones aux ordinateurs portables en passant par les véhicules autonomes et les systèmes de surveillance. Ce composant optoélectronique complexe transforme la lumière en signaux électriques grâce à des capteurs d'image qui peuvent désormais intégrer plusieurs dizaines de millions de pixels sur des surfaces inférieures à 1 cm². L'écosystème des caméras comprend principalement le capteur d'image (CMOS ou CCD), le groupe d'optiques (lentilles, prismes), le système de mise au point, et un circuit intégré spécialisé (ISP) pour le traitement du signal. Les avancées technologiques récentes ont permis une miniaturisation spectaculaire tout en augmentant considérablement les performances (sensibilité, dynamique, résolution). Le marché mondial des caméras pour appareils électroniques représente plus de 30 milliards de dollars, avec une croissance annuelle d'environ 8%, stimulée par la multiplication des capteurs dans les smartphones (jusqu'à 5 par appareil) et l'explosion des applications en vision artificielle. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Japon | Sony Semiconductor | Japon | 20 % | +| Japon | Nikon | Japon | 6 % | +| Japon | Canon | Japon | 5 % | +| **Japon** | **Total** | **Japon** | **31 %** | +| Chine | O-Film Technology | Chine | 11 % | +| Chine | Sunny Optical | Chine | 9 % | +| Chine | Largan Precision | Taïwan | 7 % | +| **Chine** | **Total** | **Chine** | **27 %** | +| Corée du Sud | Samsung Electronics | Corée du Sud | 11 % | +| Corée du Sud | LG Innotek | Corée du Sud | 7 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **18 %** | +| Taïwan | Foxconn | Taïwan | 5 % | +| Taïwan | TSMC | Taïwan | 5 % | +| **Taïwan** | **Total** | **Taïwan** | **10 %** | +| Vietnam | Samsung Vietnam | Corée du Sud | 7 % | +| **Vietnam** | **Total** | **Vietnam** | **7 %** | +| Allemagne | Zeiss | Allemagne | 3 % | +| **Allemagne** | **Total** | **Allemagne** | **3 %** | +| États-Unis | OmniVision | États-Unis | 2 % | +| **États-Unis** | **Total** | **États-Unis** | **2 %** | + +**Unités** : millions d'unités/an + +**Total** : 3695 + +_Note: Les capacités indiquées représentent la production annuelle en 2024-2025. Le marché est dominé par des acteurs asiatiques, avec une forte concentration au Japon, en Chine et en Corée du Sud._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Vietnam | 7.0% | 48 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 27.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 31.0% | 29 | :green-badge[Vert] (Stable) | +| Taiwan | 10.0% | 30 | :green-badge[Vert] (Stable) | +| Corée du Sud | 18.0% | 33 | :green-badge[Vert] (Stable) | +| États-Unis | 2.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Allemagne | 3.0% | 30 | :green-badge[Vert] (Stable) | + +**ISG combiné: 38 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | 10 | | | +| **Pays** | | 22 | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, indiquant une **concentration faible**. + +##### IHH par pays + +L’IHH par pays atteint **22**, traduisant une **concentration géographique modérée**. + +##### En résumé +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est modérée** (IHH 22) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 22 - :orange-badge[Orange] (Modérée) +* ISG combiné: 38 - :green-badge[Vert] (Stable) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +### Carte mère et Fabrication + +La carte mère constitue le socle fondamental de tout dispositif électronique, servant de plateforme d’interconnexion et de communication entre l’ensemble des composants. Cette plaque de circuit imprimé multicouche (PCB) comprend typiquement entre 4 et 16 couches conductrices séparées par des matériaux isolants, formant un réseau complexe d’interconnexions pour la distribution de l’alimentation et la transmission des signaux. Son rôle est triple : structurel (supporter physiquement les composants), électrique (fournir les chemins conducteurs) et fonctionnel (orchestrer les communications entre composants). + +La fabrication des cartes mères modernes implique des procédés sophistiqués de photolithographie, de micro-perçage, de métallisation et d’assemblage automatisé. Les contraintes de miniaturisation, de dissipation thermique et d’intégrité du signal sont devenues déterminantes avec l’augmentation des fréquences de fonctionnement. + +L’industrie mondiale des cartes mères représente un marché de plus de 35 milliards de dollars, principalement concentré dans les pays asiatiques, avec une spécialisation croissante selon les segments (consommation, industriel, médical, aérospatial, etc.). + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Foxconn | Taïwan | 20 % | +| Chine | Quanta Computer | Taïwan | 15 % | +| Chine | BYD Electronics | Chine | 8 % | +| **Chine** | **Total** | **Chine** | **43 %** | +| Taïwan | ASUS | Taïwan | 8 % | +| Taïwan | Gigabyte | Taïwan | 7 % | +| Taïwan | MSI | Taïwan | 7 % | +| Taïwan | Pegatron | Taïwan | 6 % | +| **Taïwan** | **Total** | **Taïwan** | **28 %** | +| Vietnam | Intel Vietnam | États-Unis | 7 % | +| Vietnam | Samsung Electronics | Corée du Sud | 4 % | +| **Vietnam** | **Total** | **Vietnam** | **11 %** | +| Malaisie | Flex | États-Unis | 4 % | +| Malaisie | Jabil Circuit | États-Unis | 3 % | +| **Malaisie** | **Total** | **Malaisie** | **7 %** | +| Mexique | Foxconn Mexico | Taïwan | 4 % | +| **Mexique** | **Total** | **Mexique** | **4 %** | +| Thaïlande | Cal-Comp | Taïwan | 3 % | +| **Thaïlande** | **Total** | **Thaïlande** | **3 %** | +| États-Unis | Intel | États-Unis | 2 % | +| **États-Unis** | **Total** | **États-Unis** | **2 %** | + +_Note: Les capacités indiquées représentent la production annuelle estimée en 2024-2025. Le marché est dominé par des fabricants sous contrat (ODM/EMS) taïwanais, avec une forte concentration de la production en Chine continentale._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Malaisie | 7.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| Taiwan | 28.0% | 30 | :green-badge[Vert] (Stable) | +| Mexique | 4.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| États-Unis | 2.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Thailande | 3.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 43.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Vietnam | 11.0% | 48 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | 10 | | | +| **Pays** | | | 28 | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, preuve d’une **concentration faible**. + +##### IHH par pays + +L’IHH par pays est de **28**, révélant une **concentration géographique élevée**. + +##### En résumé +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est élevée** (IHH 28) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 28 - :red-badge[Rouge] (Élevée) +* ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +### Batterie et Fabrication + +La batterie constitue l'élément central d'alimentation autonome des appareils électroniques portables modernes. Ce composant complexe est une cellule électrochimique convertissant l'énergie chimique en énergie électrique via des réactions d'oxydoréduction. Les batteries lithium-ion (Li-ion) et lithium-polymère (LiPo) dominent aujourd'hui le marché de l'électronique grand public, offrant une densité énergétique élevée (jusqu'à 300 Wh/kg) tout en maintenant une durée de vie prolongée (500-1000 cycles). Leur fabrication met en jeu des procédés délicats d'assemblage multicouche impliquant la préparation des électrodes (cathode et anode), leur assemblage avec un séparateur imbibé d'électrolyte, puis leur intégration dans un boîtier scellé intégrant des circuits de protection électronique. L'industrie des batteries représente un marché estimé à plus de 120 milliards de dollars en 2024, avec une croissance annuelle d'environ 12%, soutenue par l'essor de l'électronique portable, des véhicules électriques et du stockage stationnaire d'énergie. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | CATL | Chine | 34 % | +| Chine | BYD | Chine | 14 % | +| Chine | Gotion High-Tech | Chine | 6 % | +| **Chine** | **Total** | **Chine** | **54 %** | +| Corée du Sud | LG Energy Solution | Corée du Sud | 14 % | +| Corée du Sud | Samsung SDI | Corée du Sud | 8 % | +| Corée du Sud | SK Innovation | Corée du Sud | 4 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **26 %** | +| Japon | Panasonic | Japon | 8 % | +| Japon | TDK/ATL | Japon | 5 % | +| **Japon** | **Total** | **Japon** | **12 %** | +| États-Unis | Tesla/Panasonic | États-Unis | 2 % | +| **États-Unis** | **Total** | **États-Unis** | **2 %** | +| Hongrie | Samsung SDI | Corée du Sud | 2 % | +| **Hongrie** | **Total** | **Hongrie** | **2 %** | +| Pologne | LG Energy Solution | Corée du Sud | 2 % | +| **Pologne** | **Total** | **Pologne** | **2 %** | + +**Unités** : GWh/an + +**Total** : 1910 + +_Note: Capacités et parts de marché estimées pour 2024-2025. Le marché est fortement dominé par la triade Chine-Corée-Japon, avec une prépondérance croissante de la Chine qui continue d'augmenter sa capacité tandis que les fabricants sud-coréens et japonais investissent dans des usines en Europe et aux États-Unis._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Corée du Sud | 26.0% | 33 | :green-badge[Vert] (Stable) | +| Japon | 12.0% | 29 | :green-badge[Vert] (Stable) | +| Chine | 54.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| États-Unis | 2.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Pologne | 2.0% | 37 | :green-badge[Vert] (Stable) | +| Hongrie | 2.0% | 36 | :green-badge[Vert] (Stable) | + +**ISG combiné: 44 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | 18 | | +| **Pays** | | | 37 | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **18**, ce qui indique une **concentration modérée**. **CATL (34 %)**, **BYD (14 %)**, **LG Energy Solution (14 %)** détiennent une part importante du marché. + +##### IHH par pays + +L’IHH par pays atteint **37**, révélant une **concentration géographique élevée**. **Chine (54 %)**, **Corée du Sud (26 %)**, **Japon (12 %)** représentent ensemble plus de 92 % des capacités. + +##### En résumé +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 18) +- La **concentration géographique est élevée** (IHH 37) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 37 - :red-badge[Rouge] (Élevée) +* ISG combiné: 44 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +### Audio et Fabrication + +Les composants audio constituent un élément essentiel des appareils électroniques modernes, assurant la capture, le traitement et la reproduction du son. Cette catégorie englobe principalement les haut-parleurs, écouteurs, microphones, amplificateurs, codecs et circuits de traitement audio spécialisés. Les haut-parleurs et écouteurs, qui forment la majorité du marché, reposent sur des aimants permanents en terres rares pour générer le champ magnétique nécessaire à la conversion des signaux électriques en ondes sonores. Les microphones, quant à eux, fonctionnent selon divers principes (électrostatique, électrodynamique, piézoélectrique). L'industrie des composants audio représente un marché mondial de plus de 20 milliards de dollars, largement dominé par des acteurs asiatiques, avec une forte concentration en Chine. La fabrication à grande échelle nécessite des processus de précision pour les membranes et bobines acoustiques, ainsi qu'un approvisionnement fiable en terres rares pour les aimants hautes performances. La demande croissante de miniaturisation, de qualité sonore supérieure et d'efficacité énergétique oriente l'évolution des technologies et des procédés de fabrication. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Goertek | Chine | 24 % | +| Chine | AAC Technologies | Chine | 18 % | +| Chine | Bose Manufacturing | États-Unis | 9 % | +| Chine | Knowles Electronics | États-Unis | 7 % | +| **Chine** | **Total** | **Chine** | **56 %** | +| Taïwan | Foxconn | Taïwan | 6 % | +| Taïwan | MediaTek | Taïwan | 5 % | +| Taïwan | Realtek | Taïwan | 4 % | +| **Taïwan** | **Total** | **Taïwan** | **15 %** | +| Corée du Sud | Samsung Electro-Mechanics | Corée du Sud | 7 % | +| Corée du Sud | LG Innotek | Corée du Sud | 3 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **10 %** | +| Japon | Sony | Japon | 4 % | +| Japon | Panasonic | Japon | 3 % | +| Japon | Alps Alpine | Japon | 1 % | +| **Japon** | **Total** | **Japon** | **8 %** | +| États-Unis | Cirrus Logic | États-Unis | 2 % | +| États-Unis | Texas Instruments | États-Unis | 2 % | +| **États-Unis** | **Total** | **États-Unis** | **4 %** | +| Allemagne | Infineon | Allemagne | 2 % | +| Allemagne | Beyer Dynamic | Allemagne | 1 % | +| **Allemagne** | **Total** | **Allemagne** | **3 %** | + +**Unités** : million d'unité/an + +**Total** : 3540 + +_Note: Les capacités indiquées représentent la production annuelle en 2024-2025. La concentration en Chine s'explique par l'intégration verticale avec les chaînes d'assemblage d'appareils électroniques._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 56.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Allemagne | 3.0% | 30 | :green-badge[Vert] (Stable) | +| Corée du Sud | 10.0% | 33 | :green-badge[Vert] (Stable) | +| Japon | 8.0% | 29 | :green-badge[Vert] (Stable) | +| Taiwan | 15.0% | 30 | :green-badge[Vert] (Stable) | +| États-Unis | 4.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 45 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | 12 | | | +| **Pays** | | | 36 | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **12**, ce qui indique une **concentration faible**. Bien que **Goertek (24 %)**, **AAC Technologies (18 %)**, **Bose Manufacturing (9 %)** regroupent plus de 51 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **36**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (56 %)**, **Taïwan (15 %)**, **Corée du Sud (10 %)**, représentant ensemble plus de 81 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 12) +- La **concentration géographique est élevée** (IHH 36) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 36 - :red-badge[Rouge] (Élevée) +* ISG combiné: 45 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +### Connectivité et Fabrication + +Les solutions de connectivité incluent les technologies et composants assurant la transmission sans fil ou filaire des données au sein des dispositifs électroniques. Les principaux protocoles incluent le Wi-Fi, Bluetooth, NFC, 5G, Ethernet, et les standards propriétaires. Avec l’expansion rapide de l’IoT et la nécessité de gérer des débits élevés avec une faible latence, la connectivité est devenue une composante critique des appareils modernes, des smartphones aux infrastructures industrielles. + +Le marché mondial des modules de connectivité dépasse 50 milliards de dollars avec une croissance annuelle estimée à environ 10 %, tirée par les besoins croissants en bande passante et les usages en mobilité et domotique. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Taïwan | MediaTek | Taïwan | 15 % | +| Taïwan | TSMC | Taïwan | 10 % | +| Taïwan | Realtek Semiconductor | Taïwan | 6 % | +| **Taïwan** | **Total** | **Taïwan** | **31 %** | +| États-Unis | Qualcomm | États-Unis | 12 % | +| États-Unis | Broadcom | États-Unis | 7 % | +| États-Unis | Intel | États-Unis | 5 % | +| **États-Unis** | **Total** | **États-Unis** | **24 %** | +| Chine | HiSilicon (Huawei) | Chine | 13 % | +| Chine | UNISOC | Chine | 6 % | +| **Chine** | **Total** | **Chine** | **19 %** | +| Corée du Sud | Samsung Electronics | Corée du Sud | 11 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **11 %** | +| Japon | Murata Manufacturing | Japon | 6 % | +| **Japon** | **Total** | **Japon** | **6 %** | +| Israël | Intel Israel | États-Unis | 4 % | +| **Israël** | **Total** | **Israël** | **4 %** | +| Pays-Bas | NXP Semiconductors | Pays-Bas | 3 % | +| **Pays-Bas** | **Total** | **Pays-Bas** | **3 %** | + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| États-Unis | 24.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 6.0% | 29 | :green-badge[Vert] (Stable) | +| Taiwan | 31.0% | 30 | :green-badge[Vert] (Stable) | +| Israel | 4.0% | 59 | :orange-badge[Orange] (Intermédiaire) | +| Corée du Sud | 11.0% | 33 | :green-badge[Vert] (Stable) | +| Chine | 19.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Pays-Bas | 3.0% | 28 | :green-badge[Vert] (Stable) | + +**ISG combiné: 39 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs**| 10 | | | +| **Pays** | | 21 | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, ce qui indique une **concentration faible**. Bien que **MediaTek (15 %)**, **HiSilicon (Huawei) (13 %)**, **Qualcomm (12 %)** regroupent plus de 40 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **21**, révélant une **concentration géographique modérée**. La répartition est dominée par **Taïwan (31 %)**, **États-Unis (24 %)**, **Chine (19 %)**, représentant ensemble plus de 74 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est modérée** (IHH 21) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 21 - :orange-badge[Orange] (Modérée) +* ISG combiné: 39 - :green-badge[Vert] (Stable) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +### Mémoire RAM et Fabrication + +La mémoire RAM (Random Access Memory) constitue un élément essentiel des systèmes informatiques, servant de mémoire de travail temporaire à accès rapide pour le processeur. Ce composant à semi-conducteurs utilise principalement des cellules de stockage basées sur des transistors et des condensateurs (DRAM) organisés en matrices denses. La technologie dominante actuelle, la DDR (Double Data Rate), a évolué à travers plusieurs générations, avec la DDR4 en fin de cycle et la DDR5 en déploiement progressif, offrant des fréquences atteignant 6400 MT/s et des capacités par module de 2 Go à 256 Go. Contrairement à la mémoire de stockage permanente, la RAM est volatile, perdant ses données en l'absence d'alimentation électrique. Sa fabrication implique des procédés de lithographie avancés, avec des finesses de gravure descendant jusqu'à 10-14nm, nécessitant des salles blanches ultra-propres et des équipements de précision extrême. Le marché mondial de la mémoire RAM représente environ 95 milliards de dollars, caractérisé par une forte cyclicité des prix et dominé par trois acteurs majeurs contrôlant plus de 95% de la production. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Corée du Sud | Samsung Electronics | Corée du Sud | 43 % | +| Corée du Sud | SK Hynix | Corée du Sud | 28 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **71 %** | +| États-Unis | Micron | États-Unis | 23 % | +| **États-Unis** | **Total** | **États-Unis** | **23 %** | +| Taïwan | Nanya | Taïwan | 2 % | +| Taïwan | Winbond | Taïwan | 1 % | +| **Taïwan** | **Total** | **Taïwan** | **3 %** | + +**Unités** : million d'unité/an + +**Total** : 2045 + +_Note: Les capacités indiquées représentent la production annuelle estimée en 2024-2025. Le marché est extrêmement concentré, avec trois acteurs majeurs contrôlant plus de 94% de la production mondiale._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Corée du Sud | 71.0% | 33 | :green-badge[Vert] (Stable) | +| Taiwan | 3.0% | 30 | :green-badge[Vert] (Stable) | +| États-Unis | 23.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 35 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **32** | +| **Pays** | | | **56** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **32**, signalant une **concentration élevée**. Le marché est largement dominé par **Samsung Electronics (43 %)**, **SK Hynix (28 %)**, **Micron (23 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **56**, révélant une **concentration géographique élevée**. La répartition est dominée par **Corée du Sud (71 %)**, **États-Unis (23 %)**, **Taïwan (3 %)**, représentant ensemble plus de 97 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 32) +- La **concentration géographique est élevée** (IHH 56) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 56 - :red-badge[Rouge] (Élevée) +* ISG combiné: 35 - :green-badge[Vert] (Stable) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +### SSD 2.5 et Fabrication + +Les SSD 2.5" (Solid State Drive) représentent une évolution majeure du stockage informatique, remplaçant les disques durs mécaniques par une technologie entièrement électronique sans pièces mobiles. Ces périphériques de stockage non-volatiles utilisent des cellules mémoire flash NAND pour conserver les données, même en l'absence d'alimentation électrique. Leur format physique standard de 2,5 pouces (69,85 x 100,2 mm) permet un remplacement direct des disques durs traditionnels dans la plupart des appareils. Les SSD modernes offrent des capacités allant de 128 Go à 8 To pour les modèles grand public, avec des vitesses de lecture séquentielle atteignant 560 Mo/s sur l'interface SATA III (6 Gb/s) qui représente la limite théorique de cette connexion. Les avantages majeurs incluent une résistance accrue aux chocs, une consommation électrique réduite (2-4W contre 6-8W pour les HDD), un fonctionnement silencieux et surtout des performances supérieures, particulièrement pour les opérations aléatoires (IOPS). Leur durée de vie est généralement mesurée en TBW (Terabytes Written), avec des modèles grand public garantis entre 150 et 600 TBW selon leur capacité et gamme. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Corée du Sud | Samsung | Corée du Sud | 32 % | +| Corée du Sud | SK Hynix | Corée du Sud | 12 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **44 %** | +| États-Unis | Western Digital | États-Unis | 16 % | +| États-Unis | Micron/Crucial | États-Unis | 12 % | +| États-Unis | Kingston | États-Unis | 8 % | +| États-Unis | Seagate | États-Unis | 5 % | +| **États-Unis** | **Total** | **États-Unis** | **41 %** | +| Japon | Kioxia | Japon | 9 % | +| **Japon** | **Total** | **Japon** | **9 %** | +| Chine | Yangtze Memory | Chine | 2 % | +| Chine | Longsys/Lexar | Chine | 1 % | +| **Chine** | **Total** | **Chine** | **3 %** | +| Taïwan | ADATA | Taïwan | 2 % | +| Taïwan | Transcend | Taïwan | 1 % | +| **Taïwan** | **Total** | **Taïwan** | **3 %** | + +**Unités** : million d'unité/an + +**Total** : 267 + +_Note: De nombreux fabricants (Kingston, ADATA, etc.) n'ont pas leurs propres usines de NAND et assemblent leurs produits à partir de puces achetées auprès des fabricants intégrés comme Samsung, Micron ou SK Hynix._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| États-Unis | 41.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 9.0% | 29 | :green-badge[Vert] (Stable) | +| Chine | 3.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Corée du Sud | 44.0% | 33 | :green-badge[Vert] (Stable) | +| Taiwan | 3.0% | 30 | :green-badge[Vert] (Stable) | + +**ISG combiné: 37 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **17** | | +| **Pays** | | | **37** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **17**, ce qui indique une **concentration modérée**. **Samsung (32 %)**, **Western Digital (16 %)**, **SK Hynix (12 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **37**, révélant une **concentration géographique élevée**. La répartition est dominée par **Corée du Sud (44 %)**, **États-Unis (41 %)**, **Japon (9 %)**, représentant ensemble plus de 94 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 17) +- La **concentration géographique est élevée** (IHH 37) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 37 - :red-badge[Rouge] (Élevée) +* ISG combiné: 37 - :green-badge[Vert] (Stable) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +### Capteurs et Fabrication + +Les capteurs représentent une classe diversifiée de composants électroniques qui convertissent des grandeurs physiques (lumière, mouvement, température, pression, etc.) en signaux électriques exploitables par les systèmes numériques. Ces interfaces critiques entre le monde physique et numérique se caractérisent par une miniaturisation croissante, une précision accrue et une intégration de plus en plus poussée. Le marché mondial des capteurs pour l'électronique grand public, estimé à 28 milliards de dollars en 2024, se structure autour de plusieurs familles clés : capteurs d'image (CMOS, CCD), capteurs inertiels (accéléromètres, gyroscopes), capteurs environnementaux (température, humidité, pression), capteurs biométriques (empreintes digitales, reconnaissance faciale) et capteurs de proximité/position. La fabrication de ces composants repose largement sur les technologies de micro-électromécanique (MEMS) et la photolithographie avancée pour les capteurs d'image. Le secteur connaît une croissance annuelle d'environ 9%, stimulée par l'expansion de l'IoT, la sophistication croissante des smartphones et l'électrification automobile. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Foxconn | Taïwan | 20 % | +| Chine | Luxshare Precision | Chine | 14 % | +| Chine | Lotes | Taïwan | 8 % | +| **Chine** | **Total** | **Chine** | **42 %** | +| Taïwan | Foxconn Taiwan | Taïwan | 12 % | +| Taïwan | Cheng Uei | Taïwan | 6 % | +| **Taïwan** | **Total** | **Taïwan** | **18 %** | +| Japon | Hirose Electric | Japon | 9 % | +| Japon | Japan Aviation Electronics | Japon | 5 % | +| **Japon** | **Total** | **Japon** | **14 %** | +| États-Unis | TE Connectivity | États-Unis | 6 % | +| États-Unis | Amphenol | États-Unis | 4 % | +| **États-Unis** | **Total** | **États-Unis** | **10 %** | +| Allemagne | HARTING | Allemagne | 4 % | +| Allemagne | ERNI | Allemagne | 3 % | +| **Allemagne** | **Total** | **Allemagne** | **7 %** | +| Corée du Sud | Samsung Electro-Mechanics | Corée du Sud | 5 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **5 %** | +| Suisse | Fischer Connectors | Suisse | 2 % | +| **Suisse** | **Total** | **Suisse** | **2 %** | + +**Unités** : millions d'unités/an + +**Total** : 4550 + +_Note: Les capacités de production sont à jour pour 2024-2025. Le marché des capteurs est particulièrement fragmenté avec des acteurs spécialisés par type de technologie et secteur d'application._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 23.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Pays-Bas | 3.0% | 28 | :green-badge[Vert] (Stable) | +| Allemagne | 12.0% | 30 | :green-badge[Vert] (Stable) | +| États-Unis | 16.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Corée du Sud | 9.0% | 33 | :green-badge[Vert] (Stable) | +| Suisse | 2.0% | 22 | :green-badge[Vert] (Stable) | +| Japon | 26.0% | 29 | :green-badge[Vert] (Stable) | +| Taiwan | 7.0% | 30 | :green-badge[Vert] (Stable) | + +**ISG combiné: 37 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | 10 | | | +| **Pays** | | 17 | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, signe d’une **concentration faible**. + +##### IHH par pays + +L’IHH par pays atteint **17**, indiquant une **concentration géographique modérée**. + +##### En résumé +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est modérée** (IHH 17) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 17 - :orange-badge[Orange] (Modérée) +* ISG combiné: 37 - :green-badge[Vert] (Stable) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +### Écran Mini et Micro LED et Fabrication + +Les écrans Mini LED représentent une évolution majeure des technologies d'affichage, positionnée entre les écrans LCD traditionnels et les OLED. Cette technologie conserve la structure de base d'un écran LCD (couche de cristaux liquides entre deux substrats) mais révolutionne le système de rétroéclairage en remplaçant les quelques dizaines de LED conventionnelles par des milliers de mini-LED, chacune mesurant généralement entre 50 et 200 microns. Cette multiplication des sources lumineuses permet d'obtenir un contrôle beaucoup plus précis de l'éclairage par zones (local dimming), améliorant significativement le contraste, la luminosité maximale et la précision des noirs. Les écrans Mini LED atteignent ainsi des performances visuelles proches des OLED tout en conservant les avantages des LCD: durée de vie prolongée, absence de risque de marquage et luminosité supérieure. Cette technologie est principalement employée dans les téléviseurs premium, les moniteurs professionnels, les ordinateurs portables haut de gamme et certains appareils mobiles. Le marché mondial des écrans Mini LED connaît une croissance rapide, estimée à plus de 35% annuellement, atteignant environ 6 milliards de dollars en 2024. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Taïwan | Epistar | Taïwan | 12 % | +| Taïwan | AU Optronics | Taïwan | 10 % | +| Taïwan | Innolux | Taïwan | 8 % | +| **Taïwan** | **Total** | **Taïwan** | **30 %** | +| Corée du Sud | Samsung Display | Corée du Sud | 14 % | +| Corée du Sud | LG Display | Corée du Sud | 11 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **25 %** | +| Chine | BOE Technology | Chine | 11 % | +| Chine | CSOT (TCL) | Chine | 9 % | +| **Chine** | **Total** | **Chine** | **20 %** | +| Japon | Sony | Japon | 5 % | +| Japon | Sharp/Foxconn | Japon | 4 % | +| **Japon** | **Total** | **Japon** | **9 %** | +| États-Unis | Apple (design) | États-Unis | 4 % | +| États-Unis | Lumileds | États-Unis | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **7 %** | +| Allemagne | OSRAM Opto | Allemagne | 4 % | +| **Allemagne** | **Total** | **Allemagne** | **4 %** | +| Vietnam | Samsung Vietnam | Corée du Sud | 3 % | +| **Vietnam** | **Total** | **Vietnam** | **3 %** | + +**Unités** : million d'unité/an + +**Total** : 81 + +_Note: Les capacités de production concernent spécifiquement les écrans Mini LED (pas LCD standard) et sont estimées pour 2024-2025. Le marché est en évolution rapide avec une augmentation notable des capacités prévue dans les prochaines années._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Corée du Sud | 25.0% | 33 | :green-badge[Vert] (Stable) | +| États-Unis | 7.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Allemagne | 4.0% | 30 | :green-badge[Vert] (Stable) | +| Vietnam | 3.0% | 48 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 9.0% | 29 | :green-badge[Vert] (Stable) | +| Taiwan | 30.0% | 30 | :green-badge[Vert] (Stable) | +| Chine | 20.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 37 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **9** | | | +| **Pays** | | **21** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **9**, ce qui indique une **concentration faible**. Bien que **Samsung Display (14 %)**, **Epistar (12 %)**, **LG Display (11 %)** regroupent plus de 37 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **21**, révélant une **concentration géographique modérée**. La répartition est dominée par **Taïwan (30 %)**, **Corée du Sud (25 %)**, **Chine (20 %)**, représentant ensemble plus de 75 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 9) +- La **concentration géographique est modérée** (IHH 21) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 21 - :orange-badge[Orange] (Modérée) +* ISG combiné: 37 - :green-badge[Vert] (Stable) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +### Écran LCD/TFT et Fabrication + +Les écrans LCD (Liquid Crystal Display) représentent une technologie d'affichage mature et largement répandue, utilisant les propriétés optiques des cristaux liquides pour moduler la lumière. Le principe fondamental repose sur des cristaux liquides emprisonnés entre deux plaques de verre polarisées, qui changent d'orientation sous l'effet d'un champ électrique, permettant ou bloquant ainsi le passage de la lumière émise par un rétroéclairage. La variante TFT (Thin-Film Transistor) ajoute une matrice active de transistors contrôlant individuellement chaque pixel, améliorant significativement la qualité d'image et le temps de réponse. Les écrans LCD modernes intègrent diverses technologies d'amélioration comme l'IPS (In-Plane Switching) pour des angles de vision plus larges, le VA (Vertical Alignment) pour un meilleur contraste, ou le local dimming pour des noirs plus profonds. Bien que concurrencés par les technologies OLED et Mini-LED sur le segment premium, les écrans LCD continuent de dominer le marché global grâce à leur excellent rapport coût/performance, leur longévité et leur adaptabilité à diverses tailles, des smartphones aux grands téléviseurs en passant par les moniteurs professionnels. + +#### Principaux fabricants + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | BOE Technology | Chine | 25 % | +| Chine | CSOT (TCL) | Chine | 13 % | +| Chine | CEC-Panda | Chine | 5 % | +| **Chine** | **Total** | **Chine** | **43 %** | +| Corée du Sud | LG Display | Corée du Sud | 13 % | +| Corée du Sud | Samsung Display | Corée du Sud | 12 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **25 %** | +| Taïwan | AU Optronics | Taïwan | 9 % | +| Taïwan | Innolux | Taïwan | 8 % | +| **Taïwan** | **Total** | **Taïwan** | **17 %** | +| Japon | Sharp/Foxconn | Japon | 4 % | +| Japon | Japan Display Inc. | Japon | 3 % | +| **Japon** | **Total** | **Japon** | **7 %** | +| Mexique | LG Display Mexico | Corée du Sud | 3 % | +| **Mexique** | **Total** | **Mexique** | **3 %** | +| Pologne | LG Display Poland | Corée du Sud | 2 % | +| **Pologne** | **Total** | **Pologne** | **2 %** | +| Vietnam | Samsung Display Vietnam | Corée du Sud | 2 % | +| **Vietnam** | **Total** | **Vietnam** | **2 %** | + +**Unités** : million d'unité/an + +**Total** : 707 + +_Note: Les capacités indiquées représentent la production annuelle estimée en 2024-2025. Le marché LCD connaît une concentration accrue suite à la concurrence des technologies OLED et Mini-LED sur les segments premium._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Corée du Sud | 25.0% | 33 | :green-badge[Vert] (Stable) | +| Pologne | 2.0% | 37 | :green-badge[Vert] (Stable) | +| Chine | 43.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Taiwan | 17.0% | 30 | :green-badge[Vert] (Stable) | +| Mexique | 3.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 7.0% | 29 | :green-badge[Vert] (Stable) | +| Vietnam | 2.0% | 48 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 42 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **13** | | | +| **Pays** | | | **28** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **13**, ce qui indique une **concentration faible**. Bien que **BOE Technology (25 %)**, **CSOT (TCL) (13 %)**, **LG Display (13 %)** regroupent plus de 51 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **28**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (43 %)**, **Corée du Sud (25 %)**, **Taïwan (17 %)**, représentant ensemble plus de 85 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 13) +- La **concentration géographique est élevée** (IHH 28) + +#### Vulnérabilité combinée IHH-ISG + +* IHH: 28 - :red-badge[Rouge] (Élevée) +* ISG combiné: 42 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +## Détails des minerais + +--- + +### Chrome + +Le chrome est un métal de transition de numéro atomique 24, symbole Cr, caractérisé par sa dureté exceptionnelle, sa résistance à la corrosion et sa capacité à former des alliages de haute performance. Découvert en 1797 par Louis-Nicolas Vauquelin, ce métal gris-argenté possède plusieurs états d'oxydation (de -II à VI), dont les plus courants sont le chrome trivalent (Cr III) et le chrome hexavalent (Cr VI). Le chrome trivalent est considéré comme un oligoélément essentiel à faible dose, tandis que le chrome hexavalent est reconnu pour sa toxicité. Le chrome métal ne s'oxyde pas à température ambiante grâce à la formation spontanée d'une couche protectrice d'oxyde de chrome (Cr₂O₃). En raison de ses propriétés anticorrosion, de sa résistance à l'usure et de sa capacité à former des composés colorés, le chrome est largement utilisé dans la métallurgie, la fabrication d'aciers inoxydables, le revêtement de surfaces, les matériaux réfractaires, les pigments et la tannerie. Son extraction et traitement impliquent des procédés complexes en raison de sa présence dans des minéraux où il est fortement lié à d'autres éléments, principalement le fer. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Boitier | 0.39 | 0.30 | 0.40 | 0.50 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Boîtier | 0.39 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.39 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 0 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Bains de chromage électronique, pigments pour circuits imprimés, catalyseurs pour microélectronique +*Secteurs concurrents* +Aciers inoxydables, réfractaires, revêtements industriels, tannerie, pigments, métallurgie +*Remarques* +*Usage numérique faible mais croissant (applications spécialisées). Marché dominé par l’acier inoxydable (~85%).* +*Répartition des usages* +* Numérique final : 5 +* Numérique embarqué : 2 +* Autres secteurs : 93 +*Tendance* +* Demande : 2.5 +* Production : 2.5 +* Ratio capacité/demande : 1.0 +*Concurrence & tension* +* Ratio concurrence : 19.0 +* Tension marché : 0.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.39 - :orange-badge[Orange] (Moyenne) +* IVC: 0 - :green-badge[Vert] (Faible) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Afrique du Sud | Glencore | Suisse | 14 % | +| Afrique du Sud | Samancor Chrome | Afrique du Sud | 13 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **43 %** | +| Kazakhstan | Kazchrome | Kazakhstan | 16 % | +| **Kazakhstan** | **Total** | **Kazakhstan** | **18 %** | +| Turquie | Eti Krom | Turquie | 7 % | +| **Turquie** | **Total** | **Turquie** | **11 %** | +| Inde | Tata Steel | Inde | 5 % | +| **Inde** | **Total** | **Inde** | **10 %** | +| Finlande | Outokumpu | Finlande | 4 % | +| **Finlande** | **Total** | **Finlande** | **6 %** | + +Unités : t/an + +Total : 41000000 + +_Note: Les parts de marché sont des estimations basées sur les capacités de production connues et les données disponibles dans les recherches sectorielles._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Afrique du Sud | 43.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Turquie | 11.0% | 64 | :orange-badge[Orange] (Intermédiaire) | +| Kazakhstan | 18.0% | 51 | :orange-badge[Orange] (Intermédiaire) | +| Finlande | 6.0% | 25 | :green-badge[Vert] (Stable) | +| Inde | 10.0% | 60 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 56 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **7** | | | +| **Pays** | | **24** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **7**, ce qui indique une **concentration faible**. Bien que **Kazchrome (16 %)**, **Glencore (14 %)**, **Samancor Chrome - Plaquage, résistance à la corrosion, alliages (13 %)** regroupent plus de 43 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **24**, révélant une **concentration géographique modérée**. La répartition est dominée par **Afrique du Sud (43 %)**, **Kazakhstan (18 %)**, **Turquie (11 %)**, représentant ensemble plus de 72 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 7) +- La **concentration géographique est modérée** (IHH 24) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 24 - :orange-badge[Orange] (Modérée) +* ISG combiné: 56 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Afrique du Sud | GlencoreMerafe | Suisse | | 28 % | +| Afrique du Sud | Samancor Chrome | Afrique du Sud | | 15 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **-** | **43 %** | +| Kazakhstan | Kazchrome | Kazakhstan | | 17 % | +| **Kazakhstan** | **Total** | **Kazakhstan** | **-** | **17 %** | +| Chine | Shanxi Taigang | Chine | | 5 % | +| Chine | Jilin Ferroalloys | Chine | | 4 % | +| **Chine** | **Total** | **Chine** | **-** | **9 %** | +| Inde | Tata Steel | Inde | | 4 % | +| Inde | Indian Metals \& Ferro Alloys | Inde | | 3 % | +| **Inde** | **Total** | **Inde** | **-** | **7 %** | +| Finlande | Outokumpu | Finlande | | 7 % | +| **Finlande** | **Total** | **Finlande** | **-** | **7 %** | +| Russie | Chelyabinsk | Russie | | 5 % | +| **Russie** | **Total** | **Russie** | **-** | **5 %** | + +Unités : t/an + +Total : 8230000 + +_Note: Les capacités indiquées représentent principalement la production de ferrochrome, le principal produit industriel du chrome. L'Afrique du Sud, qui possède environ 70% des réserves mondiales de chrome, domine sa production minière et métallurgique._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Afrique du Sud | 43.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 5.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Kazakhstan | 17.0% | 51 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 9.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 7.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Finlande | 7.0% | 25 | :green-badge[Vert] (Stable) | + +**ISG combiné: 55 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **14** | | | +| **Pays** | | **23** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **14**, ce qui indique une **concentration faible**. Bien que **GlencoreMerafe (28 %)**, **Kazchrome (17 %)**, **Samancor Chrome - Plaquage, résistance à la corrosion, alliages (15 %)** regroupent plus de 60 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **23**, révélant une **concentration géographique modérée**. La répartition est dominée par **Afrique du Sud (43 %)**, **Kazakhstan (17 %)**, **Chine (9 %)**, représentant ensemble plus de 69 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 14) +- La **concentration géographique est modérée** (IHH 23) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 23 - :orange-badge[Orange] (Modérée) +* ISG combiné: 55 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +--- + +### Gallium + +Le gallium est un métal rare aux propriétés physico-chimiques remarquables, découvert en 1875 par le français Paul Émile Lecoq de Boisbaudran. Sa particularité réside dans son point de fusion très bas (29,76°C), ce qui le rend liquide près de la température ambiante, tandis que son point d'ébullition est extrêmement élevé (2 403°C). Bien que présent dans la croûte terrestre à hauteur d'environ 19 ppm, le gallium n'existe pas à l'état natif et n'est jamais extrait pour lui-même mais toujours comme sous-produit d'autres métaux. Sa production repose principalement sur l'hydrométallurgie, avec trois sources principales : les liqueurs Bayer du traitement de la bauxite (90% de la production mondiale), les résidus de la métallurgie du zinc, et dans une moindre mesure, les cendres volantes de charbon. Le raffinage du gallium requiert des procédés sophistiqués pour atteindre les puretés extrêmes (jusqu'à 99,9999999%) nécessaires aux applications électroniques avancées, notamment les semi-conducteurs GaAs et GaN qui constituent ses principaux débouchés. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.47 | 0.50 | 0.40 | 0.50 | +| Batterie | 0.47 | 0.50 | 0.40 | 0.50 | +| Camera | 0.47 | 0.50 | 0.40 | 0.50 | +| Capteurs | 0.47 | 0.50 | 0.40 | 0.50 | +| Connecteurs | 0.47 | 0.50 | 0.40 | 0.50 | +| DisqueDur | 0.47 | 0.50 | 0.40 | 0.50 | +| Connectivite | 0.47 | 0.50 | 0.40 | 0.50 | +| Camera | 0.41 | 0.50 | 0.40 | 0.30 | +| EcranLCD | 0.41 | 0.50 | 0.40 | 0.30 | +| EcranOLED | 0.41 | 0.50 | 0.40 | 0.30 | +| EcranMiniLED | 0.41 | 0.50 | 0.40 | 0.30 | +| EcranSpecifique | 0.41 | 0.50 | 0.40 | 0.30 | +| CreusetGraphite | 0.37 | 0.40 | 0.30 | 0.40 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Écran OLED | 0.41 | :orange-badge[Orange] (Moyenne) | +| Connecteurs | 0.47 | :orange-badge[Orange] (Moyenne) | +| Caméra | 0.47 | :orange-badge[Orange] (Moyenne) | +| Batterie | 0.47 | :orange-badge[Orange] (Moyenne) | +| Audio | 0.47 | :orange-badge[Orange] (Moyenne) | +| Connectivité | 0.47 | :orange-badge[Orange] (Moyenne) | +| Capteurs | 0.47 | :orange-badge[Orange] (Moyenne) | +| Écran Mini et Micro LED | 0.41 | :orange-badge[Orange] (Moyenne) | +| Écran LCD/TFT | 0.41 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.45 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 1 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Semi-conducteurs (GaAs, GaN), circuits haute fréquence, LEDs, capteurs +*Secteurs concurrents* +Alliages, matériaux basse température, médecine +*Remarques* +Ressource hautement stratégique pour l'électronique. Forte croissance. Concurrence modérée. +*Répartition des usages* +* Numérique final : 80% +* Numérique embarqué : 15% +* Autres secteurs : 5% +*Tendance* +* Demande : +9% +* Production : +6% +* Ratio capacité/demande : 0.97 +*Concurrence & tension* +* Ratio concurrence : 0.26 +* Tension marché : 3.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.45 - :orange-badge[Orange] (Moyenne) +* IVC: 1 - :green-badge[Vert] (Faible) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | 35 % | +| Chine | Zhuhai SEZ Fangyan | Chine | 30 % | +| Chine | Zhuzhou Smelter | Chine | 25 % | +| **Chine** | **Total** | **Chine** | **97 %** | +| Corée du Sud | Samsung | Corée du Sud | 3 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **1 %** | +| Japon | Sumitomo Chemical | Japon | 3 % | +| **Japon** | **Total** | **Japon** | **1 %** | +| Russie | Non spécifiée | Russie | 4 % | +| **Russie** | **Total** | **Russie** | **1 %** | + +Unités : t/an + +Total : 320 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 97.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Corée du Sud | 1.0% | 33 | :green-badge[Vert] (Stable) | +| Japon | 1.0% | 29 | :green-badge[Vert] (Stable) | +| Russie | 1.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **28** | +| **Pays** | | | **94** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **28**, signalant une **concentration élevée**. Le marché est largement dominé par **China Minmetals (35 %)**, **Zhuhai SEZ Fangyan (30 %)**, **Zhuzhou Smelter (25 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **94**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (97 %)**, **Corée du Sud (1 %)**, **Japon (1 %)**, représentant ensemble plus de 99 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 28) +- La **concentration géographique est élevée** (IHH 94) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 94 - :red-badge[Rouge] (Élevée) +* ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Chine (90%) | 32 % | +| Chine | Zhuzhou Smelter | Chine | Chine (100%) | 21 % | +| Chine | East Hope | Chine | Chine (100%) | 9 % | +| **Chine** | **Total** | **Chine** | **-** | **62 %** | +| Japon | Sumitomo Chemical | Japon | Japon (40%) | 11 % | +| Japon | Dowa | Japon | Corée du Sud (20%), Japon (30%) | 5 % | +| **Japon** | **Total** | **Japon** | **-** | **16 %** | +| Allemagne | PPM Pure Metals | Allemagne | Corée du Sud (20%), Japon (30%) | 9 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **9 %** | +| États-Unis | 5N Plus | Canada | Corée du Sud (20%), Japon (30%) | 5 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **5 %** | +| Corée du Sud | Samsung | Corée du Sud | Corée du Sud (40%) | 4 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **-** | **4 %** | +| Royaume-Uni | IQE | Royaume-Uni | | 4 % | +| **Royaume-Uni** | **Total** | **Royaume-Uni** | **-** | **4 %** | + +Unités : t/an + +Total : 380 + +_Note: La capacité de traitement fait référence à la quantité de gallium pouvant être produite annuellement. Ces données sont des estimations basées sur les informations disponibles sur le marché._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Allemagne | 9.0% | 30 | :green-badge[Vert] (Stable) | +| Chine | 62.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| RoyaumeUni | 4.0% | 35 | :green-badge[Vert] (Stable) | +| EtatsUnis | 5.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 16.0% | 29 | :green-badge[Vert] (Stable) | +| Corée du Sud | 4.0% | 33 | :green-badge[Vert] (Stable) | + +**ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **18** | | +| **Pays** | | | **42** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **18**, ce qui indique une **concentration modérée**. **China Minmetals (32 %)**, **Zhuzhou Smelter (21 %)**, **Sumitomo Chemical (11 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **42**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (62 %)**, **Japon (16 %)**, **Allemagne (9 %)**, représentant ensemble plus de 87 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 18) +- La **concentration géographique est élevée** (IHH 42) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 42 - :red-badge[Rouge] (Élevée) +* ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Scandium + +Le scandium est un métal de transition léger appartenant au groupe IIIB du tableau périodique, découvert en 1879 par le chimiste suédois Lars Fredrik Nilson. Ce métal blanc-argenté, malléable et ductile, se caractérise par son point de fusion élevé (1541°C), sa faible densité (2,99 g/cm³) et sa capacité à former des alliages remarquablement résistants, particulièrement avec l'aluminium. Le scandium est l'un des éléments les plus dispersés dans la croûte terrestre, présent en faibles concentrations dans plus de 800 minéraux, mais rarement en quantités permettant une exploitation économique directe. Son extraction implique des procédés complexes qui varient selon la source - principalement comme sous-produit de l'extraction d'autres métaux (titane, uranium, terres rares, nickel) ou à partir de résidus industriels. La production mondiale est limitée (15-25 tonnes par an) et concentrée principalement en Chine et en Russie. Malgré cette rareté, le scandium possède des applications stratégiques dans l'aérospatiale, l'industrie sportive de haute performance, les piles à combustible à oxyde solide et l'électronique, grâce à sa capacité à améliorer significativement les propriétés mécaniques et thermiques des alliages d'aluminium tout en réduisant leur poids. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| ProcesseurARM | 0.70 | 0.70 | 0.70 | 0.70 | +| ProcesseurASIC | 0.70 | 0.70 | 0.70 | 0.70 | +| ProcesseurX86 | 0.70 | 0.70 | 0.70 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.70 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.70 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 108 - :red-badge[Rouge] (Forte)** + +*Usage numérique* +Alliages pour composants électroniques légers, LED, capteurs optiques, cellules combustibles. +*Secteurs concurrents* +Aéronautique, défense, industrie des superalliages +*Remarques* +Éléments critiques pour l’aéronautique et les LED. Usages numériques émergents mais stratégiques. Offre très concentrée et faible volume de production. +*Répartition des usages* +* Numérique final : 20% +* Numérique embarqué : 15% +* Autres secteurs : 65% +*Tendance* +* Demande : +25% +* Production : +10% +* Ratio capacité/demande : 0.88 +*Concurrence & tension* +* Ratio concurrence : 4.0 +* Tension marché : 15.0 +*Réserves* +* Niveau : Très limité +* Pondération : 1.8 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.70 - :red-badge[Rouge] (Difficile) +* IVC: 108 - :red-badge[Rouge] (Forte) +* Poids combiné: 9 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Northern Rare Earth Group | Chine | 50 % | +| **Chine** | **Total** | **Chine** | **50 %** | +| Russie | Rosatom | Russie | 20 % | +| **Russie** | **Total** | **Russie** | **20 %** | +| Philippines | Nickel Asia Corporation | Philippines | 15 % | +| **Philippines** | **Total** | **Philippines** | **15 %** | +| Canada | Rio Tinto | Royaume-Uni | 8 % | +| **Canada** | **Total** | **Canada** | **8 %** | + +**Unités** : t/an + +**Total** : 40 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Philippines | 15.0% | 59 | :orange-badge[Orange] (Intermédiaire) | +| Canada | 8.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 50.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 20.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 55 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **32** | +| **Pays** | | | **32** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **32**, signalant une **concentration élevée**. Le marché est largement dominé par **China Northern Rare Earth Group (50 %)**, **Rosatom (20 %)**, **Nickel - Batteries, revêtements conducteurs, alliages Asia Corporation (15 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **32**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (50 %)**, **Russie (20 %)**, **Philippines (15 %)**, représentant ensemble plus de 85 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 32) +- La **concentration géographique est élevée** (IHH 32) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 32 - :red-badge[Rouge] (Élevée) +* ISG combiné: 55 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Baogang Rare Earth | Chine | | 35 % | +| Chine | Hunan NonFerrous Metals | Chine | | 15 % | +| **Chine** | **Total** | **Chine** | **-** | **50 %** | +| Russie | Urals Aluminum | Russie | | 20 % | +| **Russie** | **Total** | **Russie** | **-** | **20 %** | +| Ukraine | SRSU/ISM | Ukraine | | 10 % | +| **Ukraine** | **Total** | **Ukraine** | **-** | **10 %** | +| Philippines | Scandium International | Canada | | 8 % | +| **Philippines** | **Total** | **Philippines** | **-** | **8 %** | +| Australie | Clean TeQ Sunrise | Australie | | 5 % | +| **Australie** | **Total** | **Australie** | **-** | **5 %** | +| Japon | Sumitomo Metal Mining | Japon | | 4 % | +| **Japon** | **Total** | **Japon** | **-** | **4 %** | +| Canada | Rio Tinto | Royaume-Uni | | 4 % | +| **Canada** | **Total** | **Canada** | **-** | **4 %** | + +**Unités** : t + +**Total** : 20 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Japon | 4.0% | 29 | :green-badge[Vert] (Stable) | +| Canada | 4.0% | 26 | :green-badge[Vert] (Stable) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| Philippines | 8.0% | 59 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 50.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Ukraine | 10.0% | 73 | :red-badge[Rouge] (Instable) | +| Russie | 20.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 55 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **21** | +| **Pays** | | | **31** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **21**, ce qui indique une **concentration modérée**. **Baogang Rare Earth (35 %)**, **Urals Aluminum (20 %)**, **Hunan NonFerrous Metals (15 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **31**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (50 %)**, **Russie (20 %)**, **Ukraine (10 %)**, représentant ensemble plus de 80 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 21) +- La **concentration géographique est élevée** (IHH 31) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 31 - :red-badge[Rouge] (Élevée) +* ISG combiné: 55 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Hafnium + +Le hafnium est un métal de transition rare, gris-argenté, ductile et lustré, découvert en 1923 par Dirk Coster et George de Hevesy. Ce métal se distingue par ses propriétés exceptionnelles : résistance remarquable à la corrosion, point de fusion très élevé (2227°C), excellente ductilité et une exceptionnelle capacité d'absorption des neutrons. Sa production implique des procédés métallurgiques complexes, principalement comme sous-produit du traitement du zirconium, avec lequel il partage une similitude chimique frappante qui rend leur séparation particulièrement difficile. Le traitement du hafnium est techniquement exigeant, énergétiquement intensif et économiquement coûteux, ce qui explique sa valeur élevée sur les marchés. Bien que sa production mondiale soit relativement modeste (environ 70-80 tonnes par an), le hafnium est devenu un métal stratégique indispensable à l'industrie nucléaire, l'aéronautique et, plus récemment, l'électronique avancée. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| ProcedeEUV | 0.90 | 0.90 | 0.90 | 0.90 | +| ProcesseurASIC | 0.84 | 0.90 | 0.80 | 0.80 | +| ProcesseurX86 | 0.84 | 0.90 | 0.80 | 0.80 | +| CarteMere | 0.81 | 0.90 | 0.80 | 0.70 | +| ProcesseurX86 | 0.81 | 0.90 | 0.80 | 0.70 | +| ProcesseurARM | 0.81 | 0.90 | 0.80 | 0.70 | +| ProcesseurASIC | 0.81 | 0.90 | 0.80 | 0.70 | +| MemoireRAM | 0.80 | 0.80 | 0.80 | 0.80 | +| ProcesseurARM | 0.80 | 0.80 | 0.80 | 0.80 | +| SSD25 | 0.80 | 0.80 | 0.80 | 0.80 | +| SSDM2 | 0.80 | 0.80 | 0.80 | 0.80 | +| StockageEMMC | 0.80 | 0.80 | 0.80 | 0.80 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.80 | :red-badge[Rouge] (Difficile) | +| Carte mère | 0.81 | :red-badge[Rouge] (Difficile) | +| Mémoire RAM | 0.80 | :red-badge[Rouge] (Difficile) | +| SSD 2.5 | 0.80 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.80 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 126 - :red-badge[Rouge] (Forte)** + +*Usage numérique* +Électronique avancée, microprocesseurs, composants haute température +*Secteurs concurrents* +Nucléaire, aéronautique, superalliages +*Remarques* +Ressource critique à double usage stratégique. Très forte croissance de la demande numérique avancée et tension géopolitique liée à sa rareté et sa concentration. +*Répartition des usages* +* Numérique final : 30% +* Numérique embarqué : 10% +* Autres secteurs : 60% +*Tendance* +* Demande : +40% +* Production : +10% +* Ratio capacité/demande : 0.79 +*Concurrence & tension* +* Ratio concurrence : 2.33 +* Tension marché : 30 +*Réserves* +* Niveau : Très limité +* Pondération : 1.8 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.80 - :red-badge[Rouge] (Difficile) +* IVC: 126 - :red-badge[Rouge] (Forte) +* Poids combiné: 9 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| France | Framatome | France | 43 % | +| **France** | **Total** | **France** | **43 %** | +| États-Unis | ATI | États-Unis | 38 % | +| **États-Unis** | **Total** | **États-Unis** | **38 %** | +| Chine | China Minmetals | Chine | 10 % | +| **Chine** | **Total** | **Chine** | **13 %** | + +Unités : t/an + +Total : 79 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 38.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 13.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| France | 43.0% | 34 | :green-badge[Vert] (Stable) | + +**ISG combiné: 40 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **34** | +| **Pays** | | | **35** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **34**, signalant une **concentration élevée**. Le marché est largement dominé par **Framatome (43 %)**, **ATI (38 %)**, **China Minmetals (10 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **35**, révélant une **concentration géographique élevée**. La répartition est dominée par **France (43 %)**, **États-Unis (38 %)**, **Chine (13 %)**, représentant ensemble plus de 94 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 34) +- La **concentration géographique est élevée** (IHH 35) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 35 - :red-badge[Rouge] (Élevée) +* ISG combiné: 40 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| France | Orano | France | États-Unis (20%) | 44 % | +| **France** | **Total** | **France** | **-** | **44 %** | +| États-Unis | ATI Specialty Materials | États-Unis | États-Unis (80%) | 19 % | +| États-Unis | Materion | États-Unis | États-Unis (100%) | 6 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **25 %** | +| Chine | China Nuclear | Chine | Chine (70%) | 10 % | +| **Chine** | **Total** | **Chine** | **-** | **10 %** | +| Russie | TVEL | Russie | États-Unis (80%) | 9 % | +| **Russie** | **Total** | **Russie** | **-** | **9 %** | +| Ukraine | State Concern Nuclear Fuel | Ukraine | États-Unis (80%) | 5 % | +| **Ukraine** | **Total** | **Ukraine** | **-** | **5 %** | +| Inde | Nuclear Fuel Complex | Inde | Chine (70%) | 4 % | +| **Inde** | **Total** | **Inde** | **-** | **4 %** | + +Unités : t/an + +Total : 80 + +_Note: Les capacités indiquées représentent la production de hafnium métal et composés. La production est fortement liée à celle du zirconium de qualité nucléaire, dont le hafnium doit être séparé pour les applications en réacteurs nucléaires._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 10.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| France | 44.0% | 34 | :green-badge[Vert] (Stable) | +| Inde | 4.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Ukraine | 5.0% | 73 | :red-badge[Rouge] (Instable) | +| Russie | 9.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 25.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 44 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **26** | +| **Pays** | | | **28** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **26**, signalant une **concentration élevée**. Le marché est largement dominé par **Orano (44 %)**, **ATI Specialty Materials (19 %)**, **China Nuclear (10 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **28**, révélant une **concentration géographique élevée**. La répartition est dominée par **France (44 %)**, **États-Unis (25 %)**, **Chine (10 %)**, représentant ensemble plus de 79 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 26) +- La **concentration géographique est élevée** (IHH 28) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 28 - :red-badge[Rouge] (Élevée) +* ISG combiné: 44 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Germanium + +Le germanium est un métalloïde gris-blanc, élément numéro 32 du tableau périodique, découvert en 1886 par Clemens Winkler. Ni vraiment métal ni non-métal, il se distingue par ses propriétés semi-conductrices exceptionnelles qui en font un matériau stratégique pour l'électronique avancée. Sa production est principalement issue de deux sources : comme sous-produit de la métallurgie du zinc (environ 80%) et, dans une moindre mesure, du traitement des cendres de certains charbons. Le traitement du germanium implique des procédés hydrométallurgiques complexes suivis de multiples étapes de purification pour atteindre les niveaux de pureté extrêmes (jusqu'à 99,99999%) requis pour les applications électroniques. L'affinité chimique du germanium pour l'oxygène nécessite des précautions particulières lors de sa manipulation. Les technologies de traitement ont considérablement évolué depuis les années 1950, notamment avec le développement de la fusion de zone qui permet d'obtenir des monocristaux d'une pureté sans précédent, avec seulement un atome d'impureté pour 10^10 atomes de germanium. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.64 | 0.70 | 0.60 | 0.60 | +| Batterie | 0.64 | 0.70 | 0.60 | 0.60 | +| Camera | 0.64 | 0.70 | 0.60 | 0.60 | +| Capteurs | 0.64 | 0.70 | 0.60 | 0.60 | +| CarteMere | 0.64 | 0.70 | 0.60 | 0.60 | +| Connecteurs | 0.64 | 0.70 | 0.60 | 0.60 | +| Connectivite | 0.64 | 0.70 | 0.60 | 0.60 | +| DisqueDur | 0.64 | 0.70 | 0.60 | 0.60 | +| ProcesseurARM | 0.64 | 0.70 | 0.60 | 0.60 | +| ProcesseurASIC | 0.64 | 0.70 | 0.60 | 0.60 | +| ProcesseurX86 | 0.64 | 0.70 | 0.60 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.64 | :red-badge[Rouge] (Difficile) | +| Connecteurs | 0.64 | :red-badge[Rouge] (Difficile) | +| Caméra | 0.64 | :red-badge[Rouge] (Difficile) | +| Carte mère | 0.64 | :red-badge[Rouge] (Difficile) | +| Batterie | 0.64 | :red-badge[Rouge] (Difficile) | +| Audio | 0.64 | :red-badge[Rouge] (Difficile) | +| Connectivité | 0.64 | :red-badge[Rouge] (Difficile) | +| Capteurs | 0.64 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.64 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 1 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Fibres optiques, photodétecteurs, cellules solaires concentrées, optoélectronique +*Secteurs concurrents* +Catalyseurs, optique IR, recherche +*Remarques* +Très utilisé dans les technologies avancées. Forte croissance mais concurrence modérée. +*Répartition des usages* +* Numérique final : 80% +* Numérique embarqué : 10% +* Autres secteurs : 10% +*Tendance* +* Demande : +5.5% +* Production : +4% +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 0.25 +* Tension marché : 1.5 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.64 - :red-badge[Rouge] (Difficile) +* IVC: 1 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Yunnan Chihong Germanium and Zinc Co Ltd | Chine | 20 % | +| Chine | Yunnan Lincang Xinyuan Germanium Industry Co Ltd | Chine | 19 % | +| **Chine** | **Total** | **Chine** | **94 %** | +| Russie | JSC Germanium | Russie | 4 % | +| **Russie** | **Total** | **Russie** | **4 %** | +| États-Unis | Teck Resources | Canada | 1 % | +| **États-Unis** | **Total** | **États-Unis** | **1 %** | + +Unités : t/an + +Total : 180 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Russie | 4.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 94.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 1.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **8** | | | +| **Pays** | | | **89** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **8**, ce qui indique une **concentration faible**. Bien que **Yunnan Chihong Germanium - Semi-conducteurs, détecteurs infrarouge, fibre optique and Zinc - Galvanisation, protection contre corrosion, batteries Co Ltd (20 %)**, **Yunnan Lincang Xinyuan Germanium - Semi-conducteurs, détecteurs infrarouge, fibre optique Industry Co Ltd (19 %)**, **JSC Germanium - Semi-conducteurs, détecteurs infrarouge, fibre optique (4 %)** regroupent plus de 43 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **89**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (94 %)**, **Russie (4 %)**, **États-Unis (1 %)**, représentant ensemble plus de 99 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 8) +- La **concentration géographique est élevée** (IHH 89) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 89 - :red-badge[Rouge] (Élevée) +* ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Yunnan Germanium | Chine | Chine (90%) | 30 % | +| Chine | Yunnan Chihong Zinc | Chine | Chine (100%) | 20 % | +| **Chine** | **Total** | **Chine** | **-** | **50 %** | +| Belgique | Umicore | Belgique | Chine (30%) | 17 % | +| **Belgique** | **Total** | **Belgique** | **-** | **17 %** | +| Russie | Germaniy JSC | Russie | Russie (100%) | 10 % | +| **Russie** | **Total** | **Russie** | **-** | **10 %** | +| États-Unis | Indium Corporation | États-Unis | États-Unis (60%) | 10 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **10 %** | +| Canada | 5N Plus | Canada | États-Unis (20%) | 7 % | +| **Canada** | **Total** | **Canada** | **-** | **7 %** | +| Japon | Teck Resources | Canada | Chine (90%) | 5 % | +| **Japon** | **Total** | **Japon** | **-** | **5 %** | +| Allemagne | PPM Pure Metals | Allemagne | États-Unis (60%) | 1 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **1 %** | + +Unités : t/an + +Total : 150 + +_Note: La capacité de traitement fait référence à la quantité de germanium pouvant être produite annuellement. Les données reflètent les capacités installées et les origines estimées des matières premières traitées._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Belgique | 17.0% | 34 | :green-badge[Vert] (Stable) | +| EtatsUnis | 10.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 50.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 5.0% | 29 | :green-badge[Vert] (Stable) | +| Canada | 7.0% | 26 | :green-badge[Vert] (Stable) | +| Allemagne | 1.0% | 30 | :green-badge[Vert] (Stable) | +| Russie | 10.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 47 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **19** | | +| **Pays** | | | **31** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **19**, ce qui indique une **concentration modérée**. **Yunnan Germanium - Semi-conducteurs, détecteurs infrarouge, fibre optique (30 %)**, **Yunnan Chihong Zinc - Galvanisation, protection contre corrosion, batteries (20 %)**, **Umicore (17 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **31**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (50 %)**, **Belgique (17 %)**, **États-Unis (10 %)**, représentant ensemble plus de 77 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 19) +- La **concentration géographique est élevée** (IHH 31) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 31 - :red-badge[Rouge] (Élevée) +* ISG combiné: 47 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Silicium + +Le silicium est un élément chimique non métallique de numéro atomique 14 (symbole Si), représentant environ 28% de la masse de la croûte terrestre, ce qui en fait le deuxième élément le plus abondant après l'oxygène. De couleur gris foncé avec un éclat sub-métallique, il existe sous forme amorphe (poudre brunâtre) ou cristalline (solide gris métallique). Le silicium se caractérise par ses propriétés semiconductrices, sa résistance à haute température et sa capacité à former des alliages et composés aux propriétés diverses. + +Il ne se trouve pas à l'état natif dans la nature mais sous forme de silice (SiO₂) ou de silicates. Sa transformation implique des procédés métallurgiques énergivores, principalement la carboréduction du quartz dans des fours à arc électrique pour le silicium métallurgique, et des procédés complexes de purification comme le procédé Siemens pour obtenir du polysilicium de grade électronique ou solaire. + +Le silicium constitue un matériau stratégique pour de nombreux secteurs industriels, notamment la métallurgie (alliages d'aluminium), la chimie (silicones), l'électronique (semi-conducteurs) et l'énergie photovoltaïque. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.90 | 0.90 | 0.90 | 0.90 | +| Batterie | 0.90 | 0.90 | 0.90 | 0.90 | +| Camera | 0.90 | 0.90 | 0.90 | 0.90 | +| Capteurs | 0.90 | 0.90 | 0.90 | 0.90 | +| CarteMere | 0.90 | 0.90 | 0.90 | 0.90 | +| Connecteurs | 0.90 | 0.90 | 0.90 | 0.90 | +| Connectivite | 0.90 | 0.90 | 0.90 | 0.90 | +| DisqueDur | 0.90 | 0.90 | 0.90 | 0.90 | +| MemoireRAM | 0.90 | 0.90 | 0.90 | 0.90 | +| ProcesseurARM | 0.90 | 0.90 | 0.90 | 0.90 | +| ProcesseurASIC | 0.90 | 0.90 | 0.90 | 0.90 | +| ProcesseurX86 | 0.90 | 0.90 | 0.90 | 0.90 | +| SSD25 | 0.90 | 0.90 | 0.90 | 0.90 | +| SSDM2 | 0.90 | 0.90 | 0.90 | 0.90 | +| StockageEMMC | 0.90 | 0.90 | 0.90 | 0.90 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.90 | :red-badge[Rouge] (Difficile) | +| Connecteurs | 0.90 | :red-badge[Rouge] (Difficile) | +| Caméra | 0.90 | :red-badge[Rouge] (Difficile) | +| Carte mère | 0.90 | :red-badge[Rouge] (Difficile) | +| Batterie | 0.90 | :red-badge[Rouge] (Difficile) | +| Audio | 0.90 | :red-badge[Rouge] (Difficile) | +| Connectivité | 0.90 | :red-badge[Rouge] (Difficile) | +| Mémoire RAM | 0.90 | :red-badge[Rouge] (Difficile) | +| SSD 2.5 | 0.90 | :red-badge[Rouge] (Difficile) | +| Capteurs | 0.90 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.90 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 5 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Composant central des semi-conducteurs, microprocesseurs, capteurs, composants logiques et mémoires. +*Secteurs concurrents* +Photovoltaïque, électroménager, automobile +*Remarques* +Ressource critique dans les chaînes numériques. Tensions croissantes avec le développement massif du photovoltaïque. +*Répartition des usages* +* Numérique final : 60% +* Numérique embarqué : 20% +* Autres secteurs : 20% +*Tendance* +* Demande : +35% +* Production : +28% +* Ratio capacité/demande : 0.95 +*Concurrence & tension* +* Ratio concurrence : 0.67 +* Tension marché : 7.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.90 - :red-badge[Rouge] (Difficile) +* IVC: 5 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | 22 % | +| Chine | Elkem | Norvège | 15 % | +| **Chine** | **Total** | **Chine** | **65 %** | +| Brésil | Ferroglobe | Espagne | 10 % | +| **Brésil** | **Total** | **Brésil** | **13 %** | +| États-Unis | Globe Specialty Metals | États-Unis | 8 % | +| **États-Unis** | **Total** | **États-Unis** | **8 %** | +| Norvège | Elkem | Norvège | 6 % | +| **Norvège** | **Total** | **Norvège** | **6 %** | +| France | Ferropem | Espagne | 3 % | +| **France** | **Total** | **France** | **3 %** | + +Unités : t/an + +Total : 3000000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Bresil | 13.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 8.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 65.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| France | 3.0% | 34 | :green-badge[Vert] (Stable) | +| Norvege | 6.0% | 23 | :green-badge[Vert] (Stable) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **9** | | | +| **Pays** | | | **45** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **9**, ce qui indique une **concentration faible**. Bien que **China Minmetals (22 %)**, **Elkem (15 %)**, **Ferroglobe (10 %)** regroupent plus de 47 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **45**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (65 %)**, **Brésil (13 %)**, **États-Unis (8 %)**, représentant ensemble plus de 86 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 9) +- La **concentration géographique est élevée** (IHH 45) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 45 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | GCLPoly Energy | Chine | Chine (100%) | 18 % | +| Chine | Tongwei | Chine | Chine (100%) | 14 % | +| Chine | East Hope | Chine | Chine (100%) | 11 % | +| **Chine** | **Total** | **Chine** | **-** | **53 %** | +| États-Unis | Hemlock Semiconductor | États-Unis | États-Unis (80%) | 9 % | +| États-Unis | REC Silicon | États-Unis | États-Unis (100%) | 6 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **15 %** | +| Norvège | Elkem | Norvège | Norvège (70%) | 10 % | +| **Norvège** | **Total** | **Norvège** | **-** | **10 %** | +| Corée du Sud | OCI | Corée du Sud | Norvège (70%) | 7 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **-** | **7 %** | +| Allemagne | Wacker Chemie | Allemagne | Norvège (70%) | 6 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **6 %** | +| Brésil | Grupo Ferroatlantica | Espagne | Brésil (100%) | 4 % | +| **Brésil** | **Total** | **Brésil** | **-** | **4 %** | +| France | FerroPem | France | France (100%) | 3 % | +| **France** | **Total** | **France** | **-** | **3 %** | + +Unités : t/an + +Total : 1400000 + +_Note: Les capacités indiquées représentent la production annuelle estimée de silicium métallique et de polysilicium. La production mondiale de silicium métallique est d'environ 3 millions de tonnes, tandis que celle de polysilicium est d'environ 1,4 million de tonnes._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 15.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Norvege | 10.0% | 23 | :green-badge[Vert] (Stable) | +| Corée du Sud | 7.0% | 33 | :green-badge[Vert] (Stable) | +| Bresil | 4.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 53.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Allemagne | 6.0% | 30 | :green-badge[Vert] (Stable) | +| France | 3.0% | 34 | :green-badge[Vert] (Stable) | + +**ISG combiné: 45 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **10** | | | +| **Pays** | | | **32** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, ce qui indique une **concentration faible**. Bien que **GCLPoly Energy (18 %)**, **Tongwei (14 %)**, **East Hope (11 %)** regroupent plus de 43 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **32**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (53 %)**, **États-Unis (15 %)**, **Norvège (10 %)**, représentant ensemble plus de 78 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est élevée** (IHH 32) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 32 - :red-badge[Rouge] (Élevée) +* ISG combiné: 45 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Phosphore + +Le phosphore est un élément chimique non métallique, essentiel à toute forme de vie, découvert en 1669 par Hennig Brand. Cet élément existe sous plusieurs formes allotropiques, dont les principales sont le phosphore blanc (hautement réactif, toxique et spontanément inflammable à l'air), le phosphore rouge (moins réactif et plus stable) et le phosphore noir (conducteur semi-métallique). Le phosphore ne se trouve pas à l'état natif dans la nature en raison de sa forte réactivité, mais principalement sous forme de phosphates dans des minerais comme l'apatite [Ca₅(PO₄)₃(F,Cl,OH)] et la phosphorite. Sa production implique des procédés métallurgiques et chimiques énergivores, principalement à partir de roches phosphatées avec des teneurs typiques de 5 à 40% en P₂O₅. Le traitement du phosphore est techniquement complexe et génère des impacts environnementaux significatifs, notamment des émissions de gaz à effet de serre et des résidus miniers (phosphogypse). Sa chaîne d'approvisionnement est caractérisée par une forte concentration géographique, le Maroc possédant environ 70% des réserves mondiales connues. Malgré sa toxicité sous certaines formes, le phosphore est indispensable à l'agriculture mondiale comme composant essentiel des engrais, et trouve également des applications dans de nombreux secteurs industriels. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.50 | 0.50 | 0.40 | 0.60 | +| Batterie | 0.50 | 0.50 | 0.40 | 0.60 | +| Camera | 0.50 | 0.50 | 0.40 | 0.60 | +| Capteurs | 0.50 | 0.50 | 0.40 | 0.60 | +| CarteMere | 0.50 | 0.50 | 0.40 | 0.60 | +| Connecteurs | 0.50 | 0.50 | 0.40 | 0.60 | +| Connectivite | 0.50 | 0.50 | 0.40 | 0.60 | +| DisqueDur | 0.50 | 0.50 | 0.40 | 0.60 | +| ProcesseurARM | 0.50 | 0.50 | 0.40 | 0.60 | +| ProcesseurASIC | 0.50 | 0.50 | 0.40 | 0.60 | +| ProcesseurX86 | 0.50 | 0.50 | 0.40 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.50 | :orange-badge[Orange] (Moyenne) | +| Connecteurs | 0.50 | :orange-badge[Orange] (Moyenne) | +| Caméra | 0.50 | :orange-badge[Orange] (Moyenne) | +| Carte mère | 0.50 | :orange-badge[Orange] (Moyenne) | +| Batterie | 0.50 | :orange-badge[Orange] (Moyenne) | +| Audio | 0.50 | :orange-badge[Orange] (Moyenne) | +| Connectivité | 0.50 | :orange-badge[Orange] (Moyenne) | +| Capteurs | 0.50 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.50 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 32 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Semi-conducteurs (dopage), batteries sodium-ion, substrats pour puces, produits phosphorés pour écrans. +*Secteurs concurrents* +Agriculture (engrais), alimentation animale, pharmaceutique +*Remarques* +Largement dominé par l'usage agricole. Le numérique reste très minoritaire mais croissant avec le développement des batteries alternatives. +*Répartition des usages* +* Numérique final : 3% +* Numérique embarqué : 2% +* Autres secteurs : 95% +*Tendance* +* Demande : +4% +* Production : +3% +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 32.33 +* Tension marché : 1.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.50 - :orange-badge[Orange] (Moyenne) +* IVC: 32 - :orange-badge[Orange] (Modérée) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | 12 % | +| **Chine** | **Total** | **Chine** | **39 %** | +| Maroc | Office Chérifien des Phosphates | Maroc | 18 % | +| **Maroc** | **Total** | **Maroc** | **18 %** | +| États-Unis | Mosaic | États-Unis | 9 % | +| États-Unis | Nutrien | Canada | 3 % | +| États-Unis | Simplot | États-Unis | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **10 %** | +| Russie | PhosAgro | Russie | 6 % | +| **Russie** | **Total** | **Russie** | **6 %** | +| Jordanie | Jordan Phosphate Mines Company | Jordanie | 4 % | +| **Jordanie** | **Total** | **Jordanie** | **4 %** | +| Brésil | Vale Fertilizantes | Brésil | 2 % | +| **Brésil** | **Total** | **Brésil** | **2 %** | +| Arabie Saoudite | Maaden | Arabie Saoudite | 4 % | +| **Arabie Saoudite** | **Total** | **Arabie Saoudite** | **0 %** | + +Unités : t/an + +Total : 220000000 + +_Sources: USGS 2021-2022, L'Élémentarium 2022. Les chiffres représentent la production de roche phosphatée, non le contenu en P₂O₅. Les réserves sont exprimées en tonnes de roche phosphatée._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 39.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 10.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| ArabieSaoudite | 0.0% | 51 | :orange-badge[Orange] (Intermédiaire) | +| Maroc | 18.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 2.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 6.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Jordanie | 4.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **6** | | | +| **Pays** | | **20** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **6**, ce qui indique une **concentration faible**. Bien que **Office Chérifien des Phosphates (18 %)**, **China Minmetals (12 %)**, **Mosaic (9 %)** regroupent plus de 39 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **20**, révélant une **concentration géographique modérée**. La répartition est dominée par **Chine (39 %)**, **Maroc (18 %)**, **États-Unis (10 %)**, représentant ensemble plus de 67 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 6) +- La **concentration géographique est modérée** (IHH 20) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 20 - :orange-badge[Orange] (Modérée) +* ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Maroc | OCP Group | Maroc | Maroc (100%) | 30 % | +| **Maroc** | **Total** | **Maroc** | **-** | **30 %** | +| Chine | Yunnan Phosphate | Chine | Chine (100%) | 14 % | +| Chine | Wengfu Group | Chine | Chine (100%) | 8 % | +| **Chine** | **Total** | **Chine** | **-** | **22 %** | +| États-Unis | Mosaic | États-Unis | États-Unis (80%) | 11 % | +| États-Unis | Nutrien | Canada | États-Unis (100%) | 5 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **16 %** | +| Russie | PhosAgro | Russie | Russie (100%) | 9 % | +| Russie | EuroChem | Russie | Russie (100%) | 4 % | +| **Russie** | **Total** | **Russie** | **-** | **13 %** | +| Arabie Saoudite | Maaden | Arabie Saoudite | Arabie Saoudite (100%) | 8 % | +| **Arabie Saoudite** | **Total** | **Arabie Saoudite** | **-** | **8 %** | +| Jordanie | JPMC | Jordanie | Jordanie (100%) | 5 % | +| **Jordanie** | **Total** | **Jordanie** | **-** | **5 %** | + +Unités : t/an + +Total : 39000000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| ArabieSaoudite | 8.0% | 51 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 16.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 13.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Maroc | 30.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Jordanie | 5.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 22.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **15** | | +| **Pays** | | **19** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **15**, ce qui indique une **concentration modérée**. **OCP Group (30 %)**, **Yunnan Phosphate (14 %)**, **Mosaic (11 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **19**, révélant une **concentration géographique modérée**. La répartition est dominée par **Maroc (30 %)**, **Chine (22 %)**, **États-Unis (16 %)**, représentant ensemble plus de 68 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 15) +- La **concentration géographique est modérée** (IHH 19) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 19 - :orange-badge[Orange] (Modérée) +* ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +--- + +### Tungstène + +Le tungstène est un métal de transition rare aux propriétés exceptionnelles, caractérisé par le point de fusion le plus élevé de tous les métaux (3 410°C) et une résistance mécanique remarquable à haute température. Sa production industrielle combine des processus complexes de métallurgie extractive et de métallurgie des poudres, nécessaires en raison de son caractère réfractaire qui rend impossible la fonderie conventionnelle. + +Le traitement du tungstène débute généralement par l'extraction à partir de deux minerais principaux, la wolframite et la scheelite, suivie par une série d'opérations hydrométallurgiques pour obtenir des composés intermédiaires purifiés (acide tungstique ou paratungstate d'ammonium). La réduction de ces composés par hydrogène conduit à l'obtention de poudres de tungstène métallique, qui seront ensuite consolidées par frittage pour produire des pièces massives ou transformées en composés comme le carbure de tungstène. Ce métal stratégique, essentiel aux industries de haute technologie, présente des exigences de traitement particulièrement rigoureuses en raison de sa dureté et de ses caractéristiques physico-chimiques uniques. + +Principalement utilisé dans la fabrication de carbures cémentés (50% des usages), le tungstène joue également un rôle crucial dans l'industrie électrique, les alliages spéciaux et les applications de haute technologie. Le marché est marqué par une forte concentration géographique qui représente un risque stratégique majeur pour les chaînes d'approvisionnement mondiales, particulièrement dans un contexte de tensions commerciales croissantes. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.61 | 0.70 | 0.50 | 0.60 | +| ProcesseurARM | 0.60 | 0.60 | 0.50 | 0.70 | +| ProcesseurASIC | 0.60 | 0.60 | 0.50 | 0.70 | +| ProcesseurX86 | 0.60 | 0.60 | 0.50 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.60 | :red-badge[Rouge] (Difficile) | +| Audio | 0.61 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.60 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 24 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Électrodes, microcontacts, composants résistifs, dissipation thermique, blindages électroniques. +*Secteurs concurrents* +Outillage, défense, aéronautique, énergie +*Remarques* +Utilisé dans des fonctions critiques à forte densité énergétique. Forte pression du secteur industriel lourd et défense. +*Répartition des usages* +* Numérique final : 20% +* Numérique embarqué : 15% +* Autres secteurs : 65% +*Tendance* +* Demande : +25% +* Production : +20% +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 4.0 +* Tension marché : 5.0 +*Réserves* +* Niveau : Modéré +* Pondération : 1.2 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.60 - :red-badge[Rouge] (Difficile) +* IVC: 24 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Jiangxi Tungsten Industry Group | Chine | 38 % | +| Chine | Jiangxi Xianglushan | Chine | 30 % | +| **Chine** | **Total** | **Chine** | **83 %** | +| Vietnam | Masan HighTech Materials | Vietnam | 5 % | +| **Vietnam** | **Total** | **Vietnam** | **6 %** | +| Russie | Wolfram Company | Russie | 3 % | +| **Russie** | **Total** | **Russie** | **3 %** | +| Bolivie | Empresa Minera Huanuni | Bolivie | 1 % | +| **Bolivie** | **Total** | **Bolivie** | **2 %** | +| Rwanda | Rwanda Mines, Petroleum and Gas Board | Rwanda | 1 % | +| **Rwanda** | **Total** | **Rwanda** | **1 %** | +| Espagne | Almonty Industries | Canada | 1 % | +| **Espagne** | **Total** | **Espagne** | **1 %** | +| Autriche | Wolfram Bergbau und Hütten AG | Autriche | 1 % | +| **Autriche** | **Total** | **Autriche** | **1 %** | + +Unités : t/an + +Total : 81000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Vietnam | 6.0% | 48 | :orange-badge[Orange] (Intermédiaire) | +| Rwanda | 1.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 3.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Autriche | 1.0% | 29 | :green-badge[Vert] (Stable) | +| Bolivie | 2.0% | 56 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 83.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Espagne | 1.0% | 39 | :green-badge[Vert] (Stable) | + +**ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **24** | | +| **Pays** | | | **69** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **24**, ce qui indique une **concentration modérée**. **Jiangxi Tungsten Industry Group (38 %)**, **Jiangxi Xianglushan (30 %)**, **Masan HighTech Materials (5 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **69**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (83 %)**, **Vietnam (6 %)**, **Russie (3 %)**, représentant ensemble plus de 92 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 24) +- La **concentration géographique est élevée** (IHH 69) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 69 - :red-badge[Rouge] (Élevée) +* ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Xiamen Tungsten | Chine | Chine (90%) | 25 % | +| Chine | China Minmetals | Chine | Chine (95%) | 20 % | +| **Chine** | **Total** | **Chine** | **-** | **45 %** | +| Autriche | Plansee Group | Autriche | | 12 % | +| **Autriche** | **Total** | **Autriche** | **-** | **12 %** | +| États-Unis | Global Tungsten \& Powders | États-Unis | Vietnam (100%) | 11 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **11 %** | +| Russie | Wolfram Company | Russie | Russie (100%) | 9 % | +| **Russie** | **Total** | **Russie** | **-** | **9 %** | +| Allemagne | HC Starck | Allemagne | | 8 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **8 %** | +| Vietnam | Masan Resources | Vietnam | Vietnam (100%) | 6 % | +| **Vietnam** | **Total** | **Vietnam** | **-** | **6 %** | +| Japon | Japan New Metals | Japon | Bolivie (30%) | 6 % | +| **Japon** | **Total** | **Japon** | **-** | **6 %** | +| Corée du Sud | TaeguTec | Corée du Sud | | 3 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **-** | **3 %** | + +Unités : t/an + +Total : 32000 + +_Note: Les capacités indiquées représentent la transformation de tungstène en produits finis et semi-finis (métal, carbures, composés). La Chine domine nettement le marché tant au niveau de l'extraction minière que du traitement._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Autriche | 12.0% | 29 | :green-badge[Vert] (Stable) | +| Vietnam | 6.0% | 48 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 9.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 11.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 6.0% | 29 | :green-badge[Vert] (Stable) | +| Allemagne | 8.0% | 30 | :green-badge[Vert] (Stable) | +| Corée du Sud | 3.0% | 33 | :green-badge[Vert] (Stable) | +| Chine | 45.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **15** | | +| **Pays** | | | **25** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **15**, ce qui indique une **concentration modérée**. **Xiamen Tungsten (25 %)**, **China Minmetals (20 %)**, **Plansee Group (12 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **25**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (45 %)**, **Autriche (12 %)**, **États-Unis (11 %)**, représentant ensemble plus de 68 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 15) +- La **concentration géographique est élevée** (IHH 25) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 25 - :red-badge[Rouge] (Élevée) +* ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Antimoine + +L'antimoine est un métalloïde blanc-argenté, cristallin et cassant, qui présente une conductivité thermique et électrique limitée. Sa production industrielle repose principalement sur deux types de sources : les minerais sulfurés (stibine) et les minerais oxydés. Le traitement métallurgique varie considérablement selon le type de minerai et sa teneur. Pour les minerais sulfurés, la voie privilégiée combine broyage, flottation, grillage et réduction, tandis que les minerais oxydés subissent généralement une concentration par gravité suivie d'une réduction directe. L'antimoine est également récupéré comme sous-produit de la métallurgie du plomb. Sa particularité réside dans sa fragilité à température ambiante, mais aussi dans sa capacité à durcir d'autres métaux lorsqu'il est utilisé en alliage. La chaîne d'approvisionnement mondiale est largement dominée par la Chine, qui contrôle plus de 70% de la production, ce qui soulève des enjeux stratégiques importants pour les industries occidentales dépendantes de ses propriétés uniques, notamment pour les retardateurs de flamme et les alliages spéciaux. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.60 | 0.60 | 0.50 | 0.70 | +| Batterie | 0.60 | 0.60 | 0.50 | 0.70 | +| Camera | 0.60 | 0.60 | 0.50 | 0.70 | +| Capteurs | 0.60 | 0.60 | 0.50 | 0.70 | +| CarteMere | 0.60 | 0.60 | 0.50 | 0.70 | +| Connecteurs | 0.60 | 0.60 | 0.50 | 0.70 | +| Connectivite | 0.60 | 0.60 | 0.50 | 0.70 | +| DisqueDur | 0.60 | 0.60 | 0.50 | 0.70 | +| ProcesseurARM | 0.60 | 0.60 | 0.50 | 0.70 | +| ProcesseurASIC | 0.60 | 0.60 | 0.50 | 0.70 | +| ProcesseurX86 | 0.60 | 0.60 | 0.50 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.60 | :red-badge[Rouge] (Difficile) | +| Connecteurs | 0.60 | :red-badge[Rouge] (Difficile) | +| Caméra | 0.60 | :red-badge[Rouge] (Difficile) | +| Carte mère | 0.60 | :red-badge[Rouge] (Difficile) | +| Batterie | 0.60 | :red-badge[Rouge] (Difficile) | +| Audio | 0.60 | :red-badge[Rouge] (Difficile) | +| Connectivité | 0.60 | :red-badge[Rouge] (Difficile) | +| Capteurs | 0.60 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.60 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 27 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Retardateurs de flamme électroniques, infrarouge, brasures, photovoltaïque +*Secteurs concurrents* +Plasturgie, textile, batteries plomb, alliages, pharmaceutique +*Remarques* +Faible part numérique mais en forte croissance. Concurrence industrielle forte. +*Répartition des usages* +* Numérique final : 10% +* Numérique embarqué : 5% +* Autres secteurs : 85% +*Tendance* +* Demande : +4% +* Production : +2% +* Ratio capacité/demande : 0.98 +*Concurrence & tension* +* Ratio concurrence : 9.0 +* Tension marché : 2.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.60 - :red-badge[Rouge] (Difficile) +* IVC: 27 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Hunan Gold Corporation | Chine | 23 % | +| Chine | China Minmetals | Chine | 15 % | +| **Chine** | **Total** | **Chine** | **60 %** | +| Tadjikistan | Anzob Mining and Processing Plant | Tadjikistan | 15 % | +| **Tadjikistan** | **Total** | **Tadjikistan** | **17 %** | +| Russie | GeoProMining | Russie | 10 % | +| **Russie** | **Total** | **Russie** | **13 %** | +| Bolivie | Empresa Minera Manquiri | Bolivie | 3 % | +| **Bolivie** | **Total** | **Bolivie** | **4 %** | +| Australie | Mandalay Resources | Australie | 2 % | +| **Australie** | **Total** | **Australie** | **2 %** | + +Unités : kt + +Total : 100 + +Note : Les parts de marché sont des estimations basées sur les données de production disponibles pour 2024. + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 60.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Tadjikistan | 17.0% | 59 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 2.0% | 26 | :green-badge[Vert] (Stable) | +| Bolivie | 4.0% | 56 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 13.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 56 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **11** | | | +| **Pays** | | | **41** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **11**, ce qui indique une **concentration faible**. Bien que **Hunan Gold Corporation (23 %)**, **China Minmetals (15 %)**, **Anzob Mining and Processing Plant (15 %)** regroupent plus de 53 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **41**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (60 %)**, **Tadjikistan (17 %)**, **Russie (13 %)**, représentant ensemble plus de 90 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 11) +- La **concentration géographique est élevée** (IHH 41) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 41 - :red-badge[Rouge] (Élevée) +* ISG combiné: 56 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Chine (95%) | 45 % | +| Chine | Hunan Gold | Chine | Chine (100%) | 19 % | +| **Chine** | **Total** | **Chine** | **-** | **64 %** | +| Russie | Polyus | Russie | Russie (100%) | 12 % | +| **Russie** | **Total** | **Russie** | **-** | **12 %** | +| Belgique | Umicore | Belgique | Russie (100%) | 6 % | +| **Belgique** | **Total** | **Belgique** | **-** | **6 %** | +| Tadjikistan | Aprelevka | Tadjikistan | Tadjikistan (100%) | 4 % | +| **Tadjikistan** | **Total** | **Tadjikistan** | **-** | **4 %** | +| Bolivie | Empresa Minera Unificada | Bolivie | Bolivie (100%) | 4 % | +| **Bolivie** | **Total** | **Bolivie** | **-** | **4 %** | +| Afrique du Sud | Consolidated Murchison | Afrique du Sud | Russie (100%) | 3 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **-** | **3 %** | +| États-Unis | Montana Resources | États-Unis | Chine (95%) | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **3 %** | +| Australie | Mandalay Resources | Australie | Australie (100%) | 3 % | +| **Australie** | **Total** | **Australie** | **-** | **3 %** | + +Unités : kt/an + +Total : 26000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 3.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Bolivie | 4.0% | 56 | :orange-badge[Orange] (Intermédiaire) | +| Tadjikistan | 4.0% | 59 | :orange-badge[Orange] (Intermédiaire) | +| Belgique | 6.0% | 34 | :green-badge[Vert] (Stable) | +| Australie | 3.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 64.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 12.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Afrique du Sud | 3.0% | 60 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **26** | +| **Pays** | | | **43** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **26**, signalant une **concentration élevée**. Le marché est largement dominé par **China Minmetals (45 %)**, **Hunan Gold (19 %)**, **Polyus (12 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **43**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (64 %)**, **Russie (12 %)**, **Belgique (6 %)**, représentant ensemble plus de 82 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 26) +- La **concentration géographique est élevée** (IHH 43) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 43 - :red-badge[Rouge] (Élevée) +* ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Palladium + +Le palladium est un métal précieux du groupe des platinoïdes, découvert en 1803 par William Hyde Wollaston. Ce métal blanc-argenté est caractérisé par sa rareté, sa résistance à la corrosion et ses propriétés catalytiques exceptionnelles. Plus léger que le platine mais avec des propriétés chimiques similaires, le palladium possède une densité de 12,02 g/cm³ et un point de fusion de 1555°C. Sa structure cristalline cubique à faces centrées lui confère une excellente ductilité et malléabilité. Il se distingue notamment par sa capacité unique à absorber jusqu'à 900 fois son volume en hydrogène, propriété exploitée dans diverses applications industrielles. + +Le palladium n'existe pas à l'état natif dans la nature mais se trouve principalement associé aux minerais de nickel, de cuivre et de platine. Son extraction implique des procédés métallurgiques et hydrométallurgiques complexes, généralement comme sous-produit de l'exploitation d'autres métaux. Le raffinement du palladium nécessite une série d'opérations chimiques sophistiquées pour atteindre les niveaux de pureté requis par l'industrie, typiquement supérieurs à 99,95%. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| CarteMere | 0.50 | 0.50 | 0.40 | 0.60 | +| Connecteurs | 0.50 | 0.50 | 0.40 | 0.60 | +| ProcesseurARM | 0.50 | 0.50 | 0.40 | 0.60 | +| ProcesseurX86 | 0.50 | 0.50 | 0.40 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.50 | :orange-badge[Orange] (Moyenne) | +| Connecteurs | 0.50 | :orange-badge[Orange] (Moyenne) | +| Carte mère | 0.50 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.50 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 24 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Électrodes, connecteurs, circuits imprimés, métallisation haute fiabilité, semi-conducteurs. +*Secteurs concurrents* +Catalyse automobile (>75% de la demande), chimie, bijouterie, investissement +*Remarques* +Très forte dépendance au secteur automobile. Le numérique reste secondaire malgré une forte valeur ajoutée dans ses usages. +*Répartition des usages* +* Numérique final : 20% +* Numérique embarqué : 5% +* Autres secteurs : 75% +*Tendance* +* Demande : 4.0 +* Production : 5.0 +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 4.0 +* Tension marché : 5.0 +*Réserves* +* Niveau : Modéré +* Pondération : 1.2 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.50 - :orange-badge[Orange] (Moyenne) +* IVC: 24 - :orange-badge[Orange] (Modérée) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Russie | Norilsk Nickel | Russie | 40 % | +| **Russie** | **Total** | **Russie** | **45 %** | +| Afrique du Sud | Anglo American Platinum | Royaume-Uni | 15 % | +| Afrique du Sud | Impala Platinum | Afrique du Sud | 10 % | +| Afrique du Sud | SibanyeStillwater | Afrique du Sud | 8 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **35 %** | +| Canada | North American Palladium | Canada | 3 % | +| Canada | Vale | Brésil | 2 % | +| **Canada** | **Total** | **Canada** | **8 %** | +| Zimbabwe | Zimplats Holdings | Australie | 3 % | +| Zimbabwe | Impala Platinum | Afrique du Sud | 2 % | +| **Zimbabwe** | **Total** | **Zimbabwe** | **7 %** | +| États-Unis | SibanyeStillwater | Afrique du Sud | 4 % | +| **États-Unis** | **Total** | **États-Unis** | **5 %** | + +Unités : t/an + +Total : 203 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 5.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Afrique du Sud | 35.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Canada | 8.0% | 26 | :green-badge[Vert] (Stable) | +| Russie | 45.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Zimbabwe | 7.0% | 70 | :red-badge[Rouge] (Instable) | + +**ISG combiné: 59 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **20** | | +| **Pays** | | | **34** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **20**, ce qui indique une **concentration modérée**. **Norilsk Nickel - Batteries, revêtements conducteurs, alliages (40 %)**, **Anglo American Platinum (15 %)**, **Impala Platinum (10 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **34**, révélant une **concentration géographique élevée**. La répartition est dominée par **Russie (45 %)**, **Afrique du Sud (35 %)**, **Canada (8 %)**, représentant ensemble plus de 88 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 20) +- La **concentration géographique est élevée** (IHH 34) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 34 - :red-badge[Rouge] (Élevée) +* ISG combiné: 59 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Russie | Norilsk Nickel | Russie | Russie (100%) | 41 % | +| **Russie** | **Total** | **Russie** | **-** | **41 %** | +| Afrique du Sud | Anglo American Platinum | Royaume-Uni | Afrique du Sud (100%), États-Unis (100%) | 21 % | +| Afrique du Sud | Impala Platinum | Afrique du Sud | Afrique du Sud (100%), États-Unis (100%) | 12 % | +| Afrique du Sud | SibanyeStillwater | Afrique du Sud | Afrique du Sud (100%), États-Unis (100%) | 5 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **-** | **38 %** | +| Canada | Vale | Brésil | Canada (100%), États-Unis (100%) | 6 % | +| Canada | Glencore | Suisse | Canada (100%), États-Unis (100%) | 4 % | +| **Canada** | **Total** | **Canada** | **-** | **10 %** | +| Zimbabwe | Zimplats | Afrique du Sud | Zimbabwe (100%), États-Unis (100%) | 4 % | +| Zimbabwe | Mimosa | Afrique du Sud | Zimbabwe (100%), États-Unis (100%) | 2 % | +| **Zimbabwe** | **Total** | **Zimbabwe** | **-** | **6 %** | +| États-Unis | SibanyeStillwater | Afrique du Sud | Afrique du Sud (100%), États-Unis (100%) | 6 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **6 %** | + +Unités : t/an + +Total : 210 + +_Note: Les capacités indiquées représentent la production mondiale de palladium raffiné en 2019, selon les données de l'USGS._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Afrique du Sud | 38.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 6.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 41.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Zimbabwe | 6.0% | 70 | :red-badge[Rouge] (Instable) | +| Canada | 10.0% | 26 | :green-badge[Vert] (Stable) | + +**ISG combiné: 58 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **24** | | +| **Pays** | | | **33** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **24**, ce qui indique une **concentration modérée**. **Norilsk Nickel - Batteries, revêtements conducteurs, alliages (41 %)**, **Anglo American Platinum (21 %)**, **Impala Platinum (12 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **33**, révélant une **concentration géographique élevée**. La répartition est dominée par **Russie (41 %)**, **Afrique du Sud (38 %)**, **Canada (10 %)**, représentant ensemble plus de 89 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 24) +- La **concentration géographique est élevée** (IHH 33) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 33 - :red-badge[Rouge] (Élevée) +* ISG combiné: 58 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Or + +L'or est un métal précieux qui occupe une place irremplaçable dans l'industrie électronique moderne en raison de ses propriétés physiques et chimiques exceptionnelles. Avec une conductivité électrique remarquable (71% de celle du cuivre) combinée à une résistance totale à l'oxydation et à la corrosion, l'or garantit des connexions électriques ultra-fiables essentielles pour les applications critiques. + +Sa malléabilité extrême permet la création de fils aussi fins que 20 micromètres pour les connexions internes des puces (wire bonding) et de revêtements d'une épaisseur de quelques atomes seulement. Bien que représentant une faible proportion en masse des équipements électroniques (environ 0,0016% du poids d'un smartphone), l'or est crucial pour leur fonctionnement, notamment dans les contacts électriques, les broches de connecteurs, les revêtements de circuits imprimés et les microsoudures. + +Le traitement de l'or pour applications électroniques exige des niveaux de pureté exceptionnels (99,99% minimum, souvent 99,999%) et des procédés spécifiques pour obtenir les formes utilisables par l'industrie, comme les fils, les feuilles, les poudres et les solutions électrolytiques pour galvanoplastie. + +Le secteur technologique émerge comme nouveau pilier de croissance (+7% en 2024), tandis que la bijouterie recule (-11%). Le recyclage contribue à hauteur de 1 200 tonnes/an (24% de l'offre). Un risque géopolitique majeur persiste avec la surreprésentation des banques centrales dans la demande (20%). + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Connecteurs | 0.64 | 0.70 | 0.60 | 0.60 | +| ProcesseurARM | 0.60 | 0.60 | 0.60 | 0.60 | +| ProcesseurASIC | 0.60 | 0.60 | 0.60 | 0.60 | +| ProcesseurX86 | 0.60 | 0.60 | 0.60 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Processeur ARM | 0.60 | :red-badge[Rouge] (Difficile) | +| Connecteurs | 0.64 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.62 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 68 - :red-badge[Rouge] (Forte)** + +*Usage numérique* +Connecteurs, circuits imprimés, puces, composants haute fiabilité pour équipements électroniques. +*Secteurs concurrents* +Joaillerie, réserve de valeur, finance, banque centrale +*Remarques* +Le secteur du numérique ne représente qu'une faible part de la demande. Forte concurrence de la joaillerie et du secteur financier. Usage stratégique malgré un faible volume. +*Répartition des usages* +* Numérique final : 10% +* Numérique embarqué : 2% +* Autres secteurs : 88% +*Tendance* +* Demande : +15% +* Production : +10% +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 9.0 +* Tension marché : 5.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.62 - :red-badge[Rouge] (Difficile) +* IVC: 68 - :red-badge[Rouge] (Forte) +* Poids combiné: 9 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Australie | Newcrest Mining | Australie | 5 % | +| **Australie** | **Total** | **Australie** | **9 %** | +| Russie | Polyus | Russie | 6 % | +| **Russie** | **Total** | **Russie** | **9 %** | +| Canada | Agnico Eagle Mines | Canada | 2 % | +| **Canada** | **Total** | **Canada** | **6 %** | +| Afrique du Sud | AngloGold Ashanti | Afrique du Sud | 4 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **3 %** | + +Unités : t/an + +Total : 3300 + +_Sources : World Gold Council 2024, USGS Mineral Commodity Summaries 2025._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Canada | 6.0% | 26 | :green-badge[Vert] (Stable) | +| Australie | 9.0% | 26 | :green-badge[Vert] (Stable) | +| Afrique du Sud | 3.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 9.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 43 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **1** | | | +| **Pays** | **2** | | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **1**, ce qui indique une **concentration faible**. Bien que **Polyus (6 %)**, **Newcrest Mining (5 %)**, **AngloGold Ashanti (4 %)** regroupent plus de 15 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **2**, révélant une **concentration géographique faible**. La répartition est dominée par **Australie (9 %)**, **Russie (9 %)**, **Canada (6 %)**, représentant ensemble plus de 24 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 1) +- La **concentration géographique est faible** (IHH 2) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 2 - :green-badge[Vert] (Faible) +* ISG combiné: 43 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Suisse | Metalor Technologies | Suisse | Canada (5%), Australie (15%) | 18 % | +| Suisse | PAMP | Suisse | Afrique du Sud (8%), Australie (15%) | 12 % | +| Suisse | Valcambi | Inde | Australie (15%), Australie (15%) | 11 % | +| Suisse | ArgorHeraeus | Allemagne | Afrique du Sud (10%), Australie (15%) | 10 % | +| **Suisse** | **Total** | **Suisse** | **-** | **51 %** | +| Japon | Tanaka Kikinzoku Kogyo | Japon | Canada (15%), Australie (20%) | 14 % | +| Japon | Asahi Refining | Japon | Canada (10%), Australie (30%) | 7 % | +| **Japon** | **Total** | **Japon** | **-** | **21 %** | +| États-Unis | Heraeus | Allemagne | Canada (30%), Australie (15%) | 6 % | +| États-Unis | Johnson Matthey | Royaume-Uni | Canada (25%), Australie (15%) | 5 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **11 %** | +| Allemagne | Umicore | Belgique | | 7 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **7 %** | +| Afrique du Sud | Rand Refinery | Afrique du Sud | Afrique du Sud (65%), Australie (15%) | 5 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **-** | **5 %** | +| Australie | Perth Mint | Australie | Australie (75%) | 5 % | +| **Australie** | **Total** | **Australie** | **-** | **5 %** | + +Unités : t/an + +Total : 3610 + +_Note: Les capacités indiquées représentent le traitement d'or pour toutes applications, dont environ 12-15% sont destinées spécifiquement à l'électronique et aux applications numériques. La Suisse, bien que ne produisant pas d'or minier, domine le raffinage mondial grâce à son expertise historique et ses infrastructures financières._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Suisse | 51.0% | 22 | :green-badge[Vert] (Stable) | +| Allemagne | 7.0% | 30 | :green-badge[Vert] (Stable) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| EtatsUnis | 11.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Afrique du Sud | 5.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 21.0% | 29 | :green-badge[Vert] (Stable) | + +**ISG combiné: 28 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **11** | | | +| **Pays** | | | **33** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **11**, ce qui indique une **concentration faible**. Bien que **Metalor Technologies (18 %)**, **Tanaka Kikinzoku Kogyo (14 %)**, **PAMP (12 %)** regroupent plus de 44 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **33**, révélant une **concentration géographique élevée**. La répartition est dominée par **Suisse (51 %)**, **Japon (21 %)**, **États-Unis (11 %)**, représentant ensemble plus de 83 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 11) +- La **concentration géographique est élevée** (IHH 33) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 33 - :red-badge[Rouge] (Élevée) +* ISG combiné: 28 - :green-badge[Vert] (Stable) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +--- + +### Platine + +Le platine est un métal précieux rare du groupe des platinoïdes, découvert en 1748 par Antonio de Ulloa mais utilisé industriellement depuis seulement 150 ans environ. Ce métal blanc-argenté, dense (21,45 g/cm³), est caractérisé par son point de fusion exceptionnellement élevé (1768°C), sa résistance remarquable à la corrosion et à l'oxydation, ses excellentes propriétés catalytiques et sa conductivité électrique et thermique. + +Le platine ne se trouve pas à l'état natif dans la nature mais principalement associé aux minerais de nickel, de cuivre et d'autres platinoïdes (palladium, rhodium, iridium, osmium et ruthénium). Son extraction implique des procédés métallurgiques complexes, généralement comme sous-produit d'autres métaux, suivis de techniques hydrométallurgiques sophistiquées pour sa séparation et sa purification. + +Malgré sa rareté (production mondiale de quelques centaines de tonnes par an), le platine est devenu un métal stratégique indispensable pour de nombreuses applications industrielles, notamment les catalyseurs automobiles, l'électronique, la joaillerie, l'industrie chimique et médicale, en raison de ses propriétés physico-chimiques uniques et irremplaçables. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Camera | 0.71 | 0.80 | 0.70 | 0.60 | +| Capteurs | 0.70 | 0.70 | 0.70 | 0.70 | +| DisqueDur | 0.64 | 0.70 | 0.60 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Caméra | 0.71 | :red-badge[Rouge] (Difficile) | +| Capteurs | 0.70 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.70 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 64 - :red-badge[Rouge] (Forte)** + +*Usage numérique* +Connecteurs haute fiabilité, thermocouples, capteurs, équipements électroniques critiques, circuits HF. +*Secteurs concurrents* +Catalyse automobile, joaillerie, investissement, chimie fine +*Remarques* +Très forte dépendance du secteur automobile. Usage dans le numérique ciblé sur fiabilité extrême ou militaire. +*Répartition des usages* +* Numérique final : 7.0 +* Numérique embarqué : 3% +* Autres secteurs : 90% +*Tendance* +* Demande : +12% +* Production : +8% +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 13.29 +* Tension marché : 4.0 +*Réserves* +* Niveau : Modéré +* Pondération : 1.2 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.70 - :red-badge[Rouge] (Difficile) +* IVC: 64 - :red-badge[Rouge] (Forte) +* Poids combiné: 9 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Afrique du Sud | Anglo American Platinum | Afrique du Sud | 25 % | +| Afrique du Sud | Impala Platinum | Afrique du Sud | 16 % | +| Afrique du Sud | SibanyeStillwater | Afrique du Sud | 13 % | +| Afrique du Sud | Northam Platinum | Afrique du Sud | 11 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **72 %** | +| Russie | Norilsk Nickel | Russie | 13 % | +| **Russie** | **Total** | **Russie** | **13 %** | +| Zimbabwe | Zimplats | Zimbabwe | 3 % | +| **Zimbabwe** | **Total** | **Zimbabwe** | **11 %** | +| Canada | Vale | Brésil | 2 % | +| **Canada** | **Total** | **Canada** | **3 %** | +| États-Unis | SibanyeStillwater | États-Unis | 1 % | +| **États-Unis** | **Total** | **États-Unis** | **2 %** | + +Unités : t/an + +Total : 180 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Zimbabwe | 11.0% | 70 | :red-badge[Rouge] (Instable) | +| Afrique du Sud | 72.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 2.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Canada | 3.0% | 26 | :green-badge[Vert] (Stable) | +| Russie | 13.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 60 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **14** | | | +| **Pays** | | | **55** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **14**, ce qui indique une **concentration faible**. Bien que **Anglo American Platinum (25 %)**, **Impala Platinum (16 %)**, **SibanyeStillwater (13 %)** regroupent plus de 54 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **55**, révélant une **concentration géographique élevée**. La répartition est dominée par **Afrique du Sud (72 %)**, **Russie (13 %)**, **Zimbabwe (11 %)**, représentant ensemble plus de 96 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 14) +- La **concentration géographique est élevée** (IHH 55) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 55 - :red-badge[Rouge] (Élevée) +* ISG combiné: 60 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Afrique du Sud | Anglo American Platinum | Royaume-Uni | Afrique du Sud (100%), États-Unis (100%) | 35 % | +| Afrique du Sud | Impala Platinum | Afrique du Sud | Afrique du Sud (100%), États-Unis (100%) | 18 % | +| Afrique du Sud | SibanyeStillwater | Afrique du Sud | Afrique du Sud (100%), États-Unis (100%) | 9 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **-** | **62 %** | +| Russie | Norilsk Nickel | Russie | Russie (100%), États-Unis (100%) | 24 % | +| **Russie** | **Total** | **Russie** | **-** | **24 %** | +| Canada | Vale | Brésil | Canada (100%), États-Unis (100%) | 5 % | +| Canada | Glencore | Suisse | Canada (100%), États-Unis (100%) | 2 % | +| **Canada** | **Total** | **Canada** | **-** | **7 %** | +| Zimbabwe | Zimplats | Afrique du Sud | Zimbabwe (100%), États-Unis (100%) | 3 % | +| Zimbabwe | Mimosa | Afrique du Sud | Zimbabwe (100%), États-Unis (100%) | 1 % | +| **Zimbabwe** | **Total** | **Zimbabwe** | **-** | **4 %** | +| États-Unis | SibanyeStillwater | Afrique du Sud | Afrique du Sud (100%), États-Unis (100%) | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **3 %** | + +Unités : t/an + +Total : 170 + +_Note: Les capacités indiquées représentent la production de platine raffiné. L'Afrique du Sud possède environ 91% des réserves mondiales de platine et domine largement la production mondiale._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Zimbabwe | 4.0% | 70 | :red-badge[Rouge] (Instable) | +| EtatsUnis | 3.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Canada | 7.0% | 26 | :green-badge[Vert] (Stable) | +| Afrique du Sud | 62.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 24.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 59 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **23** | | +| **Pays** | | | **45** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **23**, ce qui indique une **concentration modérée**. **Anglo American Platinum (35 %)**, **Norilsk Nickel - Batteries, revêtements conducteurs, alliages (24 %)**, **Impala Platinum (18 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **45**, révélant une **concentration géographique élevée**. La répartition est dominée par **Afrique du Sud (62 %)**, **Russie (24 %)**, **Canada (7 %)**, représentant ensemble plus de 93 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 23) +- La **concentration géographique est élevée** (IHH 45) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 45 - :red-badge[Rouge] (Élevée) +* ISG combiné: 59 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Quartz + +Le quartz ultra-pur 5N représente l'une des matières premières les plus critiques pour l'industrie des semi-conducteurs et du photovoltaïque. Avec une pureté atteignant 99,999% de dioxyde de silicium (SiO₂), ce matériau stratégique constitue un maillon essentiel dans la fabrication des composants électroniques avancés. Sa production implique des processus complexes de purification à partir de gisements de quartz naturel exceptionnellement purs, dont les plus importants se situent à Spruce Pine (Caroline du Nord, États-Unis) qui fournit environ 80% de la production mondiale. Le traitement du quartz pour atteindre ce niveau de pureté 5N nécessite une élimination quasi-totale des impuretés (aluminium, titane, fer, lithium, sodium, potassium), généralement présentes à seulement quelques parties par million (ppm) dans le matériau final. Ces exigences de pureté extrême font du quartz 5N un matériau rare, coûteux et stratégique, utilisé principalement pour la fabrication de creusets de haute qualité destinés à la croissance des cristaux de silicium, de tubes de quartz et d'autres composants critiques pour l'industrie électronique. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| CreusetQuartz | 0.74 | 0.80 | 0.70 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Creuset quartz | 0.74 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.74 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 34 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Substrats semi-conducteurs, optoélectronique, composants haute fréquence, fibres optiques, MEMS. +*Secteurs concurrents* +Photovoltaïque, optique, verre spécial, industrie chimique +*Remarques* +Le quartz ultra-pur (5N) est critique pour les semi-conducteurs. Fort chevauchement entre usages PV et numériques. Ressource très concentrée, sensible aux événements climatiques. +*Répartition des usages* +* Numérique final : 40% +* Numérique embarqué : 20% +* Autres secteurs : 40% +*Tendance* +* Demande : +35% +* Production : +20% +* Ratio capacité/demande : 0.89 +*Concurrence & tension* +* Ratio concurrence : 1.5 +* Tension marché : 15.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.74 - :red-badge[Rouge] (Difficile) +* IVC: 34 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| États-Unis | The Quartz Corp | Norvège | 35 % | +| États-Unis | Sibelco | Belgique | 30 % | +| **États-Unis** | **Total** | **États-Unis** | **79 %** | +| Chine | Jiangsu Pacific Quartz Co | Chine | 8 % | +| **Chine** | **Total** | **Chine** | **10 %** | +| Russie | Russian Quartz LLC | Russie | 6 % | +| **Russie** | **Total** | **Russie** | **6 %** | +| Norvège | The Quartz Corp | Norvège | 3 % | +| **Norvège** | **Total** | **Norvège** | **3 %** | + +Unités : tonnes/an + +Total : 35000 + +Analyse de pertinence : +Les données d'extraction sont pertinentes car elles reflètent la forte concentration de la production aux États-Unis, en particulier autour de Spruce Pine. Cette dépendance géographique constitue un point de fragilité important dans la chaîne d'approvisionnement mondiale. Les producteurs chinois et russes restent marginaux, limitant les alternatives en cas de perturbation géopolitique ou climatique. + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 10.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Norvege | 3.0% | 23 | :green-badge[Vert] (Stable) | +| EtatsUnis | 79.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 6.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 44 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **22** | | +| **Pays** | | | **64** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **22**, ce qui indique une **concentration modérée**. **The Quartz Ultra-Pur 5N - Pureté 99.999%, creusets pour semi-conducteurs Corp (35 %)**, **Sibelco (30 %)**, **Jiangsu Pacific Quartz Ultra-Pur 5N - Pureté 99.999%, creusets pour semi-conducteurs Co (8 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **64**, révélant une **concentration géographique élevée**. La répartition est dominée par **États-Unis (79 %)**, **Chine (10 %)**, **Russie (6 %)**, représentant ensemble plus de 95 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 22) +- La **concentration géographique est élevée** (IHH 64) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 64 - :red-badge[Rouge] (Élevée) +* ISG combiné: 44 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| États-Unis | Unimin Corporation | Belgique | États-Unis (100%) | 38 % | +| États-Unis | Quartz Corp | États-Unis | États-Unis (100%) | 25 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **63 %** | +| Norvège | Imerys | France | Norvège (100%) | 13 % | +| **Norvège** | **Total** | **Norvège** | **-** | **13 %** | +| Japon | ShinEtsu Chemical | Japon | États-Unis (70%) | 11 % | +| **Japon** | **Total** | **Japon** | **-** | **11 %** | +| Chine | Jiangsu Pacific Quartz | Chine | Chine (100%) | 6 % | +| **Chine** | **Total** | **Chine** | **-** | **6 %** | +| Russie | Kyshtym Mining | Russie | Russie (100%) | 5 % | +| **Russie** | **Total** | **Russie** | **-** | **5 %** | +| Brésil | Mineração Jundu | Brésil | Norvège (100%) | 2 % | +| **Brésil** | **Total** | **Brésil** | **-** | **2 %** | + +Unités : tonnes/an + +Total : 30000 + +Analyse de pertinence : +Les capacités de traitement suivent étroitement les lieux d'extraction, renforçant la dépendance vis-à-vis des États-Unis. La concentration des entreprises sur un nombre limité de sites rend la chaîne de transformation vulnérable à tout incident localisé, qu’il soit naturel, industriel ou géopolitique. + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 63.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Norvege | 13.0% | 23 | :green-badge[Vert] (Stable) | +| Japon | 11.0% | 29 | :green-badge[Vert] (Stable) | +| Chine | 6.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 2.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 5.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 40 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **24** | | +| **Pays** | | | **43** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **24**, ce qui indique une **concentration modérée**. **Unimin Corporation (38 %)**, **Quartz Ultra-Pur 5N - Pureté 99.999%, creusets pour semi-conducteurs Corp (25 %)**, **Imerys (13 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **43**, révélant une **concentration géographique élevée**. La répartition est dominée par **États-Unis (63 %)**, **Norvège (13 %)**, **Japon (11 %)**, représentant ensemble plus de 87 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 24) +- La **concentration géographique est élevée** (IHH 43) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 43 - :red-badge[Rouge] (Élevée) +* ISG combiné: 40 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Magnésium + +Le magnésium est un métal alcalino-terreux léger, blanc argenté, qui présente une excellente conductivité thermique et électrique. Sa production industrielle repose sur deux procédés principaux : la voie électrolytique à partir de chlorure de magnésium (MgCl₂) extrait de saumures ou d'eau de mer, et la voie métallothermique (procédé Pidgeon) utilisant la réduction de l'oxyde de magnésium (MgO) contenu dans la dolomie ou la magnésite. Ce dernier procédé, développé en 1940 par Lloyd Montgomery Pidgeon au Canada, domine la production mondiale actuelle, particulièrement en Chine où son faible coût d'investissement compense sa forte consommation énergétique. Le traitement du magnésium requiert des conditions spécifiques en raison de sa réactivité avec l'oxygène et l'eau, nécessitant des atmosphères contrôlées lors des étapes finales de production. Les applications diversifiées du magnésium, de l'allègement des structures en automobile et aéronautique au traitement de l'eau et aux suppléments nutritionnels, en font un métal stratégique dont les méthodes de production évoluent vers une meilleure efficacité énergétique et environnementale. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Boitier | 0.50 | 0.50 | 0.40 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Boîtier | 0.50 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.50 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 28 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Composants électroniques légers, boîtiers pour équipements numériques, alliages pour électronique portable. +*Secteurs concurrents* +Automobile, aéronautique, métallurgie, nutrition +*Remarques* +Principalement utilisé dans des alliages légers pour la construction. Usage numérique relativement limité. Ressource abondante et bien répartie. Croissance modérée et contrôlée. +*Répartition des usages* +* Numérique final : 15% +* Numérique embarqué : 5% +* Autres secteurs : 80% +*Tendance* +* Demande : +15% +* Production : +10% +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 5.67 +* Tension marché : 5.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.50 - :orange-badge[Orange] (Moyenne) +* IVC: 28 - :orange-badge[Orange] (Modérée) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Shaanxi Magnesium Group | Chine | 9 % | +| Chine | Taiyuan Yiwei Magnesium | Chine | 7 % | +| Chine | Ningxia Huiye Magnesium | Chine | 7 % | +| **Chine** | **Total** | **Chine** | **85 %** | +| Russie | VSMPOAVISMA | Russie | 5 % | +| **Russie** | **Total** | **Russie** | **5 %** | +| États-Unis | US Magnesium | États-Unis | 7 % | +| **États-Unis** | **Total** | **États-Unis** | **4 %** | +| Brésil | RIMA Industrial | Brésil | 2 % | +| **Brésil** | **Total** | **Brésil** | **2 %** | +| Israël | Dead Sea Magnesium | Israël | 3 % | +| **Israël** | **Total** | **Israël** | **2 %** | +| Norvège | Norsk Hydro | Norvège | 4 % | +| **Norvège** | **Total** | **Norvège** | **1 %** | + +Unités : t/an + +Total : 1120000 + +_Note: Le marché est fortement dominé par les producteurs chinois qui représentent collectivement environ 85% de la production mondiale. Deux procédés principaux sont utilisés: l'électrolyse de chlorure de magnésium (principalement hors Chine) et la réduction thermique (procédé Pidgeon, dominant en Chine)._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Russie | 5.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 85.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Israel | 2.0% | 59 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 4.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Norvege | 1.0% | 23 | :green-badge[Vert] (Stable) | +| Bresil | 2.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **3** | | | +| **Pays** | | | **73** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **3**, ce qui indique une **concentration faible**. Bien que **Shaanxi Magnésium - Alliages légers, dissipation thermique Group (9 %)**, **Ningxia Huiye Magnésium - Alliages légers, dissipation thermique (7 %)**, **Taiyuan Yiwei Magnésium - Alliages légers, dissipation thermique (7 %)** regroupent plus de 23 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **73**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (85 %)**, **Russie (5 %)**, **États-Unis (4 %)**, représentant ensemble plus de 94 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 3) +- La **concentration géographique est élevée** (IHH 73) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 73 - :red-badge[Rouge] (Élevée) +* ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Shaanxi Magnesium Group | Chine | Chine (100%) | 9 % | +| Chine | Taiyuan Yiwei Magnesium | Chine | Chine (100%) | 7 % | +| Chine | Ningxia Huiye Magnesium | Chine | Chine (100%) | 7 % | +| **Chine** | **Total** | **Chine** | **-** | **72 %** | +| États-Unis | US Magnesium | États-Unis | États-Unis (100%) | 7 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **7 %** | +| Russie | VSMPOAVISMA | Russie | Russie (100%) | 5 % | +| **Russie** | **Total** | **Russie** | **-** | **5 %** | +| Norvège | Norsk Hydro | Norvège | Norvège (100%) | 4 % | +| **Norvège** | **Total** | **Norvège** | **-** | **4 %** | +| Israël | Dead Sea Magnesium | Israël | Israël (100%) | 3 % | +| **Israël** | **Total** | **Israël** | **-** | **3 %** | +| Brésil | RIMA Industrial | Brésil | Brésil (100%) | 2 % | +| **Brésil** | **Total** | **Brésil** | **-** | **2 %** | +| Kazakhstan | Solikamsk Magnesium | Kazakhstan | Chine (100%) | 2 % | +| **Kazakhstan** | **Total** | **Kazakhstan** | **-** | **2 %** | +| Turquie | ESAN | Turquie | Brésil (100%) | 1 % | +| **Turquie** | **Total** | **Turquie** | **-** | **1 %** | +| Corée du Sud | POSCO | Corée du Sud | Chine (60%) | 1 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **-** | **1 %** | +| Ukraine | Zaporozhye Titanium | Ukraine | | 1 % | +| **Ukraine** | **Total** | **Ukraine** | **-** | **1 %** | + +Unités : t/an + +Total : 1096000 + +_Note: Les capacités indiquées représentent le magnésium métal. La production chinoise est dominée par le procédé Pidgeon, tandis que les producteurs occidentaux utilisent principalement l'électrolyse. Les données reflètent les capacités installées en 2024-2025._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Ukraine | 1.0% | 73 | :red-badge[Rouge] (Instable) | +| Corée du Sud | 1.0% | 33 | :green-badge[Vert] (Stable) | +| Kazakhstan | 2.0% | 51 | :orange-badge[Orange] (Intermédiaire) | +| Turquie | 1.0% | 64 | :orange-badge[Orange] (Intermédiaire) | +| Norvege | 4.0% | 23 | :green-badge[Vert] (Stable) | +| Chine | 72.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 2.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Israel | 3.0% | 59 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 7.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 5.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **3** | | | +| **Pays** | | | **53** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **3**, ce qui indique une **concentration faible**. Bien que **Shaanxi Magnésium - Alliages légers, dissipation thermique Group (9 %)**, **Ningxia Huiye Magnésium - Alliages légers, dissipation thermique (7 %)**, **Taiyuan Yiwei Magnésium - Alliages légers, dissipation thermique (7 %)** regroupent plus de 23 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **53**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (72 %)**, **États-Unis (7 %)**, **Russie (5 %)**, représentant ensemble plus de 84 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 3) +- La **concentration géographique est élevée** (IHH 53) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 53 - :red-badge[Rouge] (Élevée) +* ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Zinc + +Le zinc est un métal de transition blanc-bleuâtre, malléable à des températures élevées, reconnu pour ses excellentes propriétés anticorrosion qui en font un matériau stratégique pour la protection des aciers. Sa production industrielle repose majoritairement sur deux voies distinctes : la pyrométallurgie (10% de la production mondiale) et l'hydrométallurgie suivie d'électrométallurgie (90%). + +Le procédé hydrométallurgique dominant implique quatre étapes principales : le grillage des concentrés sulfurés, la lixiviation pour solubiliser le zinc, la purification pour éliminer les impuretés métalliques, et l'électrolyse pour obtenir du zinc de haute pureté (99,995%). La voie pyrométallurgique, plus ancienne, exploite la volatilité du zinc qui se vaporise à 907°C avant d'être condensé. + +La demande mondiale continue d'être soutenue par le secteur de la galvanisation qui représente près de 50% des usages, mais le zinc trouve également des applications dans les alliages, l'électronique, les piles, et divers produits chimiques industriels. La chaîne d'approvisionnement mondiale est largement dominée par la Chine, qui contrôle une part significative tant de la production minière que des capacités de traitement. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Batterie | 0.30 | 0.30 | 0.20 | 0.40 | +| Boitier | 0.30 | 0.30 | 0.20 | 0.40 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Boîtier | 0.30 | :orange-badge[Orange] (Moyenne) | +| Batterie | 0.30 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.30 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 12 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Batteries zinc-air, galvanisation de composants électroniques, oxydes pour composants transparents, circuits imprimés. +*Secteurs concurrents* +Construction, automobile, métallurgie, batteries industrielles +*Remarques* +Usage numérique en développement avec les batteries alternatives. Dominance de la galvanisation pour la construction. +*Répartition des usages* +* Numérique final : 8% +* Numérique embarqué : 2% +* Autres secteurs : 90% +*Tendance* +* Demande : +10% +* Production : +9% +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 11.5 +* Tension marché : 1.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.30 - :orange-badge[Orange] (Moyenne) +* IVC: 12 - :green-badge[Vert] (Faible) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | 12 % | +| Chine | Zijin Mining | Chine | 6 % | +| **Chine** | **Total** | **Chine** | **33 %** | +| Pérou | Nexa Resources | Brésil | 6 % | +| Pérou | Volcan Compañía Minera | Suisse | 4 % | +| **Pérou** | **Total** | **Pérou** | **12 %** | +| Australie | MMG Limited | Chine | 5 % | +| Australie | New Century Resources | Australie | 2 % | +| **Australie** | **Total** | **Australie** | **9 %** | +| Inde | Hindustan Zinc | Inde | 7 % | +| **Inde** | **Total** | **Inde** | **7 %** | +| Mexique | Industrias Peñoles | Mexique | 4 % | +| **Mexique** | **Total** | **Mexique** | **6 %** | +| Bolivie | Sinchi Wayra | Suisse | 3 % | +| **Bolivie** | **Total** | **Bolivie** | **4 %** | + +Unités : t/an + +Total : 12000000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Mexique | 6.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Perou | 12.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Bolivie | 4.0% | 56 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 9.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 33.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 7.0% | 60 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **3** | | | +| **Pays** | **14** | | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **3**, ce qui indique une **concentration faible**. Bien que **China Minmetals (12 %)**, **Hindustan Zinc - Galvanisation, protection contre corrosion, batteries (7 %)**, **Zijin Mining (6 %)** regroupent plus de 25 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **14**, révélant une **concentration géographique faible**. La répartition est dominée par **Chine (33 %)**, **Pérou (12 %)**, **Australie (9 %)**, représentant ensemble plus de 54 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 3) +- La **concentration géographique est faible** (IHH 14) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 14 - :green-badge[Vert] (Faible) +* ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Australie (15%), Chine (80%), Pérou (5%), Pérou (25%) | 18 % | +| Chine | Zijin Mining | Chine | Australie (10%), Chine (90%), Mexique (10%), Pérou (25%) | 11 % | +| Chine | Yunnan Chihong Zinc \& Germanium | Chine | Chine (95%), Chine (80%), Pérou (5%), Pérou (25%) | 9 % | +| **Chine** | **Total** | **Chine** | **-** | **38 %** | +| Corée du Sud | Korea Zinc | Corée du Sud | Australie (50%), Bolivie (15%), Mexique (10%), Pérou (25%) | 12 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **-** | **12 %** | +| Inde | Hindustan Zinc | Inde | Inde (100%), Pérou (25%), Mexique (10%), Pérou (25%) | 9 % | +| **Inde** | **Total** | **Inde** | **-** | **9 %** | +| Canada | Teck Resources | Canada | Pérou (10%), Chine (80%), Pérou (5%), Pérou (25%) | 6 % | +| **Canada** | **Total** | **Canada** | **-** | **6 %** | +| Australie | Nyrstar | Suisse | Australie (100%), Pérou (25%), Mexique (10%), Pérou (25%) | 5 % | +| **Australie** | **Total** | **Australie** | **-** | **5 %** | +| Japon | Mitsui Mining \& Smelting | Japon | Australie (40%), Pérou (30%) | 5 % | +| **Japon** | **Total** | **Japon** | **-** | **5 %** | +| Espagne | Asturiana de Zinc | Suisse | Australie (40%), Pérou (30%), Pérou (5%), Pérou (25%) | 4 % | +| **Espagne** | **Total** | **Espagne** | **-** | **4 %** | +| Belgique | Nyrstar | Suisse | Australie (100%), Pérou (25%), Pérou (5%), Pérou (25%) | 4 % | +| **Belgique** | **Total** | **Belgique** | **-** | **4 %** | +| Finlande | Boliden | Suède | Pérou (10%), Chine (80%), Pérou (5%), Pérou (25%) | 3 % | +| **Finlande** | **Total** | **Finlande** | **-** | **3 %** | +| Pérou | Nexa Resources | Brésil | Pérou (100%), Pérou (30%) | 3 % | +| **Pérou** | **Total** | **Pérou** | **-** | **3 %** | + +Unités : t/an + +Total : 5400 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 38.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Belgique | 4.0% | 34 | :green-badge[Vert] (Stable) | +| Inde | 9.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Corée du Sud | 12.0% | 33 | :green-badge[Vert] (Stable) | +| Finlande | 3.0% | 25 | :green-badge[Vert] (Stable) | +| Canada | 6.0% | 26 | :green-badge[Vert] (Stable) | +| Japon | 5.0% | 29 | :green-badge[Vert] (Stable) | +| Perou | 3.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Espagne | 4.0% | 39 | :green-badge[Vert] (Stable) | + +**ISG combiné: 44 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **9** | | | +| **Pays** | | **18** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **9**, ce qui indique une **concentration faible**. Bien que **China Minmetals (18 %)**, **Korea Zinc - Galvanisation, protection contre corrosion, batteries (12 %)**, **Zijin Mining (11 %)** regroupent plus de 41 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **18**, révélant une **concentration géographique modérée**. La répartition est dominée par **Chine (38 %)**, **Corée du Sud (12 %)**, **Inde (9 %)**, représentant ensemble plus de 59 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 9) +- La **concentration géographique est modérée** (IHH 18) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 18 - :orange-badge[Orange] (Modérée) +* ISG combiné: 44 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +--- + +### Manganèse + +Le manganèse est un métal de transition gris-argenté, dur et cassant, caractérisé par sa grande réactivité chimique et ses importantes propriétés magnétiques. Sa production industrielle repose sur deux filières principales : la pyrométallurgie, largement dominante et utilisée pour l'obtention de ferroalliages, et l'hydrométallurgie, employée pour produire du manganèse de haute pureté (>99,7%). Le manganèse est généralement extrait à partir de minerais d'oxyde (pyrolusite - MnO₂) ou de carbonate. Élément essentiel à la sidérurgie moderne, il constitue un ingrédient indispensable dans la fabrication d'aciers inoxydables et d'alliages spéciaux. Ses propriétés anticorrosion, son rôle d'agent désoxydant et désulfurant dans la métallurgie, ainsi que ses applications croissantes dans le secteur des batteries, en font un métal stratégique. La chaîne d'approvisionnement mondiale du manganèse est caractérisée par une forte concentration géographique de sa production minière, principalement en Afrique du Sud, au Gabon, en Australie et en Chine, cette dernière dominant largement les capacités de transformation. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.60 | 0.60 | 0.50 | 0.70 | +| Capteurs | 0.50 | 0.50 | 0.40 | 0.60 | +| Batterie | 0.40 | 0.40 | 0.30 | 0.50 | +| Boitier | 0.40 | 0.40 | 0.30 | 0.50 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Boîtier | 0.40 | :orange-badge[Orange] (Moyenne) | +| Batterie | 0.40 | :orange-badge[Orange] (Moyenne) | +| Audio | 0.60 | :red-badge[Rouge] (Difficile) | +| Capteurs | 0.50 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.47 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 12 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Batteries, circuits imprimés, composants électroniques, ferrites. +*Secteurs concurrents* +Sidérurgie, agriculture, chimie industrielle +*Remarques* +Très utilisé dans l'industrie sidérurgique. Usage numérique croissant pour les batteries et composants. Ressource abondante mais demande industrielle importante et continue. +*Répartition des usages* +* Numérique final : 30% +* Numérique embarqué : 5% +* Autres secteurs : 65% +*Tendance* +* Demande : +30% +* Production : +25% +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 2.33 +* Tension marché : 5.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.47 - :orange-badge[Orange] (Moyenne) +* IVC: 12 - :green-badge[Vert] (Faible) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Afrique du Sud | South32 | Australie | 19 % | +| Afrique du Sud | Assmang Ltd | Afrique du Sud | 9 % | +| Afrique du Sud | Jupiter Mines | Australie | 6 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **37 %** | +| Gabon | Eramet | France | 14 % | +| Gabon | AML | Gabon | 3 % | +| Gabon | NGM | Inde | 3 % | +| **Gabon** | **Total** | **Gabon** | **23 %** | +| Chine | TMI | Chine | 6 % | +| **Chine** | **Total** | **Chine** | **4 %** | +| Inde | MOIL | Inde | 3 % | +| **Inde** | **Total** | **Inde** | **4 %** | + +Unités : t/an + +Total : 20000000 + +_Sources: USGS Mineral Commodity Summaries 2024-2025, Eramet._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Gabon | 23.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Afrique du Sud | 37.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 4.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 4.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 59 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **7** | | | +| **Pays** | | **19** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **7**, ce qui indique une **concentration faible**. Bien que **South32 (19 %)**, **Eramet (14 %)**, **Assmang Ltd (9 %)** regroupent plus de 42 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **19**, révélant une **concentration géographique modérée**. La répartition est dominée par **Afrique du Sud (37 %)**, **Gabon (23 %)**, **Chine (4 %)**, représentant ensemble plus de 64 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 7) +- La **concentration géographique est modérée** (IHH 19) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 19 - :orange-badge[Orange] (Modérée) +* ISG combiné: 59 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Tianjin Manganese Plant | Chine | Chine (65%), Gabon (15%) | 19 % | +| Chine | CITIC Dameng Mining | Chine | Chine (80%) | 13 % | +| **Chine** | **Total** | **Chine** | **-** | **32 %** | +| Afrique du Sud | Assmang | Afrique du Sud | Afrique du Sud (100%), Gabon (15%) | 14 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **-** | **14 %** | +| Inde | MOIL | Inde | Inde (100%), Gabon (20%) | 10 % | +| **Inde** | **Total** | **Inde** | **-** | **10 %** | +| Gabon | Comilog | France | Gabon (100%) | 9 % | +| **Gabon** | **Total** | **Gabon** | **-** | **9 %** | +| Brésil | Vale | Brésil | Afrique du Sud (100%), Gabon (15%) | 6 % | +| **Brésil** | **Total** | **Brésil** | **-** | **6 %** | +| Ukraine | Nikopol | Ukraine | Gabon (100%) | 6 % | +| **Ukraine** | **Total** | **Ukraine** | **-** | **6 %** | +| Australie | South32 | Australie | Afrique du Sud (100%) | 5 % | +| **Australie** | **Total** | **Australie** | **-** | **5 %** | +| Norvège | Eramet Norway | France | Gabon (90%), Gabon (20%) | 4 % | +| **Norvège** | **Total** | **Norvège** | **-** | **4 %** | +| France | Eramet Marietta | France | Gabon (100%) | 3 % | +| **France** | **Total** | **France** | **-** | **3 %** | +| Japon | Mizushima Ferroalloy | Japon | Afrique du Sud (30%), Gabon (20%) | 2 % | +| **Japon** | **Total** | **Japon** | **-** | **2 %** | + +Unités : t/an + +Total : 6200 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Afrique du Sud | 14.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 6.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| Ukraine | 6.0% | 73 | :red-badge[Rouge] (Instable) | +| Inde | 10.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Gabon | 9.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 32.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| France | 3.0% | 34 | :green-badge[Vert] (Stable) | +| Norvege | 4.0% | 23 | :green-badge[Vert] (Stable) | +| Japon | 2.0% | 29 | :green-badge[Vert] (Stable) | + +**ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **10** | | | +| **Pays** | | **15** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, ce qui indique une **concentration faible**. Bien que **Tianjin Manganèse - Aciers spéciaux, batteries, composants électroniques Plant (19 %)**, **Assmang (14 %)**, **CITIC Dameng Mining (13 %)** regroupent plus de 46 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **15**, révélant une **concentration géographique modérée**. La répartition est dominée par **Chine (32 %)**, **Afrique du Sud (14 %)**, **Inde (10 %)**, représentant ensemble plus de 56 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est modérée** (IHH 15) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 15 - :orange-badge[Orange] (Modérée) +* ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +--- + +### Titane + +Le titane est un métal de transition léger, résistant et de couleur gris argenté qui présente des propriétés exceptionnelles expliquant son importance stratégique. Avec une densité de 4,51 g/cm³ (environ 60% de celle de l'acier), une excellente résistance mécanique et une remarquable résistance à la corrosion grâce à sa couche d'oxyde protectrice naturelle, le titane offre le meilleur rapport résistance/poids parmi les métaux structurels. Sa biocompatibilité en fait un matériau de choix pour les applications médicales, tandis que sa tenue en température jusqu'à 400°C et sa résistance aux environnements agressifs lui permettent de s'imposer dans les secteurs aéronautique et chimique. La transformation du titane à partir de ses minerais (principalement le rutile et l'ilménite) nécessite des procédés métallurgiques complexes et énergivores, notamment le procédé Kroll, dominant depuis les années 1950. Les traitements thermiques et de surface du titane requièrent des précautions particulières en raison de sa réactivité avec l'oxygène, l'azote et l'hydrogène à haute température, ce qui nécessite l'utilisation d'atmosphères contrôlées ou protectrices. La grande "hérédité structurale" du titane, c'est-à-dire la conservation de ses caractéristiques microstructurales au cours des transformations, rend ses propriétés finales fortement dépendantes de l'historique complet des traitements subis. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Boitier | 0.67 | 0.70 | 0.60 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Boîtier | 0.67 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.67 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 24 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Boîtiers de smartphones, disques durs, composants d’électronique embarquée haut de gamme, contacts conducteurs. +*Secteurs concurrents* +Aéronautique, biomédical, automobile, chimie industrielle +*Remarques* +Part numérique relativement faible, mais en croissance dans l’électronique embarquée. Forte compétition avec des secteurs à forte demande structurelle. +*Répartition des usages* +* Numérique final : 20% +* Numérique embarqué : 10% +* Autres secteurs : 70% +*Tendance* +* Demande : +25% +* Production : +20% +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 4.0 +* Tension marché : 5.0 +*Réserves* +* Niveau : Modéré +* Pondération : 1.2 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.67 - :red-badge[Rouge] (Difficile) +* IVC: 24 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Afrique du Sud | Richards Bay Minerals | Royaume-Uni | 12 % | +| **Afrique du Sud** | **Total** | **Afrique du Sud** | **24 %** | +| Australie | Iluka Resources | Australie | 9 % | +| **Australie** | **Total** | **Australie** | **20 %** | +| Chine | Pangang Group Vanadium Titanium | Chine | 10 % | +| **Chine** | **Total** | **Chine** | **15 %** | +| Ukraine | ValkiIlmenit | Ukraine | 3 % | +| **Ukraine** | **Total** | **Ukraine** | **5 %** | +| Canada | Rio Tinto Fer et Titane | Royaume-Uni | 5 % | +| **Canada** | **Total** | **Canada** | **5 %** | +| Inde | Indian Rare Earths Limited | Inde | 3 % | +| **Inde** | **Total** | **Inde** | **4 %** | + +Unités : t/an + +Total : 9200000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Canada | 5.0% | 26 | :green-badge[Vert] (Stable) | +| Australie | 20.0% | 26 | :green-badge[Vert] (Stable) | +| Inde | 4.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Afrique du Sud | 24.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 15.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Ukraine | 5.0% | 73 | :red-badge[Rouge] (Instable) | + +**ISG combiné: 48 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **4** | | | +| **Pays** | **13** | | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **4**, ce qui indique une **concentration faible**. Bien que **Richards Bay Minerals (12 %)**, **Pangang Group Vanadium Titanium (10 %)**, **Iluka Resources (9 %)** regroupent plus de 31 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **13**, révélant une **concentration géographique faible**. La répartition est dominée par **Afrique du Sud (24 %)**, **Australie (20 %)**, **Chine (15 %)**, représentant ensemble plus de 59 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 4) +- La **concentration géographique est faible** (IHH 13) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 13 - :green-badge[Vert] (Faible) +* ISG combiné: 48 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Russie | VSMPOAVISMA | Russie | Ukraine (20%), Australie (55%), Afrique du Sud (30%) | 25 % | +| **Russie** | **Total** | **Russie** | **-** | **25 %** | +| Chine | Baoji Titanium | Chine | Chine (60%), Australie (25%), Afrique du Sud (30%) | 10 % | +| Chine | Yunhai Special Metals | Chine | Chine (75%), Australie (15%), Afrique du Sud (30%) | 8 % | +| **Chine** | **Total** | **Chine** | **-** | **18 %** | +| États-Unis | Timet | États-Unis | Inde (15%), Australie (40%), Afrique du Sud (30%) | 15 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **15 %** | +| Japon | Toho Titanium | Japon | Inde (10%), Australie (55%), Afrique du Sud (30%) | 11 % | +| **Japon** | **Total** | **Japon** | **-** | **11 %** | +| Kazakhstan | UKTMP | Kazakhstan | Inde (15%), Australie (40%), Afrique du Sud (30%) | 9 % | +| **Kazakhstan** | **Total** | **Kazakhstan** | **-** | **9 %** | +| Inde | Kerala Minerals | Inde | Inde (100%), Australie (25%), Afrique du Sud (30%) | 6 % | +| **Inde** | **Total** | **Inde** | **-** | **6 %** | +| Ukraine | Zaporozhye Titanium | Ukraine | Ukraine (90%), Australie (55%), Afrique du Sud (30%) | 6 % | +| **Ukraine** | **Total** | **Ukraine** | **-** | **6 %** | + +Unités : t/an + +Total : 159000 + +_Note: Les capacités indiquent la production d'éponge de titane et de produits transformés. La baisse de production en Ukraine due au conflit a modifié les parts de marché depuis 2022._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Russie | 25.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 15.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Kazakhstan | 9.0% | 51 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 18.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Ukraine | 6.0% | 73 | :red-badge[Rouge] (Instable) | +| Japon | 11.0% | 29 | :green-badge[Vert] (Stable) | +| Inde | 6.0% | 60 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **13** | | | +| **Pays** | **14** | | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **13**, ce qui indique une **concentration faible**. Bien que **VSMPOAVISMA (25 %)**, **Timet (15 %)**, **Toho Titanium (11 %)** regroupent plus de 51 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **14**, révélant une **concentration géographique faible**. La répartition est dominée par **Russie (25 %)**, **Chine (18 %)**, **États-Unis (15 %)**, représentant ensemble plus de 58 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 13) +- La **concentration géographique est faible** (IHH 14) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 14 - :green-badge[Vert] (Faible) +* ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +--- + +### Cérium + +Le cérium est un métal lanthanide gris-argenté, malléable et ductile, découvert en 1803 par Jöns Jakob Berzelius et Wilhelm Hisinger en Suède, et simultanément par Martin Heinrich Klaproth en Allemagne. Ce métal se caractérise par sa réactivité élevée, son point de fusion relativement bas (795°C), et son existence sous quatre modifications structurales différentes (α-, β-, γ- et δ-cérium). Il est le lanthanide le plus abondant dans la croûte terrestre, représentant environ 0,0046% de celle-ci, et se trouve principalement dans les minerais de bastnæsite, monazite et loparite. Le cérium se distingue par sa capacité à exister sous deux états d'oxydation stables (Ce³⁺ et Ce⁴⁺), propriété rare parmi les lanthanides qui confère au cérium des applications particulières en catalyse et en métallurgie. Extrêmement réactif, le cérium s'oxyde rapidement à l'air, est facilement soluble dans l'eau, les acides, les bases et l'éthanol, ce qui nécessite des précautions particulières pour sa manipulation et son stockage. Son extraction et son traitement impliquent des procédés complexes de séparation des terres rares, rendus encore plus difficiles par leurs propriétés chimiques similaires. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Camera | 0.71 | 0.80 | 0.70 | 0.60 | +| EcranLCD | 0.70 | 0.70 | 0.70 | 0.70 | +| EcranOLED | 0.64 | 0.70 | 0.60 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Écran OLED | 0.64 | :red-badge[Rouge] (Difficile) | +| Caméra | 0.71 | :red-badge[Rouge] (Difficile) | +| Écran LCD/TFT | 0.70 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.68 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 0 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Polissage écrans, capteurs, nanoparticules, analyse électronique +*Secteurs concurrents* +Catalyse auto, verres, alliages, céramiques, biomédecine +*Remarques* +Surproduction chronique. Co-produit des terres rares. Croissance numérique rapide mais usage encore limité. +*Répartition des usages* +* Numérique final : 10 +* Numérique embarqué : 5 +* Autres secteurs : 85 +*Tendance* +* Demande : 2.0 +* Production : 2.0 +* Ratio capacité/demande : 1.0 +*Concurrence & tension* +* Ratio concurrence : 9.0 +* Tension marché : 0.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.68 - :red-badge[Rouge] (Difficile) +* IVC: 0 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China North Rare Earth Corp | Chine | 35 % | +| Chine | China Rare Earth Group | Chine | 25 % | +| Chine | Xiamen Tungsten | Chine | 15 % | +| **Chine** | **Total** | **Chine** | **70 %** | +| États-Unis | MP Materials | États-Unis | 10 % | +| **États-Unis** | **Total** | **États-Unis** | **11 %** | +| Australie | Lynas Rare Earths | Australie | 8 % | +| **Australie** | **Total** | **Australie** | **6 %** | +| Brésil | Serra Verde | Brésil | 1 % | +| **Brésil** | **Total** | **Brésil** | **1 %** | + +**Unités** : kt/an + +**Total** : 381 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Australie | 6.0% | 26 | :green-badge[Vert] (Stable) | +| EtatsUnis | 11.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 70.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 1.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **22** | | +| **Pays** | | | **51** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **22**, ce qui indique une **concentration modérée**. **China North Rare Earth Corp (35 %)**, **China Rare Earth Group (25 %)**, **Xiamen Tungsten (15 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **51**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (70 %)**, **États-Unis (11 %)**, **Australie (6 %)**, représentant ensemble plus de 87 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 22) +- La **concentration géographique est élevée** (IHH 51) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 51 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Chine (95%) | 56 % | +| Chine | Northern Rare Earth | Chine | Chine (100%) | 15 % | +| **Chine** | **Total** | **Chine** | **-** | **73 %** | +| États-Unis | MP Materials | États-Unis | États-Unis (100%) | 12 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **12 %** | +| Australie | Lynas Rare Earths | Australie | Australie (100%) | 5 % | +| **Australie** | **Total** | **Australie** | **-** | **5 %** | +| Russie | Solikamsk Magnesium | Russie | États-Unis (100%) | 3 % | +| **Russie** | **Total** | **Russie** | **-** | **3 %** | +| Inde | Indian Rare Earths | Inde | | 2 % | +| **Inde** | **Total** | **Inde** | **-** | **2 %** | + +**Unités** : kt/an + +**Total** : 103 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Inde | 2.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 3.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 73.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| EtatsUnis | 12.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **35** | +| **Pays** | | | **55** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **35**, signalant une **concentration élevée**. Le marché est largement dominé par **China Minmetals (56 %)**, **Northern Rare Earth (15 %)**, **MP Materials (12 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **55**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (73 %)**, **États-Unis (12 %)**, **Australie (5 %)**, représentant ensemble plus de 90 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 35) +- La **concentration géographique est élevée** (IHH 55) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 55 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Yttrium + +L'yttrium est un métal de transition du groupe 3, souvent classé parmi les terres rares bien qu'il ne soit pas un lanthanide. Découvert en 1794 par Johan Gadolin dans un minéral provenant du village d'Ytterby en Suède, ce métal gris-argenté se caractérise par sa stabilité relative à l'air, son point de fusion élevé (1522°C), et sa capacité à former des composés hautement luminescents. L'yttrium présente des propriétés chimiques remarquablement similaires à celles des lanthanides lourds, notamment l'holmium et le dysprosium, ce qui explique sa présence naturelle dans les mêmes gisements. On le trouve principalement dans les minéraux de xénotime, monazite et les argiles ioniques chinoises, généralement en association avec d'autres terres rares. Son extraction et sa séparation impliquent des procédés hydrometallurgiques complexes, rendus particulièrement difficiles par sa similitude chimique avec les autres terres rares. La chaîne d'approvisionnement mondiale est fortement dominée par la Chine, qui contrôle plus de 85% de la production. Malgré une abondance relative supérieure à celle de nombreuses autres terres rares, l'yttrium est considéré comme critique en raison de ses applications stratégiques dans les phosphores pour écrans, les céramiques hautes performances et les superalliages. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| EcranMiniLED | 0.74 | 0.80 | 0.70 | 0.70 | +| EcranOLED | 0.70 | 0.70 | 0.70 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Écran OLED | 0.70 | :red-badge[Rouge] (Difficile) | +| Écran Mini et Micro LED | 0.74 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.72 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 28 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Phosphores pour écrans et LED, supraconducteurs, lasers, capteurs optiques, céramiques électroniques. +*Secteurs concurrents* +Optique médicale, transition énergétique, aéronautique, défense +*Remarques* +Usage numérique soutenu par la demande en LED et optoélectronique. Ressource critique, fortement concentrée et souvent co-produite. +*Répartition des usages* +* Numérique final : 45% +* Numérique embarqué : 20% +* Autres secteurs : 35% +*Tendance* +* Demande : +40% +* Production : +25% +* Ratio capacité/demande : 0.89 +*Concurrence & tension* +* Ratio concurrence : 1.22 +* Tension marché : 15.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.72 - :red-badge[Rouge] (Difficile) +* IVC: 28 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Northern Rare Earth Group | Chine | 71 % | +| **Chine** | **Total** | **Chine** | **71 %** | +| Myanmar | Diverses entreprises locales | Myanmar | 13 % | +| **Myanmar** | **Total** | **Myanmar** | **13 %** | +| États-Unis | MP Materials | États-Unis | 7 % | +| **États-Unis** | **Total** | **États-Unis** | **7 %** | +| Australie | Lynas Rare Earths | Australie | 4 % | +| **Australie** | **Total** | **Australie** | **4 %** | +| Inde | Indian Rare Earths Limited | Inde | 3 % | +| **Inde** | **Total** | **Inde** | **3 %** | + +**Unités** : t/an + +**Total** : 11200 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 7.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 71.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 4.0% | 26 | :green-badge[Vert] (Stable) | +| Inde | 3.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Myanmar | 13.0% | 84 | :red-badge[Rouge] (Instable) | + +**ISG combiné: 56 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **53** | +| **Pays** | | | **53** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **53**, signalant une **concentration élevée**. Le marché est largement dominé par **China Northern Rare Earth Group (71 %)**, **Diverses entreprises locales (13 %)**, **MP Materials (7 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **53**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (71 %)**, **Myanmar (13 %)**, **États-Unis (7 %)**, représentant ensemble plus de 91 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 53) +- La **concentration géographique est élevée** (IHH 53) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 53 - :red-badge[Rouge] (Élevée) +* ISG combiné: 56 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | | 35 % | +| Chine | China Northern Rare Earth | Chine | | 25 % | +| Chine | Xiamen Tungsten | Chine | | 17 % | +| **Chine** | **Total** | **Chine** | **-** | **77 %** | +| Malaisie | Lynas Advanced Materials | Australie | | 8 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **8 %** | +| Estonie | NPM Silmet | Canada | | 6 % | +| **Estonie** | **Total** | **Estonie** | **-** | **6 %** | +| États-Unis | MP Materials | États-Unis | | 5 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **5 %** | +| Vietnam | Vietnam Rare Earth | Vietnam | | 2 % | +| **Vietnam** | **Total** | **Vietnam** | **-** | **2 %** | + +**Unités** : t/an + +**Total** : 7150 + +_Note: Les capacités indiquées représentent la production d'oxyde d'yttrium (Y₂O₃). La Chine domine largement le marché mondial grâce à ses gisements d'argiles ioniques du sud du pays, particulièrement riches en yttrium._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 77.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Malaisie | 8.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 5.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Estonie | 6.0% | 33 | :green-badge[Vert] (Stable) | +| Vietnam | 2.0% | 48 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **23** | | +| **Pays** | | | **61** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **23**, ce qui indique une **concentration modérée**. **China Minmetals (35 %)**, **China Northern Rare Earth (25 %)**, **Xiamen Tungsten (17 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **61**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (77 %)**, **Malaisie (8 %)**, **Estonie (6 %)**, représentant ensemble plus de 91 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 23) +- La **concentration géographique est élevée** (IHH 61) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 61 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Béryllium + +Le béryllium est un métal alcalino-terreux léger, dur et fragile, découvert en 1798 par Louis Nicolas Vauquelin. Ce métal gris-acier se caractérise par sa faible densité (1,85 g/cm³), son point de fusion élevé (1287°C), sa résistance mécanique exceptionnelle et sa toxicité pulmonaire aiguë. Principalement extrait du béryl (Be₃Al₂Si₆O₁₈) et de la bertrandite (Be₄Si₂O₇(OH)₂), le béryllium nécessite des procédés métallurgiques complexes en raison de sa réactivité chimique et de sa dispersion dans les minerais. Sa production mondiale est dominée par les États-Unis, la Chine et la Russie, avec des applications stratégiques dans l'aérospatiale, le nucléaire et l'électronique haute performance. Les défis majeurs incluent la gestion des risques sanitaires et environnementaux liés à sa toxicité. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Connecteurs | 0.64 | 0.70 | 0.60 | 0.60 | +| Connectivite | 0.64 | 0.70 | 0.60 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Connecteurs | 0.64 | :red-badge[Rouge] (Difficile) | +| Connectivité | 0.64 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.64 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 15 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Connecteurs, substrats BeO, circuits HF, télécoms, alliages Cu-Be numériques +*Secteurs concurrents* +Aérospatial, défense, nucléaire, métallurgie générale, outils industriels +*Remarques* +Forte pression multi-sectorielle. Toxicité réglementée. Croissance numérique dynamique. +*Répartition des usages* +* Numérique final : 15 +* Numérique embarqué : 10 +* Autres secteurs : 75 +*Tendance* +* Demande : 3.0 +* Production : 1.5 +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 5.67 +* Tension marché : 2 +*Réserves* +* Niveau : Très limité +* Pondération : 1.8 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.64 - :red-badge[Rouge] (Difficile) +* IVC: 15 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| États-Unis | Materion Corporation | États-Unis | 50 % | +| **États-Unis** | **Total** | **États-Unis** | **50 %** | +| Brésil | Companhia Brasileira de Alumínio | Brésil | 15 % | +| Brésil | Mibra | Brésil | 7 % | +| **Brésil** | **Total** | **Brésil** | **22 %** | +| Chine | Fuyun Hengsheng Beryllium Industry | Chine | 12 % | +| Chine | Xinjiang Nonferrous Metals | Chine | 9 % | +| **Chine** | **Total** | **Chine** | **21 %** | + +Unités : t/an + +Total : 360 + +_Note: Materion Corporation (anciennement Brush Wellman) est de loin le principal producteur mondial, exploitant le gisement de bertrandite à Spor Mountain, Utah, qui fournit environ 85% du béryllium mondial avec un minerai plus facile à traiter que le béryl._ +_Sources: USGS 2024, Statista 2025. Seules les réserves américaines sont évaluées avec précision, estimées à 19 000 tonnes de béryllium contenu, équivalentes à plus de 100 ans de production au rythme actuel. Les ressources mondiales totales sont estimées à plus de 100 000 tonnes._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 50.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 22.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 21.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 48 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **30** | +| **Pays** | | | **34** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **30**, signalant une **concentration élevée**. Le marché est largement dominé par **Materion Corporation (50 %)**, **Companhia Brasileira de Alumínio (15 %)**, **Fuyun Hengsheng Béryllium - Alliages aérospatiaux, composants électroniques, modérateurs nucléaires Industry (12 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **34**, révélant une **concentration géographique élevée**. La répartition est dominée par **États-Unis (50 %)**, **Brésil (22 %)**, **Chine (21 %)**, représentant ensemble plus de 93 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 30) +- La **concentration géographique est élevée** (IHH 34) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 34 - :red-badge[Rouge] (Élevée) +* ISG combiné: 48 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| États-Unis | Materion Corp | États-Unis | États-Unis (100%) | 65 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **65 %** | +| Chine | CNMC Nickel | Chine | Chine (100%) | 20 % | +| **Chine** | **Total** | **Chine** | **-** | **20 %** | +| Russie | Ulba Metallurgical Plant | Russie | Russie (100%) | 10 % | +| **Russie** | **Total** | **Russie** | **-** | **10 %** | + +Unités : t/an + +Total : 380 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Russie | 10.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 20.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 58 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **47** | +| **Pays** | | | **47** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les producteurs est de **30**, signalant une **concentration élevée**. Le marché est largement dominé par **Materion Corporation (65 %)**, **CMNC Nickel (20 %)**, **Ulba Metallurgical Plant (10 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **34**, révélant une **concentration géographique élevée**. La répartition est dominée par **États-Unis (65 %)**, **Chine (20 %)**, **Russie (10 %)**, représentant ensemble 95 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 30) +- La **concentration géographique est élevée** (IHH 34) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 47 - :red-badge[Rouge] (Élevée) +* ISG combiné: 58 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Étain + +L'étain est un métal gris-argent, malléable, moyennement ductile à température ambiante qui occupe une place centrale dans l'industrie électronique. Élément chimique de numéro atomique 50 (symbole Sn, du latin stannum), il se distingue par son excellente résistance à la corrosion par l'eau de mer et l'eau douce, bien qu'il puisse être attaqué par les acides forts. Sa transformation allotropique en dessous de 13°C (passage de l'étain blanc à l'étain gris) constitue un phénomène remarquable connu sous le nom de "peste de l'étain", particulièrement préoccupant pour les applications électroniques. En raison de sa faible température de fusion, de sa conductivité électrique et de sa capacité à former des alliages aux propriétés spécifiques, l'étain est devenu incontournable dans les applications de brasage de composants électroniques, notamment depuis la transition vers les soudures sans plomb. Sa propriété de former des whiskers (fils micrométriques susceptibles de provoquer des courts-circuits) représente toutefois un défi technique pour l'industrie, nécessitant l'ajout d'autres métaux comme l'argent ou le cuivre pour stabiliser ses propriétés. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| CarteMere | 0.60 | 0.60 | 0.60 | 0.60 | +| Connecteurs | 0.60 | 0.60 | 0.60 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Connecteurs | 0.60 | :red-badge[Rouge] (Difficile) | +| Carte mère | 0.60 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.60 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 1 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Brasures électroniques, composants de soudure pour cartes électroniques, contacts électriques +*Secteurs concurrents* +Étain alimentaire, médical, brasage industriel +*Remarques* +Ressource largement utilisée dans l'électronique. Forte pression sur les brasures avec le numérique. +*Répartition des usages* +* Numérique final : 60% +* Numérique embarqué : 30% +* Autres secteurs : 10% +*Tendance* +* Demande : +3% +* Production : +2% +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 0.67 +* Tension marché : 1.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.60 - :red-badge[Rouge] (Difficile) +* IVC: 1 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Yunnan Tin Group | Chine | 16 % | +| **Chine** | **Total** | **Chine** | **34 %** | +| Indonésie | PT Timah | Indonésie | 14 % | +| **Indonésie** | **Total** | **Indonésie** | **28 %** | +| Pérou | Minsur | Pérou | 7 % | +| **Pérou** | **Total** | **Pérou** | **8 %** | +| Bolivie | Empresa Minera Huanuni | Bolivie | 5 % | +| **Bolivie** | **Total** | **Bolivie** | **6 %** | +| Brésil | Mineração Taboca | Pérou | 3 % | +| **Brésil** | **Total** | **Brésil** | **4 %** | +| Australie | Metals X / Yunnan Tin Group | Chine | 3 % | +| **Australie** | **Total** | **Australie** | **2 %** | +| RD Congo | Alphamin Resources | Canada | 4 % | +| **RD Congo** | **Total** | **RD Congo** | **1 %** | + +Unités : t/an + +Total : 308000 + +_Note : Les parts de marché sont des estimations basées sur les informations parcellaires disponibles dans les sources. Le marché est relativement concentré avec la présence dominante des entreprises chinoises et indonésiennes._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Indonesie | 28.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Perou | 8.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Bolivie | 6.0% | 56 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 2.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 34.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| RDCongo | 1.0% | 84 | :red-badge[Rouge] (Instable) | +| Bresil | 4.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **6** | | | +| **Pays** | | **21** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **6**, ce qui indique une **concentration faible**. Bien que **Yunnan Tin Group (16 %)**, **PT Timah (14 %)**, **Minsur (7 %)** regroupent plus de 37 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **21**, révélant une **concentration géographique modérée**. La répartition est dominée par **Chine (34 %)**, **Indonésie (28 %)**, **Pérou (8 %)**, représentant ensemble plus de 70 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 6) +- La **concentration géographique est modérée** (IHH 21) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 21 - :orange-badge[Orange] (Modérée) +* ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Yunnan Tin | Chine | Chine (60%), Indonésie (10%), Australie (20%) | 20 % | +| **Chine** | **Total** | **Chine** | **-** | **20 %** | +| Indonésie | PT Timah | Indonésie | Indonésie (90%), Indonésie (40%), Australie (20%) | 13 % | +| **Indonésie** | **Total** | **Indonésie** | **-** | **13 %** | +| Pérou | Minsur | Pérou | Pérou (90%), Brésil (10%), RD Congo (10%) | 12 % | +| **Pérou** | **Total** | **Pérou** | **-** | **12 %** | +| Malaisie | Malaysia Smelting Corporation | Malaisie | Bolivie (10%), Indonésie (40%), Australie (20%) | 12 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **12 %** | +| Thaïlande | Thaisarco | Thaïlande | Indonésie (40%), Indonésie (40%), Australie (20%) | 10 % | +| **Thaïlande** | **Total** | **Thaïlande** | **-** | **10 %** | +| Belgique | MetalloChimique | Belgique | Bolivie (10%), Pérou (15%), RD Congo (10%) | 5 % | +| **Belgique** | **Total** | **Belgique** | **-** | **5 %** | +| Bolivie | Empresa Metalúrgica Vinto | Bolivie | Bolivie (95%), Pérou (5%), RD Congo (10%) | 3 % | +| **Bolivie** | **Total** | **Bolivie** | **-** | **3 %** | + +Unités : t/an + +Total : 350000 + +_Note: Les capacités indiquées représentent le traitement d'étain pour toutes applications, dont environ 35-40% sont destinées spécifiquement à l'électronique et aux applications numériques._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Indonesie | 13.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Bolivie | 3.0% | 56 | :orange-badge[Orange] (Intermédiaire) | +| Thailande | 10.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 20.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Belgique | 5.0% | 34 | :green-badge[Vert] (Stable) | +| Malaisie | 12.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| Perou | 12.0% | 57 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **10** | | | +| **Pays** | **10** | | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, ce qui indique une **concentration faible**. Bien que **Yunnan Tin (20 %)**, **PT Timah (13 %)**, **Malaysia Smelting Corporation (12 %)** regroupent plus de 45 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **10**, révélant une **concentration géographique faible**. La répartition est dominée par **Chine (20 %)**, **Indonésie (13 %)**, **Malaisie (12 %)**, représentant ensemble plus de 45 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est faible** (IHH 10) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 10 - :green-badge[Vert] (Faible) +* ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +--- + +### Nickel + +Le nickel est un métal de transition gris-argenté, ferromagnétique, prisé pour sa résistance exceptionnelle à la corrosion, sa ductilité et ses propriétés électrochimiques remarquables. Sa production industrielle repose sur deux filières principales : la pyrométallurgie, historiquement dominante pour le traitement des minerais sulfurés, et l'hydrométallurgie, en plein essor notamment pour les minerais latéritiques. Le nickel primaire est obtenu à partir de deux sources principales : les minerais sulfurés (pentlandite) qui contiennent 0,5-3% de nickel, et les latérites (garniérite, limonite) avec des teneurs de 1-2,5%. Le traitement métallurgique varie significativement selon le type de minerai et la pureté visée, avec des procédés particulièrement complexes pour atteindre les grades de haute pureté (99,99%) requis pour les applications électroniques avancées. La chaîne d'approvisionnement mondiale du nickel connaît actuellement une transformation majeure avec l'explosion de la demande pour les batteries des véhicules électriques, qui exige un nickel de classe 1 (>99,8%) et stimule le développement de nouvelles capacités de raffinage. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Connecteurs | 0.50 | 0.50 | 0.40 | 0.60 | +| Batterie | 0.44 | 0.50 | 0.40 | 0.40 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Connecteurs | 0.50 | :orange-badge[Orange] (Moyenne) | +| Batterie | 0.44 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.47 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 20 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Batteries (NiMH, Li-ion), composants électrochimiques, alliages pour électronique haute performance. +*Secteurs concurrents* +Acier inoxydable, aéronautique, énergie +*Remarques* +Le secteur de l'acier domine encore, mais la transition énergétique et l'électronique embarquée augmentent rapidement. Croissance équilibrée mais dépendance régionale. +*Répartition des usages* +* Numérique final : 20% +* Numérique embarqué : 15% +* Autres secteurs : 65% +*Tendance* +* Demande : +30% +* Production : +25% +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 4.0 +* Tension marché : 5.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.47 - :orange-badge[Orange] (Moyenne) +* IVC: 20 - :orange-badge[Orange] (Modérée) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Indonésie | Vale Indonesia | Brésil | 9 % | +| Indonésie | PT Aneka Tambang | Indonésie | 5 % | +| **Indonésie** | **Total** | **Indonésie** | **50 %** | +| Philippines | Nickel Asia | Philippines | 5 % | +| **Philippines** | **Total** | **Philippines** | **11 %** | +| Russie | Norilsk Nickel | Russie | 6 % | +| **Russie** | **Total** | **Russie** | **6 %** | +| France | Eramet | France | 4 % | +| **France** | **Total** | **France** | **6 %** | +| Canada | Vale | Brésil | 3 % | +| Canada | Glencore | Suisse | 2 % | +| **Canada** | **Total** | **Canada** | **5 %** | +| Australie | BHP | Australie | 4 % | +| **Australie** | **Total** | **Australie** | **4 %** | +| Chine | Jinchuan Group | Chine | 3 % | +| **Chine** | **Total** | **Chine** | **3 %** | +| Brésil | Vale | Brésil | 7 % | +| **Brésil** | **Total** | **Brésil** | **2 %** | +| États-Unis | Eagle Mine | Canada | 0 % | +| **États-Unis** | **Total** | **États-Unis** | **0 %** | + +_Note : Les parts de marché sont des estimations basées sur les capacités de production connues et les données parcellaires disponibles dans les rapports sectoriels. Le marché est fragmenté avec de nombreux producteurs plus petits, notamment en Indonésie._ + +Unités : t/an + +Total : 3600000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Indonesie | 50.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Canada | 5.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 3.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| France | 6.0% | 34 | :green-badge[Vert] (Stable) | +| Philippines | 11.0% | 59 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 6.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 4.0% | 26 | :green-badge[Vert] (Stable) | +| EtatsUnis | 0.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 2.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **3** | | | +| **Pays** | | | **27** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **3**, ce qui indique une **concentration faible**. Bien que **Vale Indonesia (9 %)**, **Vale (7 %)**, **Norilsk Nickel - Batteries, revêtements conducteurs, alliages (6 %)** regroupent plus de 22 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **27**, révélant une **concentration géographique élevée**. La répartition est dominée par **Indonésie (50 %)**, **Philippines (11 %)**, **France (6 %)**, représentant ensemble plus de 67 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 3) +- La **concentration géographique est élevée** (IHH 27) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 27 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Tsingshan Group | Chine | Indonésie (70%), Chine (30%) | 22 % | +| Chine | Jinchuan Group | Chine | Indonésie (40%), Chine (60%) | 17 % | +| **Chine** | **Total** | **Chine** | **-** | **39 %** | +| Indonésie | Vale Indonesia | Brésil | Indonésie (100%) | 18 % | +| Indonésie | PT Aneka Tambang | Indonésie | Indonésie (100%) | 8 % | +| **Indonésie** | **Total** | **Indonésie** | **-** | **26 %** | +| Russie | Norilsk Nickel | Russie | Russie (100%), Chine (60%) | 6 % | +| **Russie** | **Total** | **Russie** | **-** | **6 %** | +| Canada | Vale | Brésil | Canada (100%) | 2 % | +| Canada | Glencore | Suisse | Canada (100%) | 2 % | +| **Canada** | **Total** | **Canada** | **-** | **4 %** | +| **France** | **Total** | **France** | **-** | **4 %** | +| Finlande | Nornickel Harjavalta | Russie | Russie (80%), Indonésie (40%) | 3 % | +| **Finlande** | **Total** | **Finlande** | **-** | **3 %** | +| Japon | Sumitomo Metal Mining | Japon | Philippines (60%), Indonésie (40%) | 3 % | +| **Japon** | **Total** | **Japon** | **-** | **3 %** | +| Australie | BHP | Australie | Australie (100%), Chine (60%) | 2 % | +| **Australie** | **Total** | **Australie** | **-** | **2 %** | + +Unités : t/an + +Total : 3855000 + +_Note: Les capacités de traitement correspondent au nickel contenu dans tous les produits (nickel raffiné, ferronickel, matte, NPI, etc.). Les données reflètent la situation en 2025, après l'expansion massive des capacités indonésiennes._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Finlande | 3.0% | 25 | :green-badge[Vert] (Stable) | +| Canada | 4.0% | 26 | :green-badge[Vert] (Stable) | +| France | 4.0% | 34 | :green-badge[Vert] (Stable) | +| Chine | 39.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Indonesie | 26.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 2.0% | 26 | :green-badge[Vert] (Stable) | +| Russie | 6.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 3.0% | 29 | :green-badge[Vert] (Stable) | + +**ISG combiné: 50 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **12** | | | +| **Pays** | | **23** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **12**, ce qui indique une **concentration faible**. Bien que **Tsingshan Group (22 %)**, **Vale Indonesia (18 %)**, **Jinchuan Group (17 %)** regroupent plus de 57 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **23**, révélant une **concentration géographique modérée**. La répartition est dominée par **Chine (39 %)**, **Indonésie (26 %)**, **Russie (6 %)**, représentant ensemble plus de 71 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 12) +- La **concentration géographique est modérée** (IHH 23) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 23 - :orange-badge[Orange] (Modérée) +* ISG combiné: 50 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +--- + +### Argent + +L'argent est un métal précieux qui joue un rôle essentiel mais discret dans l'industrie électronique moderne. Avec sa conductivité électrique et thermique exceptionnelle (la plus élevée de tous les métaux), sa résistance à l'oxydation et ses propriétés anti-microbiennes, l'argent possède un profil unique de caractéristiques techniques. Dans l'industrie numérique, il est principalement utilisé comme composant d'alliages de soudure sans plomb (avec l'étain et le cuivre), dans les contacts électriques critiques nécessitant une transmission de signal parfaite, et sous forme de nano-argent dans divers composants électroniques. Bien que présent en très faibles quantités dans chaque appareil, l'argent est irremplaçable pour certaines applications de haute performance, notamment dans les équipements de télécommunication, les serveurs, et les appareils médicaux. Le traitement de l'argent implique des processus complexes d'extraction, souvent comme sous-produit d'autres métaux, suivis d'étapes de raffinage sophistiquées pour atteindre les niveaux de pureté nécessaires aux applications électroniques. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| CarteMere | 0.61 | 0.70 | 0.50 | 0.60 | +| Connecteurs | 0.61 | 0.70 | 0.50 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Connecteurs | 0.61 | :red-badge[Rouge] (Difficile) | +| Carte mère | 0.61 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.61 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 11 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Composants électroniques, photovoltaïque, nano-argent, connecteurs, brasures +*Secteurs concurrents* +Bijouterie, argenterie, médical, catalyseurs, investissement, industrie +*Remarques* +Usage stratégique et transversal. Forte croissance numérique. Offre dépendante des sous-produits. +*Répartition des usages* +* Numérique final : 15% +* Numérique embarqué : 5% +* Autres secteurs : 80% +*Tendance* +* Demande : +3.5% +* Production : +2.2% +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 5.67 +* Tension marché : 1.3 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.61 - :red-badge[Rouge] (Difficile) +* IVC: 11 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Mexique | Fresnillo | Mexique | 6 % | +| Mexique | Newmont | États-Unis | 4 % | +| **Mexique** | **Total** | **Mexique** | **27 %** | +| Chine | Zijin Mining Group | Chine | 3 % | +| **Chine** | **Total** | **Chine** | **15 %** | +| Pérou | Antamina | Pérou | 5 % | +| **Pérou** | **Total** | **Pérou** | **12 %** | +| Russie | Polymetal | Russie | 4 % | +| **Russie** | **Total** | **Russie** | **8 %** | +| Pologne | KGHM | Pologne | 4 % | +| **Pologne** | **Total** | **Pologne** | **5 %** | +| Chili | Codelco | Chili | 3 % | +| **Chili** | **Total** | **Chili** | **5 %** | +| Australie | BHP | Australie | 2 % | +| **Australie** | **Total** | **Australie** | **5 %** | + +**Unités** : kt/an + +**Total** : 25 + +_Note: Les parts de marché sont estimées d'après les données de production des principales mines mentionnées dans les rapports du Silver Institute et les publications de L'Élémentarium 2022._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Pologne | 5.0% | 37 | :green-badge[Vert] (Stable) | +| Chili | 5.0% | 41 | :orange-badge[Orange] (Intermédiaire) | +| Mexique | 27.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Perou | 12.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 15.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| Russie | 8.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **1** | | | +| **Pays** | **12** | | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **1**, ce qui indique une **concentration faible**. Bien que **Fresnillo (6 %)**, **Antamina (5 %)**, **Newmont (4 %)** regroupent plus de 15 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **12**, révélant une **concentration géographique faible**. La répartition est dominée par **Mexique (27 %)**, **Chine (15 %)**, **Pérou (12 %)**, représentant ensemble plus de 54 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 1) +- La **concentration géographique est faible** (IHH 12) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 12 - :green-badge[Vert] (Faible) +* ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Mexique | Peñoles | Mexique | Pérou (10%), Mexique (85%), Chili (5%) | 16 % | +| **Mexique** | **Total** | **Mexique** | **-** | **16 %** | +| Pologne | KGHM | Pologne | Pologne (75%), Chili (20%), Chili (15%), Australie (25%) | 14 % | +| **Pologne** | **Total** | **Pologne** | **-** | **14 %** | +| États-Unis | Asarco | Mexique | Mexique (30%) | 12 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **12 %** | +| Japon | Mitsubishi Materials | Japon | Pérou (20%), Mexique (15%), Chili (15%), Australie (25%) | 11 % | +| **Japon** | **Total** | **Japon** | **-** | **11 %** | +| Allemagne | Aurubis | Allemagne | Pérou (30%), Mexique (15%), Chili (25%) | 10 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **10 %** | +| Chine | Jiangxi Copper | Chine | Pérou (10%), Australie (15%), Chine (70%) | 9 % | +| **Chine** | **Total** | **Chine** | **-** | **9 %** | +| Canada | Glencore Canada | Suisse | Pérou (25%), Australie (10%) | 7 % | +| **Canada** | **Total** | **Canada** | **-** | **7 %** | + +**Unités** : kt/an + +**Total** : 27 + +_Note: Les capacités indiquées représentent le traitement d'argent pour toutes applications, dont environ 15-20% sont destinées spécifiquement à l'électronique et aux applications numériques._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 12.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Allemagne | 10.0% | 30 | :green-badge[Vert] (Stable) | +| Canada | 7.0% | 26 | :green-badge[Vert] (Stable) | +| Mexique | 16.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 9.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 11.0% | 29 | :green-badge[Vert] (Stable) | +| Pologne | 14.0% | 37 | :green-badge[Vert] (Stable) | + +**ISG combiné: 41 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **9** | | | +| **Pays** | **9** | | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **9**, ce qui indique une **concentration faible**. Bien que **Peñoles (16 %)**, **KGHM (14 %)**, **Asarco (12 %)** regroupent plus de 42 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **9**, révélant une **concentration géographique faible**. La répartition est dominée par **Mexique (16 %)**, **Pologne (14 %)**, **États-Unis (12 %)**, représentant ensemble plus de 42 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 9) +- La **concentration géographique est faible** (IHH 9) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 9 - :green-badge[Vert] (Faible) +* ISG combiné: 41 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +--- + +### Cuivre + +Le cuivre est un métal de transition rouge-brun, malléable et ductile, connu pour son excellente conductivité électrique et thermique. Sa production industrielle repose principalement sur deux procédés distincts : la pyrométallurgie et l'hydrométallurgie. La pyrométallurgie, voie traditionnelle et majoritaire, consiste en une série d'opérations thermiques (grillage, fusion, conversion) suivies d'un raffinage électrolytique pour atteindre une pureté supérieure à 99,99%. L'hydrométallurgie, développée pour valoriser les minerais pauvres ou complexes, implique une lixiviation acide ou acide/oxydante, suivie d'une extraction par solvant et d'une électroextraction. Ces dernières décennies ont vu l'essor considérable des procédés hydrométallurgiques SX-EW (Solvent Extraction - ElectroWinning), particulièrement adaptés aux minerais oxydés à faible teneur. La chaîne d'approvisionnement mondiale du cuivre est dominée par le Chili, premier producteur mondial, suivi par le Pérou, la Chine et les États-Unis. Le raffinage du cuivre requiert une maîtrise technique poussée pour éliminer les impuretés nocives (arsenic, antimoine, bismuth) et valoriser les sous-produits précieux (or, argent, sélénium, tellure). + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| CarteMere | 0.81 | 0.90 | 0.80 | 0.70 | +| Connecteurs | 0.81 | 0.90 | 0.80 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Connecteurs | 0.81 | :red-badge[Rouge] (Difficile) | +| Carte mère | 0.81 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.81 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 5 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Circuits imprimés, câblage informatique, composants électroniques, alliages, encres conductrices +*Secteurs concurrents* +Distribution électrique, bâtiment, transport, agriculture, traitement des eaux +*Remarques* +Matériau transversal. Usage numérique important (~25%) mais dominé par l’infrastructure électrique et la construction +*Répartition des usages* +* Numérique final : 25% +* Numérique embarqué : 15% +* Autres secteurs : 60% +*Tendance* +* Demande : +4% +* Production : +2.5% +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 3.0 +* Tension marché : 2 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.81 - :red-badge[Rouge] (Difficile) +* IVC: 5 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chili | Codelco | Chili | 8 % | +| Chili | Escondida | Australie | 7 % | +| Chili | Collahuasi Mining | Royaume-Uni | 5 % | +| **Chili** | **Total** | **Chili** | **27 %** | +| Pérou | Southern Copper | Mexique | 10 % | +| Pérou | Antamina | Canada | 6 % | +| **Pérou** | **Total** | **Pérou** | **16 %** | +| RD Congo | Tenke Fungurume Mining | Chine | 8 % | +| RD Congo | Kamoa-Kakula Copper Mine | Canada | 5 % | +| **RD Congo** | **Total** | **RD Congo** | **13 %** | +| États-Unis | FreeportMcMoRan | États-Unis | 9 % | +| États-Unis | Rio Tinto Group | Royaume-Uni | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **12 %** | +| Australie | BHP | Australie | 7 % | +| Australie | Rio Tinto | Royaume-Uni | 4 % | +| **Australie** | **Total** | **Australie** | **11 %** | +| Chine | Jiangxi Copper Company Limited | Chine | 6 % | +| **Chine** | **Total** | **Chine** | **6 %** | +| Indonésie | Grasberg Mine | États-Unis | 5 % | +| **Indonésie** | **Total** | **Indonésie** | **5 %** | + +Unités : kt/an + +Total : 26000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 6.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| RDCongo | 13.0% | 84 | :red-badge[Rouge] (Instable) | +| Perou | 16.0% | 57 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 11.0% | 26 | :green-badge[Vert] (Stable) | +| Chili | 27.0% | 41 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 12.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Indonesie | 5.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 50 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **6** | | | +| **Pays** | | **15** | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **6**, ce qui indique une **concentration faible**. Bien que **Southern Copper (10 %)**, **FreeportMcMoRan (9 %)**, **Codelco (8 %)** regroupent plus de 27 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **15**, révélant une **concentration géographique modérée**. La répartition est dominée par **Chili (27 %)**, **Pérou (16 %)**, **RD Congo (13 %)**, représentant ensemble plus de 56 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 6) +- La **concentration géographique est modérée** (IHH 15) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 15 - :orange-badge[Orange] (Modérée) +* ISG combiné: 50 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Jiangxi Copper | Chine | Chili (40%), Australie (30%), RD Congo (30%) | 18 % | +| Chine | Tongling | Chine | Chili (80%), Pérou (30%) | 12 % | +| Chine | Yunnan Copper | Chine | RD Congo (30%), Chine (20%), RD Congo (30%) | 7 % | +| Chine | Zijin Mining | Chine | RD Congo (40%), Chine (20%), RD Congo (30%) | 5 % | +| Chine | Chalco | Chine | Chili (60%), Australie (40%), RD Congo (30%) | 4 % | +| Chine | CMOC | Chine | RD Congo (70%), Australie (30%), RD Congo (30%) | 3 % | +| **Chine** | **Total** | **Chine** | **-** | **45 %** | +| Chili | Codelco | Chili | Chili (90%), Chine (20%), RD Congo (30%) | 15 % | +| Chili | Escondida | Australie | Chili (100%), Chine (20%), RD Congo (30%) | 9 % | +| **Chili** | **Total** | **Chili** | **-** | **24 %** | +| Japon | Pan Pacific Copper | Japon | Chili (70%), Pérou (30%) | 8 % | +| **Japon** | **Total** | **Japon** | **-** | **8 %** | +| États-Unis | FreeportMcMoRan | États-Unis | États-Unis (60%), Pérou (30%) | 7 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **7 %** | +| Allemagne | Aurubis | Allemagne | | 5 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **5 %** | +| Inde | Kutch Copper | Inde | Chili (80%), Pérou (30%) | 4 % | +| **Inde** | **Total** | **Inde** | **-** | **4 %** | +| Corée du Sud | LS Nikkko | Corée du Sud | Australie (50%), Chine (20%), RD Congo (30%) | 3 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **-** | **3 %** | +| Pologne | KGHM | Pologne | Chili (70%), Pérou (30%) | 2 % | +| **Pologne** | **Total** | **Pologne** | **-** | **2 %** | +| Russie | Norilsk Nickel | Russie | Chili (70%), Pérou (30%) | 2 % | +| **Russie** | **Total** | **Russie** | **-** | **2 %** | + +Unités : kt/an + +Total : 26000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Inde | 4.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 8.0% | 29 | :green-badge[Vert] (Stable) | +| Chine | 45.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Chili | 24.0% | 41 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 7.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Allemagne | 5.0% | 30 | :green-badge[Vert] (Stable) | +| Corée du Sud | 3.0% | 33 | :green-badge[Vert] (Stable) | +| Pologne | 2.0% | 37 | :green-badge[Vert] (Stable) | +| Russie | 2.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **10** | | | +| **Pays** | | | **28** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, ce qui indique une **concentration faible**. Bien que **Jiangxi Copper (18 %)**, **Codelco (15 %)**, **Tongling (12 %)** regroupent plus de 45 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **28**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (45 %)**, **Chili (24 %)**, **Japon (8 %)**, représentant ensemble plus de 77 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est élevée** (IHH 28) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 28 - :red-badge[Rouge] (Élevée) +* ISG combiné: 46 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Holmium + +L'holmium est un métal de terre rare appartenant à la famille des lanthanides, découvert en 1878 par Per Theodor Cleve en Suède, et indépendamment par Marc Delafontaine et Jacques-Louis Soret à Genève. Ce métal gris-argenté, mou, malléable et ductile se caractérise par sa perméabilité magnétique exceptionnelle, la plus élevée de tous les éléments, et son comportement paramagnétique prononcé. L'holmium ne se trouve pas à l'état natif dans la nature mais principalement dans des minéraux tels que la gadolinite et la monazite, généralement en faible concentration. Son extraction et sa séparation des autres terres rares impliquent des procédés complexes en raison de leurs similitudes chimiques. Le holmium possède des propriétés remarquables qui le rendent précieux pour diverses applications industrielles et médicales. Sa capacité à absorber les neutrons en fait un composant essentiel pour les réacteurs nucléaires, tandis que ses propriétés magnétiques exceptionnelles sont exploitées dans les alliages pour créer des champs magnétiques puissants. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Camera | 0.84 | 0.90 | 0.80 | 0.80 | +| Capteurs | 0.84 | 0.90 | 0.80 | 0.80 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Caméra | 0.84 | :red-badge[Rouge] (Difficile) | +| Capteurs | 0.84 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.84 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 30 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Aimants pour stockage de données, cibles de pulvérisation +*Secteurs concurrents* +Applications médicales (lasers), alliages magnétiques +*Remarques* +Ressource utilisée dans des composants magnétiques spécialisés, avec une montée en puissance du numérique embarqué et des applications médicales en concurrence directe. Réserves modérées mais croissance rapide de la demande. +*Répartition des usages* +* Numérique final : 50% +* Numérique embarqué : 10% +* Autres secteurs : 40% +*Tendance* +* Demande : +35% +* Production : +15% +* Ratio capacité/demande : 0.85 +*Concurrence & tension* +* Ratio concurrence : 1.0 +* Tension marché : 20.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.84 - :red-badge[Rouge] (Difficile) +* IVC: 30 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Baotou Steel RareEarth | Chine | 30 % | +| Chine | China Minmetals Rare Earth Co | Chine | 15 % | +| Chine | Xiamen Tungsten | Chine | 10 % | +| **Chine** | **Total** | **Chine** | **70 %** | +| États-Unis | MP Materials | États-Unis | 15 % | +| **États-Unis** | **Total** | **États-Unis** | **15 %** | +| Australie | Lynas Rare Earths Ltd | Australie | 5 % | +| **Australie** | **Total** | **Australie** | **5 %** | +| Inde | Indian Rare Earth | Inde | 3 % | +| **Inde** | **Total** | **Inde** | **3 %** | + +Unités : t/an + +Total : 10 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 70.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 3.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 15.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **15** | | +| **Pays** | | | **52** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **15**, ce qui indique une **concentration modérée**. **Baotou Steel RareEarth (30 %)**, **China Minmetals Rare Earth Co (15 %)**, **MP Materials (15 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **52**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (70 %)**, **États-Unis (15 %)**, **Australie (5 %)**, représentant ensemble plus de 90 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 15) +- La **concentration géographique est élevée** (IHH 52) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 52 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Chine (100%) | 60 % | +| **Chine** | **Total** | **Chine** | **-** | **60 %** | +| Inde | Indian Rare Earths | Inde | Inde (100%) | 10 % | +| **Inde** | **Total** | **Inde** | **-** | **10 %** | +| États-Unis | MP Materials | États-Unis | États-Unis (100%) | 10 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **10 %** | +| Brésil | CBMM | Brésil | Chine (100%) | 5 % | +| **Brésil** | **Total** | **Brésil** | **-** | **5 %** | +| Sri Lanka | Lanka Mineral Sands | Sri Lanka | Australie (100%) | 5 % | +| **Sri Lanka** | **Total** | **Sri Lanka** | **-** | **5 %** | +| Australie | Lynas Rare Earths | Australie | Australie (100%) | 5 % | +| **Australie** | **Total** | **Australie** | **-** | **5 %** | + +Unités : t/an + +Total : 10 + +_Note: Les capacités indiquées représentent la production mondiale d'holmium métal, estimée à environ 10 tonnes par an selon les sources disponibles._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 10.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| Inde | 10.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 60.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| SriLanka | 5.0% | 61 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 5.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **39** | +| **Pays** | | | **39** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **39**, signalant une **concentration élevée**. Le marché est largement dominé par **China Minmetals (60 %)**, **MP Materials (10 %)**, **Indian Rare Earths (10 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **39**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (60 %)**, **États-Unis (10 %)**, **Inde (10 %)**, représentant ensemble plus de 80 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 39) +- La **concentration géographique est élevée** (IHH 39) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 39 - :red-badge[Rouge] (Élevée) +* ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Lanthane + +Le lanthane est un métal de transition appartenant à la famille des terres rares, découvert en 1839 par le chimiste suédois Carl Gustaf Mosander, et nommé d'après le mot grec "lanthanein" signifiant "se cacher". Ce métal blanc-argenté, mou et malléable, se caractérise par sa forte réactivité à l'air et à l'eau, s'oxydant rapidement pour former une couche protectrice d'oxyde. Le lanthane est le prototype de la série des lanthanides et constitue, malgré sa classification parmi les terres rares, le 28ème élément le plus abondant de la croûte terrestre. Son traitement implique des procédés complexes de séparation en raison de sa similitude chimique avec les autres terres rares, ce qui explique que le métal pur n'ait été isolé qu'en 1923. La chaîne d'approvisionnement mondiale est fortement concentrée en Chine, qui domine à la fois l'extraction et le traitement. Le lanthane trouve des applications variées, des catalyseurs pour le raffinage du pétrole aux batteries NiMH, en passant par les verres optiques et le secteur médical où le carbonate de lanthane est utilisé pour traiter l'hyperphosphorémie. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Camera | 0.64 | 0.70 | 0.60 | 0.60 | +| Batterie | 0.54 | 0.60 | 0.50 | 0.50 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Caméra | 0.64 | :red-badge[Rouge] (Difficile) | +| Batterie | 0.54 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.59 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 23 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Verres optiques, alliages pour batteries, électrodes électroniques +*Secteurs concurrents* +Catalyseurs automobiles, médecine, alliages métalliques +*Remarques* +Utilisé dans des composants électroniques (verres optiques, électrodes, alliages). Concurrence forte avec les catalyseurs et le médical. Réserves disponibles et production dynamique. +*Répartition des usages* +* Numérique final : 30% +* Numérique embarqué : 25% +* Autres secteurs : 45% +*Tendance* +* Demande : +28% +* Production : +18% +* Ratio capacité/demande : 0.92 +*Concurrence & tension* +* Ratio concurrence : 2.33 +* Tension marché : 10.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.59 - :orange-badge[Orange] (Moyenne) +* IVC: 23 - :orange-badge[Orange] (Modérée) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Baotou Steel RareEarth | Chine | 30 % | +| Chine | China Minmetals Rare Earth Co | Chine | 15 % | +| Chine | Xiamen Tungsten | Chine | 10 % | +| **Chine** | **Total** | **Chine** | **56 %** | +| États-Unis | MP Materials | États-Unis | 15 % | +| **États-Unis** | **Total** | **États-Unis** | **16 %** | +| Australie | Lynas Rare Earths Ltd | Australie | 10 % | +| **Australie** | **Total** | **Australie** | **7 %** | +| Inde | Indian Rare Earth | Inde | 2 % | +| **Inde** | **Total** | **Inde** | **1 %** | + +Unités : t/an + +Total : 240700 + +_Note : Les données de production sont de 2020, les plus récentes disponibles. La durée estimée est calculée en divisant les réserves par la production annuelle actuelle._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 56.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 7.0% | 26 | :green-badge[Vert] (Stable) | +| Inde | 1.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 16.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **16** | | +| **Pays** | | | **34** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **16**, ce qui indique une **concentration modérée**. **Baotou Steel RareEarth (30 %)**, **China Minmetals Rare Earth Co (15 %)**, **MP Materials (15 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **34**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (56 %)**, **États-Unis (16 %)**, **Australie (7 %)**, représentant ensemble plus de 79 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 16) +- La **concentration géographique est élevée** (IHH 34) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 34 - :red-badge[Rouge] (Élevée) +* ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | | 33 % | +| Chine | China Northern Rare Earth | Chine | | 28 % | +| Chine | Xiamen Tungsten | Chine | | 9 % | +| **Chine** | **Total** | **Chine** | **-** | **70 %** | +| États-Unis | MP Materials | États-Unis | | 11 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **11 %** | +| Malaisie | Lynas Advanced Materials | Australie | | 7 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **7 %** | +| Estonie | NPM Silmet | Canada | | 4 % | +| **Estonie** | **Total** | **Estonie** | **-** | **4 %** | +| France | Solvay | France | | 3 % | +| **France** | **Total** | **France** | **-** | **3 %** | +| Inde | Indian Rare Earths | Inde | | 2 % | +| **Inde** | **Total** | **Inde** | **-** | **2 %** | + +Unités : t/an + +Total : 46000 + +_Note: Les capacités indiquées représentent la production d'oxyde de lanthane (La₂O₃) et de lanthane métal, selon les données de l'US Geological Survey pour 2022. La Chine domine largement le marché mondial avec environ 70% de la capacité de traitement._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Inde | 2.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Estonie | 4.0% | 33 | :green-badge[Vert] (Stable) | +| France | 3.0% | 34 | :green-badge[Vert] (Stable) | +| EtatsUnis | 11.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Malaisie | 7.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 70.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **22** | | +| **Pays** | | | **51** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **22**, ce qui indique une **concentration modérée**. **China Minmetals (33 %)**, **China Northern Rare Earth (28 %)**, **MP Materials (11 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **51**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (70 %)**, **États-Unis (11 %)**, **Malaisie (7 %)**, représentant ensemble plus de 88 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 22) +- La **concentration géographique est élevée** (IHH 51) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 51 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Tantale + +Le tantale est un métal de transition rare, gris-bleu, dense et réfractaire, découvert en 1802 par Anders Gustaf Ekeberg. Ce métal se distingue par ses propriétés exceptionnelles : résistance extrême à la corrosion (même aux acides les plus agressifs, à l'exception de l'acide fluorhydrique), point de fusion très élevé (3017°C), excellente ductilité et biocompatibilité remarquable. Sa production implique des procédés métallurgiques complexes, principalement à partir de la tantalite ou du coltan (colombite-tantalite), avec des teneurs typiques de 0,01 à 0,025%. Le traitement du tantale est techniquement exigeant, énergétiquement intensif et économiquement coûteux, ce qui explique sa valeur élevée sur les marchés. Sa chaîne d'approvisionnement est particulièrement sensible en raison de sa concentration géographique (notamment en Afrique centrale) et de son statut de "minerai de conflit", suscitant une attention particulière des législateurs internationaux. Malgré sa production relativement modeste (environ 1800 tonnes par an), le tantale est devenu un métal stratégique indispensable aux industries de haute technologie. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| MemoireRAM | 0.71 | 0.80 | 0.70 | 0.60 | +| CarteMere | 0.70 | 0.70 | 0.80 | 0.60 | +| SSD25 | 0.70 | 0.70 | 0.70 | 0.70 | +| SSDM2 | 0.70 | 0.70 | 0.70 | 0.70 | +| StockageEMMC | 0.64 | 0.70 | 0.60 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Carte mère | 0.70 | :red-badge[Rouge] (Difficile) | +| Mémoire RAM | 0.71 | :red-badge[Rouge] (Difficile) | +| SSD 2.5 | 0.70 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.70 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 4 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Condensateurs pour smartphones et ordinateurs, composants microélectroniques, circuits haute densité. +*Secteurs concurrents* +Aéronautique, défense, implantologie, métallurgie +*Remarques* +Ressource critique pour miniaturisation. Usage très concentré dans le numérique mais tension avec les secteurs stratégiques. +*Répartition des usages* +* Numérique final : 60% +* Numérique embarqué : 20% +* Autres secteurs : 20% +*Tendance* +* Demande : +15% +* Production : +10% +* Ratio capacité/demande : 0.96 +*Concurrence & tension* +* Ratio concurrence : 0.67 +* Tension marché : 5.0 +*Réserves* +* Niveau : Modéré +* Pondération : 1.2 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.70 - :red-badge[Rouge] (Difficile) +* IVC: 4 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Brésil | AMG Advanced Metallurgical Group | Pays-Bas | 0 % | +| **Brésil** | **Total** | **Brésil** | **15 %** | +| Australie | Global Advanced Metals | Australie | 0 % | +| **Australie** | **Total** | **Australie** | **2 %** | + +Unités : t/an + +Total : 2200 + +_Note : Les données de production sont pour l'année 2022._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Bresil | 15.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 2.0% | 26 | :green-badge[Vert] (Stable) | + +**ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | | +| **Pays** | **2** | | | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **0**, ce qui indique une **concentration faible**. Bien que **AMG Advanced Metallurgical Group (0 %)**, **Global Advanced Metals (0 %)** regroupent plus de 0 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **2**, révélant une **concentration géographique faible**. La répartition est dominée par **Brésil (15 %)**, **Australie (2 %)**, représentant ensemble plus de 17 % des capacités. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 0) +- La **concentration géographique est faible** (IHH 2) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 2 - :green-badge[Vert] (Faible) +* ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Brésil (20%), Australie (30%) | 32 % | +| Chine | Ningxia Orient | Chine | Brésil (20%), Australie (30%) | 19 % | +| **Chine** | **Total** | **Chine** | **-** | **51 %** | +| États-Unis | HC Starck | Allemagne | Brésil (40%), Australie (20%) | 16 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **16 %** | +| Allemagne | HC Starck | Allemagne | Brésil (40%), Australie (20%) | 11 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **11 %** | +| Japon | JX Nippon Mining | Japon | Brésil (40%), Australie (30%) | 8 % | +| **Japon** | **Total** | **Japon** | **-** | **8 %** | +| Brésil | CBMM | Brésil | Brésil (100%), Australie (30%) | 6 % | +| **Brésil** | **Total** | **Brésil** | **-** | **6 %** | +| Thaïlande | Thailand Smelting | Royaume-Uni | Australie (30%), Australie (20%) | 4 % | +| **Thaïlande** | **Total** | **Thaïlande** | **-** | **4 %** | +| Estonie | Silmet | Estonie | Brésil (70%), Australie (30%) | 2 % | +| **Estonie** | **Total** | **Estonie** | **-** | **2 %** | +| Kazakhstan | Ulba Metallurgical | Kazakhstan | | 2 % | +| **Kazakhstan** | **Total** | **Kazakhstan** | **-** | **2 %** | + +Unités : t/an + +Total : 1875 + +_Note: La capacité de traitement fait référence à la quantité de tantale métal pouvant être produite annuellement. Les données reflètent les capacités installées et les origines estimées des matières premières traitées._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Bresil | 6.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Estonie | 2.0% | 33 | :green-badge[Vert] (Stable) | +| Chine | 51.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Kazakhstan | 2.0% | 51 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 8.0% | 29 | :green-badge[Vert] (Stable) | +| Thailande | 4.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 16.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Allemagne | 11.0% | 30 | :green-badge[Vert] (Stable) | + +**ISG combiné: 47 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **19** | | +| **Pays** | | | **31** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **19**, ce qui indique une **concentration modérée**. **China Minmetals (32 %)**, **Ningxia Orient (19 %)**, **HC Starck (16 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **31**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (51 %)**, **États-Unis (16 %)**, **Allemagne (11 %)**, représentant ensemble plus de 78 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 19) +- La **concentration géographique est élevée** (IHH 31) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 31 - :red-badge[Rouge] (Élevée) +* ISG combiné: 47 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Graphite + +Le graphite est l'une des formes allotropiques naturelles du carbone, avec une structure cristalline en feuillets (structure hexagonale) qui lui confère des propriétés uniques combinant caractéristiques métalliques (conductivité thermique et électrique) et non-métalliques (cristallisation en feuillets, dureté anisotrope). Matériau noir, tendre (dureté Mohs 1-2), à éclat sub-métallique et toucher onctueux, il se distingue par son excellente conductivité électrique et thermique, sa résistance aux températures extrêmes (sublimation à 3825°C à pression atmosphérique), son inertie chimique et ses propriétés lubrifiantes. + +Le graphite existe sous deux formes principales : naturel (extrait de gisements miniers) et synthétique (produit par pyrolyse de matières carbonées). La transformation du graphite implique des procédés mécaniques et chimiques variés pour obtenir des produits aux caractéristiques adaptées à diverses applications industrielles, des simples mines de crayon aux technologies de pointe comme les batteries lithium-ion, en passant par les matériaux réfractaires et les lubrifiants industriels. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Batterie | 0.51 | 0.60 | 0.50 | 0.40 | +| CreusetGraphite | 0.44 | 0.50 | 0.30 | 0.50 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Creuset graphite | 0.44 | :orange-badge[Orange] (Moyenne) | +| Batterie | 0.51 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.47 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 1 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Anodes de batteries Li-ion, composants conducteurs, pâtes conductrices +*Secteurs concurrents* +Réfractaires, lubrifiants, métallurgie +*Remarques* +Pression forte venant des batteries (secteurs EV, stockage). Importance numérique croissante. +*Répartition des usages* +* Numérique final : 70% +* Numérique embarqué : 15% +* Autres secteurs : 15% +*Tendance* +* Demande : +8% +* Production : +6% +* Ratio capacité/demande : 0.98 +*Concurrence & tension* +* Ratio concurrence : 0.43 +* Tension marché : 2.0 +*Réserves* +* Niveau : Abondant +* Pondération : 1.0 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.47 - :orange-badge[Orange] (Moyenne) +* IVC: 1 - :green-badge[Vert] (Faible) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | 15 % | +| Chine | Qingdao Black Dragon Graphite | Chine | 10 % | +| **Chine** | **Total** | **Chine** | **79 %** | +| Mozambique | Syrah Resources | Australie | 5 % | +| **Mozambique** | **Total** | **Mozambique** | **6 %** | +| Madagascar | Établissement Gallois | Madagascar | 3 % | +| **Madagascar** | **Total** | **Madagascar** | **6 %** | +| Brésil | South Star Battery Metals | Canada | 2 % | +| **Brésil** | **Total** | **Brésil** | **5 %** | +| Tanzanie | Walkabout Resources | Australie | 2 % | +| **Tanzanie** | **Total** | **Tanzanie** | **2 %** | + +Unités : t/an + +Total : 1600000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Mozambique | 6.0% | 72 | :red-badge[Rouge] (Instable) | +| Madagascar | 6.0% | 63 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 5.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 79.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Tanzanie | 2.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 56 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **4** | | | +| **Pays** | | | **63** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **4**, ce qui indique une **concentration faible**. Bien que **China Minmetals (15 %)**, **Qingdao Black Dragon Graphite - Anodes de batterie, conducteur thermique (10 %)**, **Syrah Resources (5 %)** regroupent plus de 30 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **63**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (79 %)**, **Madagascar (6 %)**, **Mozambique (6 %)**, représentant ensemble plus de 91 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 4) +- La **concentration géographique est élevée** (IHH 63) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 63 - :red-badge[Rouge] (Élevée) +* ISG combiné: 56 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Aoyu Graphite | Chine | Chine (100%) | 20 % | +| Chine | BTR New Energy | Chine | Chine (100%) | 16 % | +| **Chine** | **Total** | **Chine** | **-** | **79 %** | +| Madagascar | Établissements Gallois | France | Madagascar (100%) | 3 % | +| Madagascar | Tirupati Graphite | Inde | Madagascar (100%) | 2 % | +| **Madagascar** | **Total** | **Madagascar** | **-** | **5 %** | +| Mozambique | Syrah Resources | Australie | Mozambique (100%) | 5 % | +| **Mozambique** | **Total** | **Mozambique** | **-** | **5 %** | +| Brésil | Nacional de Grafite | Brésil | Brésil (100%) | 3 % | +| **Brésil** | **Total** | **Brésil** | **-** | **3 %** | +| Allemagne | SGL Carbon | Allemagne | | 2 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **2 %** | +| Japon | Tokai Carbon | Japon | | 2 % | +| **Japon** | **Total** | **Japon** | **-** | **2 %** | +| Canada | Northern Graphite | Canada | | 1 % | +| Canada | Nouveau Monde Graphite | Canada | | 1 % | +| **Canada** | **Total** | **Canada** | **-** | **1 %** | +| Russie | Uralgraphite | Russie | | 1 % | +| **Russie** | **Total** | **Russie** | **-** | **1 %** | + +Unités : t/an + +Total : 1609000 + +_Note: Les capacités indiquées représentent principalement la production de graphite naturel. Les données pour le graphite synthétique (environ 1,5 million de tonnes supplémentaires) ne sont pas entièrement incluses dans ce tableau. La Chine domine la production mondiale, représentant environ 79% de la production de graphite naturel._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Mozambique | 5.0% | 72 | :red-badge[Rouge] (Instable) | +| Allemagne | 2.0% | 30 | :green-badge[Vert] (Stable) | +| Madagascar | 5.0% | 63 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 3.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 1.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 79.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 2.0% | 29 | :green-badge[Vert] (Stable) | +| Canada | 1.0% | 26 | :green-badge[Vert] (Stable) | + +**ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **7** | | | +| **Pays** | | | **63** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **7**, ce qui indique une **concentration faible**. Bien que **Aoyu Graphite - Anodes de batterie, conducteur thermique (20 %)**, **BTR New Energy (16 %)**, **Syrah Resources (5 %)** regroupent plus de 41 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **63**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (79 %)**, **Madagascar (5 %)**, **Mozambique (5 %)**, représentant ensemble plus de 89 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 7) +- La **concentration géographique est élevée** (IHH 63) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 63 - :red-badge[Rouge] (Élevée) +* ISG combiné: 54 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Lithium + +Le lithium est un métal alcalin léger, blanc-argenté, qui se caractérise par sa faible densité (0,534 g/cm³) et sa grande réactivité. Sa production implique des procédés complexes variant selon la source d'extraction : principalement les saumures de salars (Amérique du Sud) ou les minéraux solides comme le spodumène et la lépidolite. Pour les saumures, deux méthodes dominent : l'évaporation solaire traditionnelle, lente mais éprouvée, et l'extraction directe de lithium (DLE), plus rapide et efficiente. Pour les minéraux, le traitement inclut généralement calcination, lixiviation acide ou alcaline, puis purification. Le métal étant hautement réactif, sa manipulation nécessite des précautions particulières. La chaîne d'approvisionnement est dominée par l'Australie pour l'extraction minière, tandis que la Chine contrôle majoritairement les capacités de traitement, particulièrement pour la conversion en hydroxyde de lithium, produit final privilégié pour les batteries haute performance des véhicules électriques. La demande croissante, propulsée par l'électrification des transports et le stockage d'énergie, pourrait tripler d'ici 2025, soulignant l'importance stratégique de ce minéral dans le contexte de décarbonation mondiale. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Batterie | 0.51 | 0.60 | 0.50 | 0.40 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Batterie | 0.51 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.51 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 45 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Batteries pour appareils électroniques, ordinateurs portables +*Secteurs concurrents* +Batteries pour véhicules électriques, stockage d’énergie, chimie +*Remarques* +Forte tension entre le numérique et la mobilité électrique. Concurrence massive du secteur automobile. Production croissante mais demande encore plus rapide. Réserves sous contrôle de quelques acteurs majeurs. +*Répartition des usages* +* Numérique final : 25% +* Numérique embarqué : 5% +* Autres secteurs : 70% +*Tendance* +* Demande : +45% +* Production : +35% +* Ratio capacité/demande : 0.93 +*Concurrence & tension* +* Ratio concurrence : 3.0 +* Tension marché : 10.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.51 - :orange-badge[Orange] (Moyenne) +* IVC: 45 - :orange-badge[Orange] (Modérée) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Australie | Talison Lithium | Chine | 15 % | +| Australie | Pilbara Minerals | Australie | 9 % | +| **Australie** | **Total** | **Australie** | **51 %** | +| Chili | SQM | Chili | 17 % | +| Chili | Albemarle | États-Unis | 10 % | +| **Chili** | **Total** | **Chili** | **26 %** | +| Chine | Ganfeng Lithium | Chine | 12 % | +| Chine | Tianqi Lithium | Chine | 11 % | +| **Chine** | **Total** | **Chine** | **16 %** | +| Argentine | Livent | États-Unis | 4 % | +| Argentine | Orocobre | Australie | 3 % | +| **Argentine** | **Total** | **Argentine** | **4 %** | +| Brésil | Companhia Brasileira de Lítio | Brésil | 2 % | +| **Brésil** | **Total** | **Brésil** | **2 %** | +| Zimbabwe | Bikita Minerals | Zimbabwe | 1 % | +| **Zimbabwe** | **Total** | **Zimbabwe** | **1 %** | + +Unités : t/an + +Total : 146000 (voir les explication ci-après) + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Australie | 51.0% | 26 | :green-badge[Vert] (Stable) | +| Chili | 26.0% | 41 | :orange-badge[Orange] (Intermédiaire) | +| Argentine | 4.0% | 46 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 16.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Zimbabwe | 1.0% | 70 | :red-badge[Rouge] (Instable) | +| Bresil | 2.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 36 - :green-badge[Vert] (Stable)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **10** | | | +| **Pays** | | | **36** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **10**, ce qui indique une **concentration faible**. Bien que **SQM (17 %)**, **Talison Lithium - Batteries rechargeables, stockage d'énergie (15 %)**, **Ganfeng Lithium - Batteries rechargeables, stockage d'énergie (12 %)** regroupent plus de 44 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **36**, révélant une **concentration géographique élevée**. La répartition est dominée par **Australie (51 %)**, **Chili (26 %)**, **Chine (16 %)**, représentant ensemble plus de 93 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 10) +- La **concentration géographique est élevée** (IHH 36) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 36 - :red-badge[Rouge] (Élevée) +* ISG combiné: 36 - :green-badge[Vert] (Stable) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Ganfeng Lithium | Chine | | 21 % | +| Chine | Tianqi Lithium | Chine | | 17 % | +| Chine | CATL | Chine | | 9 % | +| **Chine** | **Total** | **Chine** | **-** | **47 %** | +| Chili | SQM | Chili | | 15 % | +| Chili | Albemarle | États-Unis | | 9 % | +| **Chili** | **Total** | **Chili** | **-** | **24 %** | +| Argentine | Livent | États-Unis | | 5 % | +| Argentine | Eramet | France | | 5 % | +| **Argentine** | **Total** | **Argentine** | **-** | **10 %** | +| Australie | Pilbara Minerals | Australie | | 4 % | +| Australie | Mineral Resources | Australie | | 4 % | +| **Australie** | **Total** | **Australie** | **-** | **8 %** | +| Canada | Nemaska Lithium | Canada | | 3 % | +| **Canada** | **Total** | **Canada** | **-** | **3 %** | +| États-Unis | Albemarle | États-Unis | | 2 % | +| États-Unis | Livent | États-Unis | | 1 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **3 %** | +| Corée du Sud | POSCO | Corée du Sud | | 2 % | +| **Corée du Sud** | **Total** | **Corée du Sud** | **-** | **2 %** | +| Allemagne | AMG Lithium | Pays-Bas | | 2 % | +| **Allemagne** | **Total** | **Allemagne** | **-** | **2 %** | +| Japon | Sumitomo Metal Mining | Japon | | 1 % | +| **Japon** | **Total** | **Japon** | **-** | **1 %** | + +Unités : t/an + +Total : 460000 (voir les explication ci-après) + +_Note: La capacité de traitement est exprimée en tonnes d'équivalent carbonate de lithium (LCE) par an. Les données reflètent les capacités installées ou en développement avancé fin 2024._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Japon | 1.0% | 29 | :green-badge[Vert] (Stable) | +| Allemagne | 2.0% | 30 | :green-badge[Vert] (Stable) | +| Argentine | 10.0% | 46 | :orange-badge[Orange] (Intermédiaire) | +| Corée du Sud | 2.0% | 33 | :green-badge[Vert] (Stable) | +| EtatsUnis | 3.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 8.0% | 26 | :green-badge[Vert] (Stable) | +| Chili | 24.0% | 41 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 47.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Canada | 3.0% | 26 | :green-badge[Vert] (Stable) | + +**ISG combiné: 45 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **12** | | | +| **Pays** | | | **30** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **12**, ce qui indique une **concentration faible**. Bien que **Ganfeng Lithium - Batteries rechargeables, stockage d'énergie (21 %)**, **Tianqi Lithium - Batteries rechargeables, stockage d'énergie (17 %)**, **SQM (15 %)** regroupent plus de 53 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **30**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (47 %)**, **Chili (24 %)**, **Argentine (10 %)**, représentant ensemble plus de 81 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 12) +- La **concentration géographique est élevée** (IHH 30) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 30 - :red-badge[Rouge] (Élevée) +* ISG combiné: 45 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Cobalt + +Le cobalt est un métal de transition gris, brillant et ferromagnétique, découvert comme élément distinct en 1753 par Claude Geoffroy le Jeune. Ce métal présente des propriétés exceptionnelles : résistance à la corrosion à température ambiante, propriétés magnétiques remarquables, résistance aux températures élevées et excellente résistance mécanique. Le cobalt ne réagit ni avec l'eau ni avec l'air à température ambiante, mais devient fortement réactif à haute température. Son extraction est principalement réalisée comme sous-produit des métallurgies du cuivre et du nickel, avec 98% de la production mondiale provenant de cette filière. Seuls quelques gisements au Maroc et certains minerais arséniés canadiens sont exploités directement pour le cobalt. La transformation du cobalt implique des procédés hydrométallurgiques et pyrométallurgiques complexes, particulièrement pour le séparer du manganèse qui est souvent présent en concentrations plus élevées dans les minerais. À faible dose, le cobalt est également un oligoélément essentiel, constituant de la vitamine B12, mais sa manipulation sous forme de poussières ou vapeurs peut présenter des risques sanitaires significatifs. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Batterie | 0.57 | 0.60 | 0.60 | 0.50 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Batterie | 0.57 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.57 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 7 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Batteries lithium-ion, composants électroniques, poudres magnétiques, superalliages pour électronique +*Secteurs concurrents* +Batteries automobiles, superalliages aéronautiques, outillage, catalyse, santé, pigments +*Remarques* +Pression intersectorielle forte. Usages numériques en croissance (70% lié au stockage énergétique et électronique) +*Répartition des usages* +* Numérique final : 40 +* Numérique embarqué : 30 +* Autres secteurs : 30 +*Tendance* +* Demande : 6.0 +* Production : 3.0 +* Ratio capacité/demande : 0.97 +*Concurrence & tension* +* Ratio concurrence : 1.5 +* Tension marché : 3.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.57 - :orange-badge[Orange] (Moyenne) +* IVC: 7 - :green-badge[Vert] (Faible) +* Poids combiné: 2 +* Niveau de vulnérabilité: **:green-badge[FAIBLE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| RD Congo | Glencore | Suisse | 20 % | +| RD Congo | China Molybdenum | Chine | 15 % | +| RD Congo | Eurasian Resources Group | Luxembourg | 10 % | +| **RD Congo** | **Total** | **RD Congo** | **70 %** | +| Russie | Norilsk Nickel | Russie | 4 % | +| **Russie** | **Total** | **Russie** | **4 %** | +| Australie | BHP | Australie | 3 % | +| **Australie** | **Total** | **Australie** | **4 %** | +| Chine | Huayou Cobalt | Chine | 0 % | +| **Chine** | **Total** | **Chine** | **2 %** | +| Cuba | Sherritt International | Canada | 2 % | +| **Cuba** | **Total** | **Cuba** | **2 %** | +| Canada | Vale | Brésil | 2 % | +| **Canada** | **Total** | **Canada** | **2 %** | +| Maroc | Managem | Maroc | 1 % | +| **Maroc** | **Total** | **Maroc** | **1 %** | + +Unités : t/an + +Total : 160000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Australie | 4.0% | 26 | :green-badge[Vert] (Stable) | +| RDCongo | 70.0% | 84 | :red-badge[Rouge] (Instable) | +| Cuba | 2.0% | 45 | :orange-badge[Orange] (Intermédiaire) | +| Canada | 2.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 2.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 4.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Maroc | 1.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 77 - :red-badge[Rouge] (Instable)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **8** | | | +| **Pays** | | | **49** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **8**, ce qui indique une **concentration faible**. Bien que **Glencore (20 %)**, **China Molybdenum (15 %)**, **Eurasian Resources Group (10 %)** regroupent plus de 45 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **49**, révélant une **concentration géographique élevée**. La répartition est dominée par **RD Congo (70 %)**, **Australie (4 %)**, **Russie (4 %)**, représentant ensemble plus de 78 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 8) +- La **concentration géographique est élevée** (IHH 49) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 49 - :red-badge[Rouge] (Élevée) +* ISG combiné: 77 - :red-badge[Rouge] (Instable) +* Poids combiné: 9 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | Huayou Cobalt | Chine | Chine (20%), RD Congo (80%) | 16 % | +| Chine | GEM | Chine | Chine (25%), RD Congo (75%) | 13 % | +| Chine | Jinchuan | Chine | Chine (60%), RD Congo (40%) | 9 % | +| **Chine** | **Total** | **Chine** | **-** | **38 %** | +| RD Congo | Glencore Katanga | Suisse | RD Congo (100%) | 22 % | +| RD Congo | China Molybdenum | Chine | RD Congo (100%) | 11 % | +| **RD Congo** | **Total** | **RD Congo** | **-** | **33 %** | +| Finlande | Kokkola | Belgique | RD Congo (70%), Russie (20%) | 9 % | +| **Finlande** | **Total** | **Finlande** | **-** | **9 %** | +| Zambie | ERG | Luxembourg | RD Congo (100%) | 5 % | +| **Zambie** | **Total** | **Zambie** | **-** | **5 %** | +| Canada | Sherritt | Canada | Cuba (90%), Canada (10%) | 4 % | +| **Canada** | **Total** | **Canada** | **-** | **4 %** | +| Japon | Sumitomo | Japon | RD Congo (100%) | 3 % | +| **Japon** | **Total** | **Japon** | **-** | **3 %** | + +Unités : t/an + +Total : 160000 + +_Note: Les capacités indiquées représentent la production de cobalt raffiné. La République Démocratique du Congo domine la production minière avec environ 74% de la production mondiale, tandis que la Chine domine le raffinage avec environ 70% de la capacité mondiale._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| RDCongo | 33.0% | 84 | :red-badge[Rouge] (Instable) | +| Chine | 38.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Zambie | 5.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 3.0% | 29 | :green-badge[Vert] (Stable) | +| Canada | 4.0% | 26 | :green-badge[Vert] (Stable) | +| Finlande | 9.0% | 25 | :green-badge[Vert] (Stable) | + +**ISG combiné: 60 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **12** | | | +| **Pays** | | | **27** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **12**, ce qui indique une **concentration faible**. Bien que **Glencore Katanga (22 %)**, **Huayou Cobalt - Cathodes de batterie Li-ion, aimants permanents (16 %)**, **GEM (13 %)** regroupent plus de 51 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **27**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (38 %)**, **RD Congo (33 %)**, **Finlande (9 %)**, représentant ensemble plus de 80 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 12) +- La **concentration géographique est élevée** (IHH 27) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 27 - :red-badge[Rouge] (Élevée) +* ISG combiné: 60 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Praséodyme + +Le praséodyme est un métal de la famille des lanthanides (terres rares), découvert en 1885 par Carl Auer von Welsbach lors de la séparation du didyme. Ce métal gris-argenté, malléable et relativement mou se caractérise par sa réactivité chimique et sa capacité à former des composés colorés, typiquement verts. Le praséodyme se trouve principalement dans les minerais de bastnäsite et de monazite, toujours en association avec d'autres terres rares, particulièrement le néodyme. Son extraction et son traitement impliquent des procédés hydrometallurgiques complexes, suivis de techniques de séparation sélective par extraction par solvant ou échange d'ions. La chaîne d'approvisionnement mondiale est fortement dominée par la Chine, qui contrôle plus de 70% de la production. Malgré sa désignation comme "terre rare", le praséodyme est plus abondant dans la croûte terrestre que des métaux comme l'argent ou le mercure. Ses applications principales incluent les aimants permanents (en alliage avec le néodyme), les verres spéciaux utilisés dans les lunettes de protection, les céramiques, et les batteries rechargéables. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.41 | 0.50 | 0.40 | 0.30 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Audio | 0.41 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.41 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 45 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Aimants pour moteurs électriques, mémoire magnétique, verres optiques pour dispositifs de communication optique. +*Secteurs concurrents* +Véhicules électriques, aéronautique, stockage d’énergie +*Remarques* +Co-produit du néodyme, il partage des chaînes critiques similaires. Tensions liées à la croissance des marchés de la transition énergétique. +*Répartition des usages* +* Numérique final : 40% +* Numérique embarqué : 30% +* Autres secteurs : 30% +*Tendance* +* Demande : +50% +* Production : +30% +* Ratio capacité/demande : 0.87 +*Concurrence & tension* +* Ratio concurrence : 1.5 +* Tension marché : 20.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.41 - :orange-badge[Orange] (Moyenne) +* IVC: 45 - :orange-badge[Orange] (Modérée) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | 20 % | +| Chine | China Northern Rare Earth Group | Chine | 18 % | +| Chine | Shenghe Resources | Chine | 15 % | +| **Chine** | **Total** | **Chine** | **65 %** | +| États-Unis | MP Materials | États-Unis | 10 % | +| **États-Unis** | **Total** | **États-Unis** | **10 %** | +| Australie | Lynas Rare Earths | Australie | 8 % | +| **Australie** | **Total** | **Australie** | **9 %** | +| Myanmar | Multiple entreprises | Chine | 6 % | +| **Myanmar** | **Total** | **Myanmar** | **6 %** | +| Russie | Rostec | Russie | 4 % | +| **Russie** | **Total** | **Russie** | **4 %** | +| Inde | Indian Rare Earths Limited | Inde | 2 % | +| **Inde** | **Total** | **Inde** | **2 %** | + +Unités : t/an + +Total : 18500 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Myanmar | 6.0% | 84 | :red-badge[Rouge] (Instable) | +| Inde | 2.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 65.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 9.0% | 26 | :green-badge[Vert] (Stable) | +| EtatsUnis | 10.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 4.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | **12** | | | +| **Pays** | | | **45** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **12**, ce qui indique une **concentration faible**. Bien que **China Minmetals (20 %)**, **China Northern Rare Earth Group (18 %)**, **Shenghe Resources (15 %)** regroupent plus de 53 % du marché, plusieurs autres groupes viennent équilibrer le secteur. Cette structure permet une **certaine résilience industrielle**, avec plusieurs options en cas de tension sur un acteur majeur. + +##### IHH par pays + +L’IHH par pays atteint **45**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (65 %)**, **États-Unis (10 %)**, **Australie (9 %)**, représentant ensemble plus de 84 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs plutôt diversifiée** (IHH 12) +- La **concentration géographique est élevée** (IHH 45) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 45 - :red-badge[Rouge] (Élevée) +* ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | | 30 % | +| Chine | China Northern Rare Earth | Chine | | 26 % | +| Chine | Xiamen Tungsten | Chine | | 15 % | +| **Chine** | **Total** | **Chine** | **-** | **71 %** | +| États-Unis | MP Materials | États-Unis | | 11 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **11 %** | +| Australie | Lynas Rare Earths | Australie | | 11 % | +| **Australie** | **Total** | **Australie** | **-** | **11 %** | +| Inde | Indian Rare Earths | Inde | | 4 % | +| **Inde** | **Total** | **Inde** | **-** | **4 %** | +| Russie | Solikamsk Magnesium | Russie | | 3 % | +| **Russie** | **Total** | **Russie** | **-** | **3 %** | + +Unités : t/an + +Total : 13400 + +_Note: Les capacités indiquées représentent la production d'oxyde de praséodyme (Pr₆O₁₁). La production mondiale est estimée à environ 13 400 tonnes d'oxyde de praséodyme par an, selon les données de l'USGS et de Roskill._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 11.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 4.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 3.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 11.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 71.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 50 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **21** | | +| **Pays** | | | **53** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **21**, ce qui indique une **concentration modérée**. **China Minmetals (30 %)**, **China Northern Rare Earth (26 %)**, **Xiamen Tungsten (15 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **53**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (71 %)**, **Australie (11 %)**, **États-Unis (11 %)**, représentant ensemble plus de 93 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 21) +- La **concentration géographique est élevée** (IHH 53) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 53 - :red-badge[Rouge] (Élevée) +* ISG combiné: 50 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Néodyme + +Le néodyme est un métal de transition appartenant à la famille des lanthanides (terres rares), découvert en 1885 par le chimiste autrichien Carl Auer von Welsbach. Ce métal argenté, malléable et ductile se caractérise par ses propriétés magnétiques exceptionnelles, qui en font un composant essentiel des aimants permanents les plus puissants produits industriellement (Nd-Fe-B). Le néodyme se trouve principalement dans les minerais de bastnäsite et de monazite, généralement en association avec d'autres terres rares, ce qui rend son extraction et sa séparation particulièrement complexes. Son traitement implique des procédés hydrometallurgiques sophistiqués, suivis de techniques de séparation sélective. La chaîne d'approvisionnement mondiale est fortement concentrée en Chine, qui domine à la fois l'extraction (86% de la production mondiale) et la transformation. Malgré sa classification parmi les terres rares, le néodyme est relativement abondant dans la croûte terrestre (environ 41,5 ppm), mais sa production est limitée par des défis techniques, économiques et environnementaux. Ses applications principales incluent les aimants permanents (89%), utilisés dans les moteurs électriques, les éoliennes et de nombreux appareils électroniques, ainsi que les verres spéciaux, céramiques, et lasers. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.47 | 0.50 | 0.50 | 0.40 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Audio | 0.47 | :orange-badge[Orange] (Moyenne) | + +**ICS moyen : 0.47 - :orange-badge[Orange] (Moyenne)** + +#### IVC + +**IVC: 38 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Aimants permanents pour moteurs, disques durs, microphones, capteurs, équipements optiques. +*Secteurs concurrents* +Véhicules électriques, éoliennes, robots industriels +*Remarques* +Composant central des aimants permanents à haute performance. Tensions entre numérique et transition énergétique. Ressource stratégique dominée par la Chine. +*Répartition des usages* +* Numérique final : 50% +* Numérique embarqué : 35% +* Autres secteurs : 15% +*Tendance* +* Demande : +60% +* Production : +35% +* Ratio capacité/demande : 0.84 +*Concurrence & tension* +* Ratio concurrence : 1.0 +* Tension marché : 25.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.47 - :orange-badge[Orange] (Moyenne) +* IVC: 38 - :orange-badge[Orange] (Modérée) +* Poids combiné: 4 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Northern Rare Earth HighTech | Chine | 69 % | +| **Chine** | **Total** | **Chine** | **69 %** | +| États-Unis | MP Materials | États-Unis | 11 % | +| **États-Unis** | **Total** | **États-Unis** | **11 %** | +| Myanmar | Diverses entreprises locales | Myanmar | 11 % | +| **Myanmar** | **Total** | **Myanmar** | **11 %** | +| Australie | Lynas Rare Earths | Australie | 5 % | +| **Australie** | **Total** | **Australie** | **5 %** | +| **Thaïlande** | **Total** | **Thaïlande** | **2 %** | +| Inde | Indian Rare Earths Limited | Inde | 1 % | +| **Inde** | **Total** | **Inde** | **1 %** | +| Russie | TriArk Mining | Russie | 1 % | +| **Russie** | **Total** | **Russie** | **1 %** | +| Vietnam | Vietnam Rare Earth JSC | Vietnam | 0 % | +| **Vietnam** | **Total** | **Vietnam** | **0 %** | +| Madagascar | Diverses entreprises locales | Madagascar | 0 % | +| **Madagascar** | **Total** | **Madagascar** | **0 %** | + +**Unités** : t/an + +**Total** : 75000 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Thailande | 2.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| EtatsUnis | 11.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 69.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Myanmar | 11.0% | 84 | :red-badge[Rouge] (Instable) | +| Russie | 1.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Vietnam | 0.0% | 48 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 1.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Madagascar | 0.0% | 63 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 55 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **50** | +| **Pays** | | | **50** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **50**, signalant une **concentration élevée**. Le marché est largement dominé par **China Northern Rare Earth HighTech (69 %)**, **MP Materials (11 %)**, **Diverses entreprises locales (11 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **50**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (69 %)**, **États-Unis (11 %)**, **Myanmar (11 %)**, représentant ensemble plus de 91 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 50) +- La **concentration géographique est élevée** (IHH 50) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 50 - :red-badge[Rouge] (Élevée) +* ISG combiné: 55 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | | 30 % | +| Chine | China Northern Rare Earth | Chine | | 26 % | +| Chine | Xiamen Tungsten | Chine | | 15 % | +| **Chine** | **Total** | **Chine** | **-** | **71 %** | +| États-Unis | MP Materials | États-Unis | | 11 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **11 %** | +| Australie | Lynas Rare Earths | Australie | | 11 % | +| **Australie** | **Total** | **Australie** | **-** | **11 %** | +| Inde | Indian Rare Earths | Inde | | 4 % | +| **Inde** | **Total** | **Inde** | **-** | **4 %** | +| Russie | Solikamsk Magnesium | Russie | | 3 % | +| **Russie** | **Total** | **Russie** | **-** | **3 %** | + +**Unités** : t/an + +**Total** : 54000 + +_Note: Les capacités indiquées représentent la production d'oxyde de néodyme (Nd₂O₃). Selon les données précises de MineralInfo, la production mondiale était de 54 000 tonnes en 2021, basée sur les chiffres de l'USGS, Roskill et les sociétés du secteur._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Inde | 4.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 71.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 11.0% | 26 | :green-badge[Vert] (Stable) | +| EtatsUnis | 11.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 3.0% | 65 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 50 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **21** | | +| **Pays** | | | **53** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **21**, ce qui indique une **concentration modérée**. **China Minmetals (30 %)**, **China Northern Rare Earth (26 %)**, **Xiamen Tungsten (15 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **53**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (71 %)**, **Australie (11 %)**, **États-Unis (11 %)**, représentant ensemble plus de 93 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 21) +- La **concentration géographique est élevée** (IHH 53) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 53 - :red-badge[Rouge] (Élevée) +* ISG combiné: 50 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Dysprosium + +Le dysprosium est un métal lanthanide rare de couleur blanc-argenté, découvert en 1886 par le Français Lecoq de Boisbaudran. Ce métal au nom issu du grec "dysprositos" (difficile à obtenir) se caractérise par sa malléabilité, sa ductilité, sa forte réactivité à l'air et à l'eau, et ses remarquables propriétés magnétiques. Classé parmi les terres rares lourdes, il est faiblement concentré dans la croûte terrestre (environ 0,3 ppm). Le dysprosium se distingue par sa capacité à renforcer considérablement la résistance à la démagnétisation à haute température des aimants permanents, propriété qui représente 98% de ses applications et en fait un élément stratégique pour les technologies de pointe. Sa production mondiale est largement dominée par la Chine, qui maintient une situation de quasi-monopole sur l'ensemble de la chaîne de valeur, depuis l'extraction jusqu'à la transformation en produits finis. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Audio | 0.70 | 0.70 | 0.70 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Audio | 0.70 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.70 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 36 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Aimants pour capteurs, disques durs, nanoparticules pour électronique, cibles de pulvérisation +*Secteurs concurrents* +Moteurs EV, éoliennes, alliages industriels, technologies de défense +*Remarques* +Très forte dépendance au numérique embarqué. Usage final numérique plus faible mais stratégique. +*Répartition des usages* +* Numérique final : 20% +* Numérique embarqué : 30% +* Autres secteurs : 50% +*Tendance* +* Demande : +8% +* Production : +3% +* Ratio capacité/demande : 0.95 +*Concurrence & tension* +* Ratio concurrence : 4.0 +* Tension marché : 5.0 +*Réserves* +* Niveau : Très limité +* Pondération : 1.8 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.70 - :red-badge[Rouge] (Difficile) +* IVC: 36 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Northern Rare Earth Group | Chine | 75 % | +| **Chine** | **Total** | **Chine** | **98 %** | +| Australie | Northern Minerals | Australie | 2 % | +| **Australie** | **Total** | **Australie** | **2 %** | +| États-Unis | MP Materials | États-Unis | 0 % | +| **États-Unis** | **Total** | **États-Unis** | **0 %** | + +Unités : tonnes/an + +Total : 1835 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 0.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 2.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 98.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **56** | +| **Pays** | | | **96** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **56**, signalant une **concentration élevée**. Le marché est largement dominé par **China Northern Rare Earth Group (75 %)**, **Northern Minerals (2 %)**, **MP Materials (0 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **96**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (98 %)**, **Australie (2 %)**, **États-Unis (0 %)**, représentant ensemble plus de 100 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 56) +- La **concentration géographique est élevée** (IHH 96) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 96 - :red-badge[Rouge] (Élevée) +* ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Chine (95%) | 39 % | +| Chine | Northern Rare Earth | Chine | Chine (100%) | 20 % | +| **Chine** | **Total** | **Chine** | **-** | **59 %** | +| Malaisie | Lynas Advanced Materials | Australie | Australie (100%) | 13 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **13 %** | +| États-Unis | MP Materials | États-Unis | États-Unis (100%) | 10 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **10 %** | +| Estonie | NPM Silmet | Canada | Australie (100%) | 6 % | +| **Estonie** | **Total** | **Estonie** | **-** | **6 %** | +| Inde | Indian Rare Earths | Inde | Chine (100%) | 5 % | +| **Inde** | **Total** | **Inde** | **-** | **5 %** | +| Russie | Solikamsk Magnesium | Russie | États-Unis (100%) | 4 % | +| **Russie** | **Total** | **Russie** | **-** | **4 %** | + +Unités : tonnes/an + +Total : 900 + +_Note: La production de dysprosium est étroitement liée à celle des autres terres rares, créant une interdépendance complexe entre l'offre et la demande des différents éléments._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 10.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 59.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Malaisie | 13.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 4.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 5.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Estonie | 6.0% | 33 | :green-badge[Vert] (Stable) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **23** | | +| **Pays** | | | **38** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **23**, ce qui indique une **concentration modérée**. **China Minmetals (39 %)**, **Northern Rare Earth (20 %)**, **Lynas Advanced Materials (13 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **38**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (59 %)**, **Malaisie (13 %)**, **États-Unis (10 %)**, représentant ensemble plus de 82 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 23) +- La **concentration géographique est élevée** (IHH 38) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 38 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Terbium + +Le terbium est un élément métallique du groupe des lanthanides, découvert en 1843 par Carl Gustaf Mosander lors de l'analyse de la "terre d'yttria". Ce métal de couleur gris-argenté, malléable et ductile se caractérise par sa relative stabilité à l'air, ses propriétés magnétiques remarquables et sa capacité à émettre une luminescence verte lorsqu'il est excité. Le terbium se trouve principalement dans des minéraux comme la bastnäsite, la monazite et la xénotime, toujours en association avec d'autres terres rares et en concentrations extrêmement faibles. Son extraction et sa séparation impliquent des procédés hydrometallurgiques complexes, notamment l'extraction par solvant et l'échange d'ions, rendus particulièrement difficiles par sa similitude chimique avec les autres lanthanides. La chaîne d'approvisionnement mondiale est fortement concentrée en Chine, qui contrôle environ 85% de la production. Malgré sa rareté (production mondiale d'environ 450 tonnes par an), le terbium est essentiel pour plusieurs applications de haute technologie, notamment les phosphores verts pour écrans, les disques magnéto-optiques, les aimants permanents Terfenol-D et les dispositifs sonar. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| EcranLCD | 0.81 | 0.90 | 0.80 | 0.70 | +| EcranMiniLED | 0.81 | 0.90 | 0.80 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Écran Mini et Micro LED | 0.81 | :red-badge[Rouge] (Difficile) | +| Écran LCD/TFT | 0.81 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.81 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 28 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Phosphores pour écrans, LED, disques durs, aimants pour moteurs, capteurs de position. +*Secteurs concurrents* +Éclairage, transition énergétique (VE, éoliennes), optique +*Remarques* +Ressource rare, cruciale pour les écrans et les aimants à haut rendement. Fortes tensions avec les industries de la transition énergétique. +*Répartition des usages* +* Numérique final : 45% +* Numérique embarqué : 30% +* Autres secteurs : 25% +*Tendance* +* Demande : +50% +* Production : +35% +* Ratio capacité/demande : 0.9 +*Concurrence & tension* +* Ratio concurrence : 1.22 +* Tension marché : 15.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.81 - :red-badge[Rouge] (Difficile) +* IVC: 28 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Northern Rare Earth Group | Chine | 71 % | +| **Chine** | **Total** | **Chine** | **71 %** | +| États-Unis | MP Materials | États-Unis | 14 % | +| **États-Unis** | **Total** | **États-Unis** | **14 %** | +| Myanmar | Diverses entreprises locales | Myanmar | 7 % | +| **Myanmar** | **Total** | **Myanmar** | **7 %** | +| Australie | Lynas Rare Earths | Australie | 4 % | +| **Australie** | **Total** | **Australie** | **4 %** | +| Brésil | CBMM | Brésil | 3 % | +| **Brésil** | **Total** | **Brésil** | **3 %** | + +**Unités** : t/an + +**Total** : 700 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Australie | 4.0% | 26 | :green-badge[Vert] (Stable) | +| Myanmar | 7.0% | 84 | :red-badge[Rouge] (Instable) | +| Bresil | 3.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 14.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 71.0% | 54 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **53** | +| **Pays** | | | **53** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **53**, signalant une **concentration élevée**. Le marché est largement dominé par **China Northern Rare Earth Group (71 %)**, **MP Materials (14 %)**, **Diverses entreprises locales (7 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **53**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (71 %)**, **États-Unis (14 %)**, **Myanmar (7 %)**, représentant ensemble plus de 92 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 53) +- La **concentration géographique est élevée** (IHH 53) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 53 - :red-badge[Rouge] (Élevée) +* ISG combiné: 53 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | | 33 % | +| Chine | China Northern Rare Earth | Chine | | 27 % | +| Chine | Xiamen Tungsten | Chine | | 18 % | +| **Chine** | **Total** | **Chine** | **-** | **78 %** | +| Malaisie | Lynas Advanced Materials | Australie | | 7 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **7 %** | +| Estonie | NPM Silmet | Canada | | 4 % | +| **Estonie** | **Total** | **Estonie** | **-** | **4 %** | +| États-Unis | MP Materials | États-Unis | | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **3 %** | +| Inde | Indian Rare Earths | Inde | | 3 % | +| **Inde** | **Total** | **Inde** | **-** | **3 %** | +| Vietnam | Vietnam Rare Earth | Vietnam | | 2 % | +| **Vietnam** | **Total** | **Vietnam** | **-** | **2 %** | + +**Unités** : t/an + +**Total** : 450 + +_Note: Les capacités indiquées représentent la production d'oxyde de terbium (Tb₄O₇). La Chine domine largement le marché mondial avec environ 78% de la capacité de traitement, notamment grâce à ses gisements d'argiles ioniques du sud du pays, particulièrement riches en terres rares lourdes._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Malaisie | 7.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| Estonie | 4.0% | 33 | :green-badge[Vert] (Stable) | +| Chine | 78.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 3.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 3.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Vietnam | 2.0% | 48 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **22** | | +| **Pays** | | | **62** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **22**, ce qui indique une **concentration modérée**. **China Minmetals (33 %)**, **China Northern Rare Earth (27 %)**, **Xiamen Tungsten (18 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **62**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (78 %)**, **Malaisie (7 %)**, **Estonie (4 %)**, représentant ensemble plus de 89 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 22) +- La **concentration géographique est élevée** (IHH 62) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 62 - :red-badge[Rouge] (Élevée) +* ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Europium + +L'europium est un métal de la série des lanthanides, découvert en 1901 par Eugène-Anatole Demarçay et nommé d'après le continent européen. Ce métal blanc-argenté, extrêmement réactif et le plus tendre des lanthanides, se caractérise par sa densité de 5,24 g/cm³, son point de fusion de 822°C et son point d'ébullition de 1529°C. L'europium présente la particularité d'exister facilement sous deux états d'oxydation (Eu²⁺ et Eu³⁺), contrairement aux autres lanthanides qui existent principalement sous forme trivalente. Cette propriété est exploitée dans les procédés de séparation industriels. L'europium ne se trouve pas à l'état natif dans la nature mais est présent en faibles concentrations dans certains minéraux comme la bastnaésite, la monazite et la xénotime. Sa production mondiale est limitée (quelques centaines de tonnes par an) mais stratégique en raison de ses applications uniques, notamment dans les phosphores rouges pour les écrans et l'éclairage, les dispositifs luminescents et certaines applications médicales spécialisées. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| EcranLCD | 0.81 | 0.90 | 0.80 | 0.70 | +| EcranMiniLED | 0.81 | 0.90 | 0.80 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Écran Mini et Micro LED | 0.81 | :red-badge[Rouge] (Difficile) | +| Écran LCD/TFT | 0.81 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.81 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 0 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Phosphores rouges pour écrans et LED, luminophores pour imagerie +*Secteurs concurrents* +Luminophores pour éclairage, catalyse +*Remarques* +Essentiellement utilisé dans des composants optiques. Réserves très limitées. +*Répartition des usages* +* Numérique final : 75% +* Numérique embarqué : 15% +* Autres secteurs : 10% +*Tendance* +* Demande : +2% +* Production : +1.5% +* Ratio capacité/demande : 1 +*Concurrence & tension* +* Ratio concurrence : 0.33 +* Tension marché : 0.5 +*Réserves* +* Niveau : Très limité +* Pondération : 1.8 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.81 - :red-badge[Rouge] (Difficile) +* IVC: 0 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Baotou Steel RareEarth | Chine | 30 % | +| Chine | Xiamen Tungsten | Chine | 15 % | +| **Chine** | **Total** | **Chine** | **56 %** | +| États-Unis | MP Materials | États-Unis | 15 % | +| **États-Unis** | **Total** | **États-Unis** | **16 %** | +| Australie | Lynas Rare Earths Ltd | Australie | 10 % | +| **Australie** | **Total** | **Australie** | **7 %** | +| Inde | Indian Rare Earth | Inde | 2 % | +| **Inde** | **Total** | **Inde** | **1 %** | + +Unités : t/an + +Total : 241 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 16.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Chine | 56.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 7.0% | 26 | :green-badge[Vert] (Stable) | +| Inde | 1.0% | 60 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **15** | | +| **Pays** | | | **34** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **15**, ce qui indique une **concentration modérée**. **Baotou Steel RareEarth (30 %)**, **Xiamen Tungsten (15 %)**, **MP Materials (15 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **34**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (56 %)**, **États-Unis (16 %)**, **Australie (7 %)**, représentant ensemble plus de 79 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 15) +- La **concentration géographique est élevée** (IHH 34) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 34 - :red-badge[Rouge] (Élevée) +* ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Chine (95%) | 40 % | +| Chine | Shenghe Resources | Chine | Chine (100%) | 22 % | +| **Chine** | **Total** | **Chine** | **-** | **62 %** | +| Malaisie | Lynas Advanced Materials | Australie | Australie (100%) | 14 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **14 %** | +| France | Solvay | France | Chine (40%) | 11 % | +| **France** | **Total** | **France** | **-** | **11 %** | +| Estonie | NPM Silmet | Canada | Australie (100%) | 7 % | +| **Estonie** | **Total** | **Estonie** | **-** | **7 %** | +| Japon | ShinEtsu | Japon | Chine (70%) | 5 % | +| **Japon** | **Total** | **Japon** | **-** | **5 %** | + +Unités : kt/an + +Total : 177 + +_Note: Les capacités indiquées représentent la production d'oxyde d'europium (Eu₂O₃) et d'europium métal.__ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Japon | 5.0% | 29 | :green-badge[Vert] (Stable) | +| Estonie | 7.0% | 33 | :green-badge[Vert] (Stable) | +| Chine | 62.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Malaisie | 14.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| France | 11.0% | 34 | :green-badge[Vert] (Stable) | + +**ISG combiné: 48 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **25** | +| **Pays** | | | **42** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **25**, signalant une **concentration élevée**. Le marché est largement dominé par **China Minmetals (40 %)**, **Shenghe Resources (22 %)**, **Lynas Advanced Materials (14 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **42**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (62 %)**, **Malaisie (14 %)**, **France (11 %)**, représentant ensemble plus de 87 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 25) +- La **concentration géographique est élevée** (IHH 42) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 42 - :red-badge[Rouge] (Élevée) +* ISG combiné: 48 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Samarium + +Le samarium est un métal des terres rares du groupe des lanthanides, caractérisé par son aspect argenté, sa forte malléabilité et son oxydabilité à l'air. Sa production implique un processus complexe de séparation des autres terres rares contenues dans les mêmes minerais, puis de multiples étapes d'affinage et de transformation pour obtenir le métal pur ou des alliages spécifiques. Le procédé de traitement commence généralement par l'extraction du minerai, principalement de la monazite, de la bastnäsite ou de la samarskite, suivi par la séparation des terres rares et enfin par des procédés de réduction pour obtenir le métal pur. La particularité du samarium, qui s'enflamme spontanément vers 150°C, nécessite des techniques spéciales de manipulation et de stockage, généralement sous atmosphère d'argon ou huile protectrice. La chaîne d'approvisionnement mondiale est dominée par la Chine, qui contrôle la majorité des capacités de traitement, ce qui soulève des préoccupations stratégiques pour les industries occidentales dépendantes de ce métal aux propriétés magnétiques exceptionnelles. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Capteurs | 0.74 | 0.80 | 0.70 | 0.70 | +| Connectivite | 0.64 | 0.70 | 0.60 | 0.60 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Connectivité | 0.64 | :red-badge[Rouge] (Difficile) | +| Capteurs | 0.74 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.69 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 35 - :orange-badge[Orange] (Modérée)** + +*Usage numérique* +Aimants Samarium-Cobalt pour équipements critiques, dispositifs optiques, lasers, mémoire magnétique. +*Secteurs concurrents* +Défense, spatial, équipements haute température +*Remarques* +Matériau stratégique pour les aimants à haute stabilité thermique. Fort chevauchement entre numérique embarqué et défense. +*Répartition des usages* +* Numérique final : 30% +* Numérique embarqué : 20% +* Autres secteurs : 50% +*Tendance* +* Demande : +35% +* Production : +25% +* Ratio capacité/demande : 0.93 +*Concurrence & tension* +* Ratio concurrence : 2.33 +* Tension marché : 10.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.69 - :red-badge[Rouge] (Difficile) +* IVC: 35 - :orange-badge[Orange] (Modérée) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Northern Rare Earth Group | Chine | 70 % | +| **Chine** | **Total** | **Chine** | **71 %** | +| États-Unis | MP Materials | États-Unis | 15 % | +| **États-Unis** | **Total** | **États-Unis** | **14 %** | +| Inde | Indian Rare Earths Limited | Inde | 7 % | +| **Inde** | **Total** | **Inde** | **7 %** | +| Australie | Lynas Rare Earths | Australie | 4 % | +| **Australie** | **Total** | **Australie** | **4 %** | +| Brésil | CBMM | Brésil | 3 % | +| **Brésil** | **Total** | **Brésil** | **3 %** | + +Unités : t/an + +Total : 700 + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Australie | 4.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 71.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Bresil | 3.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 14.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 7.0% | 60 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **52** | +| **Pays** | | | **53** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **52**, signalant une **concentration élevée**. Le marché est largement dominé par **China Northern Rare Earth Group (70 %)**, **MP Materials (15 %)**, **Indian Rare Earths Limited (7 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **53**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (71 %)**, **États-Unis (14 %)**, **Inde (7 %)**, représentant ensemble plus de 92 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 52) +- La **concentration géographique est élevée** (IHH 53) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 53 - :red-badge[Rouge] (Élevée) +* ISG combiné: 52 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Northern Rare Earth Group | Chine | Chine (95%), Australie (10%) | 50 % | +| Chine | Shenghe Resources | Chine | Chine (85%), Australie (15%) | 12 % | +| Chine | China Minmetals | Chine | Chine (80%), Australie (10%) | 8 % | +| Chine | Neo Performance Materials | Canada | Chine (70%), Australie (30%) | 4 % | +| **Chine** | **Total** | **Chine** | **-** | **74 %** | +| États-Unis | MP Materials | États-Unis | États-Unis (100%) | 10 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **10 %** | +| Malaisie | Lynas Corporation | Australie | Australie (100%) | 8 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **8 %** | +| Japon | Santoku Corporation | Japon | Chine (80%), Australie (15%) | 3 % | +| **Japon** | **Total** | **Japon** | **-** | **3 %** | +| Inde | Indian Rare Earths Limited | Inde | Inde (100%) | 3 % | +| **Inde** | **Total** | **Inde** | **-** | **3 %** | +| Brésil | CBMM | Brésil | Brésil (100%) | 2 % | +| **Brésil** | **Total** | **Brésil** | **-** | **2 %** | + +Unités : t/an + +Total : 600 + +_Note: La capacité de traitement fait référence à la quantité de samarium (sous forme d'oxyde ou de métal) pouvant être produite annuellement._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Chine | 74.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Malaisie | 8.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 10.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Inde | 3.0% | 60 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 3.0% | 29 | :green-badge[Vert] (Stable) | +| Bresil | 2.0% | 55 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **29** | +| **Pays** | | | **57** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **29**, signalant une **concentration élevée**. Le marché est largement dominé par **China Northern Rare Earth Group (50 %)**, **Shenghe Resources (12 %)**, **MP Materials (10 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **57**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (74 %)**, **États-Unis (10 %)**, **Malaisie (8 %)**, représentant ensemble plus de 92 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 29) +- La **concentration géographique est élevée** (IHH 57) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 57 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Erbium + +L'erbium est un métal de transition appartenant au groupe des terres rares, découvert en 1843 par Carl Gustaf Mosander et isolé sous forme pure en 1879 par Per Teodor Cleve. Ce métal gris-argenté, malléable et ductile se caractérise par sa forte réactivité chimique, sa capacité d'absorption des neutrons et ses remarquables propriétés optiques. L'erbium se distingue par sa capacité à émettre dans la bande télécom (1550 nm), propriété exploitée dans les amplificateurs optiques pour les communications par fibre optique. Il n'existe pas à l'état natif mais se trouve principalement dans des minéraux comme la xenotime, la monazite et la gadolinite, et dans les argiles ioniques en Chine. Son extraction et sa séparation des autres terres rares sont particulièrement complexes en raison des similitudes chimiques au sein de cette famille d'éléments, nécessitant des procédés sophistiqués d'hydrométallurgie et de séparation sélective. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Connectivite | 0.74 | 0.80 | 0.70 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Connectivité | 0.74 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.74 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 5 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Fibres optiques, amplificateurs de signal, composants laser pour télécommunications +*Secteurs concurrents* +Verres colorés, céramiques, médecine nucléaire +*Remarques* +Forte dépendance au numérique embarqué. Réserves limitées, offre concentrée. +*Répartition des usages* +* Numérique final : 25% +* Numérique embarqué : 60% +* Autres secteurs : 15% +*Tendance* +* Demande : +4% +* Production : +3% +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 3.0 +* Tension marché : 1.0 +*Réserves* +* Niveau : Limité +* Pondération : 1.5 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.74 - :red-badge[Rouge] (Difficile) +* IVC: 5 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | China Northern Rare Earth Group | Chine | 68 % | +| **Chine** | **Total** | **Chine** | **70 %** | +| Myanmar | Entreprises locales non déclarées | Myanmar | 14 % | +| **Myanmar** | **Total** | **Myanmar** | **14 %** | +| Russie | Uralredmet | Russie | 8 % | +| **Russie** | **Total** | **Russie** | **8 %** | +| Australie | Lynas Rare Earths | Australie | 4 % | +| **Australie** | **Total** | **Australie** | **4 %** | +| États-Unis | MP Materials | États-Unis | 2 % | +| **États-Unis** | **Total** | **États-Unis** | **2 %** | + +Unités : t/an + +Total : 2595 + +_Note : Parts de marché estimées d'après les quotas de production REE 2024 [3][12]._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Australie | 4.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 70.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Russie | 8.0% | 65 | :orange-badge[Orange] (Intermédiaire) | +| Myanmar | 14.0% | 84 | :red-badge[Rouge] (Instable) | +| EtatsUnis | 2.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 58 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **49** | +| **Pays** | | | **52** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **49**, signalant une **concentration élevée**. Le marché est largement dominé par **China Northern Rare Earth Group (68 %)**, **Entreprises locales non déclarées (14 %)**, **Uralredmet (8 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **52**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (70 %)**, **Myanmar (14 %)**, **Russie (8 %)**, représentant ensemble plus de 92 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 49) +- La **concentration géographique est élevée** (IHH 52) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 52 - :red-badge[Rouge] (Élevée) +* ISG combiné: 58 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Chine (100%) | 45 % | +| Chine | Shenghe Resources | Chine | Chine (100%) | 23 % | +| **Chine** | **Total** | **Chine** | **-** | **68 %** | +| Malaisie | Lynas Advanced Materials | Australie | Australie (100%) | 10 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **10 %** | +| Estonie | NPM Silmet | Canada | Russie (80%) | 6 % | +| **Estonie** | **Total** | **Estonie** | **-** | **6 %** | +| Japon | ShinEtsu Chemical | Japon | Chine (70%) | 5 % | +| **Japon** | **Total** | **Japon** | **-** | **5 %** | +| France | Solvay | France | Chine (60%) | 4 % | +| **France** | **Total** | **France** | **-** | **4 %** | +| États-Unis | Blue Line Corp | États-Unis | Australie (100%) | 3 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **3 %** | + +Unités : t/an + +Total : 155 + +_Note: Les capacités indiquées représentent la production mondiale estimée d'oxyde d'erbium (Er₂O₃) et d'erbium métal._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| EtatsUnis | 3.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Malaisie | 10.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| France | 4.0% | 34 | :green-badge[Vert] (Stable) | +| Chine | 68.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 5.0% | 29 | :green-badge[Vert] (Stable) | +| Estonie | 6.0% | 33 | :green-badge[Vert] (Stable) | + +**ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | | **27** | +| **Pays** | | | **48** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **27**, signalant une **concentration élevée**. Le marché est largement dominé par **China Minmetals (45 %)**, **Shenghe Resources (23 %)**, **Lynas Advanced Materials (10 %)**, ce qui pourrait poser des **risques industriels en cas de défaillance**. + +##### IHH par pays + +L’IHH par pays atteint **48**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (68 %)**, **Malaisie (10 %)**, **Estonie (6 %)**, représentant ensemble plus de 84 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **concentration forte en nombre d’acteurs** (IHH 27) +- La **concentration géographique est élevée** (IHH 48) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 48 - :red-badge[Rouge] (Élevée) +* ISG combiné: 49 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +--- + +### Gadolinium + +Le gadolinium est un métal rare, blanc-argenté, appartenant à la famille des lanthanides, découvert en 1794 par Johan Gadolin en Suède et isolé à l'état pur en 1886 par le chimiste français Paul Emile Lecoq de Boisbaudran. Ce métal se caractérise par ses propriétés paramagnétiques exceptionnelles, son comportement ferromagnétique en dessous de 20°C et sa forte section efficace d'absorption des neutrons. Le gadolinium n'existe pas à l'état natif dans la nature mais principalement dans des minéraux comme la monazite et la bastnäsite, avec la lépersonite (minéral rare d'Afrique équatoriale) comme seul minéral renfermant exclusivement du gadolinium parmi les lanthanides. Son extraction et son traitement impliquent des procédés métallurgiques et chimiques complexes, notamment pour le séparer des autres terres rares. Malgré sa toxicité intrinsèque, le gadolinium complexé par des macromolécules voit sa toxicité réduite d'un facteur 100 environ, ce qui permet son utilisation en médecine comme agent de contraste pour l'imagerie par résonance magnétique (IRM), sa principale application commerciale. + +#### ICS + +| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique | +| :-- | :--: | :--: | :--: | :--: | +| Capteurs | 0.74 | 0.80 | 0.70 | 0.70 | + +##### Valeurs d'ICS par composant + +| Composant | ICS | Criticité | +| :-- | :-- | :-- | +| Capteurs | 0.74 | :red-badge[Rouge] (Difficile) | + +**ICS moyen : 0.74 - :red-badge[Rouge] (Difficile)** + +#### IVC + +**IVC: 7 - :green-badge[Vert] (Faible)** + +*Usage numérique* +Matériaux magnétiques, stockage de données, composants optiques et capteurs +*Secteurs concurrents* +Médecine nucléaire, IRM, alliages +*Remarques* +Concurrence médicale forte. Croissance numérique modérée. Réserves très limitées. +*Répartition des usages* +* Numérique final : 20% +* Numérique embarqué : 5% +* Autres secteurs : 75% +*Tendance* +* Demande : +3% +* Production : +2% +* Ratio capacité/demande : 0.99 +*Concurrence & tension* +* Ratio concurrence : 4.0 +* Tension marché : 1.0 +*Réserves* +* Niveau : Très limité +* Pondération : 1.8 + +#### Vulnérabilité combinée ICS-IVC + +* ICS moyen: 0.74 - :red-badge[Rouge] (Difficile) +* IVC: 7 - :green-badge[Vert] (Faible) +* Poids combiné: 3 +* Niveau de vulnérabilité: **MOYENNE** + +#### Extraction + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Part de marché** | +| :-- | :-- | :-- | :-- | +| Chine | Baotou Steel RareEarth | Chine | 30 % | +| Chine | Xiamen Tungsten | Chine | 15 % | +| **Chine** | **Total** | **Chine** | **70 %** | +| États-Unis | MP Materials | États-Unis | 15 % | +| **États-Unis** | **Total** | **États-Unis** | **15 %** | +| Australie | Lynas Rare Earths Ltd | Australie | 10 % | +| **Australie** | **Total** | **Australie** | **5 %** | +| Brésil | CBMM | Brésil | 5 % | +| **Brésil** | **Total** | **Brésil** | **5 %** | + +Unités : t/an + +Total : 400 + +_Note : Les données de production sont basées sur les estimations de 2024, les plus récentes disponibles._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Bresil | 5.0% | 55 | :orange-badge[Orange] (Intermédiaire) | +| Australie | 5.0% | 26 | :green-badge[Vert] (Stable) | +| Chine | 70.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 15.0% | 42 | :orange-badge[Orange] (Intermédiaire) | + +**ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Extraction +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **15** | | +| **Pays** | | | **52** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **15**, ce qui indique une **concentration modérée**. **Baotou Steel RareEarth (30 %)**, **Xiamen Tungsten (15 %)**, **MP Materials (15 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **52**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (70 %)**, **États-Unis (15 %)**, **Australie (5 %)**, représentant ensemble plus de 90 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 15) +- La **concentration géographique est élevée** (IHH 52) + +##### Vulnérabilité combinée IHH-ISG pour l'extraction + +* IHH: 52 - :red-badge[Rouge] (Élevée) +* ISG combiné: 51 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +#### Traitement + +| **Pays d'implantation** | **Entreprise** | **Pays d'origine** | **Origines du minerai** | **Part de marché** | +| :-- | :-- | :-- | :-- | :-- | +| Chine | China Minmetals | Chine | Chine (100%) | 40 % | +| Chine | Shenghe Resources | Chine | Chine (100%) | 21 % | +| **Chine** | **Total** | **Chine** | **-** | **61 %** | +| Malaisie | Lynas Advanced Materials | Australie | Australie (100%), Brésil (40%) | 12 % | +| **Malaisie** | **Total** | **Malaisie** | **-** | **12 %** | +| France | Solvay | France | Chine (60%), Brésil (40%) | 9 % | +| **France** | **Total** | **France** | **-** | **9 %** | +| Estonie | NPM Silmet | Canada | États-Unis (100%), Brésil (40%) | 8 % | +| **Estonie** | **Total** | **Estonie** | **-** | **8 %** | +| États-Unis | MP Materials | États-Unis | États-Unis (100%), Brésil (40%) | 5 % | +| **États-Unis** | **Total** | **États-Unis** | **-** | **5 %** | +| Japon | ShinEtsu | Japon | Chine (70%) | 4 % | +| **Japon** | **Total** | **Japon** | **-** | **4 %** | + +Unités : t/an + +Total : 380 + +_Note: Les capacités indiquées représentent la production de gadolinium sous forme d'oxyde et de métal._ + +##### ISG des pays impliqués + +| Pays | Part de marché | ISG | Criticité | +| :-- | :-- | :-- | :-- | +| Estonie | 8.0% | 33 | :green-badge[Vert] (Stable) | +| Chine | 61.0% | 54 | :orange-badge[Orange] (Intermédiaire) | +| EtatsUnis | 5.0% | 42 | :orange-badge[Orange] (Intermédiaire) | +| Malaisie | 12.0% | 44 | :orange-badge[Orange] (Intermédiaire) | +| Japon | 4.0% | 29 | :green-badge[Vert] (Stable) | +| France | 9.0% | 34 | :green-badge[Vert] (Stable) | + +**ISG combiné: 48 - :orange-badge[Orange] (Intermédiaire)** + +#### Indice de Herfindahl-Hirschmann - Traitement +| **IHH** | **Faible** | **Modéré** | **Élevé** | +| :-- | :-- | :-- | :-- | +| **Acteurs** | | **24** | | +| **Pays** | | | **41** | + +##### IHH par entreprise (acteurs) + +L’IHH pour les assembleurs est de **24**, ce qui indique une **concentration modérée**. **China Minmetals (40 %)**, **Shenghe Resources (21 %)**, **Lynas Advanced Materials (12 %)** regroupent une part importante du marché. Cette structure permet une **résilience relative**, mais dépend encore de quelques grands groupes. + +##### IHH par pays + +L’IHH par pays atteint **41**, révélant une **concentration géographique élevée**. La répartition est dominée par **Chine (61 %)**, **Malaisie (12 %)**, **France (9 %)**, représentant ensemble plus de 82 % des capacités. Cette configuration expose la chaîne à des **risques géopolitiques ou logistiques localisés**. + +##### En résumé + +- Le secteur présente une **structure d’acteurs moyennement concentrée** (IHH 24) +- La **concentration géographique est élevée** (IHH 41) + +##### Vulnérabilité combinée IHH-ISG pour le traitement + +* IHH: 41 - :red-badge[Rouge] (Élevée) +* ISG combiné: 48 - :orange-badge[Orange] (Intermédiaire) +* Poids combiné: 6 +* Niveau de vulnérabilité: **:red-badge[ÉLEVÉE à CRITIQUE]** + +## Méthodologie d'analyse des risques + +### Indices et seuils + +La méthode d'évaluation intègre 4 indices et leurs combinaisons pour identifier les chemins critiques. + +#### IHH (Herfindahl-Hirschmann) : concentration géographiques ou industrielle d'une opération + +Cet indice consiste à sommer les parts de marché au carré pour évaluer la concentration d'une industrie ou d'une zone géographique. + +Plus l'IHH est faible (typiquement en dessous de 15), plus le secteur considéré est diversifié en acteurs ou en pays producteurs ; plus il est élevé (au-delà de 25), plus on se rapproche d'une situation de quasi-monopole ou de forte dépendance, qu'elle soit géopolitique ou industrielle ; un IHH de 100 indique un monopole. + +$$ +\text{IHH} = \frac{\sum_{i=1}^{N} s_i^2}{100} +$$ + +où : +- $N$ : Nombre total d'acteurs ou d'entités considérées. +- $s_{i}$ : Part relative de l'acteur ${i}$, exprimée en proportion mondiale (entre 1 et 100). + + * Seuils : <15 = :green-badge[Vert] (Faible), 15-25 = :orange-badge[Orange] (Modérée), >25 = :red-badge[Rouge] (Élevée) + +#### ISG (Stabilité Géopolitique) : stabilité des pays + +Cet indicateur synthétise trois bases reconnues : + +* WGI (World Bank, “Political Stability & Absence of Violence”), +* FSI (Fragile States Index) +* et ND-GAIN (Climat & capacité d'adaptation) + +afin de mesurer, pour chaque pays, la probabilité qu'un choc politique, social ou climatique perturbe la chaîne d'approvisionnement. + +Plus l'indice est faible et plus la pays est stable. + +$$ +\text{ISG}=% +\operatorname{arrondi}\!\left( + 100 \times \Bigl( + 50\% \times \frac{2.5-\mathrm{WGI}}{5} + + 30\% \times \frac{\mathrm{FSI}}{120} + + 20\% \times \frac{100-\mathrm{NDGAIN}}{100} + \Bigr) +\right) +$$ + + * Seuils : <40 = :green-badge[Vert] (Stable), 40-60 = :orange-badge[Orange], >60 = :red-badge[Rouge] (Instable) + +#### ICS (Criticité de Substituabilité) : capacité à remplacer / substituer un élément + +La criticité a pour objectif de pondérer une part de marché qui se trouve sur un chemin critique de la chaine d'approvisionnement. +À titre d'exemple, un minerai utilisé dans le numérique et dont le traitement est réalisé à 95% par un acteur, mais dont la criticité est faible aura un moindre impact qu'un autre minerai à 80% de part de marché pour un acteur, mais avec une forte criticité. + +Sur la base des 3 axes suivants, le calcul de l'ICS se fait sous la forme suivante : + +$$ +\text{ICS}=40\% \times F_{tech} + 30\% \times D_{impl} + 30\% \times I_{eco} +$$ + +(où $F_{tech}$ = faisabilité technique, $D_{impl}$ = délai d’implémentation, $I_{eco}$ = impact économique). + + * Seuils : <0.3 = :green-badge[Vert] (Facile), 0.3-0.6 = :orange-badge[Orange] (Moyenne), >0.6 = :red-badge[Rouge] (Difficile) + +#### IVC (Vulnérabilité de Concurrence) : pression concurrentielle avec d'autres secteurs + +Dans un contexte de transition numérique et énergétique simultanée, de nombreuses ressources minérales sont soumises à **une demande croissante et multisectorielle**. Le numérique n’est plus le seul acteur consommateur : les véhicules électriques, les technologies vertes, la défense ou le médical mobilisent les mêmes minerais. + +Cette situation crée un **effet d’éviction potentiel**, où les ressources disponibles pourraient être absorbées par des usages concurrents. C’est ce phénomène que l’IVC cherche à évaluer.L’Indice de Vulnérabilité Concurrentielle (IVC) est un indicateur composite qui estime le risque que le secteur numérique **ne puisse plus accéder à une ressource**, non pas à cause de la concentration ou d’un manque d’alternatives, mais parce que **d’autres secteurs captent l’essentiel de la ressource**. + +Il permet de quantifier la pression que les usages **non numériques** font peser sur les **usages numériques** pour une ressource donnée. + +Sur la base des 3 axes suivants, le calcul de l'IVC se fait sous la forme suivante : + +$$ +\text{IVC} = \left( \frac{C_c}{C_n} \right) \times \left( \frac{P_c}{P_n} \right) \times T_m \times W_r +$$ + +où : +$C_{c}$ est *Croissance des concurrents*, +$C_{n}$ est *Croissance numérique*, +$P_{c}$ est *Part des concurrents*, +$P_{n}$ est *Part numérique*, +$T_{m}$ est *Tension du marché*, +$W_{r}$ est *Pondération des réserves*. + + * Seuils : <5 = :green-badge[Vert] (Faible), 5-15 = :orange-badge[Orange] (Modérée), >15 = :red-badge[Rouge] (Forte) + +### Combinaison des indices + +**IHH et ISG** + +Ces deux indices s'appliquent à toutes les opérations et se combinent dans l'évaluation du risque (niveau d'impact et probabilité de survenance) : + +* l'IHH donne le niveau d'impact => une forte concentration implique un fort impact si le risque est avéré +* l'ISG donne la probabilité de survenance => plus les pays sont instables (et donc plus l'ISG est élevé) et plus la survenance du risque est élevée + +Pour évaluer le risque pour une opération, les ISG des pays sont pondérés par les parts de marché respectives pour donner un ISG combiné dont le calcul est : +ISG_combiné = (Somme des ISG des pays multipliée par leur part de marché) / Sommes de leur part de marché + +On établit alors une matrice (:green-badge[Vert] = 1, :orange-badge[Orange] = 2, :red-badge[Rouge] = 3) et en faisant le produit des poids de l'ISG combiné et de l'IHH + +| ISG combiné / IHH | :green-badge[Vert] | :orange-badge[Orange] | :red-badge[Rouge] | +| :-- | :-- | :-- | :-- | +| :green-badge[Vert] | 1 | 2 | 3 | +| :orange-badge[Orange] | 2 | 4 | 6 | +| :red-badge[Rouge] | 3 | 6 | 9 | + +Les vulnérabilités se classent en trois niveaux pour chaque opération : + +* Vulnérabilité combinée élevée à critique : poids 6 et 9 +* Vulnérabilité combinée moyenne : poids 3 et 4 +* Vulnérabilité combinée faible : poids 1 et 2 + +**ICS et IVC** + +Ces deux indices se combinent dans l'évaluation du risque pour un minerai : + +* l'ICS donne le niveau d'impact => une faible substituabilité (et donc un ICS élevé) implique un fort impact si le risque est avéré ; l'ICS est associé à la relation entre un composant et un minerai +* l'IVC donne la probabilité de l'impact => une forte concurrence intersectorielle (IVC élevé) implique une plus forte probabilité de survenance + +Par simplification, on intègre un ICS moyen d'un minerai comme étant la moyenne des ICS pour chacun des composants dans lesquels il intervient. + +On établit alors une matrice (:green-badge[Vert] = 1, :orange-badge[Orange] = 2, :red-badge[Rouge] = 3) et en faisant le produit des poids de l'ICS moyen et de l'IVC. + +| ICS_moyen / IVC | :green-badge[Vert] | :orange-badge[Orange] | :red-badge[Rouge] | +| :-- | :-- | :-- | :-- | +| :green-badge[Vert] | 1 | 2 | 3 | +| :orange-badge[Orange] | 2 | 4 | 6 | +| :red-badge[Rouge] | 3 | 6 | 9 | + +Les vulnérabilités se classent en trois niveaux pour chaque minerai : + +* Vulnérabilité combinée élevée à critique : poids 6 et 9 +* Vulnérabilité combinée moyenne : poids 3 et 4 +* Vulnérabilité combinée faible : poids 1 et 2 diff --git a/app/visualisations/graphes.py b/app/visualisations/graphes.py index e5adddc..1a444d5 100644 --- a/app/visualisations/graphes.py +++ b/app/visualisations/graphes.py @@ -55,8 +55,8 @@ def afficher_graphique_altair(df): base = alt.Chart(df_cat).encode( x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries"))), y=alt.Y('ihh_acteurs:Q', title=str(_("pages.visualisations.axis_titles.ihh_actors"))), - size=alt.Size('criticite_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None), - color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])) + size=alt.Size('ics_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None), + color=alt.Color('ics_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])) ) points = base.mark_circle(opacity=0.6) @@ -162,7 +162,7 @@ def creer_graphes(donnees): st.error(f"{str(_('errors.graph_creation_error'))} {e}") -def lancer_visualisation_ihh_criticite(graph): +def lancer_visualisation_ihh_ics(graph): try: import networkx as nx from utils.graph_utils import recuperer_donnees diff --git a/app/visualisations/interface.py b/app/visualisations/interface.py index 49d9db8..5bad13c 100644 --- a/app/visualisations/interface.py +++ b/app/visualisations/interface.py @@ -1,25 +1,25 @@ import streamlit as st +from utils.widgets import html_expander from utils.translations import _ from .graphes import ( - lancer_visualisation_ihh_criticite, + lancer_visualisation_ihh_ics, lancer_visualisation_ihh_ivc ) def interface_visualisations(G_temp, G_temp_ivc): st.markdown(f"# {str(_('pages.visualisations.title'))}") - with st.expander(str(_("pages.visualisations.help")), expanded=False): - st.markdown("\n".join(_("pages.visualisations.help_content"))) + html_expander(f"{str(_('pages.visualisations.help'))}", content="\n".join(_("pages.visualisations.help_content")), open_by_default=False, details_class="details_introduction") st.markdown("---") st.markdown(f"""## {str(_("pages.visualisations.ihh_criticality"))} {str(_("pages.visualisations.ihh_criticality_desc"))} """) - if st.button(str(_("buttons.run")), key="btn_ihh_criticite"): + if st.button(str(_("buttons.run")), key="btn_ihh_ics", icon=":material/bubble_chart:"): try: - lancer_visualisation_ihh_criticite(G_temp) + lancer_visualisation_ihh_ics(G_temp) except Exception as e: st.error(f"{str(_('errors.ihh_criticality_error'))} {e}") @@ -28,7 +28,7 @@ def interface_visualisations(G_temp, G_temp_ivc): {str(_("pages.visualisations.ihh_ivc_desc"))} """) - if st.button(str(_("buttons.run")), key="btn_ihh_ivc"): + if st.button(str(_("buttons.run")), key="btn_ihh_ivc", icon=":material/bubble_chart:"): try: lancer_visualisation_ihh_ivc(G_temp_ivc) except Exception as e: diff --git a/assets/README.md b/assets/README.md index 0140a86..1c8bc9d 100644 --- a/assets/README.md +++ b/assets/README.md @@ -2,9 +2,12 @@ ## Styles -Le fichier **styles.css** a été construit pour agir sur le styme produit par Streamlit ou pour décorer des éléments construits par fabnum.py +Le fichier **base.css** a été construit pour agir sur le style produit par Streamlit ou pour décorer des éléments construits par fabnum.py, indépendamment du thème choisi +Les deux fichiers **theme-light.css** et **theme-dark.css** contiennent les variables utilisées par base.css pour afficher les couleurs. -Il sera important de regarder s'il est possible d'interagir avec le css de Streamlit sans passer par des déclarations !important +Streamlit utilise le theme par défaut du système du poste de travail de l'internaute. Afin de maîtriser complètement le thème avec base.css, il est important que la configuration côté serveur de Streamlit soit forcée au thème light dans le fichier .streamlit/config.toml : + [theme] + base = "light" ## Icone @@ -37,4 +40,4 @@ La dernière colonne donne le label de l'item associé à la fiche. Les labels associés aux fiches dans ce fichier doivent, bien évidemment, être **totalement égaux** aux labels de la gestion des tickets. -Ces labels avec en plus la branche permettent de faire le lien exact entre une fiche présentée dans l'application et le système de gestion des tickets. \ No newline at end of file +Ces labels avec en plus la branche permettent de faire le lien exact entre une fiche présentée dans l'application et le système de gestion des tickets. diff --git a/assets/config.yaml b/assets/config.yaml index de5e3bc..324d904 100644 --- a/assets/config.yaml +++ b/assets/config.yaml @@ -1,11 +1,11 @@ version: 1.1 -date: 2025-05-06 +date: 2025-05-27 seuils: IVC: # Indice de vulnérabilité concurrentielle - vert: { max: 5 } - orange: { min: 5, max: 15 } - rouge: { min: 15 } + vert: { max: 15 } + orange: { min: 15, max: 60 } + rouge: { min: 60 } IHH: # Index Herfindahl-Hirschman vert: { max: 15 } diff --git a/assets/locales/en.json b/assets/locales/en.json index 86a5128..b47792d 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -1,276 +1,331 @@ { - "app": { - "title": "Fabnum – Chain Analysis", - "description": "Ecosystem exploration and vulnerability identification.", - "dev_mode": "You are in the development environment." - }, - "header": { - "title": "FabNum - Digital Manufacturing Chain", - "subtitle": "Ecosystem exploration and vulnerability identification." - }, - "footer": { - "copyright": "Fabnum © 2025", - "contact": "Contact", - "license": "License", - "license_text": "CC BY-NC-ND", - "eco_note": "🌱 CO₂ calculations via", - "eco_provider": "The Green Web Foundation", - "powered_by": "🚀 Powered by", - "powered_by_name": "Streamlit" - }, - "sidebar": { - "menu": "Main Menu", - "navigation": "Main Navigation", - "theme": "Theme", - "theme_light": "Light", - "theme_dark": "Dark", - "theme_instructions_only": "Theme changes can only be made from the Instructions tab.", - "impact": "Environmental Impact", - "loading": "Loading..." - }, - "auth": { - "title": "Authentication", - "username": "Username_token", - "token": "Gitea Personal Access Token", - "login": "Login", - "logout": "Logout", - "logged_as": "Logged in as", - "error": "❌ Access denied.", - "gitea_error": "❌ Unable to verify user with Gitea.", - "success": "Successfully logged out." - }, - "navigation": { - "instructions": "Instructions", - "personnalisation": "Customization", - "analyse": "Analysis", - "visualisations": "Visualizations", - "fiches": "Cards" - }, - "pages": { - "instructions": { - "title": "Instructions" + "app": { + "title": "Fabnum – Chain Analysis", + "description": "Ecosystem exploration and vulnerability identification.", + "dev_mode": "You are in the development environment." }, - "personnalisation": { - "title": "Final Product Customization", - "help": "How to use this tab?", - "help_content": [ - "1. Click on \"Add a final product\" to create a new product", - "2. Give your product a name", - "3. Select an appropriate assembly operation (if relevant)", - "4. Choose the components that make up your product from the list provided", - "5. Save your configuration for future reuse", - "6. You will be able to modify or delete your custom products later" - ], - "add_new_product": "Add a new final product", - "new_product_name": "New product name (unique)", - "assembly_operation": "Assembly operation (optional)", - "none": "-- None --", - "components_to_link": "Components to link", - "create_product": "Create product", - "added": "added", - "modify_product": "Modify an added final product", - "products_to_modify": "Products to modify", - "delete": "Delete", - "linked_assembly_operation": "Linked assembly operation", - "components_linked_to": "Components linked to", - "update": "Update", - "updated": "updated", - "deleted": "deleted", - "save_restore_config": "Save or restore configuration", - "export_config": "Export configuration", - "download_json": "Download (JSON)", - "import_config": "Import a JSON configuration (max 100 KB)", - "file_too_large": "File too large (max 100 KB).", - "no_products_found": "No products found in the file.", - "select_products_to_restore": "Select products to restore", - "products_to_restore": "Products to restore", - "restore_selected": "Restore selected items", - "config_restored": "Partial configuration successfully restored.", - "import_error": "Import error:" + "header": { + "title": "FabNum - Digital Manufacturing Chain", + "subtitle": "Ecosystem exploration and vulnerability identification." }, - "analyse": { - "title": "Graph Analysis", - "help": "How to use this tab?", - "help_content": [ - "1. Select the starting level (final product, component, or mineral)", - "2. Choose the desired destination level", - "3. Refine your selection by specifying either one or more specific minerals to target or specific items at each level (optional)", - "4. Define the analysis criteria by selecting the relevant vulnerability indices", - "5. Choose the index combination mode (AND/OR) according to your analysis needs", - "6. Explore the generated graph using zoom and panning controls; you can switch to full screen mode for the graph" - ], - "selection_nodes": "Selection of start and end nodes", - "select_level": "-- Select a level --", - "start_level": "Start level", - "end_level": "End level", - "select_minerals": "Select one or more minerals", - "filter_by_minerals": "Filter by minerals (optional)", - "fine_selection": "Fine selection of items", - "filter_start_nodes": "Filter by start nodes (optional)", - "filter_end_nodes": "Filter by end nodes (optional)", - "vulnerability_filters": "Selection of filters to identify vulnerabilities", - "filter_ics": "Filter paths containing at least one critical mineral for a component (ICS > 66%)", - "filter_ivc": "Filter paths containing at least one critical mineral in relation to sectoral competition (IVC > 30)", - "filter_ihh": "Filter paths containing at least one critical operation in relation to geographical or industrial concentration (IHH countries or actors > 25)", - "apply_ihh_filter": "Apply IHH filter on:", - "countries": "Countries", - "actors": "Actors", - "filter_isg": "Filter paths containing an unstable country (ISG ≥ 60)", - "filter_logic": "Filter logic", - "or": "OR", - "and": "AND", - "run_analysis": "Run analysis", - "sankey": { - "no_paths": "No paths found for the specified criteria.", - "no_matching_paths": "No paths match the criteria.", - "filtered_hierarchy": "Hierarchy filtered by levels and nodes", - "download_dot": "Download filtered DOT file", - "relation": "Relation" - } + "footer": { + "copyright": "Fabnum © 2025", + "contact": "Contact", + "license": "License", + "license_text": "CC BY-NC-ND", + "eco_note": "🌱 CO₂ calculations via", + "eco_provider": "The Green Web Foundation", + "powered_by": "🚀 Powered by", + "powered_by_name": "Streamlit" }, - "visualisations": { - "title": "Visualizations", - "help": "How to use this tab?", - "help_content": [ - "1. Explore the graphs presenting the Herfindahl-Hirschmann Index (IHH)", - "2. Analyze its relationship with the average criticality of minerals or their Competitive Vulnerability Index (IVC)", - "3. Zoom in on the graphs to better discover the information", - "", - "It is important to remember that the IHH has two thresholds:", - "* below 15, concentration is considered to be low", - "* above 25, it is considered to be high", - "", - "Thus, the higher a point is positioned in the top right of the graphs, the higher the risks.", - "The graphs present 2 horizontal and vertical lines to mark these thresholds." - ], - "ihh_criticality": "Herfindahl-Hirschmann Index - IHH vs Criticality", - "ihh_criticality_desc": "The size of the points indicates the substitutability criticality of the mineral.", - "ihh_ivc": "Herfindahl-Hirschmann Index - IHH vs IVC", - "ihh_ivc_desc": "The size of the points indicates the competitive criticality of the mineral.", - "launch": "Launch", - "no_data": "No data to display.", - "categories": { - "assembly": "Assembly", - "manufacturing": "Manufacturing", - "processing": "Processing", - "extraction": "Extraction" - }, - "axis_titles": { - "ihh_countries": "IHH Countries (%)", - "ihh_actors": "IHH Actors (%)", - "ihh_extraction": "IHH Extraction (%)", - "ihh_reserves": "IHH Reserves (%)" - }, - "chart_titles": { - "concentration_criticality": "Concentration and Criticality – {0}", - "concentration_resources": "Concentration of Critical Resources vs IVC Vulnerability" - } + "sidebar": { + "menu": "Main Menu", + "navigation": "Main Navigation", + "theme": "Theme", + "theme_light": "Light", + "theme_dark": "Dark", + "theme_instructions_only": "Theme changes can only be made from the Instructions tab.", + "impact": "Environmental Impact", + "loading": "Loading..." }, - "fiches": { - "title": "Card Discovery", - "help": "How to use this tab?", - "help_content": [ - "1. Browse the list of available cards by category", - "2. Select a card to display its full content", - "3. Consult detailed data, graphs, and additional analyses", - "4. Use this information to deepen your understanding of the identified vulnerabilities", - "", - "The categories are as follows:", - "* Assembly: operation of assembling final products from components", - "* Related: various operations necessary to manufacture digital technology, but not directly entering its composition", - "* Criticalities: indices used to identify and evaluate vulnerabilities", - "* Manufacturing: operation of manufacturing components from minerals", - "* Mineral: description and operations of extraction and processing of minerals" - ], - "no_files": "No cards available at the moment.", - "choose_category": "Choose a card category", - "select_folder": "-- Select a folder --", - "choose_file": "Choose a card", - "select_file": "-- Select a card --", - "loading_error": "Error loading the card:", - "download_pdf": "Download this card as PDF", - "pdf_unavailable": "The PDF file for this card is not available.", - "ticket_management": "Ticket management for this card", - "tickets": { - "create_new": "Create a new ticket linked to this card", - "model_load_error": "Unable to load the ticket template.", - "contribution_type": "Contribution type", - "specify": "Specify", - "other": "Other", - "concerned_card": "Concerned card", - "subject": "Subject of the proposal", - "preview": "Preview ticket", - "cancel": "Cancel", - "preview_title": "Ticket preview", - "summary": "Summary", - "title": "Title", - "labels": "Labels", - "confirm": "Confirm ticket creation", - "created": "Ticket created and form cleared.", - "model_error": "Template loading error:", - "no_linked_tickets": "No tickets linked to this card.", - "associated_tickets": "Tickets associated with this card", - "moderation_notice": "ticket(s) awaiting moderation are not displayed.", - "status": { - "awaiting": "Awaiting processing", - "in_progress": "In progress", - "completed": "Completed", - "rejected": "Rejected", - "others": "Others" + "auth": { + "title": "Authentication", + "username": "Username_token", + "token": "Gitea Personal Access Token", + "login": "Login", + "logout": "Logout", + "logged_as": "Logged in as", + "error": "❌ Access denied.", + "gitea_error": "❌ Unable to verify user with Gitea.", + "success": "Successfully logged out." + }, + "navigation": { + "instructions": "Instructions", + "personnalisation": "Customization", + "analyse": "Analysis", + "ia_nalyse": "AI'nalysis", + "plan_d_action": "Actions plan", + "visualisations": "Visualizations", + "fiches": "Cards" + }, + "pages": { + "instructions": { + "title": "Instructions" }, - "no_title": "No title", - "unknown": "unknown", - "subject_label": "Subject", - "no_labels": "none", - "comments": "Comment(s):", - "no_comments": "No comments.", - "comment_error": "Error retrieving comments:", - "opened_by": "Opened by", - "on_date": "on", - "updated": "UPDATED" - } + "personnalisation": { + "title": "Final Product Customization", + "help": "How to use this tab?", + "help_content": [ + "1. Click on \"Add a final product\" to create a new product", + "2. Give your product a name", + "3. Select an appropriate assembly operation (if relevant)", + "4. Choose the components that make up your product from the list provided", + "5. Save your configuration for future reuse", + "6. You will be able to modify or delete your custom products later" + ], + "add_new_product": "Add a new final product", + "new_product_name": "New product name (unique)", + "assembly_operation": "Assembly operation (optional)", + "none": "-- None --", + "components_to_link": "Components to link", + "create_product": "Create product", + "added": "added", + "modify_product": "Modify an added final product", + "products_to_modify": "Products to modify", + "delete": "Delete", + "linked_assembly_operation": "Linked assembly operation", + "components_linked_to": "Components linked to", + "update": "Update", + "updated": "updated", + "deleted": "deleted", + "save_restore_config": "Save or restore configuration", + "export_config": "Export configuration", + "download_json": "Download (JSON)", + "import_config": "Import a JSON configuration (max 100 KB)", + "file_too_large": "File too large (max 100 KB).", + "no_products_found": "No products found in the file.", + "select_products_to_restore": "Select products to restore", + "products_to_restore": "Products to restore", + "restore_selected": "Restore selected items", + "config_restored": "Partial configuration successfully restored.", + "import_error": "Import error:" + }, + "analyse": { + "title": "Graph Analysis", + "help": "How to use this tab?", + "help_content": [ + "1. Select the starting level (final product, component, or mineral)", + "2. Choose the desired destination level", + "3. Refine your selection by specifying either one or more specific minerals to target or specific items at each level (optional)", + "4. Define the analysis criteria by selecting the relevant vulnerability indices", + "5. Choose the index combination mode (AND/OR) according to your analysis needs", + "6. Explore the generated graph using zoom and panning controls; you can switch to full screen mode for the graph" + ], + "selection_nodes": "Selection of start and end nodes", + "select_level": "-- Select a level --", + "start_level": "Start level", + "end_level": "End level", + "select_minerals": "Select one or more minerals", + "filter_by_minerals": "Filter by minerals (optional)", + "fine_selection": "Fine selection of items", + "filter_start_nodes": "Filter by start nodes (optional)", + "filter_end_nodes": "Filter by end nodes (optional)", + "vulnerability_filters": "Selection of filters to identify vulnerabilities", + "filter_ics": "Filter paths containing at least one critical mineral for a component (ICS > 66%)", + "filter_ivc": "Filter paths containing at least one critical mineral in relation to sectoral competition (IVC > 30)", + "filter_ihh": "Filter paths containing at least one critical operation in relation to geographical or industrial concentration (IHH countries or actors > 25)", + "apply_ihh_filter": "Apply IHH filter on:", + "countries": "Countries", + "actors": "Actors", + "filter_isg": "Filter paths containing an unstable country (ISG ≥ 60)", + "filter_logic": "Filter logic", + "or": "OR", + "and": "AND", + "run_analysis": "Run analysis", + "sankey": { + "no_paths": "No paths found for the specified criteria.", + "no_matching_paths": "No paths match the criteria.", + "filtered_hierarchy": "Hierarchy filtered by levels and nodes", + "download_dot": "Download filtered DOT file", + "relation": "Relation" + } + }, + "ia_nalyse": { + "title": "Graph Analysis by AI", + "help": "How to use this tab?", + "help_content": [ + "The graph covers all levels, from end products to geographic countries.\n", + "1. You can select minerals through which the paths go.", + "2. You can choose end products to perform an analysis tailored to your context.\n", + "These two selections are optional, but strongly recommended for a more relevant analysis.", + "The analysis is carried out using a private AI on a minimalist server. The result is therefore not immediate (approximately 30 minutes) and you will be notified of the progress." + ], + "select_minerals": "Select one or more minerals", + "filter_by_minerals": "Filter by minerals (optional, but highly recommended)", + "fine_selection": "End product selection", + "filter_start_nodes": "Filter by start nodes (optional, but recommended)", + "run_analysis": "Run analysis", + "confirm_download": "Confirm download", + "submit_request": "Submit your request", + "empty_graph": "The graph is empty. Please make another selection." + }, + "plan_d_action": { + "title": "Graph analysis for action", + "help": "How to use this tab?", + "help_content": [ + "The graph covers all levels, from end products to geographic countries.\n", + "1. You can select minerals through which the paths go.", + "2. You can choose end products to perform an analysis tailored to your context.\n", + "These two selections are optional, but strongly recommended for a more relevant analysis.", + "The recommendations for actions and indicator monitoring are generic. They must therefore be adapted to the context.", + "The proposed actions or indicators depend on the type of organization concerned and can be applied directly or required of digital providers." + ], + "select_minerals": "Select one or more minerals", + "filter_by_minerals": "Filter by minerals (optional, but highly recommended)", + "fine_selection": "End product selection", + "filter_start_nodes": "Filter by start nodes (optional, but recommended)", + "run_analysis": "Run analysis", + "confirm_download": "Confirm download", + "submit_request": "Submit your request", + "empty_graph": "The graph is empty. Please make another selection." + }, + "visualisations": { + "title": "Visualizations", + "help": "How to use this tab?", + "help_content": [ + "1. Explore the graphs presenting the Herfindahl-Hirschmann Index (IHH)", + "2. Analyze its relationship with the average criticality of minerals or their Competitive Vulnerability Index (IVC)", + "3. Zoom in on the graphs to better discover the information", + "", + "It is important to remember that the IHH has two thresholds:", + "* below 15, concentration is considered to be low", + "* above 25, it is considered to be high", + "", + "Thus, the higher a point is positioned in the top right of the graphs, the higher the risks.", + "The graphs present 2 horizontal and vertical lines to mark these thresholds." + ], + "ihh_criticality": "Herfindahl-Hirschmann Index - IHH vs Criticality", + "ihh_criticality_desc": "The size of the points indicates the substitutability criticality of the mineral.", + "ihh_ivc": "Herfindahl-Hirschmann Index - IHH vs IVC", + "ihh_ivc_desc": "The size of the points indicates the competitive criticality of the mineral.", + "launch": "Launch", + "no_data": "No data to display.", + "categories": { + "assembly": "Assembly", + "manufacturing": "Manufacturing", + "processing": "Processing", + "extraction": "Extraction" + }, + "axis_titles": { + "ihh_countries": "IHH Countries (%)", + "ihh_actors": "IHH Actors (%)", + "ihh_extraction": "IHH Extraction (%)", + "ihh_reserves": "IHH Reserves (%)" + }, + "chart_titles": { + "concentration_criticality": "Concentration and Criticality – {0}", + "concentration_resources": "Concentration of Critical Resources vs IVC Vulnerability" + } + }, + "fiches": { + "title": "Card Discovery", + "help": "How to use this tab?", + "help_content": [ + "1. Browse the list of available cards by category", + "2. Select a card to display its full content", + "3. Consult detailed data, graphs, and additional analyses", + "4. Use this information to deepen your understanding of the identified vulnerabilities", + "", + "The categories are as follows:", + "* Assembly: operation of assembling final products from components", + "* Related: various operations necessary to manufacture digital technology, but not directly entering its composition", + "* Criticalities: indices used to identify and evaluate vulnerabilities", + "* Manufacturing: operation of manufacturing components from minerals", + "* Mineral: description and operations of extraction and processing of minerals" + ], + "no_files": "No cards available at the moment.", + "choose_category": "Choose a card category", + "select_folder": "-- Select a folder --", + "choose_file": "Choose a card", + "select_file": "-- Select a card --", + "loading_error": "Error loading the card:", + "download_pdf": "Download this card as PDF", + "pdf_unavailable": "The PDF file for this card is not available.", + "ticket_management": "Ticket management for this card", + "tickets": { + "create_new": "Create a new ticket linked to this card", + "model_load_error": "Unable to load the ticket template.", + "contribution_type": "Contribution type", + "specify": "Specify", + "other": "Other", + "concerned_card": "Concerned card", + "subject": "Subject of the proposal", + "preview": "Preview ticket", + "cancel": "Cancel", + "preview_title": "Ticket preview", + "summary": "Summary", + "title": "Title", + "labels": "Labels", + "confirm": "Confirm ticket creation", + "created": "Ticket created and form cleared.", + "model_error": "Template loading error:", + "no_linked_tickets": "No tickets linked to this card.", + "associated_tickets": "Tickets associated with this card", + "moderation_notice": "ticket(s) awaiting moderation are not displayed.", + "status": { + "awaiting": "Awaiting processing", + "in_progress": "In progress", + "completed": "Completed", + "rejected": "Rejected", + "others": "Others" + }, + "no_title": "No title", + "unknown": "unknown", + "subject_label": "Subject", + "no_labels": "none", + "comments": "Comment(s):", + "no_comments": "No comments.", + "comment_error": "Error retrieving comments:", + "opened_by": "Opened by", + "on_date": "on", + "updated": "UPDATED", + "continue": "Continuer", + "created_success": "Ticket created and placed in moderation", + "created_error": "Ticket creation failed. Please try later", + "see_ticket": "See ticket" + } + } + }, + "node_levels": { + "0": "Final product", + "1": "Component", + "2": "Mineral", + "10": "Operation", + "11": "Operation country", + "12": "Operation actor", + "99": "Geographic country" + }, + "errors": { + "log_read_error": "Log reading error:", + "graph_preview_error": "Graph preview error:", + "graph_creation_error": "Error creating the graph:", + "ihh_criticality_error": "Error in IHH vs Criticality visualization:", + "ihh_ivc_error": "Error in IHH vs IVC visualization:", + "comment_fetch_error": "Error retrieving comments:", + "template_load_error": "Template loading error:", + "import_error": "Import error:" + }, + "buttons": { + "download": "Download", + "run": "Run", + "save": "Save", + "cancel": "Cancel", + "confirm": "Confirm", + "filter": "Filter", + "search": "Search", + "create": "Create", + "update": "Update", + "delete": "Delete", + "preview": "Preview", + "export": "Export", + "import": "Import", + "restore": "Restore", + "refresh": "Refresh", + "browse_files": "Browse files" + }, + "ui": { + "file_uploader": { + "drag_drop_here": "Drag and drop file here", + "size_limit": "100 KB limit per file • JSON" + } + }, + "batch": { + "in_queue": "In queue", + "in_progress": "Analysis in progress", + "failure": "Error", + "unknwon_error": "unknown error", + "no_task": "No task wainting or in progress", + "complete": "Analysis complete. Download the result in zip format, which contains the detailed report and analysis.", + "step": "Step" } - }, - "node_levels": { - "0": "Final product", - "1": "Component", - "2": "Mineral", - "10": "Operation", - "11": "Operation country", - "12": "Operation actor", - "99": "Geographic country" - }, - "errors": { - "log_read_error": "Log reading error:", - "graph_preview_error": "Graph preview error:", - "graph_creation_error": "Error creating the graph:", - "ihh_criticality_error": "Error in IHH vs Criticality visualization:", - "ihh_ivc_error": "Error in IHH vs IVC visualization:", - "comment_fetch_error": "Error retrieving comments:", - "template_load_error": "Template loading error:", - "import_error": "Import error:" - }, - "buttons": { - "download": "Download", - "run": "Run", - "save": "Save", - "cancel": "Cancel", - "confirm": "Confirm", - "filter": "Filter", - "search": "Search", - "create": "Create", - "update": "Update", - "delete": "Delete", - "preview": "Preview", - "export": "Export", - "import": "Import", - "restore": "Restore", - "browse_files": "Browse files" - }, - "ui": { - "file_uploader": { - "drag_drop_here": "Drag and drop file here", - "size_limit": "100 KB limit per file • JSON" - } - } } diff --git a/assets/locales/fr.json b/assets/locales/fr.json index 20da328..932f083 100644 --- a/assets/locales/fr.json +++ b/assets/locales/fr.json @@ -1,276 +1,331 @@ { - "app": { - "title": "Fabnum – Analyse de chaîne", - "description": "Parcours de l'écosystème et identification des vulnérabilités.", - "dev_mode": "Vous êtes dans l'environnement de développement." - }, - "header": { - "title": "FabNum - Chaîne de fabrication du numérique", - "subtitle": "Parcours de l'écosystème et identification des vulnérabilités." - }, - "footer": { - "copyright": "Fabnum © 2025", - "contact": "Contact", - "license": "Licence", - "license_text": "CC BY-NC-ND", - "eco_note": "🌱 Calculs CO₂ via", - "eco_provider": "The Green Web Foundation", - "powered_by": "🚀 Propulsé par", - "powered_by_name": "Streamlit" - }, - "sidebar": { - "menu": "Menu principal", - "navigation": "Navigation principale", - "theme": "Thème", - "theme_light": "Clair", - "theme_dark": "Sombre", - "theme_instructions_only": "Le changement de thème ne peut se faire que depuis l'onglet Instructions.", - "impact": "Impact environnemental", - "loading": "Chargement en cours…" - }, - "auth": { - "title": "Authentification", - "username": "Identifiant_token", - "token": "Token d'accès personnel Gitea", - "login": "Se connecter", - "logout": "Se déconnecter", - "logged_as": "Connecté en tant que", - "error": "❌ Accès refusé.", - "gitea_error": "❌ Impossible de vérifier l'utilisateur auprès de Gitea.", - "success": "Déconnecté avec succès." - }, - "navigation": { - "instructions": "Instructions", - "personnalisation": "Personnalisation", - "analyse": "Analyse", - "visualisations": "Visualisations", - "fiches": "Fiches" - }, - "pages": { - "instructions": { - "title": "Instructions" + "app": { + "title": "Fabnum – Analyse de chaîne", + "description": "Parcours de l'écosystème et identification des vulnérabilités.", + "dev_mode": "Vous êtes dans l'environnement de développement." }, - "personnalisation": { - "title": "Personnalisation des produits finaux", - "help": "Comment utiliser cet onglet ?", - "help_content": [ - "1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit", - "2. Donnez un nom à votre produit", - "3. Sélectionnez une opération d'assemblage appropriée (si pertinent)", - "4. Choisissez les composants qui constituent votre produit dans la liste proposée", - "5. Sauvegardez votre configuration pour une réutilisation future", - "6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés" - ], - "add_new_product": "Ajouter un nouveau produit final", - "new_product_name": "Nom du nouveau produit (unique)", - "assembly_operation": "Opération d'assemblage (optionnelle)", - "none": "-- Aucune --", - "components_to_link": "Composants à lier", - "create_product": "Créer le produit", - "added": "ajouté", - "modify_product": "Modifier un produit final ajouté", - "products_to_modify": "Produits à modifier", - "delete": "Supprimer", - "linked_assembly_operation": "Opération d'assemblage liée", - "components_linked_to": "Composants liés à", - "update": "Mettre à jour", - "updated": "mis à jour", - "deleted": "supprimé", - "save_restore_config": "Sauvegarder ou restaurer la configuration", - "export_config": "Exporter configuration", - "download_json": "Télécharger (JSON)", - "import_config": "Importer une configuration JSON (max 100 Ko)", - "file_too_large": "Fichier trop volumineux (max 100 Ko).", - "no_products_found": "Aucun produit trouvé dans le fichier.", - "select_products_to_restore": "Sélection des produits à restaurer", - "products_to_restore": "Produits à restaurer", - "restore_selected": "Restaurer les éléments sélectionnés", - "config_restored": "Configuration partielle restaurée avec succès.", - "import_error": "Erreur d'import :" + "header": { + "title": "FabNum - Chaîne de fabrication du numérique", + "subtitle": "Parcours de l'écosystème et identification des vulnérabilités." }, - "analyse": { - "title": "Analyse du graphe", - "help": "Comment utiliser cet onglet ?", - "help_content": [ - "1. Sélectionnez le niveau de départ (produit final, composant ou minerai)", - "2. Choisissez le niveau d'arrivée souhaité", - "3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel)", - "4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents", - "5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse", - "6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe" - ], - "selection_nodes": "Sélection des nœuds de départ et d'arrivée", - "select_level": "-- Sélectionner un niveau --", - "start_level": "Niveau de départ", - "end_level": "Niveau d'arrivée", - "select_minerals": "Sélectionner un ou plusieurs minerais", - "filter_by_minerals": "Filtrer par minerais (optionnel)", - "fine_selection": "Sélection fine des items", - "filter_start_nodes": "Filtrer par noeuds de départ (optionnel)", - "filter_end_nodes": "Filtrer par noeuds d'arrivée (optionnel)", - "vulnerability_filters": "Sélection des filtres pour identifier les vulnérabilités", - "filter_ics": "Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", - "filter_ivc": "Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", - "filter_ihh": "Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", - "apply_ihh_filter": "Appliquer le filtre IHH sur :", - "countries": "Pays", - "actors": "Acteurs", - "filter_isg": "Filtrer les chemins contenant un pays instable (ISG ≥ 60)", - "filter_logic": "Logique de filtrage", - "or": "OU", - "and": "ET", - "run_analysis": "Lancer l'analyse", - "sankey": { - "no_paths": "Aucun chemin trouvé pour les critères spécifiés.", - "no_matching_paths": "Aucun chemin ne correspond aux critères.", - "filtered_hierarchy": "Hiérarchie filtrée par niveaux et noeuds", - "download_dot": "Télécharger le fichier DOT filtré", - "relation": "Relation" - } + "footer": { + "copyright": "Fabnum © 2025", + "contact": "Contact", + "license": "Licence", + "license_text": "CC BY-NC-ND", + "eco_note": "🌱 Calculs CO₂ via", + "eco_provider": "The Green Web Foundation", + "powered_by": "🚀 Propulsé par", + "powered_by_name": "Streamlit" }, - "visualisations": { - "title": "Visualisations", - "help": "Comment utiliser cet onglet ?", - "help_content": [ - "1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH)", - "2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC)", - "3. Zoomer dans les graphes pour mieux découvrir les informations", - "", - "Il est important de se rappeler que l'IHH a deux seuils :", - "* en-dessous de 15, la concentration est considérée comme étant faible", - "* au-dessus de 25, elle est considérée comme étant forte", - "", - "Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés.", - "Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils." - ], - "ihh_criticality": "Indice de Herfindahl-Hirschmann - IHH vs Criticité", - "ihh_criticality_desc": "La taille des points donne l'indication de la criticité de substituabilité du minerai.", - "ihh_ivc": "Indice de Herfindahl-Hirschmann - IHH vs IVC", - "ihh_ivc_desc": "La taille des points donne l'indication de la criticité concurrentielle du minerai.", - "launch": "Lancer", - "no_data": "Aucune donnée à visualiser.", - "categories": { - "assembly": "Assemblage", - "manufacturing": "Fabrication", - "processing": "Traitement", - "extraction": "Extraction" - }, - "axis_titles": { - "ihh_countries": "IHH Pays (%)", - "ihh_actors": "IHH Acteurs (%)", - "ihh_extraction": "IHH Extraction (%)", - "ihh_reserves": "IHH Réserves (%)" - }, - "chart_titles": { - "concentration_criticality": "Concentration et criticité – {0}", - "concentration_resources": "Concentration des ressources critiques vs vulnérabilité IVC" - } + "sidebar": { + "menu": "Menu principal", + "navigation": "Navigation principale", + "theme": "Thème", + "theme_light": "Clair", + "theme_dark": "Sombre", + "theme_instructions_only": "Le changement de thème ne peut se faire que depuis l'onglet Instructions.", + "impact": "Impact environnemental", + "loading": "Chargement en cours…" }, - "fiches": { - "title": "Découverte des fiches", - "help": "Comment utiliser cet onglet ?", - "help_content": [ - "1. Parcourez la liste des fiches disponibles par catégorie", - "2. Sélectionnez une fiche pour afficher son contenu complet", - "3. Consultez les données détaillées, graphiques et analyses supplémentaires", - "4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées", - "", - "Les catégories sont les suivantes :", - "* Assemblage : opération d'assemblage des produits finaux à partir des composants", - "* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition", - "* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités", - "* Fabrication : opération de fabrication des composants à partir de minerais", - "* Minerai : description et opérations d'extraction et de traitement des minerais" - ], - "no_files": "Aucune fiche disponible pour le moment.", - "choose_category": "Choisissez une catégorie de fiches", - "select_folder": "-- Sélectionner un dossier --", - "choose_file": "Choisissez une fiche", - "select_file": "-- Sélectionner une fiche --", - "loading_error": "Erreur lors du chargement de la fiche :", - "download_pdf": "Télécharger cette fiche en PDF", - "pdf_unavailable": "Le fichier PDF de cette fiche n'est pas disponible.", - "ticket_management": "Gestion des tickets pour cette fiche", - "tickets": { - "create_new": "Créer un nouveau ticket lié à cette fiche", - "model_load_error": "Impossible de charger le modèle de ticket.", - "contribution_type": "Type de contribution", - "specify": "Précisez", - "other": "Autre", - "concerned_card": "Fiche concernée", - "subject": "Sujet de la proposition", - "preview": "Prévisualiser le ticket", - "cancel": "Annuler", - "preview_title": "Prévisualisation du ticket", - "summary": "Résumé", - "title": "Titre", - "labels": "Labels", - "confirm": "Confirmer la création du ticket", - "created": "Ticket créé et formulaire vidé.", - "model_error": "Erreur chargement modèle :", - "no_linked_tickets": "Aucun ticket lié à cette fiche.", - "associated_tickets": "Tickets associés à cette fiche", - "moderation_notice": "ticket(s) en attente de modération ne sont pas affichés.", - "status": { - "awaiting": "En attente de traitement", - "in_progress": "En cours", - "completed": "Terminés", - "rejected": "Non retenus", - "others": "Autres" + "auth": { + "title": "Authentification", + "username": "Identifiant_token", + "token": "Token d'accès personnel Gitea", + "login": "Se connecter", + "logout": "Se déconnecter", + "logged_as": "Connecté en tant que", + "error": "❌ Accès refusé.", + "gitea_error": "❌ Impossible de vérifier l'utilisateur auprès de Gitea.", + "success": "Déconnecté avec succès." + }, + "navigation": { + "instructions": "Instructions", + "personnalisation": "Personnalisation", + "analyse": "Analyse", + "ia_nalyse": "IA'nalyse", + "plan_d_action": "Plan d'action", + "visualisations": "Visualisations", + "fiches": "Fiches" + }, + "pages": { + "instructions": { + "title": "Instructions" }, - "no_title": "Sans titre", - "unknown": "inconnu", - "subject_label": "Sujet", - "no_labels": "aucun", - "comments": "Commentaire(s) :", - "no_comments": "Aucun commentaire.", - "comment_error": "Erreur lors de la récupération des commentaires :", - "opened_by": "Ouvert par", - "on_date": "le", - "updated": "MAJ" - } + "personnalisation": { + "title": "Personnalisation des produits finaux", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit", + "2. Donnez un nom à votre produit", + "3. Sélectionnez une opération d'assemblage appropriée (si pertinent)", + "4. Choisissez les composants qui constituent votre produit dans la liste proposée", + "5. Sauvegardez votre configuration pour une réutilisation future", + "6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés" + ], + "add_new_product": "Ajouter un nouveau produit final", + "new_product_name": "Nom du nouveau produit (unique)", + "assembly_operation": "Opération d'assemblage (optionnelle)", + "none": "-- Aucune --", + "components_to_link": "Composants à lier", + "create_product": "Créer le produit", + "added": "ajouté", + "modify_product": "Modifier un produit final ajouté", + "products_to_modify": "Produits à modifier", + "delete": "Supprimer", + "linked_assembly_operation": "Opération d'assemblage liée", + "components_linked_to": "Composants liés à", + "update": "Mettre à jour", + "updated": "mis à jour", + "deleted": "supprimé", + "save_restore_config": "Sauvegarder ou restaurer la configuration", + "export_config": "Exporter configuration", + "download_json": "Télécharger (JSON)", + "import_config": "Importer une configuration JSON (max 100 Ko)", + "file_too_large": "Fichier trop volumineux (max 100 Ko).", + "no_products_found": "Aucun produit trouvé dans le fichier.", + "select_products_to_restore": "Sélection des produits à restaurer", + "products_to_restore": "Produits à restaurer", + "restore_selected": "Restaurer les éléments sélectionnés", + "config_restored": "Configuration partielle restaurée avec succès.", + "import_error": "Erreur d'import :" + }, + "analyse": { + "title": "Analyse du graphe", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Sélectionnez le niveau de départ (produit final, composant ou minerai)", + "2. Choisissez le niveau d'arrivée souhaité", + "3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel)", + "4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents", + "5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse", + "6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe" + ], + "selection_nodes": "Sélection des nœuds de départ et d'arrivée", + "select_level": "-- Sélectionner un niveau --", + "start_level": "Niveau de départ", + "end_level": "Niveau d'arrivée", + "select_minerals": "Sélectionner un ou plusieurs minerais", + "filter_by_minerals": "Filtrer par minerais (optionnel)", + "fine_selection": "Sélection fine des items", + "filter_start_nodes": "Filtrer par noeuds de départ (optionnel)", + "filter_end_nodes": "Filtrer par noeuds d'arrivée (optionnel)", + "vulnerability_filters": "Sélection des filtres pour identifier les vulnérabilités", + "filter_ics": "Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", + "filter_ivc": "Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", + "filter_ihh": "Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", + "apply_ihh_filter": "Appliquer le filtre IHH sur :", + "countries": "Pays", + "actors": "Acteurs", + "filter_isg": "Filtrer les chemins contenant un pays instable (ISG ≥ 60)", + "filter_logic": "Logique de filtrage", + "or": "OU", + "and": "ET", + "run_analysis": "Lancer l'analyse", + "sankey": { + "no_paths": "Aucun chemin trouvé pour les critères spécifiés.", + "no_matching_paths": "Aucun chemin ne correspond aux critères.", + "filtered_hierarchy": "Hiérarchie filtrée par niveaux et noeuds", + "download_dot": "Télécharger le fichier DOT filtré", + "relation": "Relation" + } + }, + "ia_nalyse": { + "title": "Analyse du graphe par IA", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "Le graphe intègre l'ensemble des niveaux, des produits finaux aux pays géographiques.\n", + "1. Vous pouvez sélectionner des minerais par lesquels passent les chemins.", + "2. Vous pouvez choisir des produits finaux pour faire une analyse adaptée à votre contexte.\n", + " Ces deux sélections sont optionnelles, mais fortement recommandées pour avoir une meilleure pertinence de l'analyse.", + "L'analyse se réalise à l'aide d'une IA privée, sur un serveur minimaliste. Le résultat n'est donc pas immédiat (ordre de grandeur : 30 minutes) et vous serez informé de l'avancement." + ], + "select_minerals": "Sélectionner un ou plusieurs minerais", + "filter_by_minerals": "Filtrer par minerais (optionnel, mais recommandé)", + "fine_selection": "Sélection des produits finaux", + "filter_start_nodes": "Filtrer par noeuds de départ (optionnel, mais recommandé)", + "run_analysis": "Lancer l'analyse", + "confirm_download": "Confirmer le téléchargement", + "submit_request": "Soumettre votre demande", + "empty_graph": "Le graphe est vide. Veuillez faire une autre sélection." + }, + "plan_d_action": { + "title": "Analyse du graphe pour action", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "Le graphe intègre l'ensemble des niveaux, des produits finaux aux pays géographiques.\n", + "1. Vous pouvez sélectionner des minerais par lesquels passent les chemins.", + "2. Vous pouvez choisir des produits finaux pour faire une analyse adaptée à votre contexte.\n", + " Ces deux sélections sont optionnelles, mais fortement recommandées pour avoir une meilleure pertinence de l'analyse.", + "Les préconisations d'actions et de suivi d'indicateurs sont génériques. Elles doivent donc être adaptées au contexte.", + "Les actions ou les indicateurs proposés dépendent du type d'organisation concernée et peuvent être appliquées directement ou exigées des fournisseurs de numérique." + ], + "select_minerals": "Sélectionner un ou plusieurs minerais", + "filter_by_minerals": "Filtrer par minerais (optionnel, mais recommandé)", + "fine_selection": "Sélection des produits finaux", + "filter_start_nodes": "Filtrer par noeuds de départ (optionnel, mais recommandé)", + "run_analysis": "Lancer l'analyse", + "confirm_download": "Confirmer le téléchargement", + "submit_request": "Soumettre votre demande", + "empty_graph": "Le graphe est vide. Veuillez faire une autre sélection." + }, + "visualisations": { + "title": "Visualisations", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH)", + "2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC)", + "3. Zoomer dans les graphes pour mieux découvrir les informations", + "", + "Il est important de se rappeler que l'IHH a deux seuils :", + "* en-dessous de 15, la concentration est considérée comme étant faible", + "* au-dessus de 25, elle est considérée comme étant forte", + "", + "Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés.", + "Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils." + ], + "ihh_criticality": "Indice de Herfindahl-Hirschmann - IHH vs Criticité", + "ihh_criticality_desc": "La taille des points donne l'indication de la criticité de substituabilité du minerai.", + "ihh_ivc": "Indice de Herfindahl-Hirschmann - IHH vs IVC", + "ihh_ivc_desc": "La taille des points donne l'indication de la criticité concurrentielle du minerai.", + "launch": "Lancer", + "no_data": "Aucune donnée à visualiser.", + "categories": { + "assembly": "Assemblage", + "manufacturing": "Fabrication", + "processing": "Traitement", + "extraction": "Extraction" + }, + "axis_titles": { + "ihh_countries": "IHH Pays (%)", + "ihh_actors": "IHH Acteurs (%)", + "ihh_extraction": "IHH Extraction (%)", + "ihh_reserves": "IHH Réserves (%)" + }, + "chart_titles": { + "concentration_criticality": "Concentration et criticité – {0}", + "concentration_resources": "Concentration des ressources critiques vs vulnérabilité IVC" + } + }, + "fiches": { + "title": "Découverte des fiches", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Parcourez la liste des fiches disponibles par catégorie", + "2. Sélectionnez une fiche pour afficher son contenu complet", + "3. Consultez les données détaillées, graphiques et analyses supplémentaires", + "4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées", + "", + "Les catégories sont les suivantes :", + "* Assemblage : opération d'assemblage des produits finaux à partir des composants", + "* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition", + "* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités", + "* Fabrication : opération de fabrication des composants à partir de minerais", + "* Minerai : description et opérations d'extraction et de traitement des minerais" + ], + "no_files": "Aucune fiche disponible pour le moment.", + "choose_category": "Choisissez une catégorie de fiches", + "select_folder": "-- Sélectionner un dossier --", + "choose_file": "Choisissez une fiche", + "select_file": "-- Sélectionner une fiche --", + "loading_error": "Erreur lors du chargement de la fiche :", + "download_pdf": "Télécharger cette fiche en PDF", + "pdf_unavailable": "Le fichier PDF de cette fiche n'est pas disponible.", + "ticket_management": "Gestion des tickets pour cette fiche", + "tickets": { + "create_new": "Créer un nouveau ticket lié à cette fiche", + "model_load_error": "Impossible de charger le modèle de ticket.", + "contribution_type": "Type de contribution", + "specify": "Précisez", + "other": "Autre", + "concerned_card": "Fiche concernée", + "subject": "Sujet de la proposition", + "preview": "Prévisualiser le ticket", + "cancel": "Annuler", + "preview_title": "Prévisualisation du ticket", + "summary": "Résumé", + "title": "Titre", + "labels": "Labels", + "confirm": "Confirmer la création du ticket", + "created": "Ticket créé et formulaire vidé.", + "model_error": "Erreur chargement modèle :", + "no_linked_tickets": "Aucun ticket lié à cette fiche.", + "associated_tickets": "Tickets associés à cette fiche", + "moderation_notice": "ticket(s) en attente de modération ne sont pas affichés.", + "status": { + "awaiting": "En attente de traitement", + "in_progress": "En cours", + "completed": "Terminés", + "rejected": "Non retenus", + "others": "Autres" + }, + "no_title": "Sans titre", + "unknown": "inconnu", + "subject_label": "Sujet", + "no_labels": "aucun", + "comments": "Commentaire(s) :", + "no_comments": "Aucun commentaire.", + "comment_error": "Erreur lors de la récupération des commentaires :", + "opened_by": "Ouvert par", + "on_date": "le", + "updated": "MAJ", + "continue": "Continuer", + "created_success": "Ticket créé et placé en modération", + "created_error": "Échec de la création du ticket. Veuillez réessayer plus tard", + "see_ticket": "Voir le ticket" + } + } + }, + "node_levels": { + "0": "Produit final", + "1": "Composant", + "2": "Minerai", + "10": "Opération", + "11": "Pays d'opération", + "12": "Acteur d'opération", + "99": "Pays géographique" + }, + "errors": { + "log_read_error": "Erreur lecture log:", + "graph_preview_error": "Erreur de prévisualisation du graphe :", + "graph_creation_error": "Erreur lors de la création du graphique :", + "ihh_criticality_error": "Erreur dans la visualisation IHH vs Criticité :", + "ihh_ivc_error": "Erreur dans la visualisation IHH vs IVC :", + "comment_fetch_error": "Erreur lors de la récupération des commentaires :", + "template_load_error": "Erreur chargement modèle :", + "import_error": "Erreur d'import :" + }, + "buttons": { + "download": "Télécharger", + "run": "Lancer", + "save": "Enregistrer", + "cancel": "Annuler", + "confirm": "Confirmer", + "filter": "Filtrer", + "search": "Rechercher", + "create": "Créer", + "update": "Mettre à jour", + "delete": "Supprimer", + "preview": "Prévisualiser", + "export": "Exporter", + "import": "Importer", + "restore": "Restaurer", + "refresh": "Rafraîchir", + "browse_files": "Parcourir les fichiers" + }, + "ui": { + "file_uploader": { + "drag_drop_here": "Glissez-déposez votre fichier ici", + "size_limit": "Limite 100 Ko par fichier • JSON" + } + }, + "batch": { + "in_queue": "En attente", + "in_progress": "Analyse en cours", + "failure": "Échec", + "unknwon_error": "erreur inconnue", + "no_task": "Aucune tâche en attente ou en cours", + "complete": "Analyse terminée. Télécharger le résultat au format zip, qui contient le rapport détaillé et l'analyse.", + "step": "Étape" } - }, - "node_levels": { - "0": "Produit final", - "1": "Composant", - "2": "Minerai", - "10": "Opération", - "11": "Pays d'opération", - "12": "Acteur d'opération", - "99": "Pays géographique" - }, - "errors": { - "log_read_error": "Erreur lecture log:", - "graph_preview_error": "Erreur de prévisualisation du graphe :", - "graph_creation_error": "Erreur lors de la création du graphique :", - "ihh_criticality_error": "Erreur dans la visualisation IHH vs Criticité :", - "ihh_ivc_error": "Erreur dans la visualisation IHH vs IVC :", - "comment_fetch_error": "Erreur lors de la récupération des commentaires :", - "template_load_error": "Erreur chargement modèle :", - "import_error": "Erreur d'import :" - }, - "buttons": { - "download": "Télécharger", - "run": "Lancer", - "save": "Enregistrer", - "cancel": "Annuler", - "confirm": "Confirmer", - "filter": "Filtrer", - "search": "Rechercher", - "create": "Créer", - "update": "Mettre à jour", - "delete": "Supprimer", - "preview": "Prévisualiser", - "export": "Exporter", - "import": "Importer", - "restore": "Restaurer", - "browse_files": "Parcourir les fichiers" - }, - "ui": { - "file_uploader": { - "drag_drop_here": "Glissez-déposez votre fichier ici", - "size_limit": "Limite 100 Ko par fichier • JSON" - } - } } diff --git a/assets/styles/base.css b/assets/styles/base.css index dcbda92..0663f09 100644 --- a/assets/styles/base.css +++ b/assets/styles/base.css @@ -4,33 +4,33 @@ 1. Reset et base ========================================== */ .stAppHeader { - visibility: hidden; + visibility: hidden; } body, html { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, - sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, + sans-serif; } body, .stApp, .block-container { - background-color: var(--bg-color) !important; - color: var(--text-color) !important; + background-color: var(--bg-color) !important; + color: var(--text-color) !important; } /* ========================================== 2. Layout et containers ========================================== */ .block-container { - max-width: 1024px !important; - padding: 0 1rem 10rem; + max-width: 1024px !important; + padding: 0 1rem 10rem; } .stVerticalBlock { - gap: 0.5rem !important; + gap: 0.5rem !important; } /* ========================================== @@ -42,97 +42,97 @@ body, .stDownloadButton > button, .stFormSubmitButton > button, .stSlider > div > div { - background-color: darkgreen !important; - color: white !important; - border: 1px solid grey; + background-color: darkgreen !important; + color: white !important; + border: 1px solid grey; } .st-key-FormSubmitter-auth_form-Se-connecter { - margin-left: auto; - margin-right: auto; + margin-left: auto; + margin-right: auto; } section:not([data-testid="stSidebar"]) - button[data-testid="stBaseButton-primary"], + button[data-testid="stBaseButton-primary"], section:not([data-testid="stSidebar"]) - button[data-testid="stBaseButton-secondary"] { - color: white !important; - background: darkgreen !important; + button[data-testid="stBaseButton-secondary"] { + color: white !important; + background: darkgreen !important; } section:not([data-testid="stSidebar"]) - button[data-testid="stBaseButton-primary"] - p, + button[data-testid="stBaseButton-primary"] + p, section:not([data-testid="stSidebar"]) - button[data-testid="stBaseButton-secondary"] - p { - color: white !important; + button[data-testid="stBaseButton-secondary"] + p { + color: white !important; } .bouton-fictif { - display: inline-flex; - -moz-box-align: center; - align-items: center; - -moz-box-pack: center; - justify-content: center; - padding: 0.25rem 0.75rem; - border-radius: 0.5rem; - min-height: 2.5rem; - margin-bottom: 20px; - line-height: 1; - text-transform: none; - font-size: x-large; - font-family: inherit; - user-select: none; - border: 1px solid rgba(49, 51, 63, 0.2); - background-color: darkgrey !important; - color: darkgreen !important; - font-weight: bold !important; - width: 100%; - letter-spacing: 0.2em; + display: inline-flex; + -moz-box-align: center; + align-items: center; + -moz-box-pack: center; + justify-content: center; + padding: 0.25rem 0.75rem; + border-radius: 0.5rem; + min-height: 2.5rem; + margin-bottom: 20px; + line-height: 1; + text-transform: none; + font-size: x-large; + font-family: inherit; + user-select: none; + border: 1px solid rgba(49, 51, 63, 0.2); + background-color: darkgrey !important; + color: darkgreen !important; + font-weight: bold !important; + width: 100%; + letter-spacing: 0.2em; } button[data-testid="stBaseButton-headerNoPadding"] svg { - fill: var(--text-color) !important; + fill: var(--text-color) !important; } /* --- 3.2 Onglets et radiogroup --- */ div[role="radiogroup"] > label { - padding: 0.5em 1em; - border-radius: 0.4em; - margin-right: 0.5em; - cursor: pointer; - border: 1px solid #fff; + padding: 0.5em 1em; + border-radius: 0.4em; + margin-right: 0.5em; + cursor: pointer; + border: 1px solid #fff; } div[role="radiogroup"] > label[data-selected="true"] { - font-weight: bold; - border: 2px solid #145a1a; + font-weight: bold; + border: 2px solid #145a1a; } section:not([data-testid="stSidebar"]) div[role="radiogroup"] > label p { - background-color: var(--radio-bg) !important; - color: var(--radio-text) !important; + background-color: var(--radio-bg) !important; + color: var(--radio-text) !important; } section:not([data-testid="stSidebar"]) - div[role="radiogroup"] - > label[data-selected="true"] { - background-color: var(--radio-selected-bg) !important; - color: var(--radio-selected-text) !important; + div[role="radiogroup"] + > label[data-selected="true"] { + background-color: var(--radio-selected-bg) !important; + color: var(--radio-selected-text) !important; } /* --- 3.3 Champs de formulaire --- */ div[data-baseweb="select"], section:not([data-testid="stSidebar"]) div[data-baseweb="base-input"], section[data-testid="stFileUploaderDropzone"] { - border: 1px solid var(--input-border) !important; - border-radius: 5px; - padding: 4px; + border: 1px solid var(--input-border) !important; + border-radius: 5px; + padding: 4px; } small { - display: none; + display: none; } section:not([data-testid="stSidebar"]) div[data-testid="stSelectbox"] p, @@ -142,7 +142,7 @@ section:not([data-testid="stSidebar"]) div[data-testid="stCheckbox"] p, section:not([data-testid="stSidebar"]) div[data-testid="stTextInput"] p, section:not([data-testid="stSidebar"]) div[data-testid="stTextArea"] p, section:not([data-testid="stSidebar"]) div[data-testid="stAlertContentInfo"] p { - color: var(--text-color) !important; + color: var(--text-color) !important; } /* ========================================== @@ -151,98 +151,98 @@ section:not([data-testid="stSidebar"]) div[data-testid="stAlertContentInfo"] p { /* --- 4.1 Header --- */ .wide-header { - width: 100vw; - margin-left: calc(-50vw + 50%); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border-bottom: 1px solid #ddd; - text-align: center; - padding-top: 1rem; - margin-top: -1.25em; - background-color: var(--header-bg); + width: 100vw; + margin-left: calc(-50vw + 50%); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-bottom: 1px solid #ddd; + text-align: center; + padding-top: 1rem; + margin-top: -1.25em; + background-color: var(--header-bg); } .titre-header { - font-size: 2rem !important; - font-weight: bolder !important; - color: var(--header-title); + font-size: 2rem !important; + font-weight: bolder !important; + color: var(--header-title); } /* --- 4.2 Footer --- */ .wide-footer { - width: 100vw; - margin-left: calc(-50vw + 50%); - margin-top: 3rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border-top: 1px solid #ddd; - text-align: center; - padding-top: 1rem; - background-color: var(--footer-bg); + width: 100vw; + margin-left: calc(-50vw + 50%); + margin-top: 3rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-top: 1px solid #ddd; + text-align: center; + padding-top: 1rem; + background-color: var(--footer-bg); } .info-footer { - font-size: 1rem !important; - font-weight: 800; - color: var(--footer-text); + font-size: 1rem !important; + font-weight: 800; + color: var(--footer-text); } /* ========================================== 5. Sidebar ========================================== */ section[data-testid="stSidebar"] { - background-color: #ccc !important; - color: #111 !important; + background-color: #ccc !important; + color: #111 !important; } section[data-testid="stSidebar"] .stButton > button { - background-color: darkgreen !important; - color: white !important; - font-weight: bold !important; - border: 1px solid #ccc !important; - width: 100%; + background-color: darkgreen !important; + color: white !important; + font-weight: bold !important; + border: 1px solid #ccc !important; + width: 100%; } section[data-testid="stSidebar"] .decorative-heading { - font-size: 1.25rem; - font-weight: bold; - margin-bottom: 0.5rem; - text-align: center; - color: #145a1a; + font-size: 1.25rem; + font-weight: bold; + margin-bottom: 0.5rem; + text-align: center; + color: #145a1a; } section[data-testid="stSidebar"] div[role="radiogroup"] { - justify-content: center !important; - display: flex !important; - gap: 1rem; + justify-content: center !important; + display: flex !important; + gap: 1rem; } /* ========================================== 6. Tables ========================================== */ table { - border: 1px solid var(--table-border) !important; - border-collapse: collapse; - width: 100%; - margin-bottom: 1.5em; + border: 1px solid var(--table-border) !important; + border-collapse: collapse; + width: 100%; + margin-bottom: 1.5em; } th, td { - border: 1px solid var(--table-border) !important; - padding: 8px; - text-align: left; + border: 1px solid var(--table-border) !important; + padding: 8px; + text-align: left; } caption { - caption-side: top; - font-weight: bold; - padding: 0.5em; - text-align: left; - caption-side: bottom; - text-align: center; + caption-side: top; + font-weight: bold; + padding: 0.5em; + text-align: left; + caption-side: bottom; + text-align: center; } table[role="table"] th[scope="col"] { - background-color: var(--background-color); + background-color: var(--background-color); } /* ========================================== @@ -254,94 +254,99 @@ table[role="table"] th[scope="col"] { /* --- 7.2 Graphiques --- */ .stPlotlyChart text { - fill: var(--plot-text) !important; + fill: var(--plot-text) !important; } .stPlotlyChart text { - fill: black !important; - text-shadow: none !important; - font-weight: bold !important; - font-size: 14px !important; - font-family: Verdana, sans-serif !important; + fill: black !important; + text-shadow: none !important; + font-weight: bold !important; + font-size: 14px !important; + font-family: Verdana, sans-serif !important; } /* Cache complètement la section d'actions Vega */ .vega-actions { - display: none !important; + display: none !important; } /* Et aussi le
parent, s'il faut tout masquer */ details[title="Click to view actions"] { - display: none !important; + display: none !important; } /* --- 7.3 Détails et paragraphes --- */ details { - border: 1px solid #ccc; - border-radius: 6px; - padding: 0.5em; - margin-bottom: 0.5em; - background-color: var(--background-color); - border-color: var(--details-border) !important; + border: 1px solid #ccc; + border-radius: 6px; + padding: 0.5em; + margin-bottom: 0.5em; + background-color: var(--background-color); + border-color: var(--details-border) !important; } section:not([data-testid="stSidebar"]) - div:not[data-testid="stElementContainer"] - p:not(#Authentification):not(#Theme) { - color: var(--paragraph-color) !important; + div:not[data-testid="stElementContainer"] + p:not(#Authentification):not(#Theme) { + color: var(--paragraph-color) !important; } section:not([data-testid="stSidebar"]) hr { - background-color: var(--hr-color) !important; + background-color: var(--hr-color) !important; } /* --- 7.4 Conteneurs de commentaires et tickets --- */ .conteneur_commentaire, .conteneur_ticket { - background: var(--background-color); - padding: 1em; - border-radius: 8px; - margin-bottom: 1em; - border: 1px solid #ccc; + background: var(--background-color); + padding: 1em; + border-radius: 8px; + margin-bottom: 1em; + border: 1px solid #ccc; } .commentaire_auteur, .ticket_auteur { - color: var(--text-color) !important; - margin: 0; + color: var(--text-color) !important; + margin: 0; } .commentaire_contenu, .ticket_contenu { - color: var(--text-color) !important; - margin: 0.5rem 0 0; + color: var(--text-color) !important; + margin: 0.5rem 0 0; } /* --- 7.5 Blocs mathématiques --- */ .math-block { - display: block; - text-align: center; - margin: 1em 0; - border: 1px solid var(--math-block-border); - border-radius: 10px; - background: var(--math-block-bg); - font-size: x-large; - color: var(--text-color); + display: block; + text-align: center; + margin: 1em 0; + border: 1px solid var(--math-block-border); + border-radius: 10px; + background: var(--math-block-bg); + font-size: x-large; + color: var(--text-color); } .math-block math { - display: inline-block; + display: inline-block; } /* ========================================== 8. Éléments spécifiques ========================================== */ div.stElementContainer.element-container.st-key-nom_utilisateur { - display: none !important; + display: none !important; } .st-key-telecharger_fiche_pdf { - margin-left: auto; - margin-right: auto; - margin-top: 1rem; + margin-left: auto; + margin-right: auto; + margin-top: 1rem; +} + +.details_introduction { + margin-top: 0.5rem; + margin-bottom: 0.5rem; } diff --git a/batch_ia/.gitignore b/batch_ia/.gitignore new file mode 100644 index 0000000..ddb0279 --- /dev/null +++ b/batch_ia/.gitignore @@ -0,0 +1 @@ +temp_sections/ diff --git a/batch_ia/__init__.py b/batch_ia/__init__.py new file mode 100644 index 0000000..f0844e8 --- /dev/null +++ b/batch_ia/__init__.py @@ -0,0 +1,27 @@ +# batch_ia/__init__.py + +# config.py +from .utils.config import TEMPLATE_PATH, load_config + +# files.py +from .utils.files import write_report + +# graphs.py +from .utils.graphs import ( + parse_graphs, + extract_data_from_graph, + calculate_vulnerabilities +) + +# sections.py +from .utils.sections import generate_report + +__all__ = [ + "TEMPLATE_PATH", + "load_config", + "write_report", + "parse_graphs", + "extract_data_from_graph", + "calculate_vulnerabilities", + "generate_report", +] diff --git a/batch_ia/analyse_ia.py b/batch_ia/analyse_ia.py new file mode 100644 index 0000000..a351b8b --- /dev/null +++ b/batch_ia/analyse_ia.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script pour générer un rapport factorisé des vulnérabilités critiques +suivant la structure définie dans Remarques.md. +""" + +import sys +from pathlib import Path +import streamlit as st + +from utils.config import ( + TEMP_SECTIONS, + TEMPLATE_PATH, session_uuid, + load_config +) + +from utils.files import ( + write_report +) + +from utils.graphs import ( + parse_graphs, + extract_data_from_graph, + calculate_vulnerabilities +) + +from utils.sections import ( + generate_report +) + +from utils.sections_utils import ( + nettoyer_texte_fr +) + +from utils.ia import ( + ingest_document, + ia_analyse, + supprimer_fichiers, + generer_rapport_final +) + + +def main(dot_path, output_path): + """Fonction principale du script.""" + # Charger la configuration + config = load_config() + + # Analyser les graphes + graph, ref_graph = parse_graphs(dot_path) + # Extraire les données + data = extract_data_from_graph(graph, ref_graph) + # Calculer les vulnérabilités + results = calculate_vulnerabilities(data, config) + if "step" not in st.session_state: + st.session_state["step"] = 1 + # Générer le rapport + report, file_names = generate_report(data, results, config) + # Écrire le rapport + write_report(report, TEMPLATE_PATH) + ingest_document(TEMPLATE_PATH) + # Générer l'analyse par l'IA du rapport compler + analyse_finale = nettoyer_texte_fr(ia_analyse(file_names)) + analyse_fichier = TEMP_SECTIONS / TEMPLATE_PATH.name.replace(".md", " - analyse.md") + write_report(analyse_finale, analyse_fichier) + + if generer_rapport_final(TEMPLATE_PATH, analyse_fichier, output_path): + supprimer_fichiers(session_uuid) + else: + print("") + +if __name__ == "__main__": + dot_path = Path(sys.argv[1]) + output_path = Path(sys.argv[2]) + main(dot_path, output_path) diff --git a/batch_ia/batch-fabnum-dev.service b/batch_ia/batch-fabnum-dev.service new file mode 100644 index 0000000..035ffae --- /dev/null +++ b/batch_ia/batch-fabnum-dev.service @@ -0,0 +1,31 @@ +[Unit] +Description=Service batch IA pour utilisateur fabnum +After=network.target + +[Service] +Type=simple +User=fabnum +WorkingDirectory=/home/fabnum/fabnum-dev/batch_ia +Environment=PYTHONPATH=/home/fabnum/fabnum-dev +ExecStart=/home/fabnum/fabnum-dev/venv/bin/python /home/fabnum/fabnum-dev/batch_ia/batch_runner.py +Restart=always +Nice=10 +CPUSchedulingPolicy=batch + +# Limites de ressources +CPUQuota=87.5% # ~14 cores sur 16 +MemoryMax=12G # RAM maximale autorisée +TasksMax=1 # maximum 1 subprocess/thread simultané + +# Sécurité renforcée +ProtectSystem=full +ReadWritePaths=/home/fabnum/fabnum-dev/batch_ia + +# Journal propre +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target + +# semanage fcontext -a -t svirt_sandbox_file_t "/home/fabnum/fabnum-dev/batch_ia(/.*)?" diff --git a/batch_ia/batch_runner.py b/batch_ia/batch_runner.py new file mode 100644 index 0000000..1f39dd0 --- /dev/null +++ b/batch_ia/batch_runner.py @@ -0,0 +1,32 @@ +import time +import subprocess +from batch_utils import charger_status, sauvegarder_status, JOBS_DIR + + +while True: + status = charger_status() + jobs = [(login, data) for login, data in status.items() if data["status"] == "en attente"] + jobs = sorted(jobs, key=lambda x: x[1].get("timestamp", 0)) + + for i, (login, data) in enumerate(jobs): + status[login]["position"] = i + sauvegarder_status(status) + + if jobs: + login, _ = jobs[0] + dot_file = JOBS_DIR / f"{login}.dot" + result_file = JOBS_DIR / f"{login}.zip" + + status[login]["status"] = "en cours" + sauvegarder_status(status) + + try: + subprocess.run(["python3", "analyse_ia.py", str(dot_file), str(result_file)], check=True) + status[login]["status"] = "terminé" + except Exception as e: + status[login]["status"] = "échoué" + status[login]["error"] = str(e) + + sauvegarder_status(status) + + time.sleep(60) diff --git a/batch_ia/batch_utils.py b/batch_ia/batch_utils.py new file mode 100644 index 0000000..730be61 --- /dev/null +++ b/batch_ia/batch_utils.py @@ -0,0 +1,64 @@ +import json +import time +from pathlib import Path +from networkx.drawing.nx_agraph import write_dot +import streamlit as st +from utils.translations import _ + +BATCH_DIR = Path(__file__).resolve().parent +JOBS_DIR = BATCH_DIR / "jobs" +STATUS_FILE = BATCH_DIR / "status.json" +ANALYSE = " - analyse.md" +RAPPORT = " - rapport.md" + +def charger_status(): + if STATUS_FILE.exists(): + return json.loads(STATUS_FILE.read_text()) + return {} + +def sauvegarder_status(data): + STATUS_FILE.write_text(json.dumps(data, indent=2)) + +def statut_utilisateur(login): + status = charger_status() + entry = status.get(login) + if not entry: + return {"statut": None, "position": None, "telechargement": None, + "message": f"{str(_('batch.no_task'))}."} + + if entry["status"] == "en attente": + return {"statut": "en attente", "position": entry.get("position"), + "telechargement": None, + "message": f"{str(_('batch.in_queue'))} (position {entry.get('position', '?')})."} + + if entry["status"] == "en cours": + if "step" not in st.session_state: + st.session_state["step"] = 1 + return {"statut": "en cours", "position": 0, + "telechargement": None, "message": f"{str(_('batch.in_progress'))} ({str(_('batch.step'))} {st.session_state["step"]}/5)."} + + if entry["status"] == "terminé": + result_file = JOBS_DIR / f"{login}.zip" + if result_file.exists(): + return {"statut": "terminé", "position": None, + "telechargement": result_file.read_bytes(), + "message": f"{str(_('batch.complete'))}."} + + if entry["status"] == "échoué": + return {"statut": "échoué", "position": None, "telechargement": None, + "message": f"{str(_('batch.failure'))} : {entry.get('error', {str(_('batch.unknown_error'))})}"} + +def soumettre_batch(login, G): + if statut_utilisateur(login)["statut"]: + raise RuntimeError("Un batch est déjà en cours.") + write_dot(G, JOBS_DIR / f"{login}.dot") + status = charger_status() + status[login] = {"status": "en attente", "timestamp": time.time()} + sauvegarder_status(status) + +def nettoyage_post_telechargement(login): + (JOBS_DIR / f"{login}.dot").unlink(missing_ok=True) + (JOBS_DIR / f"{login}.zip").unlink(missing_ok=True) + status = charger_status() + status.pop(login, None) + sauvegarder_status(status) diff --git a/batch_ia/nettoyer_pgpt.py b/batch_ia/nettoyer_pgpt.py new file mode 100644 index 0000000..3a7c4ba --- /dev/null +++ b/batch_ia/nettoyer_pgpt.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Script de nettoyage pour PrivateGPT + +Ce script permet de lister et supprimer les documents ingérés dans PrivateGPT. +Options: +- Lister tous les documents +- Supprimer des documents par préfixe (ex: "temp_section_") +- Supprimer des documents par motif +- Supprimer tous les documents +""" + +import json +import re +import requests +import time +from typing import List, Dict, Any, Optional + +# Configuration de l'API PrivateGPT +PGPT_URL = "http://127.0.0.1:8001" +API_URL = f"{PGPT_URL}/v1" + +def list_documents() -> List[Dict[str, Any]]: + """Liste tous les documents ingérés et renvoie la liste des métadonnées""" + try: + # Récupérer la liste des documents + response = requests.get(f"{API_URL}/ingest/list") + response.raise_for_status() + data = response.json() + + # Format de réponse OpenAI + if "data" in data: + documents = data.get("data", []) + # Format alternatif + else: + documents = data.get("documents", []) + + # Construire une liste normalisée des documents + normalized_docs = [] + for doc in documents: + doc_id = doc.get("doc_id") or doc.get("id") + metadata = doc.get("doc_metadata", {}) + filename = metadata.get("file_name") or metadata.get("filename", "Inconnu") + + normalized_docs.append({ + "id": doc_id, + "filename": filename, + "metadata": metadata + }) + + return normalized_docs + + except Exception as e: + print(f"❌ Erreur lors de la récupération des documents: {e}") + return [] + + +def delete_document(doc_id: str) -> bool: + """Supprime un document par son ID""" + try: + response = requests.delete(f"{API_URL}/ingest/{doc_id}") + if response.status_code == 200: + return True + else: + print(f"⚠️ Échec de la suppression de l'ID {doc_id}: Code {response.status_code}") + return False + except Exception as e: + print(f"❌ Erreur lors de la suppression de l'ID {doc_id}: {e}") + return False + + +def delete_documents_by_criteria(pattern) -> int: + """ + Supprime des documents selon différents critères + Retourne le nombre de documents supprimés + """ + + documents = list_documents() + + if not documents or not pattern: + return 0 + + # Comptage des suppressions réussies + success_count = 0 + + # Filtrer les documents à supprimer + docs_to_delete = [] + try: + regex = re.compile(pattern) + docs_to_delete = [doc for doc in documents if regex.search(doc["filename"])] + except re.error as e: + return 0 + + # Supprimer les documents + for doc in docs_to_delete: + delete_document(doc["id"]) + # Petite pause pour éviter de surcharger l'API + time.sleep(0.1) + + return success_count diff --git a/batch_ia/status.json b/batch_ia/status.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/batch_ia/status.json @@ -0,0 +1 @@ +{} diff --git a/batch_ia/utils/__init__.py b/batch_ia/utils/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/batch_ia/utils/__init__.py @@ -0,0 +1 @@ + diff --git a/batch_ia/utils/config.py b/batch_ia/utils/config.py new file mode 100644 index 0000000..00066ca --- /dev/null +++ b/batch_ia/utils/config.py @@ -0,0 +1,93 @@ +import os +import yaml +from pathlib import Path +import uuid + +def init_uuid(): + if not TEMP_SECTIONS.exists(): + TEMP_SECTIONS.mkdir(parents=True) + session_uuid = str(uuid.uuid4())[:8] # Utiliser les 8 premiers caractères pour plus de concision + print(f"🔑 UUID de session généré: {session_uuid}") + return session_uuid + +BASE_DIR = Path(__file__).resolve().parent.parent +CORPUS_DIR = BASE_DIR.parent / "Corpus" +THRESHOLDS_PATH = BASE_DIR.parent / "assets" / "config.yaml" +REFERENCE_GRAPH_PATH = BASE_DIR.parent / "schema.txt" +GRAPH_PATH = BASE_DIR.parent / "graphe.dot" +TEMP_SECTIONS = BASE_DIR / "temp_sections" +session_uuid = init_uuid() +TEMPLATE_PATH = TEMP_SECTIONS / f"rapport_final - {session_uuid}.md" + +PGPT_URL = "http://127.0.0.1:8001" +API_URL = f"{PGPT_URL}/v1" +PROMPT_METHODOLOGIE = """ +Le rapport à examiner a été établi à partir de la méthodologie suivante. + +Le dispositif d’évaluation des risques proposé repose sur quatre indices clairement définis, chacun analysant un aspect spécifique des risques dans la chaîne d’approvisionnement numérique. L’indice IHH mesure la concentration géographique ou industrielle, permettant d’évaluer la dépendance vis-à-vis de certains acteurs ou régions. L’indice ISG indique la stabilité géopolitique des pays impliqués dans la chaîne de production, en intégrant des critères politiques, sociaux et climatiques. L’indice ICS quantifie la facilité ou la difficulté à remplacer ou substituer un élément spécifique dans la chaîne, évaluant ainsi les risques liés à la dépendance technologique et économique. Enfin, l’indice IVC examine la pression concurrentielle sur les ressources utilisées par le numérique, révélant ainsi le risque potentiel que ces ressources soient détournées vers d’autres secteurs industriels. + +Ces indices se combinent judicieusement par paires pour une évaluation approfondie et pertinente des risques. La combinaison IHH-ISG permet d’associer la gravité d'un impact potentiel (IHH) à la probabilité de survenance d’un événement perturbateur (ISG), créant ainsi une matrice de vulnérabilité combinée utile pour identifier rapidement les points critiques dans la chaîne de production. La combinaison ICS-IVC fonctionne selon la même logique, mais se concentre spécifiquement sur les ressources minérales : l’ICS indique la gravité potentielle d'une rupture d'approvisionnement due à une faible substituabilité, tandis que l’IVC évalue la probabilité que les ressources soient captées par d'autres secteurs industriels concurrents. Ces combinaisons permettent d’obtenir une analyse précise et opérationnelle du niveau de risque global. + +Les avantages de cette méthodologie résident dans son approche à la fois systématique et granulaire, adaptée à l'échelle décisionnelle d'un COMEX. Elle permet d’identifier avec précision les vulnérabilités majeures et leurs origines spécifiques, facilitant ainsi la prise de décision stratégique éclairée et proactive. En combinant des facteurs géopolitiques, industriels, technologiques et concurrentiels, ces indices offrent un suivi efficace de la chaîne de fabrication numérique, garantissant ainsi une gestion optimale des risques et la continuité opérationnelle à long terme. +""" + +DICTIONNAIRE_CRITICITES = { + "IHH": {"vert": "Faible", "orange": "Modérée", "rouge": "Élevée"}, + "ISG": {"vert": "Stable", "orange": "Intermédiaire", "rouge": "Instable"}, + "ICS": {"vert": "Facile", "orange": "Moyenne", "rouge": "Difficile"}, + "IVC": {"vert": "Faible", "orange": "Modérée", "rouge": "Forte"} +} +POIDS_COULEURS = { + "Vert": 1, + "Orange": 2, + "Rouge": 3 +} + +def load_config(thresholds_path=THRESHOLDS_PATH): + """Charge la configuration depuis les fichiers YAML.""" + config = {} + # Charger les seuils + if os.path.exists(thresholds_path): + with open(thresholds_path, 'r', encoding='utf-8') as f: + thresholds = yaml.safe_load(f) + config['thresholds'] = thresholds.get('seuils', {}) + return config + +def determine_threshold_color(value, index_type, thresholds): + """ + Détermine la couleur du seuil en fonction du type d'indice et de sa valeur. + Utilise les seuils de config.yaml si disponibles. + """ + + # Récupérer les seuils pour cet indice + if index_type in thresholds: + index_thresholds = thresholds[index_type] + # Déterminer la couleur + if "vert" in index_thresholds and "max" in index_thresholds["vert"] and \ + index_thresholds["vert"]["max"] is not None and value < index_thresholds["vert"]["max"]: + suffix = get_suffix_for_index(index_type, "vert") + return "Vert", suffix + elif "orange" in index_thresholds and "min" in index_thresholds["orange"] and "max" in index_thresholds["orange"] and \ + index_thresholds["orange"]["min"] is not None and index_thresholds["orange"]["max"] is not None and \ + index_thresholds["orange"]["min"] <= value < index_thresholds["orange"]["max"]: + suffix = get_suffix_for_index(index_type, "orange") + return "Orange", suffix + elif "rouge" in index_thresholds and "min" in index_thresholds["rouge"] and \ + index_thresholds["rouge"]["min"] is not None and value >= index_thresholds["rouge"]["min"]: + suffix = get_suffix_for_index(index_type, "rouge") + return "Rouge", suffix + + return "Non déterminé", "" + +def get_suffix_for_index(index_type, color): + """Retourne le suffixe approprié pour chaque indice et couleur.""" + suffixes = DICTIONNAIRE_CRITICITES + + if index_type in suffixes and color in suffixes[index_type]: + return suffixes[index_type][color] + return "" + +def get_weight_for_color(color): + """Retourne le poids correspondant à une couleur.""" + weights = POIDS_COULEURS + return weights.get(color, 0) diff --git a/batch_ia/utils/files.py b/batch_ia/utils/files.py new file mode 100644 index 0000000..99de4be --- /dev/null +++ b/batch_ia/utils/files.py @@ -0,0 +1,133 @@ +import os +import re + +from .config import ( + CORPUS_DIR +) + +def strip_prefix(name): + """Supprime le préfixe numérique éventuel d'un nom de fichier ou de dossier.""" + return re.sub(r'^\d+[-_ ]*', '', name).lower() + +def find_prefixed_directory(pattern, base_path=None): + """ + Recherche un sous-répertoire dont le nom (sans préfixe) correspond au pattern. + + Args: + pattern: Nom du répertoire sans préfixe + base_path: Répertoire de base où chercher + + Returns: + Le chemin relatif du répertoire trouvé (avec préfixe) ou None + """ + if base_path: + search_path = os.path.join(CORPUS_DIR, base_path) + else: + search_path = CORPUS_DIR + + if not os.path.exists(search_path): + # print(f"Chemin inexistant: {search_path}") + return None + + for d in os.listdir(search_path): + dir_path = os.path.join(search_path, d) + if os.path.isdir(dir_path) and strip_prefix(d) == pattern.lower(): + return os.path.relpath(dir_path, CORPUS_DIR) + + # print(f"Aucun répertoire correspondant à: '{pattern}' trouvé dans {search_path}") + return None + +def find_corpus_file(pattern, base_path=None): + """ + Recherche récursive dans le corpus d'un fichier en ignorant les préfixes numériques dans les dossiers et fichiers. + + Args: + pattern: Chemin relatif type "sous-dossier/nom-fichier" + base_path: Dossier de base à partir duquel chercher + + Returns: + Chemin relatif du fichier trouvé ou None + """ + + if base_path: + search_path = os.path.join(CORPUS_DIR, base_path) + else: + search_path = CORPUS_DIR + + # # print(f"Recherche de: '{pattern}' dans {search_path}") + + if not os.path.exists(search_path): + # print(pattern) + # print(base_path) + # print(f"Chemin inexistant: {search_path}") + return None + + if '/' not in pattern: + # Recherche directe d'un fichier + for file in os.listdir(search_path): + if not file.endswith('.md'): + continue + if strip_prefix(os.path.splitext(file)[0]) == pattern.lower(): + rel_path = os.path.relpath(os.path.join(search_path, file), CORPUS_DIR) + # # print(f"Fichier trouvé: {rel_path}") + return rel_path + else: + # Séparation du chemin en dossier/fichier + first, rest = pattern.split('/', 1) + matched_dir = find_prefixed_directory(first, base_path) + if matched_dir: + return find_corpus_file(rest, matched_dir) + + # print(f"Aucun fichier correspondant à: '{pattern}' trouvé dans {base_path}.") + return None + + +def read_corpus_file(file_path, remove_first_title=False, shift_titles=0): + """ + Lit un fichier du corpus et applique les transformations demandées. + + Args: + file_path: Chemin relatif du fichier dans le corpus + remove_first_title: Si True, supprime la première ligne de titre + shift_titles: Nombre de niveaux à ajouter aux titres + + Returns: + Le contenu du fichier avec les transformations appliquées + """ + full_path = os.path.join(CORPUS_DIR, file_path) + + if not os.path.exists(full_path): + # print(f"Fichier non trouvé: {full_path}") + return f"Fichier non trouvé: {file_path}" + + # # print(f"Lecture du fichier: {full_path}") + with open(full_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + + # Supprimer la première ligne si c'est un titre et si demandé + if remove_first_title and lines and lines[0].startswith('#'): + # # print(f"Suppression du titre: {lines[0].strip()}") + lines = lines[1:] + + # Décaler les niveaux de titre si demandé + if shift_titles > 0: + for i in range(len(lines)): + if lines[i].startswith('#'): + lines[i] = '#' * shift_titles + lines[i] + + # Nettoyer les retours à la ligne superflus + content = ''.join(lines) + # Supprimer les retours à la ligne en fin de contenu + content = content.rstrip('\n') + '\n' + + return content + +def write_report(report, fichier): + """Écrit le rapport généré dans le fichier spécifié.""" + + report = re.sub(r'', '', report) + report = re.sub(r'\n\n\n+', '\n\n', report) + + with open(fichier, 'w', encoding='utf-8') as f: + f.write(report) + # print(f"Rapport généré avec succès: {TEMPLATE_PATH}") diff --git a/batch_ia/utils/graphs.py b/batch_ia/utils/graphs.py new file mode 100644 index 0000000..67915c9 --- /dev/null +++ b/batch_ia/utils/graphs.py @@ -0,0 +1,591 @@ +import os +import sys +from networkx.drawing.nx_agraph import read_dot + +from .config import ( + REFERENCE_GRAPH_PATH, + determine_threshold_color, get_weight_for_color +) + +def parse_graphs(graphe_path): + """ + Charge et analyse les graphes DOT (analyse et référence). + """ + print(graphe_path) + # Charger le graphe à analyser + if not os.path.exists(graphe_path): + print(f"Fichier de graphe à analyser introuvable: {graphe_path}") + sys.exit(1) + + # Charger le graphe de référence + reference_path = REFERENCE_GRAPH_PATH + if not os.path.exists(reference_path): + print(f"Fichier de graphe de référence introuvable: {reference_path}") + sys.exit(1) + + try: + # Charger les graphes avec NetworkX + graph = read_dot(graphe_path) + ref_graph = read_dot(reference_path) + + # Convertir les attributs en types appropriés pour les deux graphes + for g in [graph, ref_graph]: + for node, attrs in g.nodes(data=True): + for key, value in list(attrs.items()): + # Convertir les valeurs numériques + if key in ['niveau', 'ihh_acteurs', 'ihh_pays', 'isg', 'ivc']: + try: + if key in ['isg', 'ivc', 'ihh_acteurs', 'ihh_pays', 'niveau']: + attrs[key] = int(value.strip('"')) + else: + attrs[key] = float(value.strip('"')) + except (ValueError, TypeError): + # Garder la valeur originale si la conversion échoue + pass + elif key == 'label': + # Nettoyer les guillemets des étiquettes + attrs[key] = value.strip('"') + + # Convertir les attributs des arêtes + for u, v, attrs in g.edges(data=True): + for key, value in list(attrs.items()): + if key in ['ics', 'cout', 'delai', 'technique']: + try: + attrs[key] = float(value.strip('"')) + except (ValueError, TypeError): + pass + elif key == 'label' and '%' in value: + # Extraire le pourcentage + try: + percentage = value.strip('"').replace('%', '') + attrs['percentage'] = float(percentage) + except (ValueError, TypeError): + pass + + return graph, ref_graph + + except Exception as e: + print(f"Erreur lors de l'analyse des graphes: {str(e)}") + sys.exit(1) + +def extract_data_from_graph(graph, ref_graph): + """ + Extrait toutes les données pertinentes des graphes DOT. + """ + data = { + "products": {}, # Produits finaux (N0) + "components": {}, # Composants (N1) + "minerals": {}, # Minerais (N2) + "operations": {}, # Opérations (N10) + "countries": {}, # Pays (N11) + "geo_countries": {}, # Pays géographiques (N99) + "actors": {} # Acteurs (N12) + } + + # Extraire tous les pays géographiques du graphe de référence + for node, attrs in ref_graph.nodes(data=True): + if attrs.get('niveau') == 99: + country_name = attrs.get('label', node) + isg_value = attrs.get('isg', 0) + + data["geo_countries"][country_name] = { + "id": node, + "isg": isg_value + } + + # Extraire les nœuds du graphe à analyser + for node, attrs in graph.nodes(data=True): + level = attrs.get('niveau', -1) + label = attrs.get('label', node) + + if level == 0 or level == 1000: # Produit final + data["products"][node] = { + "label": label, + "components": [], + "assembly": None, + "level": level + } + elif level == 1 or level == 1001: # Composant + data["components"][node] = { + "label": label, + "minerals": [], + "manufacturing": None + } + elif level == 2: # Minerai + data["minerals"][node] = { + "label": label, + "ivc": attrs.get('ivc', 0), + "extraction": None, + "treatment": None, + "ics_values": {} + } + elif level == 10 or level == 1010: # Opération + op_type = label.lower() + data["operations"][node] = { + "label": label, + "type": op_type, + "ihh_acteurs": attrs.get('ihh_acteurs', 0), + "ihh_pays": attrs.get('ihh_pays', 0), + "countries": {} + } + elif level == 11 or level == 1011: # Pays + data["countries"][node] = { + "label": label, + "actors": {}, + "geo_country": None, + "market_share": 0 + } + elif level == 12 or level == 1012: # Acteur + data["actors"][node] = { + "label": label, + "country": None, + "market_share": 0 + } + + # Extraire les relations et attributs des arêtes + for source, target, edge_attrs in graph.edges(data=True): + if source not in graph.nodes or target not in graph.nodes: + continue + + source_level = graph.nodes[source].get('niveau', -1) + target_level = graph.nodes[target].get('niveau', -1) + + # Extraire part de marché + market_share = 0 + if 'percentage' in edge_attrs: + market_share = edge_attrs['percentage'] + elif 'label' in edge_attrs and '%' in edge_attrs['label']: + try: + market_share = float(edge_attrs['label'].strip('"').replace('%', '')) + except (ValueError, TypeError): + pass + + # Relations produit → composant + if (source_level == 0 and target_level == 1) or (source_level == 1000 and target_level == 1001): + if target not in data["products"][source]["components"]: + data["products"][source]["components"].append(target) + + # Relations produit → opération (assemblage) + elif (source_level == 0 and target_level == 10) or (source_level == 1000 and target_level == 1010): + if graph.nodes[target].get('label', '').lower() == 'assemblage': + data["products"][source]["assembly"] = target + + # Relations composant → minerai avec ICS + elif (source_level == 1 or source_level == 1001) and target_level == 2: + if target not in data["components"][source]["minerals"]: + data["components"][source]["minerals"].append(target) + + # Stocker l'ICS s'il est présent + if 'ics' in edge_attrs: + ics_value = edge_attrs['ics'] + data["minerals"][target]["ics_values"][source] = ics_value + + # Relations composant → opération (fabrication) + elif (source_level == 1 or source_level == 1001) and target_level == 10: + if graph.nodes[target].get('label', '').lower() == 'fabrication': + data["components"][source]["manufacturing"] = target + + # Relations minerai → opération (extraction/traitement) + elif source_level == 2 and target_level == 10: + op_label = graph.nodes[target].get('label', '').lower() + if 'extraction' in op_label: + data["minerals"][source]["extraction"] = target + elif 'traitement' in op_label: + data["minerals"][source]["treatment"] = target + + # Relations opération → pays avec part de marché + elif (source_level == 10 and target_level == 11) or (source_level == 1010 and target_level == 1011): + data["operations"][source]["countries"][target] = market_share + data["countries"][target]["market_share"] = market_share + + # Relations pays → acteur avec part de marché + elif (source_level == 11 and target_level == 12) or (source_level == 1011 and target_level == 1012): + data["countries"][source]["actors"][target] = market_share + data["actors"][target]["market_share"] = market_share + data["actors"][target]["country"] = source + + # Relations pays → pays géographique + elif (source_level == 11 or source_level == 1011) and target_level == 99: + country_name = graph.nodes[target].get('label', '') + data["countries"][source]["geo_country"] = country_name + + # Compléter les opérations manquantes pour les produits et composants + # en les récupérant du graphe de référence si elles existent + + # Pour les produits finaux (N0) + for product_id, product_data in data["products"].items(): + if product_data["assembly"] is None: + # Chercher l'opération d'assemblage dans le graphe de référence + for source, target, edge_attrs in ref_graph.edges(data=True): + if (source == product_id and + ((ref_graph.nodes[source].get('niveau') == 0 and + ref_graph.nodes[target].get('niveau') == 10) or + (ref_graph.nodes[source].get('niveau') == 1000 and + ref_graph.nodes[target].get('niveau') == 1010)) and + ref_graph.nodes[target].get('label', '').lower() == 'assemblage'): + + # L'opération existe dans le graphe de référence + assembly_id = target + product_data["assembly"] = assembly_id + + # Ajouter l'opération si elle n'existe pas déjà + if assembly_id not in data["operations"]: + data["operations"][assembly_id] = { + "label": ref_graph.nodes[assembly_id].get('label', assembly_id), + "type": "assemblage", + "ihh_acteurs": ref_graph.nodes[assembly_id].get('ihh_acteurs', 0), + "ihh_pays": ref_graph.nodes[assembly_id].get('ihh_pays', 0), + "countries": {} + } + + # Extraire les relations de l'opération vers les pays + for op_source, op_target, op_edge_attrs in ref_graph.edges(data=True): + if (op_source == assembly_id and + (ref_graph.nodes[op_target].get('niveau') == 11 or ref_graph.nodes[op_target].get('niveau') == 1011)): + + country_id = op_target + + # Extraire part de marché + market_share = 0 + if 'percentage' in op_edge_attrs: + market_share = op_edge_attrs['percentage'] + elif 'label' in op_edge_attrs and '%' in op_edge_attrs['label']: + try: + market_share = float(op_edge_attrs['label'].strip('"').replace('%', '')) + except (ValueError, TypeError): + pass + + # Ajouter le pays à l'opération + data["operations"][assembly_id]["countries"][country_id] = market_share + + # Ajouter le pays s'il n'existe pas déjà + if country_id not in data["countries"]: + data["countries"][country_id] = { + "label": ref_graph.nodes[country_id].get('label', country_id), + "actors": {}, + "geo_country": None, + "market_share": market_share + } + else: + data["countries"][country_id]["market_share"] = market_share + + # Extraire les relations du pays vers les acteurs + for country_source, country_target, country_edge_attrs in ref_graph.edges(data=True): + if (country_source == country_id and + (ref_graph.nodes[country_target].get('niveau') == 12 or ref_graph.nodes[country_target].get('niveau') == 1012)): + + actor_id = country_target + + # Extraire part de marché + actor_market_share = 0 + if 'percentage' in country_edge_attrs: + actor_market_share = country_edge_attrs['percentage'] + elif 'label' in country_edge_attrs and '%' in country_edge_attrs['label']: + try: + actor_market_share = float(country_edge_attrs['label'].strip('"').replace('%', '')) + except (ValueError, TypeError): + pass + + # Ajouter l'acteur au pays + data["countries"][country_id]["actors"][actor_id] = actor_market_share + + # Ajouter l'acteur s'il n'existe pas déjà + if actor_id not in data["actors"]: + data["actors"][actor_id] = { + "label": ref_graph.nodes[actor_id].get('label', actor_id), + "country": country_id, + "market_share": actor_market_share + } + else: + data["actors"][actor_id]["market_share"] = actor_market_share + data["actors"][actor_id]["country"] = country_id + + # Extraire la relation du pays vers le pays géographique + for geo_source, geo_target, geo_edge_attrs in ref_graph.edges(data=True): + if (geo_source == country_id and + ref_graph.nodes[geo_target].get('niveau') == 99): + + geo_country_name = ref_graph.nodes[geo_target].get('label', '') + data["countries"][country_id]["geo_country"] = geo_country_name + + break # Une seule opération d'assemblage par produit + + # Pour les composants (N1) + for component_id, component_data in data["components"].items(): + if component_data["manufacturing"] is None: + # Chercher l'opération de fabrication dans le graphe de référence + for source, target, edge_attrs in ref_graph.edges(data=True): + if (source == component_id and + ((ref_graph.nodes[source].get('niveau') == 1 and + ref_graph.nodes[target].get('niveau') == 10) or + (ref_graph.nodes[source].get('niveau') == 1001 and + ref_graph.nodes[target].get('niveau') == 1010)) and + ref_graph.nodes[target].get('label', '').lower() == 'fabrication'): + + # L'opération existe dans le graphe de référence + manufacturing_id = target + component_data["manufacturing"] = manufacturing_id + + # Ajouter l'opération si elle n'existe pas déjà + if manufacturing_id not in data["operations"]: + data["operations"][manufacturing_id] = { + "label": ref_graph.nodes[manufacturing_id].get('label', manufacturing_id), + "type": "fabrication", + "ihh_acteurs": ref_graph.nodes[manufacturing_id].get('ihh_acteurs', 0), + "ihh_pays": ref_graph.nodes[manufacturing_id].get('ihh_pays', 0), + "countries": {} + } + + # Extraire les relations de l'opération vers les pays + for op_source, op_target, op_edge_attrs in ref_graph.edges(data=True): + if (op_source == manufacturing_id and + (ref_graph.nodes[op_target].get('niveau') == 11 or ref_graph.nodes[op_target].get('niveau') == 1011)): + + country_id = op_target + + # Extraire part de marché + market_share = 0 + if 'percentage' in op_edge_attrs: + market_share = op_edge_attrs['percentage'] + elif 'label' in op_edge_attrs and '%' in op_edge_attrs['label']: + try: + market_share = float(op_edge_attrs['label'].strip('"').replace('%', '')) + except (ValueError, TypeError): + pass + + # Ajouter le pays à l'opération + data["operations"][manufacturing_id]["countries"][country_id] = market_share + + # Ajouter le pays s'il n'existe pas déjà + if country_id not in data["countries"]: + data["countries"][country_id] = { + "label": ref_graph.nodes[country_id].get('label', country_id), + "actors": {}, + "geo_country": None, + "market_share": market_share + } + else: + data["countries"][country_id]["market_share"] = market_share + + # Extraire les relations du pays vers les acteurs + for country_source, country_target, country_edge_attrs in ref_graph.edges(data=True): + if (country_source == country_id and + (ref_graph.nodes[country_target].get('niveau') == 12 or ref_graph.nodes[country_target].get('niveau') == 1012)): + + actor_id = country_target + + # Extraire part de marché + actor_market_share = 0 + if 'percentage' in country_edge_attrs: + actor_market_share = country_edge_attrs['percentage'] + elif 'label' in country_edge_attrs and '%' in country_edge_attrs['label']: + try: + actor_market_share = float(country_edge_attrs['label'].strip('"').replace('%', '')) + except (ValueError, TypeError): + pass + + # Ajouter l'acteur au pays + data["countries"][country_id]["actors"][actor_id] = actor_market_share + + # Ajouter l'acteur s'il n'existe pas déjà + if actor_id not in data["actors"]: + data["actors"][actor_id] = { + "label": ref_graph.nodes[actor_id].get('label', actor_id), + "country": country_id, + "market_share": actor_market_share + } + else: + data["actors"][actor_id]["market_share"] = actor_market_share + data["actors"][actor_id]["country"] = country_id + + # Extraire la relation du pays vers le pays géographique + for geo_source, geo_target, geo_edge_attrs in ref_graph.edges(data=True): + if (geo_source == country_id and + ref_graph.nodes[geo_target].get('niveau') == 99): + + geo_country_name = ref_graph.nodes[geo_target].get('label', '') + data["countries"][country_id]["geo_country"] = geo_country_name + + break # Une seule opération de fabrication par composant + + return data + +def calculate_vulnerabilities(data, config): + """ + Calcule les vulnérabilités combinées pour toutes les opérations et minerais. + """ + thresholds = config.get('thresholds', {}) + results = { + "ihh_isg_combined": {}, # Pour chaque opération + "ics_ivc_combined": {}, # Pour chaque minerai + "chains": [] # Pour stocker tous les chemins possibles + } + + # 1. Calculer ISG_combiné pour chaque opération + for op_id, operation in data["operations"].items(): + isg_weighted_sum = 0 + total_share = 0 + + # Parcourir chaque pays impliqué dans l'opération + for country_id, share in operation["countries"].items(): + country = data["countries"][country_id] + geo_country = country.get("geo_country") + + if geo_country and geo_country in data["geo_countries"]: + isg_value = data["geo_countries"][geo_country]["isg"] + isg_weighted_sum += isg_value * share + total_share += share + + # Calculer la moyenne pondérée + isg_combined = 0 + if total_share > 0: + isg_combined = isg_weighted_sum / total_share + + # Déterminer couleurs et poids + ihh_value = operation["ihh_pays"] + ihh_color, ihh_suffix = determine_threshold_color(ihh_value, "IHH", thresholds) + isg_color, isg_suffix = determine_threshold_color(isg_combined, "ISG", thresholds) + + # Calculer poids combiné + ihh_weight = get_weight_for_color(ihh_color) + isg_weight = get_weight_for_color(isg_color) + combined_weight = ihh_weight * isg_weight + + # Déterminer vulnérabilité combinée + if combined_weight in [6, 9]: + vulnerability = "ÉLEVÉE à CRITIQUE" + elif combined_weight in [3, 4]: + vulnerability = "MOYENNE" + else: # 1, 2 + vulnerability = "FAIBLE" + + # Stocker résultats + results["ihh_isg_combined"][op_id] = { + "ihh_value": ihh_value, + "ihh_color": ihh_color, + "ihh_suffix": ihh_suffix, + "isg_combined": isg_combined, + "isg_color": isg_color, + "isg_suffix": isg_suffix, + "combined_weight": combined_weight, + "vulnerability": vulnerability + } + + # 2. Calculer ICS_moyen pour chaque minerai + for mineral_id, mineral in data["minerals"].items(): + ics_values = list(mineral["ics_values"].values()) + ics_average = 0 + + if ics_values: + ics_average = sum(ics_values) / len(ics_values) + + ivc_value = mineral.get("ivc", 0) + + # Déterminer couleurs et poids + ics_color, ics_suffix = determine_threshold_color(ics_average, "ICS", thresholds) + ivc_color, ivc_suffix = determine_threshold_color(ivc_value, "IVC", thresholds) + + # Calculer poids combiné + ics_weight = get_weight_for_color(ics_color) + ivc_weight = get_weight_for_color(ivc_color) + combined_weight = ics_weight * ivc_weight + + # Déterminer vulnérabilité combinée + if combined_weight in [6, 9]: + vulnerability = "ÉLEVÉE à CRITIQUE" + elif combined_weight in [3, 4]: + vulnerability = "MOYENNE" + else: # 1, 2 + vulnerability = "FAIBLE" + + # Stocker résultats + results["ics_ivc_combined"][mineral_id] = { + "ics_average": ics_average, + "ics_color": ics_color, + "ics_suffix": ics_suffix, + "ivc_value": ivc_value, + "ivc_color": ivc_color, + "ivc_suffix": ivc_suffix, + "combined_weight": combined_weight, + "vulnerability": vulnerability + } + + # 3. Identifier tous les chemins et leurs vulnérabilités + for product_id, product in data["products"].items(): + for component_id in product["components"]: + component = data["components"][component_id] + + for mineral_id in component["minerals"]: + mineral = data["minerals"][mineral_id] + + # Collecter toutes les vulnérabilités dans ce chemin + path_vulnerabilities = [] + + # Assemblage (si présent) + assembly_id = product["assembly"] + if assembly_id and assembly_id in results["ihh_isg_combined"]: + path_vulnerabilities.append({ + "type": "assemblage", + "vulnerability": results["ihh_isg_combined"][assembly_id]["vulnerability"], + "operation_id": assembly_id + }) + + # Fabrication (si présent) + manufacturing_id = component["manufacturing"] + if manufacturing_id and manufacturing_id in results["ihh_isg_combined"]: + path_vulnerabilities.append({ + "type": "fabrication", + "vulnerability": results["ihh_isg_combined"][manufacturing_id]["vulnerability"], + "operation_id": manufacturing_id + }) + + # Minerai (ICS+IVC) + if mineral_id in results["ics_ivc_combined"]: + path_vulnerabilities.append({ + "type": "minerai", + "vulnerability": results["ics_ivc_combined"][mineral_id]["vulnerability"], + "mineral_id": mineral_id + }) + + # Extraction (si présent) + extraction_id = mineral["extraction"] + if extraction_id and extraction_id in results["ihh_isg_combined"]: + path_vulnerabilities.append({ + "type": "extraction", + "vulnerability": results["ihh_isg_combined"][extraction_id]["vulnerability"], + "operation_id": extraction_id + }) + + # Traitement (si présent) + treatment_id = mineral["treatment"] + if treatment_id and treatment_id in results["ihh_isg_combined"]: + path_vulnerabilities.append({ + "type": "traitement", + "vulnerability": results["ihh_isg_combined"][treatment_id]["vulnerability"], + "operation_id": treatment_id + }) + + # Classifier le chemin + path_info = { + "product": product_id, + "component": component_id, + "mineral": mineral_id, + "vulnerabilities": path_vulnerabilities + } + + # Déterminer le niveau de risque du chemin + critical_count = path_vulnerabilities.count({"vulnerability": "ÉLEVÉE à CRITIQUE"}) + medium_count = path_vulnerabilities.count({"vulnerability": "MOYENNE"}) + + if any(v["vulnerability"] == "ÉLEVÉE à CRITIQUE" for v in path_vulnerabilities): + path_info["risk_level"] = "critique" + elif medium_count >= 3: + path_info["risk_level"] = "majeur" + elif any(v["vulnerability"] == "MOYENNE" for v in path_vulnerabilities): + path_info["risk_level"] = "moyen" + else: + path_info["risk_level"] = "faible" + + results["chains"].append(path_info) + + return results diff --git a/batch_ia/utils/ia.py b/batch_ia/utils/ia.py new file mode 100644 index 0000000..d22f7ea --- /dev/null +++ b/batch_ia/utils/ia.py @@ -0,0 +1,286 @@ +import re +from pathlib import Path +import requests +import json +import time +import zipfile +import streamlit as st + +from nettoyer_pgpt import ( + delete_documents_by_criteria +) + +from utils.config import ( + TEMP_SECTIONS, + session_uuid, + API_URL, PROMPT_METHODOLOGIE +) + +def ingest_document(file_path: Path) -> bool: + """Ingère un document dans PrivateGPT""" + try: + with open(file_path, "rb") as f: + file_name = file_path.name + + files = {"file": (file_name, f, "text/markdown")} + # Ajouter des métadonnées pour identifier facilement ce fichier d'entrée + metadata = { + "type": "input_file", + "session_id": session_uuid, + "document_type": "rapport_analyse_input" + } + response = requests.post( + f"{API_URL}/ingest/file", + files=files, + data={"metadata": json.dumps(metadata)} if "metadata" in requests.get(f"{API_URL}/ingest/file").text else None + ) + response.raise_for_status() + print(f"✅ Document '{file_path}' ingéré avec succès sous le nom '{file_name}'") + return True + except FileNotFoundError: + print(f"❌ Fichier '{file_path}' introuvable") + return False + except requests.RequestException as e: + print(f"❌ Erreur lors de l'ingestion du document: {e}") + return False + +def generate_text(input_file, full_prompt, system_message, temperature = "0.3", use_context = True): + """Génère du texte avec l'API PrivateGPT""" + try: + + # Définir les paramètres de la requête + payload = { + "messages": [ + {"role": "system", "content": system_message}, + {"role": "user", "content": full_prompt} + ], + "use_context": use_context, # Active la recherche RAG dans les documents ingérés + "temperature": temperature, # Température réduite pour plus de cohérence + "stream": False + } + + # Tenter d'ajouter un filtre de contexte (fonctionnalité expérimentale qui peut ne pas être supportée) + if input_file: + try: + # Vérifier si le filtre de contexte est supporté sans faire de requête supplémentaire + liste_des_fichiers = list(TEMP_SECTIONS.glob(f"*{session_uuid}*.md")) + filter_metadata = { + "document_name": [input_file.name] + [f.name for f in liste_des_fichiers] + } + payload["filter_metadata"] = filter_metadata + except Exception as e: + print(f"ℹ️ Remarque: Impossible d'appliquer le filtre de contexte: {e}") + + # Envoyer la requête + response = requests.post( + f"{API_URL}/chat/completions", + json=payload, + headers={"accept": "application/json"} + ) + response.raise_for_status() + + # Extraire la réponse générée + result = response.json() + if "choices" in result and len(result["choices"]) > 0: + return result["choices"][0]["message"]["content"] + else: + print("⚠️ Format de réponse inattendu:", json.dumps(result, indent=2)) + return None + + except requests.RequestException as e: + print(f"❌ Erreur lors de la génération de texte: {e}") + if hasattr(e, 'response') and e.response is not None: + print(f"Détails: {e.response.text}") + return None + +def ia_analyse(file_names): + for file in file_names: + ingest_document(file) + time.sleep(5) + + reponse = {} + for file in file_names: + produit_final = re.search(r"chemins critiques (.+)\.md$", file.name).group(1) + + # Préparer le prompt avec le contexte précédent si disponible et demandé + full_prompt = f""" + Rédigez une synthèse du fichier {file.name} dédiée au produit final '{produit_final}'. + Cette synthèse, destinée spécifiquement au Directeur des Risques, membre du COMEX d'une grande entreprise utilisant ce produit, doit être claire et concise (environ 10 lignes). + + En utilisant impérativement la méthodologie fournie, expliquez en termes simples mais précis, pourquoi et comment les vulnérabilités identifiées constituent un risque concret pour l'entreprise. Mentionnez clairement : + + - Les composants spécifiques du produit '{produit_final}' concernés par ces vulnérabilités. + - Les minerais précis responsables de ces vulnérabilités et leur rôle dans l’impact sur les composants. + - Les points critiques exacts identifiés dans la chaîne d'approvisionnement (par exemple : faible substituabilité, forte concentration géographique, instabilité géopolitique, concurrence élevée entre secteurs industriels). + - Identifier autant que faire se peut, les pays générant la forte concentration géographiques ou qui sont sujet à instabilité géopolitique, les secteurs en concurrence avec le numérique pour les minerais. + + Respectez strictement les consignes suivantes : + + - N'utilisez aucun acronyme ni valeur numérique ; uniquement leur équivalent textuel (ex : criticité de substituabilité, vulnérabilité élevée ou critique, etc.). + - N'incluez à ce stade aucune préconisation ni recommandation. + + Votre texte doit être parfaitement adapté à une compréhension rapide par des dirigeants d’entreprise. + """ + + + # Définir les paramètres de la requête + system_message = f""" + Vous êtes un assistant stratégique expert chargé de rédiger des synthèses destinées à des décideurs de très haut niveau (Directeurs des Risques, membres du COMEX, stratèges industriels). Vous analysez exclusivement les vulnérabilités systémiques affectant les produits numériques, à partir des données précises fournies dans le fichier {file.name}. + + Votre analyse doit être rigoureuse, accessible, pertinente pour la prise de décision stratégique, et conforme à la méthodologie définie ci-dessous : + + {PROMPT_METHODOLOGIE} + + /no_think + """ + + reponse[produit_final] = f"\n**{produit_final}**\n\n" + generate_text(file, full_prompt, system_message).split("")[-1].strip() + # print(reponse[produit_final]) + + corps = "\n\n".join(reponse.values()) + print("Corps") + + st.session_state["step"] = 2 + + full_prompt = corps + "\n\n" + PROMPT_METHODOLOGIE + + system_message = """ + Vous êtes un expert en rédaction de rapports stratégiques destinés à un COMEX ou une Direction des Risques. + + Votre mission est d'écrire une introduction professionnelle, claire et synthétique (maximum 7 lignes) à partir des éléments suivants : + 1. Un corps d’analyse décrivant les vulnérabilités identifiées pour un produit numérique. + 2. La méthodologie détaillée utilisée pour cette analyse (fourni en deuxième partie). + + Votre introduction doit : + - Présenter brièvement le sujet traité (vulnérabilités du produit final), quels sont les produits finaux et les minerais concernés. + - Annoncer clairement le contenu et l'objectif de l'analyse présentée dans le corps. + - Résumer succinctement les axes méthodologiques principaux (concentration géographique ou industrielle, stabilité géopolitique, criticité de substituabilité, concurrence intersectorielle des minerais). + - Être facilement compréhensible par des décideurs de haut niveau (pas d'acronymes, ni chiffres ; uniquement des formulations textuelles). + - Être fluide, agréable à lire, avec un ton sobre et professionnel. + + Répondez uniquement avec l'introduction rédigée. Ne fournissez aucune autre explication complémentaire. + + /no_think + """ + + + introduction = generate_text("", full_prompt, system_message).split("")[-1].strip() + print("Introduction") + + st.session_state["step"] = 3 + + full_prompt = corps + "\n\n" + PROMPT_METHODOLOGIE + + system_message = """ + Vous êtes un expert stratégique en gestion des risques liés à la chaîne de valeur numérique. Vous conseillez directement le COMEX et la Direction des Risques de grandes entreprises utilisatrices de produits numériques. Ces entreprises n'ont pour levier d’action que le choix de leurs fournisseurs ou l'allongement de la durée de vie de leur matériel. + + À partir des vulnérabilités identifiées dans la première partie du prompt (corps d'analyse) et en tenant compte du contexte et de la méthodologie décrite en deuxième partie, rédigez un texte clair, structuré en deux parties distinctes : + + 1. **Préconisations stratégiques :** + Proposez clairement des axes concrets pour limiter les risques identifiés dans l’analyse. Ces préconisations doivent impérativement être réalistes et directement actionnables par les dirigeants compte tenu de leurs leviers limités. + + 2. **Indicateurs de suivi :** + Identifiez précisément les indicateurs pertinents à suivre pour évaluer régulièrement l’évolution de ces risques. Ces indicateurs doivent être inspirés directement des axes méthodologiques fournis (concentration géographique, stabilité géopolitique, substituabilité, concurrence intersectorielle) ou s’appuyer sur des bonnes pratiques reconnues. + + Votre rédaction doit être fluide, concise, très professionnelle, et directement accessible à un COMEX. Évitez strictement toute explication complémentaire ou ajout superflu. Ne proposez que le texte demandé. + """ + + preconisations = generate_text("", full_prompt, system_message, "0.5").split("")[-1].strip() + print("Préconisations") + + st.session_state["step"] = 4 + + full_prompt = corps + "\n\n" + preconisations + system_message = """ + Vous êtes un expert stratégique spécialisé dans les risques liés à la chaîne de valeur du numérique. Vous conseillez directement le COMEX et la Direction des Risques de grandes entreprises dépendantes du numérique, dont les leviers d’action se limitent au choix des fournisseurs et à l’allongement de la durée d’utilisation du matériel. + + À partir du résultat de l'analyse des vulnérabilités présenté en première partie du prompt (corps) et des préconisations stratégiques formulées en deuxième partie, rédigez une conclusion synthétique et percutante (environ 6 à 8 lignes maximum) afin de : + + - Résumer clairement les principaux risques identifiés. + - Souligner brièvement les axes prioritaires proposés pour agir concrètement. + - Inviter de manière dynamique le COMEX à passer immédiatement à l'action. + + Votre rédaction doit être fluide, professionnelle, claire et immédiatement exploitable par des dirigeants. Ne fournissez aucune explication supplémentaire. Ne répondez que par la conclusion demandée. + + /no_think + """ + + conclusion = generate_text("", full_prompt, system_message, "0.7").split("")[-1].strip() + print("Conclusion") + + st.session_state["step"] = 5 + + analyse = "# Rapport d'analyse\n\n" + \ + "\n\n## Introduction\n\n" + \ + introduction + \ + "\n\n## Analyse des produits finaux\n\n" + \ + corps + \ + "\n\n## Préconisations\n\n" + \ + preconisations + \ + "\n\n## Conclusion\n\n" + \ + conclusion + \ + "\n\n## Méthodologie\n\n" + \ + PROMPT_METHODOLOGIE + + # fichier_a_reviser = Path(TEMPLATE_PATH.name.replace(".md", " - analyse à relire.md")) + # write_report(analyse, TEMP_SECTIONS / fichier_a_reviser) + # ingest_document(TEMP_SECTIONS / fichier_a_reviser) + + full_prompt = """ + Suivre scrupuleusement les consignes. + """ + + system_message = f""" + Vous êtes un réviseur professionnel expert en écriture stratégique, maîtrisant parfaitement la langue française et habitué à réviser des textes destinés à des dirigeants de haut niveau (COMEX). + + Votre tâche unique est d'améliorer strictement la qualité rédactionnelle du texte suivant, sans modifier en aucune manière : + - la structure existante (sections, titres, sous-titres), + - l'ordre des paragraphes et des idées, + - le sens précis du contenu original, + - sans ajouter aucune information nouvelle. + + Votre révision doit impérativement respecter les points suivants : + - Éliminer toutes répétitions ou redondances et varier systématiquement les tournures entre les paragraphes. + - Rendre chaque phrase claire, directe et concise. Si une phrase est trop longue, scindez-la clairement en plusieurs phrases courtes. + - Structurer chaque paragraphe en 2 à 3 parties cohérentes, reliées entre elles par des termes logiques (coordination, implication, opposition, etc.) et séparées par des retours à la ligne. + - Remplacer systématiquement les acronymes par ces expressions précises : + - ICS → « capacité à substituer un minerai » + - IHH → « concentration géographique ou industrielle » + - ISG → « stabilité géopolitique » + - IVC → « concurrence intersectorielle pour les minerais » + + Votre texte final doit être parfaitement fluide, agréable à lire, adapté à un COMEX, avec un ton professionnel et sobre. + + **Important : Ne répondez strictement que par le texte révisé ci-dessous, sans aucun commentaire ou explication supplémentaire.** + + Voici le texte à réviser précisément : + + {analyse} + + /no_think + """ + revision = generate_text("", full_prompt, system_message, "0.1", False).split("")[-1].strip() + print("Relecture") + + return revision + +def supprimer_fichiers(session_uuid): + try: + delete_documents_by_criteria(session_uuid) + for temp_file in TEMP_SECTIONS.glob(f"*{session_uuid}*.md"): + temp_file.unlink() + return True + except: + return False + +def generer_rapport_final(rapport, analyse, resultat): + try: + rapport = Path(rapport) + analyse = Path(analyse) + with zipfile.ZipFile(resultat, "w") as zipf: + zipf.write(rapport, arcname=rapport.name) + zipf.write(analyse, arcname=analyse.name) + return True + except Exception as e: + print(f"Erreur lors du zip : {e}") + return False diff --git a/batch_ia/utils/sections.py b/batch_ia/utils/sections.py new file mode 100644 index 0000000..59d5a15 --- /dev/null +++ b/batch_ia/utils/sections.py @@ -0,0 +1,770 @@ +import os +import re + +from .config import ( + CORPUS_DIR, + TEMPLATE_PATH, + determine_threshold_color +) + +from .files import ( + find_prefixed_directory, + find_corpus_file, + write_report, + read_corpus_file +) + +from .sections_utils import ( + trouver_dossier_composant, + extraire_sections_par_mot_cle +) + +def generate_introduction_section(data): + """ + Génère la section d'introduction du rapport. + """ + products = [p["label"] for p in data["products"].values()] + components = [c["label"] for c in data["components"].values()] + minerals = [m["label"] for m in data["minerals"].values()] + + template = [] + template.append("## Introduction\n") + template.append("Ce rapport analyse les vulnérabilités de la chaîne de fabrication du numérique pour :\n") + + template.append("* les produits finaux : " + ", ".join(products)) + template.append("* les composants : " + ", ".join(components)) + template.append("* les minerais : " + ", ".join(minerals) + "\n") + + return "\n".join(template) + +def generate_methodology_section(): + """ + Génère la section méthodologie du rapport. + """ + template = [] + template.append("## Méthodologie d'analyse des risques\n") + template.append("### Indices et seuils\n") + template.append("La méthode d'évaluation intègre 4 indices et leurs combinaisons pour identifier les chemins critiques.\n") + + # IHH + template.append("#### IHH (Herfindahl-Hirschmann) : concentration géographiques ou industrielle d'une opération\n") + + # Essayer d'abord avec le chemin exact + ihh_context_file = "Criticités/Fiche technique IHH/00-contexte-et-objectif.md" + if os.path.exists(os.path.join(CORPUS_DIR, ihh_context_file)): + template.append(read_corpus_file(ihh_context_file, remove_first_title=True)) + else: + # Fallback à la recherche par motif + ihh_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique IHH") + if ihh_context_file: + template.append(read_corpus_file(ihh_context_file, remove_first_title=True)) + + # Essayer d'abord avec le chemin exact + ihh_calc_file = "Criticités/Fiche technique IHH/01-mode-de-calcul/_intro.md" + if os.path.exists(os.path.join(CORPUS_DIR, ihh_calc_file)): + template.append(read_corpus_file(ihh_calc_file, remove_first_title=True)) + else: + # Fallback à la recherche par motif + ihh_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique IHH") + if ihh_calc_file: + template.append(read_corpus_file(ihh_calc_file, remove_first_title=True)) + + template.append(" * Seuils : <15 = Vert (Faible), 15-25 = Orange (Modérée), >25 = Rouge (Élevée)\n") + + # ISG + template.append("#### ISG (Stabilité Géopolitique) : stabilité des pays\n") + + # Essayer d'abord avec le chemin exact + isg_context_file = "Criticités/Fiche technique ISG/00-contexte-et-objectif.md" + if os.path.exists(os.path.join(CORPUS_DIR, isg_context_file)): + template.append(read_corpus_file(isg_context_file, remove_first_title=True)) + else: + # Fallback à la recherche par motif + isg_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique ISG") + if isg_context_file: + template.append(read_corpus_file(isg_context_file, remove_first_title=True)) + + # Essayer d'abord avec le chemin exact + isg_calc_file = "Criticités/Fiche technique ISG/01-mode-de-calcul/_intro.md" + if os.path.exists(os.path.join(CORPUS_DIR, isg_calc_file)): + template.append(read_corpus_file(isg_calc_file, remove_first_title=True)) + else: + # Fallback à la recherche par motif + isg_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique ISG") + if isg_calc_file: + template.append(read_corpus_file(isg_calc_file, remove_first_title=True)) + + template.append(" * Seuils : <40 = Vert (Stable), 40-60 = Orange, >60 = Rouge (Instable)\n") + + # ICS + template.append("#### ICS (Criticité de Substituabilité) : capacité à remplacer / substituer un élément\n") + + # Essayer d'abord avec le chemin exact + ics_context_file = "Criticités/Fiche technique ICS/00-contexte-et-objectif.md" + if os.path.exists(os.path.join(CORPUS_DIR, ics_context_file)): + template.append(read_corpus_file(ics_context_file, remove_first_title=True)) + else: + # Fallback à la recherche par motif + ics_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique ICS") + if ics_context_file: + template.append(read_corpus_file(ics_context_file, remove_first_title=True)) + + # Essayer d'abord avec le chemin exact + ics_calc_file = "Criticités/Fiche technique ICS/01-mode-de-calcul/_intro.md" + if os.path.exists(os.path.join(CORPUS_DIR, ics_calc_file)): + template.append(read_corpus_file(ics_calc_file, remove_first_title=True)) + else: + # Fallback à la recherche par motif + ics_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique ICS") + if ics_calc_file: + template.append(read_corpus_file(ics_calc_file, remove_first_title=True)) + + template.append(" * Seuils : <0.3 = Vert (Facile), 0.3-0.6 = Orange (Moyenne), >0.6 = Rouge (Difficile)\n") + + # IVC + template.append("#### IVC (Vulnérabilité de Concurrence) : pression concurrentielle avec d'autres secteurs\n") + + # Essayer d'abord avec le chemin exact + ivc_context_file = "Criticités/Fiche technique IVC/00-contexte-et-objectif.md" + if os.path.exists(os.path.join(CORPUS_DIR, ivc_context_file)): + template.append(read_corpus_file(ivc_context_file, remove_first_title=True)) + else: + # Fallback à la recherche par motif + ivc_context_file = find_corpus_file("contexte-et-objectif", "Criticités/Fiche technique IVC") + if ivc_context_file: + template.append(read_corpus_file(ivc_context_file, remove_first_title=True)) + + # Essayer d'abord avec le chemin exact + ivc_calc_file = "Criticités/Fiche technique IVC/01-mode-de-calcul/_intro.md" + if os.path.exists(os.path.join(CORPUS_DIR, ivc_calc_file)): + template.append(read_corpus_file(ivc_calc_file, remove_first_title=True)) + else: + # Fallback à la recherche par motif + ivc_calc_file = find_corpus_file("mode-de-calcul/_intro", "Criticités/Fiche technique IVC") + if ivc_calc_file: + template.append(read_corpus_file(ivc_calc_file, remove_first_title=True)) + + template.append(" * Seuils : <5 = Vert (Faible), 5-15 = Orange (Modérée), >15 = Rouge (Forte)\n") + + # Combinaison des indices + template.append("### Combinaison des indices\n") + + # IHH et ISG + template.append("**IHH et ISG**\n") + template.append("Ces deux indices s'appliquent à toutes les opérations et se combinent dans l'évaluation du risque (niveau d'impact et probabilité de survenance) :\n") + template.append("* l'IHH donne le niveau d'impact => une forte concentration implique un fort impact si le risque est avéré") + template.append("* l'ISG donne la probabilité de survenance => plus les pays sont instables (et donc plus l'ISG est élevé) et plus la survenance du risque est élevée\n") + + template.append("Pour évaluer le risque pour une opération, les ISG des pays sont pondérés par les parts de marché respectives pour donner un ISG combiné dont le calcul est :") + template.append("ISG_combiné = (Somme des ISG des pays multipliée par leur part de marché) / Sommes de leur part de marché\n") + + template.append("On établit alors une matrice (Vert = 1, Orange = 2, Rouge = 3) et en faisant le produit des poids de l'ISG combiné et de l'IHH\n") + + template.append("| ISG combiné / IHH | Vert | Orange | Rouge |") + template.append("| :-- | :-- | :-- | :-- |") + template.append("| Vert | 1 | 2 | 3 |") + template.append("| Orange | 2 | 4 | 6 |") + template.append("| Rouge | 3 | 6 | 9 |\n") + + template.append("Les vulnérabilités se classent en trois niveaux pour chaque opération :\n") + template.append("* Vulnérabilité combinée élevée à critique : poids 6 et 9") + template.append("* Vulnérabilité combinée moyenne : poids 3 et 4") + template.append("* Vulnérabilité combinée faible : poids 1 et 2\n") + + # ICS et IVC + template.append("**ICS et IVC**\n") + template.append("Ces deux indices se combinent dans l'évaluation du risque pour un minerai :\n") + template.append("* l'ICS donne le niveau d'impact => une faible substituabilité (et donc un ICS élevé) implique un fort impact si le risque est avéré ; l'ICS est associé à la relation entre un composant et un minerai") + template.append("* l'IVC donne la probabilité de l'impact => une forte concurrence intersectorielle (IVC élevé) implique une plus forte probabilité de survenance\n") + + template.append("Par simplification, on intègre un ICS moyen d'un minerai comme étant la moyenne des ICS pour chacun des composants dans lesquels il intervient.\n") + + template.append("On établit alors une matrice (Vert = 1, Orange = 2, Rouge = 3) et en faisant le produit des poids de l'ICS moyen et de l'IVC.\n") + + template.append("| ICS_moyen / IVC | Vert | Orange | Rouge |") + template.append("| :-- | :-- | :-- | :-- |") + template.append("| Vert | 1 | 2 | 3 |") + template.append("| Orange | 2 | 4 | 6 |") + template.append("| Rouge | 3 | 6 | 9 |\n") + + template.append("Les vulnérabilités se classent en trois niveaux pour chaque minerai :\n") + template.append("* Vulnérabilité combinée élevée à critique : poids 6 et 9") + template.append("* Vulnérabilité combinée moyenne : poids 3 et 4") + template.append("* Vulnérabilité combinée faible : poids 1 et 2\n") + + return "\n".join(template) + +def generate_operations_section(data, results, config): + """ + Génère la section détaillant les opérations (assemblage, fabrication, extraction, traitement). + """ + # # print("DEBUG: Génération de la section des opérations") + # # print(f"DEBUG: Nombre de produits: {len(data['products'])}") + # # print(f"DEBUG: Nombre de composants: {len(data['components'])}") + # # print(f"DEBUG: Nombre d'opérations: {len(data['operations'])}") + + template = [] + template.append("## Détails des opérations\n") + + # 1. Traiter les produits finaux (assemblage) + for product_id, product in data["products"].items(): + # # print(f"DEBUG: Produit {product_id} ({product['label']}), assembly = {product['assembly']}") + if product["assembly"]: + template.append(f"### {product['label']} et Assemblage\n") + + # Récupérer la présentation synthétique + # product_slug = product['label'].lower().replace(' ', '-') + sous_repertoire = f"{product['label']}" + if product["level"] == 0: + type = "Assemblage" + else: + type = "Connexe" + sous_repertoire = trouver_dossier_composant(sous_repertoire, type, "Fiche assemblage ") + product_slug = sous_repertoire.split(' ', 2)[2] + presentation_file = find_corpus_file("présentation-synthétique", f"{type}/Fiche assemblage {product_slug}") + if presentation_file: + template.append(read_corpus_file(presentation_file, remove_first_title=True)) + template.append("") + + # Récupérer les principaux assembleurs + assembleurs_file = find_corpus_file("principaux-assembleurs", f"{type}/Fiche assemblage {product_slug}") + if assembleurs_file: + template.append(read_corpus_file(assembleurs_file, shift_titles=2)) + template.append("") + + # ISG des pays impliqués + assembly_id = product["assembly"] + operation = data["operations"][assembly_id] + + template.append("##### ISG des pays impliqués\n") + template.append("| Pays | Part de marché | ISG | Criticité |") + template.append("| :-- | :-- | :-- | :-- |") + + isg_weighted_sum = 0 + total_share = 0 + + for country_id, share in operation["countries"].items(): + country = data["countries"][country_id] + geo_country = country.get("geo_country") + + if geo_country and geo_country in data["geo_countries"]: + isg_value = data["geo_countries"][geo_country]["isg"] + color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds')) + template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |") + + isg_weighted_sum += isg_value * share + total_share += share + + # Calculer ISG combiné + if total_share > 0: + isg_combined = isg_weighted_sum / total_share + color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds')) + template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**") + + # IHH + ihh_file = find_corpus_file("matrice-des-risques-liés-à-l-assemblage/indice-de-herfindahl-hirschmann", f"{type}/Fiche assemblage {product_slug}") + if ihh_file: + template.append(read_corpus_file(ihh_file, shift_titles=1)) + template.append("\n") + + # Vulnérabilité combinée + if assembly_id in results["ihh_isg_combined"]: + combined = results["ihh_isg_combined"][assembly_id] + template.append("#### Vulnérabilité combinée IHH-ISG\n") + template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})") + template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})") + template.append(f"* Poids combiné: {combined['combined_weight']}") + template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") + + # 2. Traiter les composants (fabrication) + for component_id, component in data["components"].items(): + # # print(f"DEBUG: Composant {component_id} ({component['label']}), manufacturing = {component['manufacturing']}") + if component["manufacturing"]: + template.append(f"### {component['label']} et Fabrication\n") + + # Récupérer la présentation synthétique + # component_slug = component['label'].lower().replace(' ', '-') + sous_repertoire = f"{component['label']}" + sous_repertoire = trouver_dossier_composant(sous_repertoire, "Fabrication", "Fiche fabrication ") + component_slug = sous_repertoire.split(' ', 2)[2] + presentation_file = find_corpus_file("présentation-synthétique", f"Fabrication/Fiche fabrication {component_slug}") + if presentation_file: + template.append(read_corpus_file(presentation_file, remove_first_title=True)) + template.append("\n") + + # Récupérer les principaux fabricants + fabricants_file = find_corpus_file("principaux-fabricants", f"Fabrication/Fiche fabrication {component_slug}") + if fabricants_file: + template.append(read_corpus_file(fabricants_file, shift_titles=2)) + template.append("\n") + + # ISG des pays impliqués + manufacturing_id = component["manufacturing"] + operation = data["operations"][manufacturing_id] + + template.append("##### ISG des pays impliqués\n") + template.append("| Pays | Part de marché | ISG | Criticité |") + template.append("| :-- | :-- | :-- | :-- |") + + isg_weighted_sum = 0 + total_share = 0 + + for country_id, share in operation["countries"].items(): + country = data["countries"][country_id] + geo_country = country.get("geo_country") + + if geo_country and geo_country in data["geo_countries"]: + isg_value = data["geo_countries"][geo_country]["isg"] + color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds')) + template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |") + + isg_weighted_sum += isg_value * share + total_share += share + + # Calculer ISG combiné + if total_share > 0: + isg_combined = isg_weighted_sum / total_share + color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds')) + template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n\n") + + # IHH + ihh_file = find_corpus_file("matrice-des-risques-liés-à-la-fabrication/indice-de-herfindahl-hirschmann", f"Fabrication/Fiche fabrication {component_slug}") + if ihh_file: + template.append(read_corpus_file(ihh_file, shift_titles=1)) + template.append("\n") + + # Vulnérabilité combinée + if manufacturing_id in results["ihh_isg_combined"]: + combined = results["ihh_isg_combined"][manufacturing_id] + template.append("#### Vulnérabilité combinée IHH-ISG\n") + template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})") + template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})") + template.append(f"* Poids combiné: {combined['combined_weight']}") + template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") + + # 3. Traiter les minerais (détaillés dans une section séparée) + + result = "\n".join(template) + # # print(f"DEBUG: Fin de génération de la section des opérations. Taille: {len(result)} caractères") + if len(result) <= 30: # Juste le titre de section + # # print("DEBUG: ALERTE - La section des opérations est vide ou presque vide!") + # Ajout d'une section de débogage dans le rapport + template.append("### DÉBOGAGE - Opérations manquantes\n") + template.append("Aucune opération d'assemblage ou de fabrication n'a été trouvée dans les données.\n") + template.append("Informations disponibles:\n") + template.append(f"* Nombre de produits: {len(data['products'])}\n") + template.append(f"* Nombre de composants: {len(data['components'])}\n") + template.append(f"* Nombre d'opérations: {len(data['operations'])}\n") + template.append("\nDétail des produits et de leurs opérations d'assemblage:\n") + for pid, p in data["products"].items(): + template.append(f"* {p['label']}: {'Assemblage: ' + str(p['assembly']) if p['assembly'] else 'Pas d\'assemblage'}\n") + template.append("\nDétail des composants et de leurs opérations de fabrication:\n") + for cid, c in data["components"].items(): + template.append(f"* {c['label']}: {'Fabrication: ' + str(c['manufacturing']) if c['manufacturing'] else 'Pas de fabrication'}\n") + result = "\n".join(template) + + return result + +def generate_minerals_section(data, results, config): + """ + Génère la section détaillant les minerais et leurs opérations d'extraction et traitement. + """ + template = [] + template.append("## Détails des minerais\n") + + for mineral_id, mineral in data["minerals"].items(): + mineral_slug = mineral['label'].lower().replace(' ', '-') + fiche_dir = f"{CORPUS_DIR}/Minerai/Fiche minerai {mineral_slug}" + if not os.path.exists(fiche_dir): + continue + + template.append(f"---\n\n### {mineral['label']}\n") + + # Récupérer la présentation synthétique + presentation_file = find_corpus_file("présentation-synthétique", f"Minerai/Fiche minerai {mineral_slug}") + if presentation_file: + template.append(read_corpus_file(presentation_file, remove_first_title=True)) + template.append("\n") + + # ICS + template.append("#### ICS\n") + + ics_intro_file = find_corpus_file("risque-de-substituabilité/_intro", f"Minerai/Fiche minerai {mineral_slug}") + if ics_intro_file: + template.append(read_corpus_file(ics_intro_file, remove_first_title=True)) + template.append("\n") + + # Calcul de l'ICS moyen + ics_values = list(mineral["ics_values"].values()) + if ics_values: + ics_average = sum(ics_values) / len(ics_values) + color, suffix = determine_threshold_color(ics_average, "ICS", config.get('thresholds')) + + template.append("##### Valeurs d'ICS par composant\n") + template.append("| Composant | ICS | Criticité |") + template.append("| :-- | :-- | :-- |") + + for comp_id, ics_value in mineral["ics_values"].items(): + comp_name = data["components"][comp_id]["label"] + comp_color, comp_suffix = determine_threshold_color(ics_value, "ICS", config.get('thresholds')) + template.append(f"| {comp_name} | {ics_value:.2f} | {comp_color} ({comp_suffix}) |") + + template.append(f"\n**ICS moyen : {ics_average:.2f} - {color} ({suffix})**\n") + + # IVC + template.append("#### IVC\n\n") + + # Valeur IVC + ivc_value = mineral.get("ivc", 0) + color, suffix = determine_threshold_color(ivc_value, "IVC", config.get('thresholds')) + template.append(f"**IVC: {ivc_value} - {color} ({suffix})**\n") + + # Récupérer toutes les sections de vulnérabilité de concurrence + ivc_sections = [] + ivc_dir = find_prefixed_directory("vulnérabilité-de-concurrence", f"Minerai/Fiche minerai {mineral_slug}") + corpus_path = os.path.join(CORPUS_DIR, ivc_dir) if os.path.exists(os.path.join(CORPUS_DIR, ivc_dir)) else None + if corpus_path: + for file in sorted(os.listdir(corpus_path)): + if file.endswith('.md') and "_intro.md" not in file and "sources" not in file: + ivc_sections.append(os.path.join(ivc_dir, file)) + + # Inclure chaque section IVC + for section_file in ivc_sections: + content = read_corpus_file(section_file, remove_first_title=False) + # Nettoyer les balises des fichiers IVC + content = re.sub(r'```.*?```', '', content, flags=re.DOTALL) + + # Mettre le titre en italique s'il commence par un # (format Markdown pour titre) + if content and '\n' in content: + first_line, rest = content.split('\n', 1) + if first_line.strip().startswith('#'): + # Extraire le texte du titre sans les # et les espaces + title_text = first_line.strip().lstrip('#').strip() + content = f"\n*{title_text}*\n{rest.strip()}" + + # Ne pas ajouter de contenu vide + if content.strip(): + template.append(content.strip()) + + # ICS et IVC combinés + if mineral_id in results["ics_ivc_combined"]: + combined = results["ics_ivc_combined"][mineral_id] + template.append("\n#### Vulnérabilité combinée ICS-IVC\n") + template.append(f"* ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']} ({combined['ics_suffix']})") + template.append(f"* IVC: {combined['ivc_value']} - {combined['ivc_color']} ({combined['ivc_suffix']})") + template.append(f"* Poids combiné: {combined['combined_weight']}") + template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") + + # Extraction + if mineral["extraction"]: + template.append("#### Extraction\n") + + # Récupérer les principaux producteurs + producers_file = find_corpus_file("principaux-producteurs-extraction", f"Minerai/Fiche minerai {mineral_slug}") + if producers_file: + template.append(read_corpus_file(producers_file, remove_first_title=True)) + template.append("\n") + + # ISG des pays impliqués + extraction_id = mineral["extraction"] + operation = data["operations"][extraction_id] + + template.append("##### ISG des pays impliqués\n") + template.append("| Pays | Part de marché | ISG | Criticité |") + template.append("| :-- | :-- | :-- | :-- |") + + isg_weighted_sum = 0 + total_share = 0 + + for country_id, share in operation["countries"].items(): + country = data["countries"][country_id] + geo_country = country.get("geo_country") + + if geo_country and geo_country in data["geo_countries"]: + isg_value = data["geo_countries"][geo_country]["isg"] + color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds')) + template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |") + + isg_weighted_sum += isg_value * share + total_share += share + + # Calculer ISG combiné + if total_share > 0: + isg_combined = isg_weighted_sum / total_share + color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds')) + template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n") + + # IHH extraction + ihh_file = find_corpus_file("matrice-des-risques/indice-de-herfindahl-hirschmann-extraction", f"Minerai/Fiche minerai {mineral_slug}") + if ihh_file: + template.append(read_corpus_file(ihh_file, shift_titles=1)) + template.append("\n") + + # Vulnérabilité combinée + if extraction_id in results["ihh_isg_combined"]: + combined = results["ihh_isg_combined"][extraction_id] + template.append("##### Vulnérabilité combinée IHH-ISG pour l'extraction\n") + template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})") + template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})") + template.append(f"* Poids combiné: {combined['combined_weight']}") + template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") + + # Traitement + if mineral["treatment"]: + template.append("#### Traitement\n") + + # Récupérer les principaux producteurs + producers_file = find_corpus_file("principaux-producteurs-traitement", f"Minerai/Fiche minerai {mineral_slug}") + if producers_file: + template.append(read_corpus_file(producers_file, remove_first_title=True)) + template.append("\n") + + # ISG des pays impliqués + treatment_id = mineral["treatment"] + operation = data["operations"][treatment_id] + + template.append("##### ISG des pays impliqués\n") + template.append("| Pays | Part de marché | ISG | Criticité |") + template.append("| :-- | :-- | :-- | :-- |") + + isg_weighted_sum = 0 + total_share = 0 + + for country_id, share in operation["countries"].items(): + country = data["countries"][country_id] + geo_country = country.get("geo_country") + + if geo_country and geo_country in data["geo_countries"]: + isg_value = data["geo_countries"][geo_country]["isg"] + color, suffix = determine_threshold_color(isg_value, "ISG", config.get('thresholds')) + template.append(f"| {country['label']} | {share}% | {isg_value} | {color} ({suffix}) |") + + isg_weighted_sum += isg_value * share + total_share += share + + # Calculer ISG combiné + if total_share > 0: + isg_combined = isg_weighted_sum / total_share + color, suffix = determine_threshold_color(isg_combined, "ISG", config.get('thresholds')) + template.append(f"\n**ISG combiné: {isg_combined:.0f} - {color} ({suffix})**\n") + + # IHH traitement + ihh_file = find_corpus_file("matrice-des-risques/indice-de-herfindahl-hirschmann-traitement", f"Minerai/Fiche minerai {mineral_slug}") + if ihh_file: + template.append(read_corpus_file(ihh_file, shift_titles=1)) + template.append("\n") + + # Vulnérabilité combinée + if treatment_id in results["ihh_isg_combined"]: + combined = results["ihh_isg_combined"][treatment_id] + template.append("##### Vulnérabilité combinée IHH-ISG pour le traitement\n") + template.append(f"* IHH: {combined['ihh_value']} - {combined['ihh_color']} ({combined['ihh_suffix']})") + template.append(f"* ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']} ({combined['isg_suffix']})") + template.append(f"* Poids combiné: {combined['combined_weight']}") + template.append(f"* Niveau de vulnérabilité: **{combined['vulnerability']}**\n") + + return "\n".join(template) + +def generate_critical_paths_section(data, results): + """ + Génère la section des chemins critiques. + """ + template = [] + template.append("## Chemins critiques\n") + + # Récupérer les chaînes par niveau de risque + critical_chains = [] + major_chains = [] + medium_chains = [] + + for chain in results["chains"]: + if chain["risk_level"] == "critique": + critical_chains.append(chain) + elif chain["risk_level"] == "majeur": + major_chains.append(chain) + elif chain["risk_level"] == "moyen": + medium_chains.append(chain) + + # 1. Chaînes critiques + template.append("### Chaînes avec risque critique\n") + template.append("*Ces chaînes comprennent au moins une vulnérabilité combinée élevée à critique*\n") + + if critical_chains: + for chain in critical_chains: + product_name = data["products"][chain["product"]]["label"] + component_name = data["components"][chain["component"]]["label"] + mineral_name = data["minerals"][chain["mineral"]]["label"] + + template.append(f"#### {product_name} → {component_name} → {mineral_name}\n") + + # Vulnérabilités + template.append("**Vulnérabilités identifiées:**\n") + for vuln in chain["vulnerabilities"]: + vuln_type = vuln["type"].capitalize() + vuln_level = vuln["vulnerability"] + + if vuln_type == "Minerai": + mineral_id = vuln["mineral_id"] + template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}") + if mineral_id in results["ics_ivc_combined"]: + combined = results["ics_ivc_combined"][mineral_id] + template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}") + template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}") + else: + op_id = vuln["operation_id"] + op_label = data["operations"][op_id]["label"] + template.append(f"* {vuln_type} ({op_label}): {vuln_level}") + if op_id in results["ihh_isg_combined"]: + combined = results["ihh_isg_combined"][op_id] + template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}") + template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}") + + template.append("\n") + else: + template.append("Aucune chaîne à risque critique identifiée.\n") + + # 2. Chaînes majeures + template.append("### Chaînes avec risque majeur\n") + template.append("*Ces chaînes comprennent au moins trois vulnérabilités combinées moyennes*\n") + + if major_chains: + for chain in major_chains: + product_name = data["products"][chain["product"]]["label"] + component_name = data["components"][chain["component"]]["label"] + mineral_name = data["minerals"][chain["mineral"]]["label"] + + template.append(f"#### {product_name} → {component_name} → {mineral_name}\n") + + # Vulnérabilités + template.append("**Vulnérabilités identifiées:**\n") + for vuln in chain["vulnerabilities"]: + vuln_type = vuln["type"].capitalize() + vuln_level = vuln["vulnerability"] + + if vuln_type == "Minerai": + mineral_id = vuln["mineral_id"] + template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}\n") + if mineral_id in results["ics_ivc_combined"]: + combined = results["ics_ivc_combined"][mineral_id] + template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}\n") + template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}\n") + else: + op_id = vuln["operation_id"] + op_label = data["operations"][op_id]["label"] + template.append(f"* {vuln_type} ({op_label}): {vuln_level}\n") + if op_id in results["ihh_isg_combined"]: + combined = results["ihh_isg_combined"][op_id] + template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}\n") + template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}\n") + + template.append("\n") + else: + template.append("Aucune chaîne à risque majeur identifiée.\n") + + # 3. Chaînes moyennes + template.append("### Chaînes avec risque moyen\n") + template.append("*Ces chaînes comprennent au moins une vulnérabilité combinée moyenne*\n") + + if medium_chains: + for chain in medium_chains: + product_name = data["products"][chain["product"]]["label"] + component_name = data["components"][chain["component"]]["label"] + mineral_name = data["minerals"][chain["mineral"]]["label"] + + template.append(f"#### {product_name} → {component_name} → {mineral_name}\n") + + # Vulnérabilités + template.append("**Vulnérabilités identifiées:**\n") + for vuln in chain["vulnerabilities"]: + vuln_type = vuln["type"].capitalize() + vuln_level = vuln["vulnerability"] + + if vuln_type == "Minerai": + mineral_id = vuln["mineral_id"] + template.append(f"* {vuln_type} ({mineral_name}): {vuln_level}\n") + if mineral_id in results["ics_ivc_combined"]: + combined = results["ics_ivc_combined"][mineral_id] + template.append(f" * ICS moyen: {combined['ics_average']:.2f} - {combined['ics_color']}\n") + template.append(f" * IVC: {combined['ivc_value']} - {combined['ivc_color']}\n") + else: + op_id = vuln["operation_id"] + op_label = data["operations"][op_id]["label"] + template.append(f"* {vuln_type} ({op_label}): {vuln_level}\n") + if op_id in results["ihh_isg_combined"]: + combined = results["ihh_isg_combined"][op_id] + template.append(f" * IHH: {combined['ihh_value']} - {combined['ihh_color']}\n") + template.append(f" * ISG combiné: {combined['isg_combined']:.0f} - {combined['isg_color']}\n") + + template.append("\n") + else: + template.append("Aucune chaîne à risque moyen identifiée.\n") + + return "\n".join(template) + +def slugify(text): + return re.sub(r'\W+', '-', text.strip()).strip('-').lower() + +def generate_report(data, results, config): + """ + Génère le rapport complet structuré selon les spécifications. + """ + # Titre principal + report_titre = ["# Évaluation des vulnérabilités critiques\n"] + + # Section d'introduction + report_introduction = generate_introduction_section(data) + # report.append(generate_introduction_section(data)) + + # Section méthodologie + report_methodologie = generate_methodology_section() + # report.append(generate_methodology_section()) + + # Section détails des opérations + report_operations = generate_operations_section(data, results, config) + # report.append(generate_operations_section(data, results, config)) + + # Section détails des minerais + report_minerals = generate_minerals_section(data, results, config) + # report.append(generate_minerals_section(data, results, config)) + + # Section chemins critiques + report_critical_paths = generate_critical_paths_section(data, results) + + suffixe = " - chemins critiques" + fichier = TEMPLATE_PATH.name.replace(".md", f"{suffixe}.md") + fichier_path = TEMPLATE_PATH.parent / fichier + # Élever les titres Markdown dans report_critical_paths + report_critical_paths = re.sub(r'^(#{2,})', lambda m: '#' * (len(m.group(1)) - 1), report_critical_paths, flags=re.MULTILINE) + write_report(report_critical_paths, fichier_path) + + # Récupérer les sections critiques décomposées par mot-clé + chemins_critiques_sections = extraire_sections_par_mot_cle(fichier_path) + + file_names = [] + + # Pour chaque mot-clé, écrire un fichier individuel + for mot_cle, contenu in chemins_critiques_sections.items(): + print(mot_cle) + mot_cle_slug = slugify(mot_cle) + suffixe = f" - chemins critiques {mot_cle_slug}" + fichier_personnalise = TEMPLATE_PATH.with_name( + TEMPLATE_PATH.name.replace(".md", f"{suffixe}.md") + ) + # Ajouter du texte au début du contenu + introduction = f"# Détail des chemins critiques pour : {mot_cle}\n\n" + contenu = introduction + contenu + write_report(contenu, fichier_personnalise) + file_names.append(fichier_personnalise) + # report.append(generate_critical_paths_section(data, results)) + + # Ordre de composition final + report = ( + report_titre + + [report_introduction] + + [report_critical_paths] + + [report_operations] + + [report_minerals] + + [report_methodologie] + ) + + return "\n".join(report), file_names diff --git a/batch_ia/utils/sections_utils.py b/batch_ia/utils/sections_utils.py new file mode 100644 index 0000000..5b27dc3 --- /dev/null +++ b/batch_ia/utils/sections_utils.py @@ -0,0 +1,92 @@ +import os +import re +from pathlib import Path +from collections import defaultdict + +from .config import ( + CORPUS_DIR +) + +def composant_match(nom_composant, nom_dossier): + """ + Vérifie si le nom du composant correspond approximativement à un nom de dossier (lettres et chiffres dans le même ordre). + """ + def clean(s): + return ''.join(c.lower() for c in s if c.isalnum()) + + cleaned_comp = clean(nom_composant) + cleaned_dir = clean(nom_dossier) + + # Vérifie que chaque caractère de cleaned_comp est présent dans cleaned_dir dans le bon ordre + it = iter(cleaned_dir) + return all(c in it for c in cleaned_comp) + +def trouver_dossier_composant(nom_composant, base_path, prefixe): + """ + Parcourt les sous-répertoires de base_path et retourne celui qui correspond au composant. + """ + search_path = os.path.join(CORPUS_DIR, base_path) + if not os.path.exists(search_path): + return None + + for d in os.listdir(search_path): + if os.path.isdir(os.path.join(search_path, d)): + if composant_match(f"{prefixe}{nom_composant}", d): + return os.path.join(base_path, d) + return None + +def extraire_sections_par_mot_cle(fichier_markdown: Path) -> dict: + """ + Extrait les sections de niveau 3 uniquement dans la section + '## Chaînes avec risque critique' du fichier Markdown, + et les regroupe par mot-clé (ce qui se trouve entre '### ' et ' →'). + Réduit chaque titre d’un niveau (#). + """ + with fichier_markdown.open(encoding="utf-8") as f: + contenu = f.read() + + # Extraire uniquement la section '## Chaînes avec risque critique' + match_section = re.search( + r"## Chaînes avec risque critique(.*?)(?=\n## |\Z)", contenu, re.DOTALL + ) + if not match_section: + return {} + + section_critique = match_section.group(1) + + # Extraire les mots-clés entre '### ' et ' →' + mots_cles = set(re.findall(r"^### (.+?) →", section_critique, re.MULTILINE)) + + # Extraire tous les blocs de niveau 3 dans cette section uniquement + blocs_sections = re.findall(r"(### .+?)(?=\n### |\n## |\Z)", section_critique, re.DOTALL) + + # Regrouper les blocs par mot-clé + regroupement = defaultdict(list) + for bloc in blocs_sections: + match = re.match(r"### (.+?) →", bloc) + if match: + mot = match.group(1) + if mot in mots_cles: + # Réduction du niveau des titres + bloc_modifie = re.sub(r"^###", "##", bloc, flags=re.MULTILINE) + bloc_modifie = re.sub(r"^###", "##", bloc_modifie, flags=re.MULTILINE) + regroupement[mot].append(bloc_modifie) + + return {mot: "\n\n".join(blocs) for mot, blocs in regroupement.items()} + +def nettoyer_texte_fr(texte: str) -> str: + # Apostrophes droites -> typographiques + texte = texte.replace("'", "’") + # Guillemets droits -> guillemets français (avec espace fine insécable) + texte = re.sub(r'"(.*?)"', r'« \1 »', texte) + # Espaces fines insécables avant : ; ! ? + texte = re.sub(r' (?=[:;!?])', '\u202F', texte) + # Unités : espace insécable entre chiffre et unité + texte = re.sub(r'(\d) (?=\w+)', lambda m: f"{m.group(1)}\u202F", texte) + # Suppression des doubles espaces + texte = re.sub(r' {2,}', ' ', texte) + # Remplacement optionnel des tirets simples (optionnel) + texte = texte.replace(" - ", " – ") + # Nettoyage ponctuation multiple accidentelle + texte = re.sub(r'\s+([.,;!?])', r'\1', texte) + return texte diff --git a/components/README.md b/components/README.md index 99d5b45..0905368 100644 --- a/components/README.md +++ b/components/README.md @@ -10,11 +10,12 @@ Le module components comprend plusieurs fichiers clés : - **header.py** : Composant d'en-tête unifié pour toutes les pages - **footer.py** : Pied de page standardisé incluant les mentions légales et informations de contact - **fiches.py** : Composants spécifiques à l'affichage et à la manipulation des fiches +- **connexion.py** : Composants spécifiques à la gestion de la connexion / déconnexion sur la base d'un token Gitea ## Fonctionnalités ### Barre latérale (sidebar.py) -- Menu de navigation principal entre les différentes sections +- Menu de navigation prin>>>>>>>>>>>>> cipal entre les différentes sections - Options de configuration et de personnalisation - Affichage des informations sur l'impact environnemental - Gestion du thème (clair/sombre) @@ -45,4 +46,4 @@ afficher_menu() afficher_pied_de_page() ``` -Cette approche modulaire permet de maintenir une interface cohérente tout en facilitant les mises à jour de l'interface utilisateur. \ No newline at end of file +Cette approche modulaire permet de maintenir une interface cohérente tout en facilitant les mises à jour de l'interface utilisateur. diff --git a/components/connexion.py b/components/connexion.py index a5edc98..c110a22 100644 --- a/components/connexion.py +++ b/components/connexion.py @@ -42,9 +42,10 @@ def connexion(): with st.form("auth_form"): # Ajout d'un champ identifiant fictif pour activer l'autocomplétion navigateur # et permettre de stocker le token comme un mot de passe par le navigateur + # L'identifiant n'est donc pas utilisé par la suite ; il est caché en CSS identifiant = st.text_input(str(_("auth.username")), value="fabnum-connexion", key="nom_utilisateur") token = st.text_input(str(_("auth.token")), type="password") - submitted = st.form_submit_button(str(_("auth.login"))) + submitted = st.form_submit_button(str(_("auth.login")), icon=":material/login:") if submitted and token: erreur = True @@ -100,7 +101,7 @@ def bouton_deconnexion(): """) st.sidebar.markdown(f"{str(_('auth.logged_as'))} `{st.session_state.username}`") - if st.sidebar.button(str(_("auth.logout"))): + if st.sidebar.button(str(_("auth.logout")), icon=":material/logout:"): st.session_state.logged_in = False st.session_state.username = "" st.session_state.token = "" diff --git a/components/sidebar.py b/components/sidebar.py index 63e88bb..78effe9 100644 --- a/components/sidebar.py +++ b/components/sidebar.py @@ -21,6 +21,8 @@ def afficher_menu(): str(_("navigation.instructions")), str(_("navigation.personnalisation")), str(_("navigation.analyse")), + *([str(_("navigation.ia_nalyse"))] if st.session_state.get("logged_in", False) else []), + *([str(_("navigation.plan_d_action"))]), str(_("navigation.visualisations")), str(_("navigation.fiches")) ] @@ -99,7 +101,12 @@ def afficher_menu(): st.session_state.theme_mode = theme st.rerun() - +# +# Important : +# Avec Selinux, il faut donner les bons droits +# +# sudo chcon -Rt httpd_sys_content_t /chemin/d/acces/assets/ +# def afficher_impact(total_bytes): impact_label = str(_("sidebar.impact")) loading_text = str(_("sidebar.loading")) diff --git a/fabnum-dev.service b/fabnum-dev.service new file mode 100644 index 0000000..b741ae7 --- /dev/null +++ b/fabnum-dev.service @@ -0,0 +1,15 @@ +[Unit] +Description=Fabnum Dev - Streamlit App +After=network.target + +[Service] +User=fabnum +WorkingDirectory=/home/fabnum/fabnum-dev +ExecStart=/home/fabnum/fabnum-dev/venv/bin/streamlit run /home/fabnum/fabnum-dev +/fabnum.py --server.port 8502 +Restart=always +RestartSec=5 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.targe diff --git a/fabnum.py b/fabnum.py index cb1d0b3..f6a4ae5 100644 --- a/fabnum.py +++ b/fabnum.py @@ -19,6 +19,8 @@ from utils.gitea import ( # Import du module de traductions from utils.translations import init_translations, _, set_language +from utils.widgets import html_expander + def afficher_instructions_avec_expanders(markdown_content): """ Affiche le contenu markdown avec les sections de niveau 2 (## Titre) dans des expanders @@ -57,8 +59,8 @@ def afficher_instructions_avec_expanders(markdown_content): # Affichage dans un expander status = True if i == 1 else False - with st.expander(f"## {titre_section}", expanded=status): - st.markdown(contenu_section, unsafe_allow_html=True) + # with st.expander(f"## {titre_section}", expanded=status): + html_expander(f"{titre_section}", content=contenu_section, open_by_default=status, details_class="details_introduction") from utils.graph_utils import ( charger_graphe @@ -76,6 +78,8 @@ from app.fiches import interface_fiches from app.visualisations import interface_visualisations from app.personnalisation import interface_personnalisation from app.analyse import interface_analyse +from app.ia_nalyse import interface_ia_nalyse +from app.plan_d_action import interface_plan_d_action # Initialisation des traductions (langue française par défaut) init_translations() @@ -166,6 +170,8 @@ instructions_tab = _("navigation.instructions") fiches_tab = _("navigation.fiches") personnalisation_tab = _("navigation.personnalisation") analyse_tab = _("navigation.analyse") +ia_nalyse_tab = _("navigation.ia_nalyse") +plan_d_action_tab = _("navigation.plan_d_action") visualisations_tab = _("navigation.visualisations") if st.session_state.onglet == instructions_tab: @@ -179,15 +185,27 @@ elif st.session_state.onglet == fiches_tab: else: # Charger le graphe une seule fois # Le graphe n'est pas nécessaire pour Instructions ou Fiches - G_temp, G_temp_ivc, dot_file_path = charger_graphe() + dot_file_path = charger_graphe() if dot_file_path and st.session_state.onglet == analyse_tab: + G_temp = st.session_state["G_temp"] interface_analyse(G_temp) +elif dot_file_path and st.session_state.onglet == ia_nalyse_tab: + G_temp = st.session_state["G_temp"] + interface_ia_nalyse(G_temp) + +elif dot_file_path and st.session_state.onglet == plan_d_action_tab: + G_temp = st.session_state["G_temp"] + interface_plan_d_action(G_temp) + elif dot_file_path and st.session_state.onglet == visualisations_tab: + G_temp = st.session_state["G_temp"] + G_temp_ivc = st.session_state["G_temp_ivc"] interface_visualisations(G_temp, G_temp_ivc) elif dot_file_path and st.session_state.onglet == personnalisation_tab: + G_temp = st.session_state["G_temp"] G_temp = interface_personnalisation(G_temp) fermer_page() diff --git a/pgpt/.docker/router.yml b/pgpt/.docker/router.yml new file mode 100644 index 0000000..3b55df9 --- /dev/null +++ b/pgpt/.docker/router.yml @@ -0,0 +1,16 @@ +http: + services: + ollama: + loadBalancer: + healthCheck: + interval: 5s + path: / + servers: + - url: http://ollama-cpu:11434 + - url: http://ollama-cuda:11434 + - url: http://host.docker.internal:11434 + + routers: + ollama-router: + rule: "PathPrefix(`/`)" + service: ollama \ No newline at end of file diff --git a/pgpt/.gitignore b/pgpt/.gitignore new file mode 100644 index 0000000..c7b7525 --- /dev/null +++ b/pgpt/.gitignore @@ -0,0 +1,8 @@ +local_data/* +qdrant_data/* +models/ +qdrant_data/ +poetry.lock +.dockerignore +CITATION.cff +scripts diff --git a/pgpt/Dockerfile.ollama b/pgpt/Dockerfile.ollama new file mode 100644 index 0000000..83fd129 --- /dev/null +++ b/pgpt/Dockerfile.ollama @@ -0,0 +1,51 @@ +FROM python:3.11.6-slim-bookworm AS base + +# Install poetry +RUN pip install pipx +RUN python3 -m pipx ensurepath +RUN pipx install poetry==1.8.3 +ENV PATH="/root/.local/bin:$PATH" +ENV PATH=".venv/bin/:$PATH" + +# https://python-poetry.org/docs/configuration/#virtualenvsin-project +ENV POETRY_VIRTUALENVS_IN_PROJECT=true + +FROM base AS dependencies +WORKDIR /home/worker/app +COPY pyproject.toml poetry.lock ./ + +ARG POETRY_EXTRAS="ui vector-stores-qdrant llms-ollama embeddings-ollama" +RUN poetry install --no-root --extras "${POETRY_EXTRAS}" + +FROM base AS app +ENV PYTHONUNBUFFERED=1 +ENV PORT=8080 +ENV APP_ENV=prod +ENV PYTHONPATH="$PYTHONPATH:/home/worker/app/private_gpt/" +EXPOSE 8080 + +# Prepare a non-root user +# More info about how to configure UIDs and GIDs in Docker: +# https://github.com/systemd/systemd/blob/main/docs/UIDS-GIDS.md + +# Define the User ID (UID) for the non-root user +# UID 100 is chosen to avoid conflicts with existing system users +ARG UID=100 + +# Define the Group ID (GID) for the non-root user +# GID 65534 is often used for the 'nogroup' or 'nobody' group +ARG GID=65534 + +RUN adduser --system --gid ${GID} --uid ${UID} --home /home/worker worker +WORKDIR /home/worker/app + +RUN chown worker /home/worker/app +RUN mkdir local_data && chown worker local_data +RUN mkdir models && chown worker models +COPY --chown=worker --from=dependencies /home/worker/app/.venv/ .venv +COPY --chown=worker private_gpt/ private_gpt +COPY --chown=worker *.yaml . +COPY --chown=worker scripts/ scripts + +USER worker +ENTRYPOINT python -m private_gpt diff --git a/pgpt/docker-compose.yaml b/pgpt/docker-compose.yaml new file mode 100644 index 0000000..1575757 --- /dev/null +++ b/pgpt/docker-compose.yaml @@ -0,0 +1,121 @@ +services: + #----------------------------------- + #---- Private-GPT services --------- + #----------------------------------- + + # Private-GPT service for the Ollama CPU and GPU modes + # This service builds from an external Dockerfile and runs the Ollama mode. + private-gpt-ollama: + image: ${PGPT_IMAGE:-zylonai/private-gpt}:${PGPT_TAG:-0.6.2}-ollama # x-release-please-version + user: root + build: + context: . + dockerfile: Dockerfile.ollama + volumes: + - /home/fabnum/fabnum-dev/Fiches:/home/worker/app/local_data/Fiches:Z + ports: + - "127.0.0.1:8001:8001" + environment: + PORT: 8001 + PGPT_PROFILES: docker + PGPT_MODE: ollama + PGPT_EMBED_MODE: ollama + PGPT_OLLAMA_API_BASE: http://ollama:11434 + HF_TOKEN: ${HF_TOKEN:-} + profiles: + - "" + - ollama-cpu + - ollama-cuda + - ollama-api + depends_on: + ollama: + condition: service_healthy + + # Private-GPT service for the local mode + # This service builds from a local Dockerfile and runs the application in local mode. + private-gpt-llamacpp-cpu: + image: ${PGPT_IMAGE:-zylonai/private-gpt}:${PGPT_TAG:-0.6.2}-llamacpp-cpu # x-release-please-version + user: root + build: + context: . + dockerfile: Dockerfile.llamacpp-cpu + volumes: + - ./local_data/:/home/worker/app/local_data + - ./models/:/home/worker/app/models + entrypoint: sh -c ".venv/bin/python scripts/setup && .venv/bin/python -m private_gpt" + ports: + - "127.0.0.1:8001:8001" + environment: + PORT: 8001 + PGPT_PROFILES: local + HF_TOKEN: ${HF_TOKEN:-} + profiles: + - llamacpp-cpu + + #----------------------------------- + #---- Ollama services -------------- + #----------------------------------- + + # Traefik reverse proxy for the Ollama service + # This will route requests to the Ollama service based on the profile. + ollama: + image: traefik:v2.10 + healthcheck: + test: + [ + "CMD", + "sh", + "-c", + "wget -q --spider http://ollama:11434 || exit 1", + ] + interval: 10s + retries: 3 + start_period: 5s + timeout: 5s + ports: + - "127.0.0.1:8080:8080" + command: + - "--providers.file.filename=/etc/router.yml" + - "--log.level=ERROR" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:11434" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./.docker/router.yml:/etc/router.yml:ro + extra_hosts: + - "host.docker.internal:host-gateway" + profiles: + - "" + - ollama-cpu + - ollama-cuda + - ollama-api + + # Ollama service for the CPU mode + ollama-cpu: + image: ollama/ollama:latest + ports: + - "127.0.0.1:11434:11434" + volumes: + - ./models:/root/.ollama:Z + profiles: + - "" + - ollama-cpu + + # Ollama service for the CUDA mode + ollama-cuda: + image: ollama/ollama:latest + ports: + - "11434:11434" + volumes: + - ./models:/root/.ollama + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + profiles: + - ollama-cuda diff --git a/pgpt/private_gpt/__init__.py b/pgpt/private_gpt/__init__.py new file mode 100644 index 0000000..360b53a --- /dev/null +++ b/pgpt/private_gpt/__init__.py @@ -0,0 +1,27 @@ +"""private-gpt.""" + +import logging +import os + +# Set to 'DEBUG' to have extensive logging turned on, even for libraries +ROOT_LOG_LEVEL = "INFO" + +PRETTY_LOG_FORMAT = ( + "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)+25s - %(message)s" +) +logging.basicConfig(level=ROOT_LOG_LEVEL, format=PRETTY_LOG_FORMAT, datefmt="%H:%M:%S") +logging.captureWarnings(True) + +# Disable gradio analytics +# This is done this way because gradio does not solely rely on what values are +# passed to gr.Blocks(enable_analytics=...) but also on the environment +# variable GRADIO_ANALYTICS_ENABLED. `gradio.strings` actually reads this env +# directly, so to fully disable gradio analytics we need to set this env var. +os.environ["GRADIO_ANALYTICS_ENABLED"] = "False" + +# Disable chromaDB telemetry +# It is already disabled, see PR#1144 +# os.environ["ANONYMIZED_TELEMETRY"] = "False" + +# adding tiktoken cache path within repo to be able to run in offline environment. +os.environ["TIKTOKEN_CACHE_DIR"] = "tiktoken_cache" diff --git a/pgpt/private_gpt/__main__.py b/pgpt/private_gpt/__main__.py new file mode 100644 index 0000000..18b42fd --- /dev/null +++ b/pgpt/private_gpt/__main__.py @@ -0,0 +1,11 @@ +# start a fastapi server with uvicorn + +import uvicorn + +from private_gpt.main import app +from private_gpt.settings.settings import settings + +# Set log_config=None to do not use the uvicorn logging configuration, and +# use ours instead. For reference, see below: +# https://github.com/tiangolo/fastapi/discussions/7457#discussioncomment-5141108 +uvicorn.run(app, host="0.0.0.0", port=settings().server.port, log_config=None) diff --git a/pgpt/private_gpt/components/__init__.py b/pgpt/private_gpt/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/components/embedding/__init__.py b/pgpt/private_gpt/components/embedding/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/components/embedding/custom/__init__.py b/pgpt/private_gpt/components/embedding/custom/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/components/embedding/custom/sagemaker.py b/pgpt/private_gpt/components/embedding/custom/sagemaker.py new file mode 100644 index 0000000..be6b296 --- /dev/null +++ b/pgpt/private_gpt/components/embedding/custom/sagemaker.py @@ -0,0 +1,82 @@ +# mypy: ignore-errors +import json +from typing import Any + +import boto3 +from llama_index.core.base.embeddings.base import BaseEmbedding +from pydantic import Field, PrivateAttr + + +class SagemakerEmbedding(BaseEmbedding): + """Sagemaker Embedding Endpoint. + + To use, you must supply the endpoint name from your deployed + Sagemaker embedding model & the region where it is deployed. + + To authenticate, the AWS client uses the following methods to + automatically load credentials: + https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html + + If a specific credential profile should be used, you must pass + the name of the profile from the ~/.aws/credentials file that is to be used. + + Make sure the credentials / roles used have the required policies to + access the Sagemaker endpoint. + See: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html + """ + + endpoint_name: str = Field(description="") + + _boto_client: Any = boto3.client( + "sagemaker-runtime", + ) # TODO make it an optional field + + _async_not_implemented_warned: bool = PrivateAttr(default=False) + + @classmethod + def class_name(cls) -> str: + return "SagemakerEmbedding" + + def _async_not_implemented_warn_once(self) -> None: + if not self._async_not_implemented_warned: + print("Async embedding not available, falling back to sync method.") + self._async_not_implemented_warned = True + + def _embed(self, sentences: list[str]) -> list[list[float]]: + request_params = { + "inputs": sentences, + } + + resp = self._boto_client.invoke_endpoint( + EndpointName=self.endpoint_name, + Body=json.dumps(request_params), + ContentType="application/json", + ) + + response_body = resp["Body"] + response_str = response_body.read().decode("utf-8") + response_json = json.loads(response_str) + + return response_json["vectors"] + + def _get_query_embedding(self, query: str) -> list[float]: + """Get query embedding.""" + return self._embed([query])[0] + + async def _aget_query_embedding(self, query: str) -> list[float]: + # Warn the user that sync is being used + self._async_not_implemented_warn_once() + return self._get_query_embedding(query) + + async def _aget_text_embedding(self, text: str) -> list[float]: + # Warn the user that sync is being used + self._async_not_implemented_warn_once() + return self._get_text_embedding(text) + + def _get_text_embedding(self, text: str) -> list[float]: + """Get text embedding.""" + return self._embed([text])[0] + + def _get_text_embeddings(self, texts: list[str]) -> list[list[float]]: + """Get text embeddings.""" + return self._embed(texts) diff --git a/pgpt/private_gpt/components/embedding/embedding_component.py b/pgpt/private_gpt/components/embedding/embedding_component.py new file mode 100644 index 0000000..b55cef8 --- /dev/null +++ b/pgpt/private_gpt/components/embedding/embedding_component.py @@ -0,0 +1,167 @@ +import logging + +from injector import inject, singleton +from llama_index.core.embeddings import BaseEmbedding, MockEmbedding + +from private_gpt.paths import models_cache_path +from private_gpt.settings.settings import Settings + +logger = logging.getLogger(__name__) + + +@singleton +class EmbeddingComponent: + embedding_model: BaseEmbedding + + @inject + def __init__(self, settings: Settings) -> None: + embedding_mode = settings.embedding.mode + logger.info("Initializing the embedding model in mode=%s", embedding_mode) + match embedding_mode: + case "huggingface": + try: + from llama_index.embeddings.huggingface import ( # type: ignore + HuggingFaceEmbedding, + ) + except ImportError as e: + raise ImportError( + "Local dependencies not found, install with `poetry install --extras embeddings-huggingface`" + ) from e + + self.embedding_model = HuggingFaceEmbedding( + model_name=settings.huggingface.embedding_hf_model_name, + cache_folder=str(models_cache_path), + trust_remote_code=settings.huggingface.trust_remote_code, + ) + case "sagemaker": + try: + from private_gpt.components.embedding.custom.sagemaker import ( + SagemakerEmbedding, + ) + except ImportError as e: + raise ImportError( + "Sagemaker dependencies not found, install with `poetry install --extras embeddings-sagemaker`" + ) from e + + self.embedding_model = SagemakerEmbedding( + endpoint_name=settings.sagemaker.embedding_endpoint_name, + ) + case "openai": + try: + from llama_index.embeddings.openai import ( # type: ignore + OpenAIEmbedding, + ) + except ImportError as e: + raise ImportError( + "OpenAI dependencies not found, install with `poetry install --extras embeddings-openai`" + ) from e + + api_base = ( + settings.openai.embedding_api_base or settings.openai.api_base + ) + api_key = settings.openai.embedding_api_key or settings.openai.api_key + model = settings.openai.embedding_model + + self.embedding_model = OpenAIEmbedding( + api_base=api_base, + api_key=api_key, + model=model, + ) + case "ollama": + try: + from llama_index.embeddings.ollama import ( # type: ignore + OllamaEmbedding, + ) + from ollama import Client # type: ignore + except ImportError as e: + raise ImportError( + "Local dependencies not found, install with `poetry install --extras embeddings-ollama`" + ) from e + + ollama_settings = settings.ollama + + # Calculate embedding model. If not provided tag, it will be use latest + model_name = ( + ollama_settings.embedding_model + ":latest" + if ":" not in ollama_settings.embedding_model + else ollama_settings.embedding_model + ) + + self.embedding_model = OllamaEmbedding( + model_name=model_name, + base_url=ollama_settings.embedding_api_base, + ) + + if ollama_settings.autopull_models: + if ollama_settings.autopull_models: + from private_gpt.utils.ollama import ( + check_connection, + pull_model, + ) + + # TODO: Reuse llama-index client when llama-index is updated + client = Client( + host=ollama_settings.embedding_api_base, + timeout=ollama_settings.request_timeout, + ) + + if not check_connection(client): + raise ValueError( + f"Failed to connect to Ollama, " + f"check if Ollama server is running on {ollama_settings.api_base}" + ) + pull_model(client, model_name) + + case "azopenai": + try: + from llama_index.embeddings.azure_openai import ( # type: ignore + AzureOpenAIEmbedding, + ) + except ImportError as e: + raise ImportError( + "Azure OpenAI dependencies not found, install with `poetry install --extras embeddings-azopenai`" + ) from e + + azopenai_settings = settings.azopenai + self.embedding_model = AzureOpenAIEmbedding( + model=azopenai_settings.embedding_model, + deployment_name=azopenai_settings.embedding_deployment_name, + api_key=azopenai_settings.api_key, + azure_endpoint=azopenai_settings.azure_endpoint, + api_version=azopenai_settings.api_version, + ) + case "gemini": + try: + from llama_index.embeddings.gemini import ( # type: ignore + GeminiEmbedding, + ) + except ImportError as e: + raise ImportError( + "Gemini dependencies not found, install with `poetry install --extras embeddings-gemini`" + ) from e + + self.embedding_model = GeminiEmbedding( + api_key=settings.gemini.api_key, + model_name=settings.gemini.embedding_model, + ) + case "mistralai": + try: + from llama_index.embeddings.mistralai import ( # type: ignore + MistralAIEmbedding, + ) + except ImportError as e: + raise ImportError( + "Mistral dependencies not found, install with `poetry install --extras embeddings-mistral`" + ) from e + + api_key = settings.openai.api_key + model = settings.openai.embedding_model + + self.embedding_model = MistralAIEmbedding( + api_key=api_key, + model=model, + ) + case "mock": + # Not a random number, is the dimensionality used by + # the default embedding model + self.embedding_model = MockEmbedding(384) diff --git a/pgpt/private_gpt/components/ingest/__init__.py b/pgpt/private_gpt/components/ingest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/components/ingest/ingest_component.py b/pgpt/private_gpt/components/ingest/ingest_component.py new file mode 100644 index 0000000..77db970 --- /dev/null +++ b/pgpt/private_gpt/components/ingest/ingest_component.py @@ -0,0 +1,517 @@ +import abc +import itertools +import logging +import multiprocessing +import multiprocessing.pool +import os +import threading +from pathlib import Path +from queue import Queue +from typing import Any + +from llama_index.core.data_structs import IndexDict +from llama_index.core.embeddings.utils import EmbedType +from llama_index.core.indices import VectorStoreIndex, load_index_from_storage +from llama_index.core.indices.base import BaseIndex +from llama_index.core.ingestion import run_transformations +from llama_index.core.schema import BaseNode, Document, TransformComponent +from llama_index.core.storage import StorageContext + +from private_gpt.components.ingest.ingest_helper import IngestionHelper +from private_gpt.paths import local_data_path +from private_gpt.settings.settings import Settings +from private_gpt.utils.eta import eta + +logger = logging.getLogger(__name__) + + +class BaseIngestComponent(abc.ABC): + def __init__( + self, + storage_context: StorageContext, + embed_model: EmbedType, + transformations: list[TransformComponent], + *args: Any, + **kwargs: Any, + ) -> None: + logger.debug("Initializing base ingest component type=%s", type(self).__name__) + self.storage_context = storage_context + self.embed_model = embed_model + self.transformations = transformations + + @abc.abstractmethod + def ingest(self, file_name: str, file_data: Path) -> list[Document]: + pass + + @abc.abstractmethod + def bulk_ingest(self, files: list[tuple[str, Path]]) -> list[Document]: + pass + + @abc.abstractmethod + def delete(self, doc_id: str) -> None: + pass + + +class BaseIngestComponentWithIndex(BaseIngestComponent, abc.ABC): + def __init__( + self, + storage_context: StorageContext, + embed_model: EmbedType, + transformations: list[TransformComponent], + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(storage_context, embed_model, transformations, *args, **kwargs) + + self.show_progress = True + self._index_thread_lock = ( + threading.Lock() + ) # Thread lock! Not Multiprocessing lock + self._index = self._initialize_index() + + def _initialize_index(self) -> BaseIndex[IndexDict]: + """Initialize the index from the storage context.""" + try: + # Load the index with store_nodes_override=True to be able to delete them + index = load_index_from_storage( + storage_context=self.storage_context, + store_nodes_override=True, # Force store nodes in index and document stores + show_progress=self.show_progress, + embed_model=self.embed_model, + transformations=self.transformations, + ) + except ValueError: + # There are no index in the storage context, creating a new one + logger.info("Creating a new vector store index") + index = VectorStoreIndex.from_documents( + [], + storage_context=self.storage_context, + store_nodes_override=True, # Force store nodes in index and document stores + show_progress=self.show_progress, + embed_model=self.embed_model, + transformations=self.transformations, + ) + index.storage_context.persist(persist_dir=local_data_path) + return index + + def _save_index(self) -> None: + self._index.storage_context.persist(persist_dir=local_data_path) + + def delete(self, doc_id: str) -> None: + with self._index_thread_lock: + # Delete the document from the index + self._index.delete_ref_doc(doc_id, delete_from_docstore=True) + + # Save the index + self._save_index() + + +class SimpleIngestComponent(BaseIngestComponentWithIndex): + def __init__( + self, + storage_context: StorageContext, + embed_model: EmbedType, + transformations: list[TransformComponent], + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(storage_context, embed_model, transformations, *args, **kwargs) + + def ingest(self, file_name: str, file_data: Path) -> list[Document]: + logger.info("Ingesting file_name=%s", file_name) + documents = IngestionHelper.transform_file_into_documents(file_name, file_data) + logger.info( + "Transformed file=%s into count=%s documents", file_name, len(documents) + ) + logger.debug("Saving the documents in the index and doc store") + return self._save_docs(documents) + + def bulk_ingest(self, files: list[tuple[str, Path]]) -> list[Document]: + saved_documents = [] + for file_name, file_data in files: + documents = IngestionHelper.transform_file_into_documents( + file_name, file_data + ) + saved_documents.extend(self._save_docs(documents)) + return saved_documents + + def _save_docs(self, documents: list[Document]) -> list[Document]: + logger.debug("Transforming count=%s documents into nodes", len(documents)) + with self._index_thread_lock: + for document in documents: + self._index.insert(document, show_progress=True) + logger.debug("Persisting the index and nodes") + # persist the index and nodes + self._save_index() + logger.debug("Persisted the index and nodes") + return documents + + +class BatchIngestComponent(BaseIngestComponentWithIndex): + """Parallelize the file reading and parsing on multiple CPU core. + + This also makes the embeddings to be computed in batches (on GPU or CPU). + """ + + def __init__( + self, + storage_context: StorageContext, + embed_model: EmbedType, + transformations: list[TransformComponent], + count_workers: int, + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(storage_context, embed_model, transformations, *args, **kwargs) + # Make an efficient use of the CPU and GPU, the embedding + # must be in the transformations + assert ( + len(self.transformations) >= 2 + ), "Embeddings must be in the transformations" + assert count_workers > 0, "count_workers must be > 0" + self.count_workers = count_workers + + self._file_to_documents_work_pool = multiprocessing.Pool( + processes=self.count_workers + ) + + def ingest(self, file_name: str, file_data: Path) -> list[Document]: + logger.info("Ingesting file_name=%s", file_name) + documents = IngestionHelper.transform_file_into_documents(file_name, file_data) + logger.info( + "Transformed file=%s into count=%s documents", file_name, len(documents) + ) + logger.debug("Saving the documents in the index and doc store") + return self._save_docs(documents) + + def bulk_ingest(self, files: list[tuple[str, Path]]) -> list[Document]: + documents = list( + itertools.chain.from_iterable( + self._file_to_documents_work_pool.starmap( + IngestionHelper.transform_file_into_documents, files + ) + ) + ) + logger.info( + "Transformed count=%s files into count=%s documents", + len(files), + len(documents), + ) + return self._save_docs(documents) + + def _save_docs(self, documents: list[Document]) -> list[Document]: + logger.debug("Transforming count=%s documents into nodes", len(documents)) + nodes = run_transformations( + documents, # type: ignore[arg-type] + self.transformations, + show_progress=self.show_progress, + ) + # Locking the index to avoid concurrent writes + with self._index_thread_lock: + logger.info("Inserting count=%s nodes in the index", len(nodes)) + self._index.insert_nodes(nodes, show_progress=True) + for document in documents: + self._index.docstore.set_document_hash( + document.get_doc_id(), document.hash + ) + logger.debug("Persisting the index and nodes") + # persist the index and nodes + self._save_index() + logger.debug("Persisted the index and nodes") + return documents + + +class ParallelizedIngestComponent(BaseIngestComponentWithIndex): + """Parallelize the file ingestion (file reading, embeddings, and index insertion). + + This use the CPU and GPU in parallel (both running at the same time), and + reduce the memory pressure by not loading all the files in memory at the same time. + """ + + def __init__( + self, + storage_context: StorageContext, + embed_model: EmbedType, + transformations: list[TransformComponent], + count_workers: int, + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(storage_context, embed_model, transformations, *args, **kwargs) + # To make an efficient use of the CPU and GPU, the embeddings + # must be in the transformations (to be computed in batches) + assert ( + len(self.transformations) >= 2 + ), "Embeddings must be in the transformations" + assert count_workers > 0, "count_workers must be > 0" + self.count_workers = count_workers + # We are doing our own multiprocessing + # To do not collide with the multiprocessing of huggingface, we disable it + os.environ["TOKENIZERS_PARALLELISM"] = "false" + + self._ingest_work_pool = multiprocessing.pool.ThreadPool( + processes=self.count_workers + ) + + self._file_to_documents_work_pool = multiprocessing.Pool( + processes=self.count_workers + ) + + def ingest(self, file_name: str, file_data: Path) -> list[Document]: + logger.info("Ingesting file_name=%s", file_name) + # Running in a single (1) process to release the current + # thread, and take a dedicated CPU core for computation + documents = self._file_to_documents_work_pool.apply( + IngestionHelper.transform_file_into_documents, (file_name, file_data) + ) + logger.info( + "Transformed file=%s into count=%s documents", file_name, len(documents) + ) + logger.debug("Saving the documents in the index and doc store") + return self._save_docs(documents) + + def bulk_ingest(self, files: list[tuple[str, Path]]) -> list[Document]: + # Lightweight threads, used for parallelize the + # underlying IO calls made in the ingestion + + documents = list( + itertools.chain.from_iterable( + self._ingest_work_pool.starmap(self.ingest, files) + ) + ) + return documents + + def _save_docs(self, documents: list[Document]) -> list[Document]: + logger.debug("Transforming count=%s documents into nodes", len(documents)) + nodes = run_transformations( + documents, # type: ignore[arg-type] + self.transformations, + show_progress=self.show_progress, + ) + # Locking the index to avoid concurrent writes + with self._index_thread_lock: + logger.info("Inserting count=%s nodes in the index", len(nodes)) + self._index.insert_nodes(nodes, show_progress=True) + for document in documents: + self._index.docstore.set_document_hash( + document.get_doc_id(), document.hash + ) + logger.debug("Persisting the index and nodes") + # persist the index and nodes + self._save_index() + logger.debug("Persisted the index and nodes") + return documents + + def __del__(self) -> None: + # We need to do the appropriate cleanup of the multiprocessing pools + # when the object is deleted. Using root logger to avoid + # the logger to be deleted before the pool + logging.debug("Closing the ingest work pool") + self._ingest_work_pool.close() + self._ingest_work_pool.join() + self._ingest_work_pool.terminate() + logging.debug("Closing the file to documents work pool") + self._file_to_documents_work_pool.close() + self._file_to_documents_work_pool.join() + self._file_to_documents_work_pool.terminate() + + +class PipelineIngestComponent(BaseIngestComponentWithIndex): + """Pipeline ingestion - keeping the embedding worker pool as busy as possible. + + This class implements a threaded ingestion pipeline, which comprises two threads + and two queues. The primary thread is responsible for reading and parsing files + into documents. These documents are then placed into a queue, which is + distributed to a pool of worker processes for embedding computation. After + embedding, the documents are transferred to another queue where they are + accumulated until a threshold is reached. Upon reaching this threshold, the + accumulated documents are flushed to the document store, index, and vector + store. + + Exception handling ensures robustness against erroneous files. However, in the + pipelined design, one error can lead to the discarding of multiple files. Any + discarded files will be reported. + """ + + NODE_FLUSH_COUNT = 5000 # Save the index every # nodes. + + def __init__( + self, + storage_context: StorageContext, + embed_model: EmbedType, + transformations: list[TransformComponent], + count_workers: int, + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(storage_context, embed_model, transformations, *args, **kwargs) + self.count_workers = count_workers + assert ( + len(self.transformations) >= 2 + ), "Embeddings must be in the transformations" + assert count_workers > 0, "count_workers must be > 0" + self.count_workers = count_workers + # We are doing our own multiprocessing + # To do not collide with the multiprocessing of huggingface, we disable it + os.environ["TOKENIZERS_PARALLELISM"] = "false" + + # doc_q stores parsed files as Document chunks. + # Using a shallow queue causes the filesystem parser to block + # when it reaches capacity. This ensures it doesn't outpace the + # computationally intensive embeddings phase, avoiding unnecessary + # memory consumption. The semaphore is used to bound the async worker + # embedding computations to cause the doc Q to fill and block. + self.doc_semaphore = multiprocessing.Semaphore( + self.count_workers + ) # limit the doc queue to # items. + self.doc_q: Queue[tuple[str, str | None, list[Document] | None]] = Queue(20) + # node_q stores documents parsed into nodes (embeddings). + # Larger queue size so we don't block the embedding workers during a slow + # index update. + self.node_q: Queue[ + tuple[str, str | None, list[Document] | None, list[BaseNode] | None] + ] = Queue(40) + threading.Thread(target=self._doc_to_node, daemon=True).start() + threading.Thread(target=self._write_nodes, daemon=True).start() + + def _doc_to_node(self) -> None: + # Parse documents into nodes + with multiprocessing.pool.ThreadPool(processes=self.count_workers) as pool: + while True: + try: + cmd, file_name, documents = self.doc_q.get( + block=True + ) # Documents for a file + if cmd == "process": + # Push CPU/GPU embedding work to the worker pool + # Acquire semaphore to control access to worker pool + self.doc_semaphore.acquire() + pool.apply_async( + self._doc_to_node_worker, (file_name, documents) + ) + elif cmd == "quit": + break + finally: + if cmd != "process": + self.doc_q.task_done() # unblock Q joins + + def _doc_to_node_worker(self, file_name: str, documents: list[Document]) -> None: + # CPU/GPU intensive work in its own process + try: + nodes = run_transformations( + documents, # type: ignore[arg-type] + self.transformations, + show_progress=self.show_progress, + ) + self.node_q.put(("process", file_name, documents, list(nodes))) + finally: + self.doc_semaphore.release() + self.doc_q.task_done() # unblock Q joins + + def _save_docs( + self, files: list[str], documents: list[Document], nodes: list[BaseNode] + ) -> None: + try: + logger.info( + f"Saving {len(files)} files ({len(documents)} documents / {len(nodes)} nodes)" + ) + self._index.insert_nodes(nodes) + for document in documents: + self._index.docstore.set_document_hash( + document.get_doc_id(), document.hash + ) + self._save_index() + except Exception: + # Tell the user so they can investigate these files + logger.exception(f"Processing files {files}") + finally: + # Clearing work, even on exception, maintains a clean state. + nodes.clear() + documents.clear() + files.clear() + + def _write_nodes(self) -> None: + # Save nodes to index. I/O intensive. + node_stack: list[BaseNode] = [] + doc_stack: list[Document] = [] + file_stack: list[str] = [] + while True: + try: + cmd, file_name, documents, nodes = self.node_q.get(block=True) + if cmd in ("flush", "quit"): + if file_stack: + self._save_docs(file_stack, doc_stack, node_stack) + if cmd == "quit": + break + elif cmd == "process": + node_stack.extend(nodes) # type: ignore[arg-type] + doc_stack.extend(documents) # type: ignore[arg-type] + file_stack.append(file_name) # type: ignore[arg-type] + # Constant saving is heavy on I/O - accumulate to a threshold + if len(node_stack) >= self.NODE_FLUSH_COUNT: + self._save_docs(file_stack, doc_stack, node_stack) + finally: + self.node_q.task_done() + + def _flush(self) -> None: + self.doc_q.put(("flush", None, None)) + self.doc_q.join() + self.node_q.put(("flush", None, None, None)) + self.node_q.join() + + def ingest(self, file_name: str, file_data: Path) -> list[Document]: + documents = IngestionHelper.transform_file_into_documents(file_name, file_data) + self.doc_q.put(("process", file_name, documents)) + self._flush() + return documents + + def bulk_ingest(self, files: list[tuple[str, Path]]) -> list[Document]: + docs = [] + for file_name, file_data in eta(files): + try: + documents = IngestionHelper.transform_file_into_documents( + file_name, file_data + ) + self.doc_q.put(("process", file_name, documents)) + docs.extend(documents) + except Exception: + logger.exception(f"Skipping {file_data.name}") + self._flush() + return docs + + +def get_ingestion_component( + storage_context: StorageContext, + embed_model: EmbedType, + transformations: list[TransformComponent], + settings: Settings, +) -> BaseIngestComponent: + """Get the ingestion component for the given configuration.""" + ingest_mode = settings.embedding.ingest_mode + if ingest_mode == "batch": + return BatchIngestComponent( + storage_context=storage_context, + embed_model=embed_model, + transformations=transformations, + count_workers=settings.embedding.count_workers, + ) + elif ingest_mode == "parallel": + return ParallelizedIngestComponent( + storage_context=storage_context, + embed_model=embed_model, + transformations=transformations, + count_workers=settings.embedding.count_workers, + ) + elif ingest_mode == "pipeline": + return PipelineIngestComponent( + storage_context=storage_context, + embed_model=embed_model, + transformations=transformations, + count_workers=settings.embedding.count_workers, + ) + else: + return SimpleIngestComponent( + storage_context=storage_context, + embed_model=embed_model, + transformations=transformations, + ) diff --git a/pgpt/private_gpt/components/ingest/ingest_helper.py b/pgpt/private_gpt/components/ingest/ingest_helper.py new file mode 100644 index 0000000..da62568 --- /dev/null +++ b/pgpt/private_gpt/components/ingest/ingest_helper.py @@ -0,0 +1,111 @@ +import logging +from pathlib import Path + +from llama_index.core.readers import StringIterableReader +from llama_index.core.readers.base import BaseReader +from llama_index.core.readers.json import JSONReader +from llama_index.core.schema import Document + +logger = logging.getLogger(__name__) + + +# Inspired by the `llama_index.core.readers.file.base` module +def _try_loading_included_file_formats() -> dict[str, type[BaseReader]]: + try: + from llama_index.readers.file.docs import ( # type: ignore + DocxReader, + HWPReader, + PDFReader, + ) + from llama_index.readers.file.epub import EpubReader # type: ignore + from llama_index.readers.file.image import ImageReader # type: ignore + from llama_index.readers.file.ipynb import IPYNBReader # type: ignore + from llama_index.readers.file.markdown import MarkdownReader # type: ignore + from llama_index.readers.file.mbox import MboxReader # type: ignore + from llama_index.readers.file.slides import PptxReader # type: ignore + from llama_index.readers.file.tabular import PandasCSVReader # type: ignore + from llama_index.readers.file.video_audio import ( # type: ignore + VideoAudioReader, + ) + except ImportError as e: + raise ImportError("`llama-index-readers-file` package not found") from e + + default_file_reader_cls: dict[str, type[BaseReader]] = { + ".hwp": HWPReader, + ".pdf": PDFReader, + ".docx": DocxReader, + ".pptx": PptxReader, + ".ppt": PptxReader, + ".pptm": PptxReader, + ".jpg": ImageReader, + ".png": ImageReader, + ".jpeg": ImageReader, + ".mp3": VideoAudioReader, + ".mp4": VideoAudioReader, + ".csv": PandasCSVReader, + ".epub": EpubReader, + ".md": MarkdownReader, + ".mbox": MboxReader, + ".ipynb": IPYNBReader, + } + return default_file_reader_cls + + +# Patching the default file reader to support other file types +FILE_READER_CLS = _try_loading_included_file_formats() +FILE_READER_CLS.update( + { + ".json": JSONReader, + } +) + + +class IngestionHelper: + """Helper class to transform a file into a list of documents. + + This class should be used to transform a file into a list of documents. + These methods are thread-safe (and multiprocessing-safe). + """ + + @staticmethod + def transform_file_into_documents( + file_name: str, file_data: Path + ) -> list[Document]: + documents = IngestionHelper._load_file_to_documents(file_name, file_data) + for document in documents: + document.metadata["file_name"] = file_name + IngestionHelper._exclude_metadata(documents) + return documents + + @staticmethod + def _load_file_to_documents(file_name: str, file_data: Path) -> list[Document]: + logger.debug("Transforming file_name=%s into documents", file_name) + extension = Path(file_name).suffix + reader_cls = FILE_READER_CLS.get(extension) + if reader_cls is None: + logger.debug( + "No reader found for extension=%s, using default string reader", + extension, + ) + # Read as a plain text + string_reader = StringIterableReader() + return string_reader.load_data([file_data.read_text()]) + + logger.debug("Specific reader found for extension=%s", extension) + documents = reader_cls().load_data(file_data) + + # Sanitize NUL bytes in text which can't be stored in Postgres + for i in range(len(documents)): + documents[i].text = documents[i].text.replace("\u0000", "") + + return documents + + @staticmethod + def _exclude_metadata(documents: list[Document]) -> None: + logger.debug("Excluding metadata from count=%s documents", len(documents)) + for document in documents: + document.metadata["doc_id"] = document.doc_id + # We don't want the Embeddings search to receive this metadata + document.excluded_embed_metadata_keys = ["doc_id"] + # We don't want the LLM to receive these metadata in the context + document.excluded_llm_metadata_keys = ["file_name", "doc_id", "page_label"] diff --git a/pgpt/private_gpt/components/llm/__init__.py b/pgpt/private_gpt/components/llm/__init__.py new file mode 100644 index 0000000..ffdd59b --- /dev/null +++ b/pgpt/private_gpt/components/llm/__init__.py @@ -0,0 +1 @@ +"""LLM implementations.""" diff --git a/pgpt/private_gpt/components/llm/custom/__init__.py b/pgpt/private_gpt/components/llm/custom/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/components/llm/custom/sagemaker.py b/pgpt/private_gpt/components/llm/custom/sagemaker.py new file mode 100644 index 0000000..bd2aec1 --- /dev/null +++ b/pgpt/private_gpt/components/llm/custom/sagemaker.py @@ -0,0 +1,276 @@ +# mypy: ignore-errors +from __future__ import annotations + +import io +import json +import logging +from typing import TYPE_CHECKING, Any + +import boto3 # type: ignore +from llama_index.core.base.llms.generic_utils import ( + completion_response_to_chat_response, + stream_completion_response_to_chat_response, +) +from llama_index.core.bridge.pydantic import Field +from llama_index.core.llms import ( + CompletionResponse, + CustomLLM, + LLMMetadata, +) +from llama_index.core.llms.callbacks import ( + llm_chat_callback, + llm_completion_callback, +) + +if TYPE_CHECKING: + from collections.abc import Sequence + + from llama_index.callbacks import CallbackManager + from llama_index.llms import ( + ChatMessage, + ChatResponse, + ChatResponseGen, + CompletionResponseGen, + ) + +logger = logging.getLogger(__name__) + + +class LineIterator: + r"""A helper class for parsing the byte stream input from TGI container. + + The output of the model will be in the following format: + ``` + b'data:{"token": {"text": " a"}}\n\n' + b'data:{"token": {"text": " challenging"}}\n\n' + b'data:{"token": {"text": " problem" + b'}}' + ... + ``` + + While usually each PayloadPart event from the event stream will contain a byte array + with a full json, this is not guaranteed and some of the json objects may be split + across PayloadPart events. For example: + ``` + {'PayloadPart': {'Bytes': b'{"outputs": '}} + {'PayloadPart': {'Bytes': b'[" problem"]}\n'}} + ``` + + + This class accounts for this by concatenating bytes written via the 'write' function + and then exposing a method which will return lines (ending with a '\n' character) + within the buffer via the 'scan_lines' function. It maintains the position of the + last read position to ensure that previous bytes are not exposed again. It will + also save any pending lines that doe not end with a '\n' to make sure truncations + are concatinated + """ + + def __init__(self, stream: Any) -> None: + """Line iterator initializer.""" + self.byte_iterator = iter(stream) + self.buffer = io.BytesIO() + self.read_pos = 0 + + def __iter__(self) -> Any: + """Self iterator.""" + return self + + def __next__(self) -> Any: + """Next element from iterator.""" + while True: + self.buffer.seek(self.read_pos) + line = self.buffer.readline() + if line and line[-1] == ord("\n"): + self.read_pos += len(line) + return line[:-1] + try: + chunk = next(self.byte_iterator) + except StopIteration: + if self.read_pos < self.buffer.getbuffer().nbytes: + continue + raise + if "PayloadPart" not in chunk: + logger.warning("Unknown event type=%s", chunk) + continue + self.buffer.seek(0, io.SEEK_END) + self.buffer.write(chunk["PayloadPart"]["Bytes"]) + + +class SagemakerLLM(CustomLLM): + """Sagemaker Inference Endpoint models. + + To use, you must supply the endpoint name from your deployed + Sagemaker model & the region where it is deployed. + + To authenticate, the AWS client uses the following methods to + automatically load credentials: + https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html + + If a specific credential profile should be used, you must pass + the name of the profile from the ~/.aws/credentials file that is to be used. + + Make sure the credentials / roles used have the required policies to + access the Sagemaker endpoint. + See: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html + """ + + endpoint_name: str = Field(description="") + temperature: float = Field(description="The temperature to use for sampling.") + max_new_tokens: int = Field(description="The maximum number of tokens to generate.") + context_window: int = Field( + description="The maximum number of context tokens for the model." + ) + messages_to_prompt: Any = Field( + description="The function to convert messages to a prompt.", exclude=True + ) + completion_to_prompt: Any = Field( + description="The function to convert a completion to a prompt.", exclude=True + ) + generate_kwargs: dict[str, Any] = Field( + default_factory=dict, description="Kwargs used for generation." + ) + model_kwargs: dict[str, Any] = Field( + default_factory=dict, description="Kwargs used for model initialization." + ) + verbose: bool = Field(description="Whether to print verbose output.") + + _boto_client: Any = boto3.client( + "sagemaker-runtime", + ) # TODO make it an optional field + + def __init__( + self, + endpoint_name: str | None = "", + temperature: float = 0.1, + max_new_tokens: int = 512, # to review defaults + context_window: int = 2048, # to review defaults + messages_to_prompt: Any = None, + completion_to_prompt: Any = None, + callback_manager: CallbackManager | None = None, + generate_kwargs: dict[str, Any] | None = None, + model_kwargs: dict[str, Any] | None = None, + verbose: bool = True, + ) -> None: + """SagemakerLLM initializer.""" + model_kwargs = model_kwargs or {} + model_kwargs.update({"n_ctx": context_window, "verbose": verbose}) + + messages_to_prompt = messages_to_prompt or {} + completion_to_prompt = completion_to_prompt or {} + + generate_kwargs = generate_kwargs or {} + generate_kwargs.update( + {"temperature": temperature, "max_tokens": max_new_tokens} + ) + + super().__init__( + endpoint_name=endpoint_name, + temperature=temperature, + context_window=context_window, + max_new_tokens=max_new_tokens, + messages_to_prompt=messages_to_prompt, + completion_to_prompt=completion_to_prompt, + callback_manager=callback_manager, + generate_kwargs=generate_kwargs, + model_kwargs=model_kwargs, + verbose=verbose, + ) + + @property + def inference_params(self): + # TODO expose the rest of params + return { + "do_sample": True, + "top_p": 0.7, + "temperature": self.temperature, + "top_k": 50, + "max_new_tokens": self.max_new_tokens, + } + + @property + def metadata(self) -> LLMMetadata: + """Get LLM metadata.""" + return LLMMetadata( + context_window=self.context_window, + num_output=self.max_new_tokens, + model_name="Sagemaker LLama 2", + ) + + @llm_completion_callback() + def complete(self, prompt: str, **kwargs: Any) -> CompletionResponse: + self.generate_kwargs.update({"stream": False}) + + is_formatted = kwargs.pop("formatted", False) + if not is_formatted: + prompt = self.completion_to_prompt(prompt) + + request_params = { + "inputs": prompt, + "stream": False, + "parameters": self.inference_params, + } + + resp = self._boto_client.invoke_endpoint( + EndpointName=self.endpoint_name, + Body=json.dumps(request_params), + ContentType="application/json", + ) + + response_body = resp["Body"] + response_str = response_body.read().decode("utf-8") + response_dict = json.loads(response_str) + + return CompletionResponse( + text=response_dict[0]["generated_text"][len(prompt) :], raw=resp + ) + + @llm_completion_callback() + def stream_complete(self, prompt: str, **kwargs: Any) -> CompletionResponseGen: + def get_stream(): + text = "" + + request_params = { + "inputs": prompt, + "stream": True, + "parameters": self.inference_params, + } + resp = self._boto_client.invoke_endpoint_with_response_stream( + EndpointName=self.endpoint_name, + Body=json.dumps(request_params), + ContentType="application/json", + ) + + event_stream = resp["Body"] + start_json = b"{" + stop_token = "<|endoftext|>" + first_token = True + + for line in LineIterator(event_stream): + if line != b"" and start_json in line: + data = json.loads(line[line.find(start_json) :].decode("utf-8")) + special = data["token"]["special"] + stop = data["token"]["text"] == stop_token + if not special and not stop: + delta = data["token"]["text"] + # trim the leading space for the first token if present + if first_token: + delta = delta.lstrip() + first_token = False + text += delta + yield CompletionResponse(delta=delta, text=text, raw=data) + + return get_stream() + + @llm_chat_callback() + def chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponse: + prompt = self.messages_to_prompt(messages) + completion_response = self.complete(prompt, formatted=True, **kwargs) + return completion_response_to_chat_response(completion_response) + + @llm_chat_callback() + def stream_chat( + self, messages: Sequence[ChatMessage], **kwargs: Any + ) -> ChatResponseGen: + prompt = self.messages_to_prompt(messages) + completion_response = self.stream_complete(prompt, formatted=True, **kwargs) + return stream_completion_response_to_chat_response(completion_response) diff --git a/pgpt/private_gpt/components/llm/llm_component.py b/pgpt/private_gpt/components/llm/llm_component.py new file mode 100644 index 0000000..eb752e5 --- /dev/null +++ b/pgpt/private_gpt/components/llm/llm_component.py @@ -0,0 +1,225 @@ +import logging +from collections.abc import Callable +from typing import Any + +from injector import inject, singleton +from llama_index.core.llms import LLM, MockLLM +from llama_index.core.settings import Settings as LlamaIndexSettings +from llama_index.core.utils import set_global_tokenizer +from transformers import AutoTokenizer # type: ignore + +from private_gpt.components.llm.prompt_helper import get_prompt_style +from private_gpt.paths import models_cache_path, models_path +from private_gpt.settings.settings import Settings + +logger = logging.getLogger(__name__) + + +@singleton +class LLMComponent: + llm: LLM + + @inject + def __init__(self, settings: Settings) -> None: + llm_mode = settings.llm.mode + if settings.llm.tokenizer and settings.llm.mode != "mock": + # Try to download the tokenizer. If it fails, the LLM will still work + # using the default one, which is less accurate. + try: + set_global_tokenizer( + AutoTokenizer.from_pretrained( + pretrained_model_name_or_path=settings.llm.tokenizer, + cache_dir=str(models_cache_path), + token=settings.huggingface.access_token, + ) + ) + except Exception as e: + logger.warning( + f"Failed to download tokenizer {settings.llm.tokenizer}: {e!s}" + f"Please follow the instructions in the documentation to download it if needed: " + f"https://docs.privategpt.dev/installation/getting-started/troubleshooting#tokenizer-setup." + f"Falling back to default tokenizer." + ) + + logger.info("Initializing the LLM in mode=%s", llm_mode) + match settings.llm.mode: + case "llamacpp": + try: + from llama_index.llms.llama_cpp import LlamaCPP # type: ignore + except ImportError as e: + raise ImportError( + "Local dependencies not found, install with `poetry install --extras llms-llama-cpp`" + ) from e + + prompt_style = get_prompt_style(settings.llm.prompt_style) + settings_kwargs = { + "tfs_z": settings.llamacpp.tfs_z, # ollama and llama-cpp + "top_k": settings.llamacpp.top_k, # ollama and llama-cpp + "top_p": settings.llamacpp.top_p, # ollama and llama-cpp + "repeat_penalty": settings.llamacpp.repeat_penalty, # ollama llama-cpp + "n_gpu_layers": -1, + "offload_kqv": True, + } + self.llm = LlamaCPP( + model_path=str(models_path / settings.llamacpp.llm_hf_model_file), + temperature=settings.llm.temperature, + max_new_tokens=settings.llm.max_new_tokens, + context_window=settings.llm.context_window, + generate_kwargs={}, + callback_manager=LlamaIndexSettings.callback_manager, + # All to GPU + model_kwargs=settings_kwargs, + # transform inputs into Llama2 format + messages_to_prompt=prompt_style.messages_to_prompt, + completion_to_prompt=prompt_style.completion_to_prompt, + verbose=True, + ) + + case "sagemaker": + try: + from private_gpt.components.llm.custom.sagemaker import SagemakerLLM + except ImportError as e: + raise ImportError( + "Sagemaker dependencies not found, install with `poetry install --extras llms-sagemaker`" + ) from e + + self.llm = SagemakerLLM( + endpoint_name=settings.sagemaker.llm_endpoint_name, + max_new_tokens=settings.llm.max_new_tokens, + context_window=settings.llm.context_window, + ) + case "openai": + try: + from llama_index.llms.openai import OpenAI # type: ignore + except ImportError as e: + raise ImportError( + "OpenAI dependencies not found, install with `poetry install --extras llms-openai`" + ) from e + + openai_settings = settings.openai + self.llm = OpenAI( + api_base=openai_settings.api_base, + api_key=openai_settings.api_key, + model=openai_settings.model, + ) + case "openailike": + try: + from llama_index.llms.openai_like import OpenAILike # type: ignore + except ImportError as e: + raise ImportError( + "OpenAILike dependencies not found, install with `poetry install --extras llms-openai-like`" + ) from e + prompt_style = get_prompt_style(settings.llm.prompt_style) + openai_settings = settings.openai + self.llm = OpenAILike( + api_base=openai_settings.api_base, + api_key=openai_settings.api_key, + model=openai_settings.model, + is_chat_model=True, + max_tokens=settings.llm.max_new_tokens, + api_version="", + temperature=settings.llm.temperature, + context_window=settings.llm.context_window, + messages_to_prompt=prompt_style.messages_to_prompt, + completion_to_prompt=prompt_style.completion_to_prompt, + tokenizer=settings.llm.tokenizer, + timeout=openai_settings.request_timeout, + reuse_client=False, + ) + case "ollama": + try: + from llama_index.llms.ollama import Ollama # type: ignore + except ImportError as e: + raise ImportError( + "Ollama dependencies not found, install with `poetry install --extras llms-ollama`" + ) from e + + ollama_settings = settings.ollama + + settings_kwargs = { + "tfs_z": ollama_settings.tfs_z, # ollama and llama-cpp + "num_predict": ollama_settings.num_predict, # ollama only + "top_k": ollama_settings.top_k, # ollama and llama-cpp + "top_p": ollama_settings.top_p, # ollama and llama-cpp + "repeat_last_n": ollama_settings.repeat_last_n, # ollama + "repeat_penalty": ollama_settings.repeat_penalty, # ollama llama-cpp + } + + # calculate llm model. If not provided tag, it will be use latest + model_name = ( + ollama_settings.llm_model + ":latest" + if ":" not in ollama_settings.llm_model + else ollama_settings.llm_model + ) + + llm = Ollama( + model=model_name, + base_url=ollama_settings.api_base, + temperature=settings.llm.temperature, + context_window=settings.llm.context_window, + additional_kwargs=settings_kwargs, + request_timeout=ollama_settings.request_timeout, + ) + + if ollama_settings.autopull_models: + from private_gpt.utils.ollama import check_connection, pull_model + + if not check_connection(llm.client): + raise ValueError( + f"Failed to connect to Ollama, " + f"check if Ollama server is running on {ollama_settings.api_base}" + ) + pull_model(llm.client, model_name) + + if ( + ollama_settings.keep_alive + != ollama_settings.model_fields["keep_alive"].default + ): + # Modify Ollama methods to use the "keep_alive" field. + def add_keep_alive(func: Callable[..., Any]) -> Callable[..., Any]: + def wrapper(*args: Any, **kwargs: Any) -> Any: + kwargs["keep_alive"] = ollama_settings.keep_alive + return func(*args, **kwargs) + + return wrapper + + Ollama.chat = add_keep_alive(Ollama.chat) # type: ignore + Ollama.stream_chat = add_keep_alive(Ollama.stream_chat) # type: ignore + Ollama.complete = add_keep_alive(Ollama.complete) # type: ignore + Ollama.stream_complete = add_keep_alive(Ollama.stream_complete) # type: ignore + + self.llm = llm + + case "azopenai": + try: + from llama_index.llms.azure_openai import ( # type: ignore + AzureOpenAI, + ) + except ImportError as e: + raise ImportError( + "Azure OpenAI dependencies not found, install with `poetry install --extras llms-azopenai`" + ) from e + + azopenai_settings = settings.azopenai + self.llm = AzureOpenAI( + model=azopenai_settings.llm_model, + deployment_name=azopenai_settings.llm_deployment_name, + api_key=azopenai_settings.api_key, + azure_endpoint=azopenai_settings.azure_endpoint, + api_version=azopenai_settings.api_version, + ) + case "gemini": + try: + from llama_index.llms.gemini import ( # type: ignore + Gemini, + ) + except ImportError as e: + raise ImportError( + "Google Gemini dependencies not found, install with `poetry install --extras llms-gemini`" + ) from e + gemini_settings = settings.gemini + self.llm = Gemini( + model_name=gemini_settings.model, api_key=gemini_settings.api_key + ) + case "mock": + self.llm = MockLLM() diff --git a/pgpt/private_gpt/components/llm/prompt_helper.py b/pgpt/private_gpt/components/llm/prompt_helper.py new file mode 100644 index 0000000..5875411 --- /dev/null +++ b/pgpt/private_gpt/components/llm/prompt_helper.py @@ -0,0 +1,310 @@ +import abc +import logging +from collections.abc import Sequence +from typing import Any, Literal + +from llama_index.core.llms import ChatMessage, MessageRole + +logger = logging.getLogger(__name__) + + +class AbstractPromptStyle(abc.ABC): + """Abstract class for prompt styles. + + This class is used to format a series of messages into a prompt that can be + understood by the models. A series of messages represents the interaction(s) + between a user and an assistant. This series of messages can be considered as a + session between a user X and an assistant Y.This session holds, through the + messages, the state of the conversation. This session, to be understood by the + model, needs to be formatted into a prompt (i.e. a string that the models + can understand). Prompts can be formatted in different ways, + depending on the model. + + The implementations of this class represent the different ways to format a + series of messages into a prompt. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + logger.debug("Initializing prompt_style=%s", self.__class__.__name__) + + @abc.abstractmethod + def _messages_to_prompt(self, messages: Sequence[ChatMessage]) -> str: + pass + + @abc.abstractmethod + def _completion_to_prompt(self, completion: str) -> str: + pass + + def messages_to_prompt(self, messages: Sequence[ChatMessage]) -> str: + prompt = self._messages_to_prompt(messages) + logger.debug("Got for messages='%s' the prompt='%s'", messages, prompt) + return prompt + + def completion_to_prompt(self, prompt: str) -> str: + completion = prompt # Fix: Llama-index parameter has to be named as prompt + prompt = self._completion_to_prompt(completion) + logger.debug("Got for completion='%s' the prompt='%s'", completion, prompt) + return prompt + + +class DefaultPromptStyle(AbstractPromptStyle): + """Default prompt style that uses the defaults from llama_utils. + + It basically passes None to the LLM, indicating it should use + the default functions. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + # Hacky way to override the functions + # Override the functions to be None, and pass None to the LLM. + self.messages_to_prompt = None # type: ignore[method-assign, assignment] + self.completion_to_prompt = None # type: ignore[method-assign, assignment] + + def _messages_to_prompt(self, messages: Sequence[ChatMessage]) -> str: + return "" + + def _completion_to_prompt(self, completion: str) -> str: + return "" + + +class Llama2PromptStyle(AbstractPromptStyle): + """Simple prompt style that uses llama 2 prompt style. + + Inspired by llama_index/legacy/llms/llama_utils.py + + It transforms the sequence of messages into a prompt that should look like: + ```text + [INST] <> your system prompt here. <> + + user message here [/INST] assistant (model) response here + ``` + """ + + BOS, EOS = "", "" + B_INST, E_INST = "[INST]", "[/INST]" + B_SYS, E_SYS = "<>\n", "\n<>\n\n" + DEFAULT_SYSTEM_PROMPT = """\ + You are a helpful, respectful and honest assistant. \ + Always answer as helpfully as possible and follow ALL given instructions. \ + Do not speculate or make up information. \ + Do not reference any given instructions or context. \ + """ + + def _messages_to_prompt(self, messages: Sequence[ChatMessage]) -> str: + string_messages: list[str] = [] + if messages[0].role == MessageRole.SYSTEM: + # pull out the system message (if it exists in messages) + system_message_str = messages[0].content or "" + messages = messages[1:] + else: + system_message_str = self.DEFAULT_SYSTEM_PROMPT + + system_message_str = f"{self.B_SYS} {system_message_str.strip()} {self.E_SYS}" + + for i in range(0, len(messages), 2): + # first message should always be a user + user_message = messages[i] + assert user_message.role == MessageRole.USER + + if i == 0: + # make sure system prompt is included at the start + str_message = f"{self.BOS} {self.B_INST} {system_message_str} " + else: + # end previous user-assistant interaction + string_messages[-1] += f" {self.EOS}" + # no need to include system prompt + str_message = f"{self.BOS} {self.B_INST} " + + # include user message content + str_message += f"{user_message.content} {self.E_INST}" + + if len(messages) > (i + 1): + # if assistant message exists, add to str_message + assistant_message = messages[i + 1] + assert assistant_message.role == MessageRole.ASSISTANT + str_message += f" {assistant_message.content}" + + string_messages.append(str_message) + + return "".join(string_messages) + + def _completion_to_prompt(self, completion: str) -> str: + system_prompt_str = self.DEFAULT_SYSTEM_PROMPT + + return ( + f"{self.BOS} {self.B_INST} {self.B_SYS} {system_prompt_str.strip()} {self.E_SYS} " + f"{completion.strip()} {self.E_INST}" + ) + + +class Llama3PromptStyle(AbstractPromptStyle): + r"""Template for Meta's Llama 3.1. + + The format follows this structure: + <|begin_of_text|> + <|start_header_id|>system<|end_header_id|> + + [System message content]<|eot_id|> + <|start_header_id|>user<|end_header_id|> + + [User message content]<|eot_id|> + <|start_header_id|>assistant<|end_header_id|> + + [Assistant message content]<|eot_id|> + ... + (Repeat for each message, including possible 'ipython' role) + """ + + BOS, EOS = "<|begin_of_text|>", "<|end_of_text|>" + B_INST, E_INST = "<|start_header_id|>", "<|end_header_id|>" + EOT = "<|eot_id|>" + B_SYS, E_SYS = "<|start_header_id|>system<|end_header_id|>", "<|eot_id|>" + ASSISTANT_INST = "<|start_header_id|>assistant<|end_header_id|>" + DEFAULT_SYSTEM_PROMPT = """\ + You are a helpful, respectful and honest assistant. \ + Always answer as helpfully as possible and follow ALL given instructions. \ + Do not speculate or make up information. \ + Do not reference any given instructions or context. \ + """ + + def _messages_to_prompt(self, messages: Sequence[ChatMessage]) -> str: + prompt = "" + has_system_message = False + + for i, message in enumerate(messages): + if not message or message.content is None: + continue + if message.role == MessageRole.SYSTEM: + prompt += f"{self.B_SYS}\n\n{message.content.strip()}{self.E_SYS}" + has_system_message = True + else: + role_header = f"{self.B_INST}{message.role.value}{self.E_INST}" + prompt += f"{role_header}\n\n{message.content.strip()}{self.EOT}" + + # Add assistant header if the last message is not from the assistant + if i == len(messages) - 1 and message.role != MessageRole.ASSISTANT: + prompt += f"{self.ASSISTANT_INST}\n\n" + + # Add default system prompt if no system message was provided + if not has_system_message: + prompt = ( + f"{self.B_SYS}\n\n{self.DEFAULT_SYSTEM_PROMPT}{self.E_SYS}" + prompt + ) + + # TODO: Implement tool handling logic + + return prompt + + def _completion_to_prompt(self, completion: str) -> str: + return ( + f"{self.B_SYS}\n\n{self.DEFAULT_SYSTEM_PROMPT}{self.E_SYS}" + f"{self.B_INST}user{self.E_INST}\n\n{completion.strip()}{self.EOT}" + f"{self.ASSISTANT_INST}\n\n" + ) + + +class TagPromptStyle(AbstractPromptStyle): + """Tag prompt style (used by Vigogne) that uses the prompt style `<|ROLE|>`. + + It transforms the sequence of messages into a prompt that should look like: + ```text + <|system|>: your system prompt here. + <|user|>: user message here + (possibly with context and question) + <|assistant|>: assistant (model) response here. + ``` + + FIXME: should we add surrounding `` and `` tags, like in llama2? + """ + + def _messages_to_prompt(self, messages: Sequence[ChatMessage]) -> str: + """Format message to prompt with `<|ROLE|>: MSG` style.""" + prompt = "" + for message in messages: + role = message.role + content = message.content or "" + message_from_user = f"<|{role.lower()}|>: {content.strip()}" + message_from_user += "\n" + prompt += message_from_user + # we are missing the last <|assistant|> tag that will trigger a completion + prompt += "<|assistant|>: " + return prompt + + def _completion_to_prompt(self, completion: str) -> str: + return self._messages_to_prompt( + [ChatMessage(content=completion, role=MessageRole.USER)] + ) + + +class MistralPromptStyle(AbstractPromptStyle): + def _messages_to_prompt(self, messages: Sequence[ChatMessage]) -> str: + inst_buffer = [] + text = "" + for message in messages: + if message.role == MessageRole.SYSTEM or message.role == MessageRole.USER: + inst_buffer.append(str(message.content).strip()) + elif message.role == MessageRole.ASSISTANT: + text += "[INST] " + "\n".join(inst_buffer) + " [/INST]" + text += " " + str(message.content).strip() + "" + inst_buffer.clear() + else: + raise ValueError(f"Unknown message role {message.role}") + + if len(inst_buffer) > 0: + text += "[INST] " + "\n".join(inst_buffer) + " [/INST]" + + return text + + def _completion_to_prompt(self, completion: str) -> str: + return self._messages_to_prompt( + [ChatMessage(content=completion, role=MessageRole.USER)] + ) + + +class ChatMLPromptStyle(AbstractPromptStyle): + def _messages_to_prompt(self, messages: Sequence[ChatMessage]) -> str: + prompt = "<|im_start|>system\n" + for message in messages: + role = message.role + content = message.content or "" + if role.lower() == "system": + message_from_user = f"{content.strip()}" + prompt += message_from_user + elif role.lower() == "user": + prompt += "<|im_end|>\n<|im_start|>user\n" + message_from_user = f"{content.strip()}<|im_end|>\n" + prompt += message_from_user + prompt += "<|im_start|>assistant\n" + return prompt + + def _completion_to_prompt(self, completion: str) -> str: + return self._messages_to_prompt( + [ChatMessage(content=completion, role=MessageRole.USER)] + ) + + +def get_prompt_style( + prompt_style: ( + Literal["default", "llama2", "llama3", "tag", "mistral", "chatml"] | None + ) +) -> AbstractPromptStyle: + """Get the prompt style to use from the given string. + + :param prompt_style: The prompt style to use. + :return: The prompt style to use. + """ + if prompt_style is None or prompt_style == "default": + return DefaultPromptStyle() + elif prompt_style == "llama2": + return Llama2PromptStyle() + elif prompt_style == "llama3": + return Llama3PromptStyle() + elif prompt_style == "tag": + return TagPromptStyle() + elif prompt_style == "mistral": + return MistralPromptStyle() + elif prompt_style == "chatml": + return ChatMLPromptStyle() + raise ValueError(f"Unknown prompt_style='{prompt_style}'") diff --git a/pgpt/private_gpt/components/node_store/__init__.py b/pgpt/private_gpt/components/node_store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/components/node_store/node_store_component.py b/pgpt/private_gpt/components/node_store/node_store_component.py new file mode 100644 index 0000000..5cd0afb --- /dev/null +++ b/pgpt/private_gpt/components/node_store/node_store_component.py @@ -0,0 +1,68 @@ +import logging + +from injector import inject, singleton +from llama_index.core.storage.docstore import BaseDocumentStore, SimpleDocumentStore +from llama_index.core.storage.index_store import SimpleIndexStore +from llama_index.core.storage.index_store.types import BaseIndexStore + +from private_gpt.paths import local_data_path +from private_gpt.settings.settings import Settings + +logger = logging.getLogger(__name__) + + +@singleton +class NodeStoreComponent: + index_store: BaseIndexStore + doc_store: BaseDocumentStore + + @inject + def __init__(self, settings: Settings) -> None: + match settings.nodestore.database: + case "simple": + try: + self.index_store = SimpleIndexStore.from_persist_dir( + persist_dir=str(local_data_path) + ) + except FileNotFoundError: + logger.debug("Local index store not found, creating a new one") + self.index_store = SimpleIndexStore() + + try: + self.doc_store = SimpleDocumentStore.from_persist_dir( + persist_dir=str(local_data_path) + ) + except FileNotFoundError: + logger.debug("Local document store not found, creating a new one") + self.doc_store = SimpleDocumentStore() + + case "postgres": + try: + from llama_index.storage.docstore.postgres import ( # type: ignore + PostgresDocumentStore, + ) + from llama_index.storage.index_store.postgres import ( # type: ignore + PostgresIndexStore, + ) + except ImportError: + raise ImportError( + "Postgres dependencies not found, install with `poetry install --extras storage-nodestore-postgres`" + ) from None + + if settings.postgres is None: + raise ValueError("Postgres index/doc store settings not found.") + + self.index_store = PostgresIndexStore.from_params( + **settings.postgres.model_dump(exclude_none=True) + ) + + self.doc_store = PostgresDocumentStore.from_params( + **settings.postgres.model_dump(exclude_none=True) + ) + + case _: + # Should be unreachable + # The settings validator should have caught this + raise ValueError( + f"Database {settings.nodestore.database} not supported" + ) diff --git a/pgpt/private_gpt/components/vector_store/__init__.py b/pgpt/private_gpt/components/vector_store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/components/vector_store/batched_chroma.py b/pgpt/private_gpt/components/vector_store/batched_chroma.py new file mode 100644 index 0000000..54dd049 --- /dev/null +++ b/pgpt/private_gpt/components/vector_store/batched_chroma.py @@ -0,0 +1,106 @@ +from collections.abc import Generator, Sequence +from typing import TYPE_CHECKING, Any + +from llama_index.core.schema import BaseNode, MetadataMode +from llama_index.core.vector_stores.utils import node_to_metadata_dict +from llama_index.vector_stores.chroma import ChromaVectorStore # type: ignore + +if TYPE_CHECKING: + from collections.abc import Mapping + + +def chunk_list( + lst: Sequence[BaseNode], max_chunk_size: int +) -> Generator[Sequence[BaseNode], None, None]: + """Yield successive max_chunk_size-sized chunks from lst. + + Args: + lst (List[BaseNode]): list of nodes with embeddings + max_chunk_size (int): max chunk size + + Yields: + Generator[List[BaseNode], None, None]: list of nodes with embeddings + """ + for i in range(0, len(lst), max_chunk_size): + yield lst[i : i + max_chunk_size] + + +class BatchedChromaVectorStore(ChromaVectorStore): # type: ignore + """Chroma vector store, batching additions to avoid reaching the max batch limit. + + In this vector store, embeddings are stored within a ChromaDB collection. + + During query time, the index uses ChromaDB to query for the top + k most similar nodes. + + Args: + chroma_client (from chromadb.api.API): + API instance + chroma_collection (chromadb.api.models.Collection.Collection): + ChromaDB collection instance + + """ + + chroma_client: Any | None + + def __init__( + self, + chroma_client: Any, + chroma_collection: Any, + host: str | None = None, + port: str | None = None, + ssl: bool = False, + headers: dict[str, str] | None = None, + collection_kwargs: dict[Any, Any] | None = None, + ) -> None: + super().__init__( + chroma_collection=chroma_collection, + host=host, + port=port, + ssl=ssl, + headers=headers, + collection_kwargs=collection_kwargs or {}, + ) + self.chroma_client = chroma_client + + def add(self, nodes: Sequence[BaseNode], **add_kwargs: Any) -> list[str]: + """Add nodes to index, batching the insertion to avoid issues. + + Args: + nodes: List[BaseNode]: list of nodes with embeddings + add_kwargs: _ + """ + if not self.chroma_client: + raise ValueError("Client not initialized") + + if not self._collection: + raise ValueError("Collection not initialized") + + max_chunk_size = self.chroma_client.max_batch_size + node_chunks = chunk_list(nodes, max_chunk_size) + + all_ids = [] + for node_chunk in node_chunks: + embeddings: list[Sequence[float]] = [] + metadatas: list[Mapping[str, Any]] = [] + ids = [] + documents = [] + for node in node_chunk: + embeddings.append(node.get_embedding()) + metadatas.append( + node_to_metadata_dict( + node, remove_text=True, flat_metadata=self.flat_metadata + ) + ) + ids.append(node.node_id) + documents.append(node.get_content(metadata_mode=MetadataMode.NONE)) + + self._collection.add( + embeddings=embeddings, + ids=ids, + metadatas=metadatas, + documents=documents, + ) + all_ids.extend(ids) + + return all_ids diff --git a/pgpt/private_gpt/components/vector_store/vector_store_component.py b/pgpt/private_gpt/components/vector_store/vector_store_component.py new file mode 100644 index 0000000..841a118 --- /dev/null +++ b/pgpt/private_gpt/components/vector_store/vector_store_component.py @@ -0,0 +1,217 @@ +import logging +import typing + +from injector import inject, singleton +from llama_index.core.indices.vector_store import VectorIndexRetriever, VectorStoreIndex +from llama_index.core.vector_stores.types import ( + BasePydanticVectorStore, + FilterCondition, + MetadataFilter, + MetadataFilters, +) + +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.paths import local_data_path +from private_gpt.settings.settings import Settings + +logger = logging.getLogger(__name__) + + +def _doc_id_metadata_filter( + context_filter: ContextFilter | None, +) -> MetadataFilters: + filters = MetadataFilters(filters=[], condition=FilterCondition.OR) + + if context_filter is not None and context_filter.docs_ids is not None: + for doc_id in context_filter.docs_ids: + filters.filters.append(MetadataFilter(key="doc_id", value=doc_id)) + + return filters + + +@singleton +class VectorStoreComponent: + settings: Settings + vector_store: BasePydanticVectorStore + + @inject + def __init__(self, settings: Settings) -> None: + self.settings = settings + match settings.vectorstore.database: + case "postgres": + try: + from llama_index.vector_stores.postgres import ( # type: ignore + PGVectorStore, + ) + except ImportError as e: + raise ImportError( + "Postgres dependencies not found, install with `poetry install --extras vector-stores-postgres`" + ) from e + + if settings.postgres is None: + raise ValueError( + "Postgres settings not found. Please provide settings." + ) + + self.vector_store = typing.cast( + BasePydanticVectorStore, + PGVectorStore.from_params( + **settings.postgres.model_dump(exclude_none=True), + table_name="embeddings", + embed_dim=settings.embedding.embed_dim, + ), + ) + + case "chroma": + try: + import chromadb # type: ignore + from chromadb.config import ( # type: ignore + Settings as ChromaSettings, + ) + + from private_gpt.components.vector_store.batched_chroma import ( + BatchedChromaVectorStore, + ) + except ImportError as e: + raise ImportError( + "ChromaDB dependencies not found, install with `poetry install --extras vector-stores-chroma`" + ) from e + + chroma_settings = ChromaSettings(anonymized_telemetry=False) + chroma_client = chromadb.PersistentClient( + path=str((local_data_path / "chroma_db").absolute()), + settings=chroma_settings, + ) + chroma_collection = chroma_client.get_or_create_collection( + "make_this_parameterizable_per_api_call" + ) # TODO + + self.vector_store = typing.cast( + BasePydanticVectorStore, + BatchedChromaVectorStore( + chroma_client=chroma_client, chroma_collection=chroma_collection + ), + ) + + case "qdrant": + try: + from llama_index.vector_stores.qdrant import ( # type: ignore + QdrantVectorStore, + ) + from qdrant_client import QdrantClient # type: ignore + except ImportError as e: + raise ImportError( + "Qdrant dependencies not found, install with `poetry install --extras vector-stores-qdrant`" + ) from e + + if settings.qdrant is None: + logger.info( + "Qdrant config not found. Using default settings." + "Trying to connect to Qdrant at localhost:6333." + ) + client = QdrantClient() + else: + client = QdrantClient( + **settings.qdrant.model_dump(exclude_none=True) + ) + self.vector_store = typing.cast( + BasePydanticVectorStore, + QdrantVectorStore( + client=client, + collection_name="make_this_parameterizable_per_api_call", + ), # TODO + ) + + case "milvus": + try: + from llama_index.vector_stores.milvus import ( # type: ignore + MilvusVectorStore, + ) + except ImportError as e: + raise ImportError( + "Milvus dependencies not found, install with `poetry install --extras vector-stores-milvus`" + ) from e + + if settings.milvus is None: + logger.info( + "Milvus config not found. Using default settings.\n" + "Trying to connect to Milvus at local_data/private_gpt/milvus/milvus_local.db " + "with collection 'make_this_parameterizable_per_api_call'." + ) + + self.vector_store = typing.cast( + BasePydanticVectorStore, + MilvusVectorStore( + dim=settings.embedding.embed_dim, + collection_name="make_this_parameterizable_per_api_call", + overwrite=True, + ), + ) + + else: + self.vector_store = typing.cast( + BasePydanticVectorStore, + MilvusVectorStore( + dim=settings.embedding.embed_dim, + uri=settings.milvus.uri, + token=settings.milvus.token, + collection_name=settings.milvus.collection_name, + overwrite=settings.milvus.overwrite, + ), + ) + + case "clickhouse": + try: + from clickhouse_connect import ( # type: ignore + get_client, + ) + from llama_index.vector_stores.clickhouse import ( # type: ignore + ClickHouseVectorStore, + ) + except ImportError as e: + raise ImportError( + "ClickHouse dependencies not found, install with `poetry install --extras vector-stores-clickhouse`" + ) from e + + if settings.clickhouse is None: + raise ValueError( + "ClickHouse settings not found. Please provide settings." + ) + + clickhouse_client = get_client( + host=settings.clickhouse.host, + port=settings.clickhouse.port, + username=settings.clickhouse.username, + password=settings.clickhouse.password, + ) + self.vector_store = ClickHouseVectorStore( + clickhouse_client=clickhouse_client + ) + case _: + # Should be unreachable + # The settings validator should have caught this + raise ValueError( + f"Vectorstore database {settings.vectorstore.database} not supported" + ) + + def get_retriever( + self, + index: VectorStoreIndex, + context_filter: ContextFilter | None = None, + similarity_top_k: int = 2, + ) -> VectorIndexRetriever: + # This way we support qdrant (using doc_ids) and the rest (using filters) + return VectorIndexRetriever( + index=index, + similarity_top_k=similarity_top_k, + doc_ids=context_filter.docs_ids if context_filter else None, + filters=( + _doc_id_metadata_filter(context_filter) + if self.settings.vectorstore.database != "qdrant" + else None + ), + ) + + def close(self) -> None: + if hasattr(self.vector_store.client, "close"): + self.vector_store.client.close() diff --git a/pgpt/private_gpt/constants.py b/pgpt/private_gpt/constants.py new file mode 100644 index 0000000..3a1d6fb --- /dev/null +++ b/pgpt/private_gpt/constants.py @@ -0,0 +1,3 @@ +from pathlib import Path + +PROJECT_ROOT_PATH: Path = Path(__file__).parents[1] diff --git a/pgpt/private_gpt/di.py b/pgpt/private_gpt/di.py new file mode 100644 index 0000000..05021b0 --- /dev/null +++ b/pgpt/private_gpt/di.py @@ -0,0 +1,19 @@ +from injector import Injector + +from private_gpt.settings.settings import Settings, unsafe_typed_settings + + +def create_application_injector() -> Injector: + _injector = Injector(auto_bind=True) + _injector.binder.bind(Settings, to=unsafe_typed_settings) + return _injector + + +""" +Global injector for the application. + +Avoid using this reference, it will make your code harder to test. + +Instead, use the `request.state.injector` reference, which is bound to every request +""" +global_injector: Injector = create_application_injector() diff --git a/pgpt/private_gpt/launcher.py b/pgpt/private_gpt/launcher.py new file mode 100644 index 0000000..968baae --- /dev/null +++ b/pgpt/private_gpt/launcher.py @@ -0,0 +1,69 @@ +"""FastAPI app creation, logger configuration and main API routes.""" + +import logging + +from fastapi import Depends, FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from injector import Injector +from llama_index.core.callbacks import CallbackManager +from llama_index.core.callbacks.global_handlers import create_global_handler +from llama_index.core.settings import Settings as LlamaIndexSettings + +from private_gpt.server.chat.chat_router import chat_router +from private_gpt.server.chunks.chunks_router import chunks_router +from private_gpt.server.completions.completions_router import completions_router +from private_gpt.server.embeddings.embeddings_router import embeddings_router +from private_gpt.server.health.health_router import health_router +from private_gpt.server.ingest.ingest_router import ingest_router +from private_gpt.server.recipes.summarize.summarize_router import summarize_router +from private_gpt.settings.settings import Settings + +logger = logging.getLogger(__name__) + + +def create_app(root_injector: Injector) -> FastAPI: + + # Start the API + async def bind_injector_to_request(request: Request) -> None: + request.state.injector = root_injector + + app = FastAPI(dependencies=[Depends(bind_injector_to_request)]) + + app.include_router(completions_router) + app.include_router(chat_router) + app.include_router(chunks_router) + app.include_router(ingest_router) + app.include_router(summarize_router) + app.include_router(embeddings_router) + app.include_router(health_router) + + # Add LlamaIndex simple observability + global_handler = create_global_handler("simple") + if global_handler: + LlamaIndexSettings.callback_manager = CallbackManager([global_handler]) + + settings = root_injector.get(Settings) + if settings.server.cors.enabled: + logger.debug("Setting up CORS middleware") + app.add_middleware( + CORSMiddleware, + allow_credentials=settings.server.cors.allow_credentials, + allow_origins=settings.server.cors.allow_origins, + allow_origin_regex=settings.server.cors.allow_origin_regex, + allow_methods=settings.server.cors.allow_methods, + allow_headers=settings.server.cors.allow_headers, + ) + + if settings.ui.enabled: + logger.debug("Importing the UI module") + try: + from private_gpt.ui.ui import PrivateGptUi + except ImportError as e: + raise ImportError( + "UI dependencies not found, install with `poetry install --extras ui`" + ) from e + + ui = root_injector.get(PrivateGptUi) + ui.mount_in_app(app, settings.ui.path) + + return app diff --git a/pgpt/private_gpt/main.py b/pgpt/private_gpt/main.py new file mode 100644 index 0000000..58c4006 --- /dev/null +++ b/pgpt/private_gpt/main.py @@ -0,0 +1,6 @@ +"""FastAPI app creation, logger configuration and main API routes.""" + +from private_gpt.di import global_injector +from private_gpt.launcher import create_app + +app = create_app(global_injector) diff --git a/pgpt/private_gpt/open_ai/__init__.py b/pgpt/private_gpt/open_ai/__init__.py new file mode 100644 index 0000000..25a0df4 --- /dev/null +++ b/pgpt/private_gpt/open_ai/__init__.py @@ -0,0 +1 @@ +"""OpenAI compatibility utilities.""" diff --git a/pgpt/private_gpt/open_ai/extensions/__init__.py b/pgpt/private_gpt/open_ai/extensions/__init__.py new file mode 100644 index 0000000..f893034 --- /dev/null +++ b/pgpt/private_gpt/open_ai/extensions/__init__.py @@ -0,0 +1 @@ +"""OpenAI API extensions.""" diff --git a/pgpt/private_gpt/open_ai/extensions/context_filter.py b/pgpt/private_gpt/open_ai/extensions/context_filter.py new file mode 100644 index 0000000..c1a09c9 --- /dev/null +++ b/pgpt/private_gpt/open_ai/extensions/context_filter.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field + + +class ContextFilter(BaseModel): + docs_ids: list[str] | None = Field( + examples=[["c202d5e6-7b69-4869-81cc-dd574ee8ee11"]] + ) diff --git a/pgpt/private_gpt/open_ai/openai_models.py b/pgpt/private_gpt/open_ai/openai_models.py new file mode 100644 index 0000000..67b67dd --- /dev/null +++ b/pgpt/private_gpt/open_ai/openai_models.py @@ -0,0 +1,122 @@ +import time +import uuid +from collections.abc import Iterator +from typing import Literal + +from llama_index.core.llms import ChatResponse, CompletionResponse +from pydantic import BaseModel, Field + +from private_gpt.server.chunks.chunks_service import Chunk + + +class OpenAIDelta(BaseModel): + """A piece of completion that needs to be concatenated to get the full message.""" + + content: str | None + + +class OpenAIMessage(BaseModel): + """Inference result, with the source of the message. + + Role could be the assistant or system + (providing a default response, not AI generated). + """ + + role: Literal["assistant", "system", "user"] = Field(default="user") + content: str | None + + +class OpenAIChoice(BaseModel): + """Response from AI. + + Either the delta or the message will be present, but never both. + Sources used will be returned in case context retrieval was enabled. + """ + + finish_reason: str | None = Field(examples=["stop"]) + delta: OpenAIDelta | None = None + message: OpenAIMessage | None = None + sources: list[Chunk] | None = None + index: int = 0 + + +class OpenAICompletion(BaseModel): + """Clone of OpenAI Completion model. + + For more information see: https://platform.openai.com/docs/api-reference/chat/object + """ + + id: str + object: Literal["completion", "completion.chunk"] = Field(default="completion") + created: int = Field(..., examples=[1623340000]) + model: Literal["private-gpt"] + choices: list[OpenAIChoice] + + @classmethod + def from_text( + cls, + text: str | None, + finish_reason: str | None = None, + sources: list[Chunk] | None = None, + ) -> "OpenAICompletion": + return OpenAICompletion( + id=str(uuid.uuid4()), + object="completion", + created=int(time.time()), + model="private-gpt", + choices=[ + OpenAIChoice( + message=OpenAIMessage(role="assistant", content=text), + finish_reason=finish_reason, + sources=sources, + ) + ], + ) + + @classmethod + def json_from_delta( + cls, + *, + text: str | None, + finish_reason: str | None = None, + sources: list[Chunk] | None = None, + ) -> str: + chunk = OpenAICompletion( + id=str(uuid.uuid4()), + object="completion.chunk", + created=int(time.time()), + model="private-gpt", + choices=[ + OpenAIChoice( + delta=OpenAIDelta(content=text), + finish_reason=finish_reason, + sources=sources, + ) + ], + ) + + return chunk.model_dump_json() + + +def to_openai_response( + response: str | ChatResponse, sources: list[Chunk] | None = None +) -> OpenAICompletion: + if isinstance(response, ChatResponse): + return OpenAICompletion.from_text(response.delta, finish_reason="stop") + else: + return OpenAICompletion.from_text( + response, finish_reason="stop", sources=sources + ) + + +def to_openai_sse_stream( + response_generator: Iterator[str | CompletionResponse | ChatResponse], + sources: list[Chunk] | None = None, +) -> Iterator[str]: + for response in response_generator: + if isinstance(response, CompletionResponse | ChatResponse): + yield f"data: {OpenAICompletion.json_from_delta(text=response.delta)}\n\n" + else: + yield f"data: {OpenAICompletion.json_from_delta(text=response, sources=sources)}\n\n" + yield f"data: {OpenAICompletion.json_from_delta(text='', finish_reason='stop')}\n\n" + yield "data: [DONE]\n\n" diff --git a/pgpt/private_gpt/paths.py b/pgpt/private_gpt/paths.py new file mode 100644 index 0000000..59db3a4 --- /dev/null +++ b/pgpt/private_gpt/paths.py @@ -0,0 +1,18 @@ +from pathlib import Path + +from private_gpt.constants import PROJECT_ROOT_PATH +from private_gpt.settings.settings import settings + + +def _absolute_or_from_project_root(path: str) -> Path: + if path.startswith("/"): + return Path(path) + return PROJECT_ROOT_PATH / path + + +models_path: Path = PROJECT_ROOT_PATH / "models" +models_cache_path: Path = models_path / "cache" +docs_path: Path = PROJECT_ROOT_PATH / "docs" +local_data_path: Path = _absolute_or_from_project_root( + settings().data.local_data_folder +) diff --git a/pgpt/private_gpt/server/__init__.py b/pgpt/private_gpt/server/__init__.py new file mode 100644 index 0000000..5fcc285 --- /dev/null +++ b/pgpt/private_gpt/server/__init__.py @@ -0,0 +1 @@ +"""private-gpt server.""" diff --git a/pgpt/private_gpt/server/chat/__init__.py b/pgpt/private_gpt/server/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/server/chat/chat_router.py b/pgpt/private_gpt/server/chat/chat_router.py new file mode 100644 index 0000000..ed86cdb --- /dev/null +++ b/pgpt/private_gpt/server/chat/chat_router.py @@ -0,0 +1,115 @@ +from fastapi import APIRouter, Depends, Request +from llama_index.core.llms import ChatMessage, MessageRole +from pydantic import BaseModel +from starlette.responses import StreamingResponse + +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.open_ai.openai_models import ( + OpenAICompletion, + OpenAIMessage, + to_openai_response, + to_openai_sse_stream, +) +from private_gpt.server.chat.chat_service import ChatService +from private_gpt.server.utils.auth import authenticated + +chat_router = APIRouter(prefix="/v1", dependencies=[Depends(authenticated)]) + + +class ChatBody(BaseModel): + messages: list[OpenAIMessage] + use_context: bool = False + context_filter: ContextFilter | None = None + include_sources: bool = True + stream: bool = False + + model_config = { + "json_schema_extra": { + "examples": [ + { + "messages": [ + { + "role": "system", + "content": "You are a rapper. Always answer with a rap.", + }, + { + "role": "user", + "content": "How do you fry an egg?", + }, + ], + "stream": False, + "use_context": True, + "include_sources": True, + "context_filter": { + "docs_ids": ["c202d5e6-7b69-4869-81cc-dd574ee8ee11"] + }, + } + ] + } + } + + +@chat_router.post( + "/chat/completions", + response_model=None, + responses={200: {"model": OpenAICompletion}}, + tags=["Contextual Completions"], + openapi_extra={ + "x-fern-streaming": { + "stream-condition": "stream", + "response": {"$ref": "#/components/schemas/OpenAICompletion"}, + "response-stream": {"$ref": "#/components/schemas/OpenAICompletion"}, + } + }, +) +def chat_completion( + request: Request, body: ChatBody +) -> OpenAICompletion | StreamingResponse: + """Given a list of messages comprising a conversation, return a response. + + Optionally include an initial `role: system` message to influence the way + the LLM answers. + + If `use_context` is set to `true`, the model will use context coming + from the ingested documents to create the response. The documents being used can + be filtered using the `context_filter` and passing the document IDs to be used. + Ingested documents IDs can be found using `/ingest/list` endpoint. If you want + all ingested documents to be used, remove `context_filter` altogether. + + When using `'include_sources': true`, the API will return the source Chunks used + to create the response, which come from the context provided. + + When using `'stream': true`, the API will return data chunks following [OpenAI's + streaming model](https://platform.openai.com/docs/api-reference/chat/streaming): + ``` + {"id":"12345","object":"completion.chunk","created":1694268190, + "model":"private-gpt","choices":[{"index":0,"delta":{"content":"Hello"}, + "finish_reason":null}]} + ``` + """ + service = request.state.injector.get(ChatService) + all_messages = [ + ChatMessage(content=m.content, role=MessageRole(m.role)) for m in body.messages + ] + if body.stream: + completion_gen = service.stream_chat( + messages=all_messages, + use_context=body.use_context, + context_filter=body.context_filter, + ) + return StreamingResponse( + to_openai_sse_stream( + completion_gen.response, + completion_gen.sources if body.include_sources else None, + ), + media_type="text/event-stream", + ) + else: + completion = service.chat( + messages=all_messages, + use_context=body.use_context, + context_filter=body.context_filter, + ) + return to_openai_response( + completion.response, completion.sources if body.include_sources else None + ) diff --git a/pgpt/private_gpt/server/chat/chat_service.py b/pgpt/private_gpt/server/chat/chat_service.py new file mode 100644 index 0000000..efa4a19 --- /dev/null +++ b/pgpt/private_gpt/server/chat/chat_service.py @@ -0,0 +1,217 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from injector import inject, singleton +from llama_index.core.chat_engine import ContextChatEngine, SimpleChatEngine +from llama_index.core.chat_engine.types import ( + BaseChatEngine, +) +from llama_index.core.indices import VectorStoreIndex +from llama_index.core.indices.postprocessor import MetadataReplacementPostProcessor +from llama_index.core.llms import ChatMessage, MessageRole +from llama_index.core.postprocessor import ( + SentenceTransformerRerank, + SimilarityPostprocessor, +) +from llama_index.core.storage import StorageContext +from llama_index.core.types import TokenGen +from pydantic import BaseModel + +from private_gpt.components.embedding.embedding_component import EmbeddingComponent +from private_gpt.components.llm.llm_component import LLMComponent +from private_gpt.components.node_store.node_store_component import NodeStoreComponent +from private_gpt.components.vector_store.vector_store_component import ( + VectorStoreComponent, +) +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.server.chunks.chunks_service import Chunk +from private_gpt.settings.settings import Settings + +if TYPE_CHECKING: + from llama_index.core.postprocessor.types import BaseNodePostprocessor + + +class Completion(BaseModel): + response: str + sources: list[Chunk] | None = None + + +class CompletionGen(BaseModel): + response: TokenGen + sources: list[Chunk] | None = None + + +@dataclass +class ChatEngineInput: + system_message: ChatMessage | None = None + last_message: ChatMessage | None = None + chat_history: list[ChatMessage] | None = None + + @classmethod + def from_messages(cls, messages: list[ChatMessage]) -> "ChatEngineInput": + # Detect if there is a system message, extract the last message and chat history + system_message = ( + messages[0] + if len(messages) > 0 and messages[0].role == MessageRole.SYSTEM + else None + ) + last_message = ( + messages[-1] + if len(messages) > 0 and messages[-1].role == MessageRole.USER + else None + ) + # Remove from messages list the system message and last message, + # if they exist. The rest is the chat history. + if system_message: + messages.pop(0) + if last_message: + messages.pop(-1) + chat_history = messages if len(messages) > 0 else None + + return cls( + system_message=system_message, + last_message=last_message, + chat_history=chat_history, + ) + + +@singleton +class ChatService: + settings: Settings + + @inject + def __init__( + self, + settings: Settings, + llm_component: LLMComponent, + vector_store_component: VectorStoreComponent, + embedding_component: EmbeddingComponent, + node_store_component: NodeStoreComponent, + ) -> None: + self.settings = settings + self.llm_component = llm_component + self.embedding_component = embedding_component + self.vector_store_component = vector_store_component + self.storage_context = StorageContext.from_defaults( + vector_store=vector_store_component.vector_store, + docstore=node_store_component.doc_store, + index_store=node_store_component.index_store, + ) + self.index = VectorStoreIndex.from_vector_store( + vector_store_component.vector_store, + storage_context=self.storage_context, + llm=llm_component.llm, + embed_model=embedding_component.embedding_model, + show_progress=True, + ) + + def _chat_engine( + self, + system_prompt: str | None = None, + use_context: bool = False, + context_filter: ContextFilter | None = None, + ) -> BaseChatEngine: + settings = self.settings + if use_context: + vector_index_retriever = self.vector_store_component.get_retriever( + index=self.index, + context_filter=context_filter, + similarity_top_k=self.settings.rag.similarity_top_k, + ) + node_postprocessors: list[BaseNodePostprocessor] = [ + MetadataReplacementPostProcessor(target_metadata_key="window"), + ] + if settings.rag.similarity_value: + node_postprocessors.append( + SimilarityPostprocessor( + similarity_cutoff=settings.rag.similarity_value + ) + ) + + if settings.rag.rerank.enabled: + rerank_postprocessor = SentenceTransformerRerank( + model=settings.rag.rerank.model, top_n=settings.rag.rerank.top_n + ) + node_postprocessors.append(rerank_postprocessor) + + return ContextChatEngine.from_defaults( + system_prompt=system_prompt, + retriever=vector_index_retriever, + llm=self.llm_component.llm, # Takes no effect at the moment + node_postprocessors=node_postprocessors, + ) + else: + return SimpleChatEngine.from_defaults( + system_prompt=system_prompt, + llm=self.llm_component.llm, + ) + + def stream_chat( + self, + messages: list[ChatMessage], + use_context: bool = False, + context_filter: ContextFilter | None = None, + ) -> CompletionGen: + chat_engine_input = ChatEngineInput.from_messages(messages) + last_message = ( + chat_engine_input.last_message.content + if chat_engine_input.last_message + else None + ) + system_prompt = ( + chat_engine_input.system_message.content + if chat_engine_input.system_message + else None + ) + chat_history = ( + chat_engine_input.chat_history if chat_engine_input.chat_history else None + ) + + chat_engine = self._chat_engine( + system_prompt=system_prompt, + use_context=use_context, + context_filter=context_filter, + ) + streaming_response = chat_engine.stream_chat( + message=last_message if last_message is not None else "", + chat_history=chat_history, + ) + sources = [Chunk.from_node(node) for node in streaming_response.source_nodes] + completion_gen = CompletionGen( + response=streaming_response.response_gen, sources=sources + ) + return completion_gen + + def chat( + self, + messages: list[ChatMessage], + use_context: bool = False, + context_filter: ContextFilter | None = None, + ) -> Completion: + chat_engine_input = ChatEngineInput.from_messages(messages) + last_message = ( + chat_engine_input.last_message.content + if chat_engine_input.last_message + else None + ) + system_prompt = ( + chat_engine_input.system_message.content + if chat_engine_input.system_message + else None + ) + chat_history = ( + chat_engine_input.chat_history if chat_engine_input.chat_history else None + ) + + chat_engine = self._chat_engine( + system_prompt=system_prompt, + use_context=use_context, + context_filter=context_filter, + ) + wrapped_response = chat_engine.chat( + message=last_message if last_message is not None else "", + chat_history=chat_history, + ) + sources = [Chunk.from_node(node) for node in wrapped_response.source_nodes] + completion = Completion(response=wrapped_response.response, sources=sources) + return completion diff --git a/pgpt/private_gpt/server/chunks/__init__.py b/pgpt/private_gpt/server/chunks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/server/chunks/chunks_router.py b/pgpt/private_gpt/server/chunks/chunks_router.py new file mode 100644 index 0000000..4da377a --- /dev/null +++ b/pgpt/private_gpt/server/chunks/chunks_router.py @@ -0,0 +1,55 @@ +from typing import Literal + +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel, Field + +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.server.chunks.chunks_service import Chunk, ChunksService +from private_gpt.server.utils.auth import authenticated + +chunks_router = APIRouter(prefix="/v1", dependencies=[Depends(authenticated)]) + + +class ChunksBody(BaseModel): + text: str = Field(examples=["Q3 2023 sales"]) + context_filter: ContextFilter | None = None + limit: int = 10 + prev_next_chunks: int = Field(default=0, examples=[2]) + + +class ChunksResponse(BaseModel): + object: Literal["list"] + model: Literal["private-gpt"] + data: list[Chunk] + + +@chunks_router.post("/chunks", tags=["Context Chunks"]) +def chunks_retrieval(request: Request, body: ChunksBody) -> ChunksResponse: + """Given a `text`, returns the most relevant chunks from the ingested documents. + + The returned information can be used to generate prompts that can be + passed to `/completions` or `/chat/completions` APIs. Note: it is usually a very + fast API, because only the Embeddings model is involved, not the LLM. The + returned information contains the relevant chunk `text` together with the source + `document` it is coming from. It also contains a score that can be used to + compare different results. + + The max number of chunks to be returned is set using the `limit` param. + + Previous and next chunks (pieces of text that appear right before or after in the + document) can be fetched by using the `prev_next_chunks` field. + + The documents being used can be filtered using the `context_filter` and passing + the document IDs to be used. Ingested documents IDs can be found using + `/ingest/list` endpoint. If you want all ingested documents to be used, + remove `context_filter` altogether. + """ + service = request.state.injector.get(ChunksService) + results = service.retrieve_relevant( + body.text, body.context_filter, body.limit, body.prev_next_chunks + ) + return ChunksResponse( + object="list", + model="private-gpt", + data=results, + ) diff --git a/pgpt/private_gpt/server/chunks/chunks_service.py b/pgpt/private_gpt/server/chunks/chunks_service.py new file mode 100644 index 0000000..7bda5d9 --- /dev/null +++ b/pgpt/private_gpt/server/chunks/chunks_service.py @@ -0,0 +1,125 @@ +from typing import TYPE_CHECKING, Literal + +from injector import inject, singleton +from llama_index.core.indices import VectorStoreIndex +from llama_index.core.schema import NodeWithScore +from llama_index.core.storage import StorageContext +from pydantic import BaseModel, Field + +from private_gpt.components.embedding.embedding_component import EmbeddingComponent +from private_gpt.components.llm.llm_component import LLMComponent +from private_gpt.components.node_store.node_store_component import NodeStoreComponent +from private_gpt.components.vector_store.vector_store_component import ( + VectorStoreComponent, +) +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.server.ingest.model import IngestedDoc + +if TYPE_CHECKING: + from llama_index.core.schema import RelatedNodeInfo + + +class Chunk(BaseModel): + object: Literal["context.chunk"] + score: float = Field(examples=[0.023]) + document: IngestedDoc + text: str = Field(examples=["Outbound sales increased 20%, driven by new leads."]) + previous_texts: list[str] | None = Field( + default=None, + examples=[["SALES REPORT 2023", "Inbound didn't show major changes."]], + ) + next_texts: list[str] | None = Field( + default=None, + examples=[ + [ + "New leads came from Google Ads campaign.", + "The campaign was run by the Marketing Department", + ] + ], + ) + + @classmethod + def from_node(cls: type["Chunk"], node: NodeWithScore) -> "Chunk": + doc_id = node.node.ref_doc_id if node.node.ref_doc_id is not None else "-" + return cls( + object="context.chunk", + score=node.score or 0.0, + document=IngestedDoc( + object="ingest.document", + doc_id=doc_id, + doc_metadata=node.metadata, + ), + text=node.get_content(), + ) + + +@singleton +class ChunksService: + @inject + def __init__( + self, + llm_component: LLMComponent, + vector_store_component: VectorStoreComponent, + embedding_component: EmbeddingComponent, + node_store_component: NodeStoreComponent, + ) -> None: + self.vector_store_component = vector_store_component + self.llm_component = llm_component + self.embedding_component = embedding_component + self.storage_context = StorageContext.from_defaults( + vector_store=vector_store_component.vector_store, + docstore=node_store_component.doc_store, + index_store=node_store_component.index_store, + ) + + def _get_sibling_nodes_text( + self, node_with_score: NodeWithScore, related_number: int, forward: bool = True + ) -> list[str]: + explored_nodes_texts = [] + current_node = node_with_score.node + for _ in range(related_number): + explored_node_info: RelatedNodeInfo | None = ( + current_node.next_node if forward else current_node.prev_node + ) + if explored_node_info is None: + break + + explored_node = self.storage_context.docstore.get_node( + explored_node_info.node_id + ) + + explored_nodes_texts.append(explored_node.get_content()) + current_node = explored_node + + return explored_nodes_texts + + def retrieve_relevant( + self, + text: str, + context_filter: ContextFilter | None = None, + limit: int = 10, + prev_next_chunks: int = 0, + ) -> list[Chunk]: + index = VectorStoreIndex.from_vector_store( + self.vector_store_component.vector_store, + storage_context=self.storage_context, + llm=self.llm_component.llm, + embed_model=self.embedding_component.embedding_model, + show_progress=True, + ) + vector_index_retriever = self.vector_store_component.get_retriever( + index=index, context_filter=context_filter, similarity_top_k=limit + ) + nodes = vector_index_retriever.retrieve(text) + nodes.sort(key=lambda n: n.score or 0.0, reverse=True) + + retrieved_nodes = [] + for node in nodes: + chunk = Chunk.from_node(node) + chunk.previous_texts = self._get_sibling_nodes_text( + node, prev_next_chunks, False + ) + chunk.next_texts = self._get_sibling_nodes_text(node, prev_next_chunks) + retrieved_nodes.append(chunk) + + return retrieved_nodes diff --git a/pgpt/private_gpt/server/completions/__init__.py b/pgpt/private_gpt/server/completions/__init__.py new file mode 100644 index 0000000..ae635df --- /dev/null +++ b/pgpt/private_gpt/server/completions/__init__.py @@ -0,0 +1 @@ +"""Deprecated Openai compatibility endpoint.""" diff --git a/pgpt/private_gpt/server/completions/completions_router.py b/pgpt/private_gpt/server/completions/completions_router.py new file mode 100644 index 0000000..646e54b --- /dev/null +++ b/pgpt/private_gpt/server/completions/completions_router.py @@ -0,0 +1,92 @@ +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel +from starlette.responses import StreamingResponse + +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.open_ai.openai_models import ( + OpenAICompletion, + OpenAIMessage, +) +from private_gpt.server.chat.chat_router import ChatBody, chat_completion +from private_gpt.server.utils.auth import authenticated + +completions_router = APIRouter(prefix="/v1", dependencies=[Depends(authenticated)]) + + +class CompletionsBody(BaseModel): + prompt: str + system_prompt: str | None = None + use_context: bool = False + context_filter: ContextFilter | None = None + include_sources: bool = True + stream: bool = False + + model_config = { + "json_schema_extra": { + "examples": [ + { + "prompt": "How do you fry an egg?", + "system_prompt": "You are a rapper. Always answer with a rap.", + "stream": False, + "use_context": False, + "include_sources": False, + } + ] + } + } + + +@completions_router.post( + "/completions", + response_model=None, + summary="Completion", + responses={200: {"model": OpenAICompletion}}, + tags=["Contextual Completions"], + openapi_extra={ + "x-fern-streaming": { + "stream-condition": "stream", + "response": {"$ref": "#/components/schemas/OpenAICompletion"}, + "response-stream": {"$ref": "#/components/schemas/OpenAICompletion"}, + } + }, +) +def prompt_completion( + request: Request, body: CompletionsBody +) -> OpenAICompletion | StreamingResponse: + """We recommend most users use our Chat completions API. + + Given a prompt, the model will return one predicted completion. + + Optionally include a `system_prompt` to influence the way the LLM answers. + + If `use_context` + is set to `true`, the model will use context coming from the ingested documents + to create the response. The documents being used can be filtered using the + `context_filter` and passing the document IDs to be used. Ingested documents IDs + can be found using `/ingest/list` endpoint. If you want all ingested documents to + be used, remove `context_filter` altogether. + + When using `'include_sources': true`, the API will return the source Chunks used + to create the response, which come from the context provided. + + When using `'stream': true`, the API will return data chunks following [OpenAI's + streaming model](https://platform.openai.com/docs/api-reference/chat/streaming): + ``` + {"id":"12345","object":"completion.chunk","created":1694268190, + "model":"private-gpt","choices":[{"index":0,"delta":{"content":"Hello"}, + "finish_reason":null}]} + ``` + """ + messages = [OpenAIMessage(content=body.prompt, role="user")] + # If system prompt is passed, create a fake message with the system prompt. + if body.system_prompt: + messages.insert(0, OpenAIMessage(content=body.system_prompt, role="system")) + + chat_body = ChatBody( + messages=messages, + use_context=body.use_context, + stream=body.stream, + include_sources=body.include_sources, + context_filter=body.context_filter, + ) + return chat_completion(request, chat_body) diff --git a/pgpt/private_gpt/server/embeddings/__init__.py b/pgpt/private_gpt/server/embeddings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/server/embeddings/embeddings_router.py b/pgpt/private_gpt/server/embeddings/embeddings_router.py new file mode 100644 index 0000000..f698392 --- /dev/null +++ b/pgpt/private_gpt/server/embeddings/embeddings_router.py @@ -0,0 +1,35 @@ +from typing import Literal + +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel + +from private_gpt.server.embeddings.embeddings_service import ( + Embedding, + EmbeddingsService, +) +from private_gpt.server.utils.auth import authenticated + +embeddings_router = APIRouter(prefix="/v1", dependencies=[Depends(authenticated)]) + + +class EmbeddingsBody(BaseModel): + input: str | list[str] + + +class EmbeddingsResponse(BaseModel): + object: Literal["list"] + model: Literal["private-gpt"] + data: list[Embedding] + + +@embeddings_router.post("/embeddings", tags=["Embeddings"]) +def embeddings_generation(request: Request, body: EmbeddingsBody) -> EmbeddingsResponse: + """Get a vector representation of a given input. + + That vector representation can be easily consumed + by machine learning models and algorithms. + """ + service = request.state.injector.get(EmbeddingsService) + input_texts = body.input if isinstance(body.input, list) else [body.input] + embeddings = service.texts_embeddings(input_texts) + return EmbeddingsResponse(object="list", model="private-gpt", data=embeddings) diff --git a/pgpt/private_gpt/server/embeddings/embeddings_service.py b/pgpt/private_gpt/server/embeddings/embeddings_service.py new file mode 100644 index 0000000..f0f90a0 --- /dev/null +++ b/pgpt/private_gpt/server/embeddings/embeddings_service.py @@ -0,0 +1,30 @@ +from typing import Literal + +from injector import inject, singleton +from pydantic import BaseModel, Field + +from private_gpt.components.embedding.embedding_component import EmbeddingComponent + + +class Embedding(BaseModel): + index: int + object: Literal["embedding"] + embedding: list[float] = Field(examples=[[0.0023064255, -0.009327292]]) + + +@singleton +class EmbeddingsService: + @inject + def __init__(self, embedding_component: EmbeddingComponent) -> None: + self.embedding_model = embedding_component.embedding_model + + def texts_embeddings(self, texts: list[str]) -> list[Embedding]: + texts_embeddings = self.embedding_model.get_text_embedding_batch(texts) + return [ + Embedding( + index=texts_embeddings.index(embedding), + object="embedding", + embedding=embedding, + ) + for embedding in texts_embeddings + ] diff --git a/pgpt/private_gpt/server/health/__init__.py b/pgpt/private_gpt/server/health/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/server/health/health_router.py b/pgpt/private_gpt/server/health/health_router.py new file mode 100644 index 0000000..4a30b76 --- /dev/null +++ b/pgpt/private_gpt/server/health/health_router.py @@ -0,0 +1,17 @@ +from typing import Literal + +from fastapi import APIRouter +from pydantic import BaseModel, Field + +# Not authentication or authorization required to get the health status. +health_router = APIRouter() + + +class HealthResponse(BaseModel): + status: Literal["ok"] = Field(default="ok") + + +@health_router.get("/health", tags=["Health"]) +def health() -> HealthResponse: + """Return ok if the system is up.""" + return HealthResponse(status="ok") diff --git a/pgpt/private_gpt/server/ingest/__init__.py b/pgpt/private_gpt/server/ingest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/server/ingest/ingest_router.py b/pgpt/private_gpt/server/ingest/ingest_router.py new file mode 100644 index 0000000..56adba4 --- /dev/null +++ b/pgpt/private_gpt/server/ingest/ingest_router.py @@ -0,0 +1,104 @@ +from typing import Literal + +from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile +from pydantic import BaseModel, Field + +from private_gpt.server.ingest.ingest_service import IngestService +from private_gpt.server.ingest.model import IngestedDoc +from private_gpt.server.utils.auth import authenticated + +ingest_router = APIRouter(prefix="/v1", dependencies=[Depends(authenticated)]) + + +class IngestTextBody(BaseModel): + file_name: str = Field(examples=["Avatar: The Last Airbender"]) + text: str = Field( + examples=[ + "Avatar is set in an Asian and Arctic-inspired world in which some " + "people can telekinetically manipulate one of the four elements—water, " + "earth, fire or air—through practices known as 'bending', inspired by " + "Chinese martial arts." + ] + ) + + +class IngestResponse(BaseModel): + object: Literal["list"] + model: Literal["private-gpt"] + data: list[IngestedDoc] + + +@ingest_router.post("/ingest", tags=["Ingestion"], deprecated=True) +def ingest(request: Request, file: UploadFile) -> IngestResponse: + """Ingests and processes a file. + + Deprecated. Use ingest/file instead. + """ + return ingest_file(request, file) + + +@ingest_router.post("/ingest/file", tags=["Ingestion"]) +def ingest_file(request: Request, file: UploadFile) -> IngestResponse: + """Ingests and processes a file, storing its chunks to be used as context. + + The context obtained from files is later used in + `/chat/completions`, `/completions`, and `/chunks` APIs. + + Most common document + formats are supported, but you may be prompted to install an extra dependency to + manage a specific file type. + + A file can generate different Documents (for example a PDF generates one Document + per page). All Documents IDs are returned in the response, together with the + extracted Metadata (which is later used to improve context retrieval). Those IDs + can be used to filter the context used to create responses in + `/chat/completions`, `/completions`, and `/chunks` APIs. + """ + service = request.state.injector.get(IngestService) + if file.filename is None: + raise HTTPException(400, "No file name provided") + ingested_documents = service.ingest_bin_data(file.filename, file.file) + return IngestResponse(object="list", model="private-gpt", data=ingested_documents) + + +@ingest_router.post("/ingest/text", tags=["Ingestion"]) +def ingest_text(request: Request, body: IngestTextBody) -> IngestResponse: + """Ingests and processes a text, storing its chunks to be used as context. + + The context obtained from files is later used in + `/chat/completions`, `/completions`, and `/chunks` APIs. + + A Document will be generated with the given text. The Document + ID is returned in the response, together with the + extracted Metadata (which is later used to improve context retrieval). That ID + can be used to filter the context used to create responses in + `/chat/completions`, `/completions`, and `/chunks` APIs. + """ + service = request.state.injector.get(IngestService) + if len(body.file_name) == 0: + raise HTTPException(400, "No file name provided") + ingested_documents = service.ingest_text(body.file_name, body.text) + return IngestResponse(object="list", model="private-gpt", data=ingested_documents) + + +@ingest_router.get("/ingest/list", tags=["Ingestion"]) +def list_ingested(request: Request) -> IngestResponse: + """Lists already ingested Documents including their Document ID and metadata. + + Those IDs can be used to filter the context used to create responses + in `/chat/completions`, `/completions`, and `/chunks` APIs. + """ + service = request.state.injector.get(IngestService) + ingested_documents = service.list_ingested() + return IngestResponse(object="list", model="private-gpt", data=ingested_documents) + + +@ingest_router.delete("/ingest/{doc_id}", tags=["Ingestion"]) +def delete_ingested(request: Request, doc_id: str) -> None: + """Delete the specified ingested Document. + + The `doc_id` can be obtained from the `GET /ingest/list` endpoint. + The document will be effectively deleted from your storage context. + """ + service = request.state.injector.get(IngestService) + service.delete(doc_id) diff --git a/pgpt/private_gpt/server/ingest/ingest_service.py b/pgpt/private_gpt/server/ingest/ingest_service.py new file mode 100644 index 0000000..f9ae472 --- /dev/null +++ b/pgpt/private_gpt/server/ingest/ingest_service.py @@ -0,0 +1,125 @@ +import logging +import tempfile +from pathlib import Path +from typing import TYPE_CHECKING, AnyStr, BinaryIO + +from injector import inject, singleton +from llama_index.core.node_parser import SentenceWindowNodeParser +from llama_index.core.storage import StorageContext + +from private_gpt.components.embedding.embedding_component import EmbeddingComponent +from private_gpt.components.ingest.ingest_component import get_ingestion_component +from private_gpt.components.llm.llm_component import LLMComponent +from private_gpt.components.node_store.node_store_component import NodeStoreComponent +from private_gpt.components.vector_store.vector_store_component import ( + VectorStoreComponent, +) +from private_gpt.server.ingest.model import IngestedDoc +from private_gpt.settings.settings import settings + +if TYPE_CHECKING: + from llama_index.core.storage.docstore.types import RefDocInfo + +logger = logging.getLogger(__name__) + + +@singleton +class IngestService: + @inject + def __init__( + self, + llm_component: LLMComponent, + vector_store_component: VectorStoreComponent, + embedding_component: EmbeddingComponent, + node_store_component: NodeStoreComponent, + ) -> None: + self.llm_service = llm_component + self.storage_context = StorageContext.from_defaults( + vector_store=vector_store_component.vector_store, + docstore=node_store_component.doc_store, + index_store=node_store_component.index_store, + ) + node_parser = SentenceWindowNodeParser.from_defaults() + + self.ingest_component = get_ingestion_component( + self.storage_context, + embed_model=embedding_component.embedding_model, + transformations=[node_parser, embedding_component.embedding_model], + settings=settings(), + ) + + def _ingest_data(self, file_name: str, file_data: AnyStr) -> list[IngestedDoc]: + logger.debug("Got file data of size=%s to ingest", len(file_data)) + # llama-index mainly supports reading from files, so + # we have to create a tmp file to read for it to work + # delete=False to avoid a Windows 11 permission error. + with tempfile.NamedTemporaryFile(delete=False) as tmp: + try: + path_to_tmp = Path(tmp.name) + if isinstance(file_data, bytes): + path_to_tmp.write_bytes(file_data) + else: + path_to_tmp.write_text(str(file_data)) + return self.ingest_file(file_name, path_to_tmp) + finally: + tmp.close() + path_to_tmp.unlink() + + def ingest_file(self, file_name: str, file_data: Path) -> list[IngestedDoc]: + logger.info("Ingesting file_name=%s", file_name) + documents = self.ingest_component.ingest(file_name, file_data) + logger.info("Finished ingestion file_name=%s", file_name) + return [IngestedDoc.from_document(document) for document in documents] + + def ingest_text(self, file_name: str, text: str) -> list[IngestedDoc]: + logger.debug("Ingesting text data with file_name=%s", file_name) + return self._ingest_data(file_name, text) + + def ingest_bin_data( + self, file_name: str, raw_file_data: BinaryIO + ) -> list[IngestedDoc]: + logger.debug("Ingesting binary data with file_name=%s", file_name) + file_data = raw_file_data.read() + return self._ingest_data(file_name, file_data) + + def bulk_ingest(self, files: list[tuple[str, Path]]) -> list[IngestedDoc]: + logger.info("Ingesting file_names=%s", [f[0] for f in files]) + documents = self.ingest_component.bulk_ingest(files) + logger.info("Finished ingestion file_name=%s", [f[0] for f in files]) + return [IngestedDoc.from_document(document) for document in documents] + + def list_ingested(self) -> list[IngestedDoc]: + ingested_docs: list[IngestedDoc] = [] + try: + docstore = self.storage_context.docstore + ref_docs: dict[str, RefDocInfo] | None = docstore.get_all_ref_doc_info() + + if not ref_docs: + return ingested_docs + + for doc_id, ref_doc_info in ref_docs.items(): + doc_metadata = None + if ref_doc_info is not None and ref_doc_info.metadata is not None: + doc_metadata = IngestedDoc.curate_metadata(ref_doc_info.metadata) + ingested_docs.append( + IngestedDoc( + object="ingest.document", + doc_id=doc_id, + doc_metadata=doc_metadata, + ) + ) + except ValueError: + logger.warning("Got an exception when getting list of docs", exc_info=True) + pass + logger.debug("Found count=%s ingested documents", len(ingested_docs)) + return ingested_docs + + def delete(self, doc_id: str) -> None: + """Delete an ingested document. + + :raises ValueError: if the document does not exist + """ + logger.info( + "Deleting the ingested document=%s in the doc and index store", doc_id + ) + self.ingest_component.delete(doc_id) diff --git a/pgpt/private_gpt/server/ingest/ingest_watcher.py b/pgpt/private_gpt/server/ingest/ingest_watcher.py new file mode 100644 index 0000000..8bf4d17 --- /dev/null +++ b/pgpt/private_gpt/server/ingest/ingest_watcher.py @@ -0,0 +1,45 @@ +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from watchdog.events import ( + FileCreatedEvent, + FileModifiedEvent, + FileSystemEvent, + FileSystemEventHandler, +) +from watchdog.observers import Observer + + +class IngestWatcher: + def __init__( + self, watch_path: Path, on_file_changed: Callable[[Path], None] + ) -> None: + self.watch_path = watch_path + self.on_file_changed = on_file_changed + + class Handler(FileSystemEventHandler): + def on_modified(self, event: FileSystemEvent) -> None: + if isinstance(event, FileModifiedEvent): + on_file_changed(Path(event.src_path)) + + def on_created(self, event: FileSystemEvent) -> None: + if isinstance(event, FileCreatedEvent): + on_file_changed(Path(event.src_path)) + + event_handler = Handler() + observer: Any = Observer() + self._observer = observer + self._observer.schedule(event_handler, str(watch_path), recursive=True) + + def start(self) -> None: + self._observer.start() + while self._observer.is_alive(): + try: + self._observer.join(1) + except KeyboardInterrupt: + break + + def stop(self) -> None: + self._observer.stop() + self._observer.join() diff --git a/pgpt/private_gpt/server/ingest/model.py b/pgpt/private_gpt/server/ingest/model.py new file mode 100644 index 0000000..9f4139e --- /dev/null +++ b/pgpt/private_gpt/server/ingest/model.py @@ -0,0 +1,32 @@ +from typing import Any, Literal + +from llama_index.core.schema import Document +from pydantic import BaseModel, Field + + +class IngestedDoc(BaseModel): + object: Literal["ingest.document"] + doc_id: str = Field(examples=["c202d5e6-7b69-4869-81cc-dd574ee8ee11"]) + doc_metadata: dict[str, Any] | None = Field( + examples=[ + { + "page_label": "2", + "file_name": "Sales Report Q3 2023.pdf", + } + ] + ) + + @staticmethod + def curate_metadata(metadata: dict[str, Any]) -> dict[str, Any]: + """Remove unwanted metadata keys.""" + for key in ["doc_id", "window", "original_text"]: + metadata.pop(key, None) + return metadata + + @staticmethod + def from_document(document: Document) -> "IngestedDoc": + return IngestedDoc( + object="ingest.document", + doc_id=document.doc_id, + doc_metadata=IngestedDoc.curate_metadata(document.metadata), + ) diff --git a/pgpt/private_gpt/server/recipes/summarize/__init__.py b/pgpt/private_gpt/server/recipes/summarize/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/server/recipes/summarize/summarize_router.py b/pgpt/private_gpt/server/recipes/summarize/summarize_router.py new file mode 100644 index 0000000..c1770c3 --- /dev/null +++ b/pgpt/private_gpt/server/recipes/summarize/summarize_router.py @@ -0,0 +1,86 @@ +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel +from starlette.responses import StreamingResponse + +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.open_ai.openai_models import ( + to_openai_sse_stream, +) +from private_gpt.server.recipes.summarize.summarize_service import SummarizeService +from private_gpt.server.utils.auth import authenticated + +summarize_router = APIRouter(prefix="/v1", dependencies=[Depends(authenticated)]) + + +class SummarizeBody(BaseModel): + text: str | None = None + use_context: bool = False + context_filter: ContextFilter | None = None + prompt: str | None = None + instructions: str | None = None + stream: bool = False + + +class SummarizeResponse(BaseModel): + summary: str + + +@summarize_router.post( + "/summarize", + response_model=None, + summary="Summarize", + responses={200: {"model": SummarizeResponse}}, + tags=["Recipes"], +) +def summarize( + request: Request, body: SummarizeBody +) -> SummarizeResponse | StreamingResponse: + """Given a text, the model will return a summary. + + Optionally include `instructions` to influence the way the summary is generated. + + If `use_context` + is set to `true`, the model will also use the content coming from the ingested + documents in the summary. The documents being used can + be filtered by their metadata using the `context_filter`. + Ingested documents metadata can be found using `/ingest/list` endpoint. + If you want all ingested documents to be used, remove `context_filter` altogether. + + If `prompt` is set, it will be used as the prompt for the summarization, + otherwise the default prompt will be used. + + When using `'stream': true`, the API will return data chunks following [OpenAI's + streaming model](https://platform.openai.com/docs/api-reference/chat/streaming): + ``` + {"id":"12345","object":"completion.chunk","created":1694268190, + "model":"private-gpt","choices":[{"index":0,"delta":{"content":"Hello"}, + "finish_reason":null}]} + ``` + """ + service: SummarizeService = request.state.injector.get(SummarizeService) + + if body.stream: + completion_gen = service.stream_summarize( + text=body.text, + instructions=body.instructions, + use_context=body.use_context, + context_filter=body.context_filter, + prompt=body.prompt, + ) + return StreamingResponse( + to_openai_sse_stream( + response_generator=completion_gen, + ), + media_type="text/event-stream", + ) + else: + completion = service.summarize( + text=body.text, + instructions=body.instructions, + use_context=body.use_context, + context_filter=body.context_filter, + prompt=body.prompt, + ) + return SummarizeResponse( + summary=completion, + ) diff --git a/pgpt/private_gpt/server/recipes/summarize/summarize_service.py b/pgpt/private_gpt/server/recipes/summarize/summarize_service.py new file mode 100644 index 0000000..b1ec1d1 --- /dev/null +++ b/pgpt/private_gpt/server/recipes/summarize/summarize_service.py @@ -0,0 +1,182 @@ +from itertools import chain + +from injector import inject, singleton +from llama_index.core import ( + Document, + StorageContext, + SummaryIndex, +) +from llama_index.core.base.response.schema import Response, StreamingResponse +from llama_index.core.node_parser import SentenceSplitter +from llama_index.core.response_synthesizers import ResponseMode +from llama_index.core.storage.docstore.types import RefDocInfo +from llama_index.core.types import TokenGen + +from private_gpt.components.embedding.embedding_component import EmbeddingComponent +from private_gpt.components.llm.llm_component import LLMComponent +from private_gpt.components.node_store.node_store_component import NodeStoreComponent +from private_gpt.components.vector_store.vector_store_component import ( + VectorStoreComponent, +) +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.settings.settings import Settings + +####### +# Modification par SPC +# +# Forcer à utiliser le prompt de settings.yaml et non un prompt par défaut +# +# DEFAULT_SUMMARIZE_PROMPT = ( +# "Provide a comprehensive summary of the provided context information. " +# "The summary should cover all the key points and main ideas presented in " +# "the original text, while also condensing the information into a concise " +# "and easy-to-understand format. Please ensure that the summary includes " +# "relevant details and examples that support the main ideas, while avoiding " +# "any unnecessary information or repetition." +# ) + +from private_gpt.settings.settings import settings +DEFAULT_SUMMARIZE_PROMPT = settings().ui.default_summarization_system_prompt +# +# Fin modification par SPC +####### + +@singleton +class SummarizeService: + @inject + def __init__( + self, + settings: Settings, + llm_component: LLMComponent, + node_store_component: NodeStoreComponent, + vector_store_component: VectorStoreComponent, + embedding_component: EmbeddingComponent, + ) -> None: + self.settings = settings + self.llm_component = llm_component + self.node_store_component = node_store_component + self.vector_store_component = vector_store_component + self.embedding_component = embedding_component + self.storage_context = StorageContext.from_defaults( + vector_store=vector_store_component.vector_store, + docstore=node_store_component.doc_store, + index_store=node_store_component.index_store, + ) + + @staticmethod + def _filter_ref_docs( + ref_docs: dict[str, RefDocInfo], context_filter: ContextFilter | None + ) -> list[RefDocInfo]: + if context_filter is None or not context_filter.docs_ids: + return list(ref_docs.values()) + + return [ + ref_doc + for doc_id, ref_doc in ref_docs.items() + if doc_id in context_filter.docs_ids + ] + + def _summarize( + self, + use_context: bool = False, + stream: bool = False, + text: str | None = None, + instructions: str | None = None, + context_filter: ContextFilter | None = None, + prompt: str | None = None, + ) -> str | TokenGen: + + nodes_to_summarize = [] + + # Add text to summarize + if text: + text_documents = [Document(text=text)] + nodes_to_summarize += ( + SentenceSplitter.from_defaults().get_nodes_from_documents( + text_documents + ) + ) + + # Add context documents to summarize + if use_context: + # 1. Recover all ref docs + ref_docs: dict[str, RefDocInfo] | None = ( + self.storage_context.docstore.get_all_ref_doc_info() + ) + if ref_docs is None: + raise ValueError("No documents have been ingested yet.") + + # 2. Filter documents based on context_filter (if provided) + filtered_ref_docs = self._filter_ref_docs(ref_docs, context_filter) + + # 3. Get all nodes from the filtered documents + filtered_node_ids = chain.from_iterable( + [ref_doc.node_ids for ref_doc in filtered_ref_docs] + ) + filtered_nodes = self.storage_context.docstore.get_nodes( + node_ids=list(filtered_node_ids), + ) + + nodes_to_summarize += filtered_nodes + + # Create a SummaryIndex to summarize the nodes + summary_index = SummaryIndex( + nodes=nodes_to_summarize, + storage_context=StorageContext.from_defaults(), # In memory SummaryIndex + show_progress=True, + ) + + # Make a tree summarization query + # above the set of all candidate nodes + query_engine = summary_index.as_query_engine( + llm=self.llm_component.llm, + response_mode=ResponseMode.TREE_SUMMARIZE, + streaming=stream, + use_async=self.settings.summarize.use_async, + ) + + prompt = prompt or DEFAULT_SUMMARIZE_PROMPT + + summarize_query = prompt + "\n" + (instructions or "") + + response = query_engine.query(summarize_query) + if isinstance(response, Response): + return response.response or "" + elif isinstance(response, StreamingResponse): + return response.response_gen + else: + raise TypeError(f"The result is not of a supported type: {type(response)}") + + def summarize( + self, + use_context: bool = False, + text: str | None = None, + instructions: str | None = None, + context_filter: ContextFilter | None = None, + prompt: str | None = None, + ) -> str: + return self._summarize( + use_context=use_context, + stream=False, + text=text, + instructions=instructions, + context_filter=context_filter, + prompt=prompt, + ) # type: ignore + + def stream_summarize( + self, + use_context: bool = False, + text: str | None = None, + instructions: str | None = None, + context_filter: ContextFilter | None = None, + prompt: str | None = None, + ) -> TokenGen: + return self._summarize( + use_context=use_context, + stream=True, + text=text, + instructions=instructions, + context_filter=context_filter, + prompt=prompt, + ) # type: ignore diff --git a/pgpt/private_gpt/server/utils/__init__.py b/pgpt/private_gpt/server/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pgpt/private_gpt/server/utils/auth.py b/pgpt/private_gpt/server/utils/auth.py new file mode 100644 index 0000000..2eb40fe --- /dev/null +++ b/pgpt/private_gpt/server/utils/auth.py @@ -0,0 +1,69 @@ +"""Authentication mechanism for the API. + +Define a simple mechanism to authenticate requests. +More complex authentication mechanisms can be defined here, and be placed in the +`authenticated` method (being a 'bean' injected in fastapi routers). + +Authorization can also be made after the authentication, and depends on +the authentication. Authorization should not be implemented in this file. + +Authorization can be done by following fastapi's guides: +* https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/ +* https://fastapi.tiangolo.com/tutorial/security/ +* https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/ +""" + +# mypy: ignore-errors +# Disabled mypy error: All conditional function variants must have identical signatures +# We are changing the implementation of the authenticated method, based on +# the config. If the auth is not enabled, we are not defining the complex method +# with its dependencies. +import logging +import secrets +from typing import Annotated + +from fastapi import Depends, Header, HTTPException + +from private_gpt.settings.settings import settings + +# 401 signify that the request requires authentication. +# 403 signify that the authenticated user is not authorized to perform the operation. +NOT_AUTHENTICATED = HTTPException( + status_code=401, + detail="Not authenticated", + headers={"WWW-Authenticate": 'Basic realm="All the API", charset="UTF-8"'}, +) + +logger = logging.getLogger(__name__) + + +def _simple_authentication(authorization: Annotated[str, Header()] = "") -> bool: + """Check if the request is authenticated.""" + if not secrets.compare_digest(authorization, settings().server.auth.secret): + # If the "Authorization" header is not the expected one, raise an exception. + raise NOT_AUTHENTICATED + return True + + +if not settings().server.auth.enabled: + logger.debug( + "Defining a dummy authentication mechanism for fastapi, always authenticating requests" + ) + + # Define a dummy authentication method that always returns True. + def authenticated() -> bool: + """Check if the request is authenticated.""" + return True + +else: + logger.info("Defining the given authentication mechanism for the API") + + # Method to be used as a dependency to check if the request is authenticated. + def authenticated( + _simple_authentication: Annotated[bool, Depends(_simple_authentication)] + ) -> bool: + """Check if the request is authenticated.""" + assert settings().server.auth.enabled + if not _simple_authentication: + raise NOT_AUTHENTICATED + return True diff --git a/pgpt/private_gpt/settings/__init__.py b/pgpt/private_gpt/settings/__init__.py new file mode 100644 index 0000000..ebcc5dc --- /dev/null +++ b/pgpt/private_gpt/settings/__init__.py @@ -0,0 +1 @@ +"""Settings.""" diff --git a/pgpt/private_gpt/settings/settings.py b/pgpt/private_gpt/settings/settings.py new file mode 100644 index 0000000..0589f13 --- /dev/null +++ b/pgpt/private_gpt/settings/settings.py @@ -0,0 +1,638 @@ +from typing import Any, Literal + +from pydantic import BaseModel, Field + +from private_gpt.settings.settings_loader import load_active_settings + + +class CorsSettings(BaseModel): + """CORS configuration. + + For more details on the CORS configuration, see: + # * https://fastapi.tiangolo.com/tutorial/cors/ + # * https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS + """ + + enabled: bool = Field( + description="Flag indicating if CORS headers are set or not." + "If set to True, the CORS headers will be set to allow all origins, methods and headers.", + default=False, + ) + allow_credentials: bool = Field( + description="Indicate that cookies should be supported for cross-origin requests", + default=False, + ) + allow_origins: list[str] = Field( + description="A list of origins that should be permitted to make cross-origin requests.", + default=[], + ) + allow_origin_regex: list[str] = Field( + description="A regex string to match against origins that should be permitted to make cross-origin requests.", + default=None, + ) + allow_methods: list[str] = Field( + description="A list of HTTP methods that should be allowed for cross-origin requests.", + default=[ + "GET", + ], + ) + allow_headers: list[str] = Field( + description="A list of HTTP request headers that should be supported for cross-origin requests.", + default=[], + ) + + +class AuthSettings(BaseModel): + """Authentication configuration. + + The implementation of the authentication strategy must + """ + + enabled: bool = Field( + description="Flag indicating if authentication is enabled or not.", + default=False, + ) + secret: str = Field( + description="The secret to be used for authentication. " + "It can be any non-blank string. For HTTP basic authentication, " + "this value should be the whole 'Authorization' header that is expected" + ) + + +class IngestionSettings(BaseModel): + """Ingestion configuration. + + This configuration is used to control the ingestion of data into the system + using non-server methods. This is useful for local development and testing; + or to ingest in bulk from a folder. + + Please note that this configuration is not secure and should be used in + a controlled environment only (setting right permissions, etc.). + """ + + enabled: bool = Field( + description="Flag indicating if local ingestion is enabled or not.", + default=False, + ) + allow_ingest_from: list[str] = Field( + description="A list of folders that should be permitted to make ingest requests.", + default=[], + ) + + +class ServerSettings(BaseModel): + env_name: str = Field( + description="Name of the environment (prod, staging, local...)" + ) + port: int = Field(description="Port of PrivateGPT FastAPI server, defaults to 8001") + cors: CorsSettings = Field( + description="CORS configuration", default=CorsSettings(enabled=False) + ) + auth: AuthSettings = Field( + description="Authentication configuration", + default_factory=lambda: AuthSettings(enabled=False, secret="secret-key"), + ) + + +class DataSettings(BaseModel): + local_ingestion: IngestionSettings = Field( + description="Ingestion configuration", + default_factory=lambda: IngestionSettings(allow_ingest_from=["*"]), + ) + local_data_folder: str = Field( + description="Path to local storage." + "It will be treated as an absolute path if it starts with /" + ) + + +class LLMSettings(BaseModel): + mode: Literal[ + "llamacpp", + "openai", + "openailike", + "azopenai", + "sagemaker", + "mock", + "ollama", + "gemini", + ] + max_new_tokens: int = Field( + 256, + description="The maximum number of token that the LLM is authorized to generate in one completion.", + ) + context_window: int = Field( + 3900, + description="The maximum number of context tokens for the model.", + ) + tokenizer: str = Field( + None, + description="The model id of a predefined tokenizer hosted inside a model repo on " + "huggingface.co. Valid model ids can be located at the root-level, like " + "`bert-base-uncased`, or namespaced under a user or organization name, " + "like `HuggingFaceH4/zephyr-7b-beta`. If not set, will load a tokenizer matching " + "gpt-3.5-turbo LLM.", + ) + temperature: float = Field( + 0.1, + description="The temperature of the model. Increasing the temperature will make the model answer more creatively. A value of 0.1 would be more factual.", + ) + prompt_style: Literal["default", "llama2", "llama3", "tag", "mistral", "chatml"] = ( + Field( + "llama2", + description=( + "The prompt style to use for the chat engine. " + "If `default` - use the default prompt style from the llama_index. It should look like `role: message`.\n" + "If `llama2` - use the llama2 prompt style from the llama_index. Based on ``, `[INST]` and `<>`.\n" + "If `llama3` - use the llama3 prompt style from the llama_index." + "If `tag` - use the `tag` prompt style. It should look like `<|role|>: message`. \n" + "If `mistral` - use the `mistral prompt style. It shoudl look like [INST] {System Prompt} [/INST][INST] { UserInstructions } [/INST]" + "`llama2` is the historic behaviour. `default` might work better with your custom models." + ), + ) + ) + + +class VectorstoreSettings(BaseModel): + database: Literal["chroma", "qdrant", "postgres", "clickhouse", "milvus"] + + +class NodeStoreSettings(BaseModel): + database: Literal["simple", "postgres"] + + +class LlamaCPPSettings(BaseModel): + llm_hf_repo_id: str + llm_hf_model_file: str + tfs_z: float = Field( + 1.0, + description="Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting.", + ) + top_k: int = Field( + 40, + description="Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)", + ) + top_p: float = Field( + 0.9, + description="Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)", + ) + repeat_penalty: float = Field( + 1.1, + description="Sets how strongly to penalize repetitions. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. (Default: 1.1)", + ) + + +class HuggingFaceSettings(BaseModel): + embedding_hf_model_name: str = Field( + description="Name of the HuggingFace model to use for embeddings" + ) + access_token: str = Field( + None, + description="Huggingface access token, required to download some models", + ) + trust_remote_code: bool = Field( + False, + description="If set to True, the code from the remote model will be trusted and executed.", + ) + + +class EmbeddingSettings(BaseModel): + mode: Literal[ + "huggingface", + "openai", + "azopenai", + "sagemaker", + "ollama", + "mock", + "gemini", + "mistralai", + ] + ingest_mode: Literal["simple", "batch", "parallel", "pipeline"] = Field( + "simple", + description=( + "The ingest mode to use for the embedding engine:\n" + "If `simple` - ingest files sequentially and one by one. It is the historic behaviour.\n" + "If `batch` - if multiple files, parse all the files in parallel, " + "and send them in batch to the embedding model.\n" + "In `pipeline` - The Embedding engine is kept as busy as possible\n" + "If `parallel` - parse the files in parallel using multiple cores, and embedd them in parallel.\n" + "`parallel` is the fastest mode for local setup, as it parallelize IO RW in the index.\n" + "For modes that leverage parallelization, you can specify the number of " + "workers to use with `count_workers`.\n" + ), + ) + count_workers: int = Field( + 2, + description=( + "The number of workers to use for file ingestion.\n" + "In `batch` mode, this is the number of workers used to parse the files.\n" + "In `parallel` mode, this is the number of workers used to parse the files and embed them.\n" + "In `pipeline` mode, this is the number of workers that can perform embeddings.\n" + "This is only used if `ingest_mode` is not `simple`.\n" + "Do not go too high with this number, as it might cause memory issues. (especially in `parallel` mode)\n" + "Do not set it higher than your number of threads of your CPU." + ), + ) + embed_dim: int = Field( + 384, + description="The dimension of the embeddings stored in the Postgres database", + ) + + +class SagemakerSettings(BaseModel): + llm_endpoint_name: str + embedding_endpoint_name: str + + +class OpenAISettings(BaseModel): + api_base: str = Field( + None, + description="Base URL of OpenAI API. Example: 'https://api.openai.com/v1'.", + ) + api_key: str + model: str = Field( + "gpt-3.5-turbo", + description="OpenAI Model to use. Example: 'gpt-4'.", + ) + request_timeout: float = Field( + 120.0, + description="Time elapsed until openailike server times out the request. Default is 120s. Format is float. ", + ) + embedding_api_base: str = Field( + None, + description="Base URL of OpenAI API. Example: 'https://api.openai.com/v1'.", + ) + embedding_api_key: str + embedding_model: str = Field( + "text-embedding-ada-002", + description="OpenAI embedding Model to use. Example: 'text-embedding-3-large'.", + ) + + +class GeminiSettings(BaseModel): + api_key: str + model: str = Field( + "models/gemini-pro", + description="Google Model to use. Example: 'models/gemini-pro'.", + ) + embedding_model: str = Field( + "models/embedding-001", + description="Google Embedding Model to use. Example: 'models/embedding-001'.", + ) + + +class OllamaSettings(BaseModel): + api_base: str = Field( + "http://localhost:11434", + description="Base URL of Ollama API. Example: 'https://localhost:11434'.", + ) + embedding_api_base: str = Field( + "http://localhost:11434", + description="Base URL of Ollama embedding API. Example: 'https://localhost:11434'.", + ) + llm_model: str = Field( + None, + description="Model to use. Example: 'llama2-uncensored'.", + ) + embedding_model: str = Field( + None, + description="Model to use. Example: 'nomic-embed-text'.", + ) + keep_alive: str = Field( + "5m", + description="Time the model will stay loaded in memory after a request. examples: 5m, 5h, '-1' ", + ) + tfs_z: float = Field( + 1.0, + description="Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting.", + ) + num_predict: int = Field( + None, + description="Maximum number of tokens to predict when generating text. (Default: 128, -1 = infinite generation, -2 = fill context)", + ) + top_k: int = Field( + 40, + description="Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)", + ) + top_p: float = Field( + 0.9, + description="Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)", + ) + repeat_last_n: int = Field( + 64, + description="Sets how far back for the model to look back to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx)", + ) + repeat_penalty: float = Field( + 1.1, + description="Sets how strongly to penalize repetitions. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. (Default: 1.1)", + ) + request_timeout: float = Field( + 120.0, + description="Time elapsed until ollama times out the request. Default is 120s. Format is float. ", + ) + autopull_models: bool = Field( + False, + description="If set to True, the Ollama will automatically pull the models from the API base.", + ) + + +class AzureOpenAISettings(BaseModel): + api_key: str + azure_endpoint: str + api_version: str = Field( + "2023_05_15", + description="The API version to use for this operation. This follows the YYYY-MM-DD format.", + ) + embedding_deployment_name: str + embedding_model: str = Field( + "text-embedding-ada-002", + description="OpenAI Model to use. Example: 'text-embedding-ada-002'.", + ) + llm_deployment_name: str + llm_model: str = Field( + "gpt-35-turbo", + description="OpenAI Model to use. Example: 'gpt-4'.", + ) + + +class UISettings(BaseModel): + enabled: bool + path: str + default_mode: Literal["RAG", "Search", "Basic", "Summarize"] = Field( + "RAG", + description="The default mode.", + ) + default_chat_system_prompt: str = Field( + None, + description="The default system prompt to use for the chat mode.", + ) + default_query_system_prompt: str = Field( + None, description="The default system prompt to use for the query mode." + ) + default_summarization_system_prompt: str = Field( + None, + description="The default system prompt to use for the summarization mode.", + ) + delete_file_button_enabled: bool = Field( + True, description="If the button to delete a file is enabled or not." + ) + delete_all_files_button_enabled: bool = Field( + False, description="If the button to delete all files is enabled or not." + ) + + +class RerankSettings(BaseModel): + enabled: bool = Field( + False, + description="This value controls whether a reranker should be included in the RAG pipeline.", + ) + model: str = Field( + "cross-encoder/ms-marco-MiniLM-L-2-v2", + description="Rerank model to use. Limited to SentenceTransformer cross-encoder models.", + ) + top_n: int = Field( + 2, + description="This value controls the number of documents returned by the RAG pipeline.", + ) + + +class RagSettings(BaseModel): + similarity_top_k: int = Field( + 2, + description="This value controls the number of documents returned by the RAG pipeline or considered for reranking if enabled.", + ) + similarity_value: float = Field( + None, + description="If set, any documents retrieved from the RAG must meet a certain match score. Acceptable values are between 0 and 1.", + ) + rerank: RerankSettings + + +class SummarizeSettings(BaseModel): + use_async: bool = Field( + True, + description="If set to True, the summarization will be done asynchronously.", + ) + + +class ClickHouseSettings(BaseModel): + host: str = Field( + "localhost", + description="The server hosting the ClickHouse database", + ) + port: int = Field( + 8443, + description="The port on which the ClickHouse database is accessible", + ) + username: str = Field( + "default", + description="The username to use to connect to the ClickHouse database", + ) + password: str = Field( + "", + description="The password to use to connect to the ClickHouse database", + ) + database: str = Field( + "__default__", + description="The default database to use for connections", + ) + secure: bool | str = Field( + False, + description="Use https/TLS for secure connection to the server", + ) + interface: str | None = Field( + None, + description="Must be either 'http' or 'https'. Determines the protocol to use for the connection", + ) + settings: dict[str, Any] | None = Field( + None, + description="Specific ClickHouse server settings to be used with the session", + ) + connect_timeout: int | None = Field( + None, + description="Timeout in seconds for establishing a connection", + ) + send_receive_timeout: int | None = Field( + None, + description="Read timeout in seconds for http connection", + ) + verify: bool | None = Field( + None, + description="Verify the server certificate in secure/https mode", + ) + ca_cert: str | None = Field( + None, + description="Path to Certificate Authority root certificate (.pem format)", + ) + client_cert: str | None = Field( + None, + description="Path to TLS Client certificate (.pem format)", + ) + client_cert_key: str | None = Field( + None, + description="Path to the private key for the TLS Client certificate", + ) + http_proxy: str | None = Field( + None, + description="HTTP proxy address", + ) + https_proxy: str | None = Field( + None, + description="HTTPS proxy address", + ) + server_host_name: str | None = Field( + None, + description="Server host name to be checked against the TLS certificate", + ) + + +class PostgresSettings(BaseModel): + host: str = Field( + "localhost", + description="The server hosting the Postgres database", + ) + port: int = Field( + 5432, + description="The port on which the Postgres database is accessible", + ) + user: str = Field( + "postgres", + description="The user to use to connect to the Postgres database", + ) + password: str = Field( + "postgres", + description="The password to use to connect to the Postgres database", + ) + database: str = Field( + "postgres", + description="The database to use to connect to the Postgres database", + ) + schema_name: str = Field( + "public", + description="The name of the schema in the Postgres database to use", + ) + + +class QdrantSettings(BaseModel): + location: str | None = Field( + None, + description=( + "If `:memory:` - use in-memory Qdrant instance.\n" + "If `str` - use it as a `url` parameter.\n" + ), + ) + url: str | None = Field( + None, + description=( + "Either host or str of 'Optional[scheme], host, Optional[port], Optional[prefix]'." + ), + ) + port: int | None = Field(6333, description="Port of the REST API interface.") + grpc_port: int | None = Field(6334, description="Port of the gRPC interface.") + prefer_grpc: bool | None = Field( + False, + description="If `true` - use gRPC interface whenever possible in custom methods.", + ) + https: bool | None = Field( + None, + description="If `true` - use HTTPS(SSL) protocol.", + ) + api_key: str | None = Field( + None, + description="API key for authentication in Qdrant Cloud.", + ) + prefix: str | None = Field( + None, + description=( + "Prefix to add to the REST URL path." + "Example: `service/v1` will result in " + "'http://localhost:6333/service/v1/{qdrant-endpoint}' for REST API." + ), + ) + timeout: float | None = Field( + None, + description="Timeout for REST and gRPC API requests.", + ) + host: str | None = Field( + None, + description="Host name of Qdrant service. If url and host are None, set to 'localhost'.", + ) + path: str | None = Field(None, description="Persistence path for QdrantLocal.") + force_disable_check_same_thread: bool | None = Field( + True, + description=( + "For QdrantLocal, force disable check_same_thread. Default: `True`" + "Only use this if you can guarantee that you can resolve the thread safety outside QdrantClient." + ), + ) + + +class MilvusSettings(BaseModel): + uri: str = Field( + "local_data/private_gpt/milvus/milvus_local.db", + description="The URI of the Milvus instance. For example: 'local_data/private_gpt/milvus/milvus_local.db' for Milvus Lite.", + ) + token: str = Field( + "", + description=( + "A valid access token to access the specified Milvus instance. " + "This can be used as a recommended alternative to setting user and password separately. " + ), + ) + collection_name: str = Field( + "make_this_parameterizable_per_api_call", + description="The name of the collection in Milvus. Default is 'make_this_parameterizable_per_api_call'.", + ) + overwrite: bool = Field( + True, description="Overwrite the previous collection schema if it exists." + ) + + +class Settings(BaseModel): + server: ServerSettings + data: DataSettings + ui: UISettings + llm: LLMSettings + embedding: EmbeddingSettings + llamacpp: LlamaCPPSettings + huggingface: HuggingFaceSettings + sagemaker: SagemakerSettings + openai: OpenAISettings + gemini: GeminiSettings + ollama: OllamaSettings + azopenai: AzureOpenAISettings + vectorstore: VectorstoreSettings + nodestore: NodeStoreSettings + rag: RagSettings + summarize: SummarizeSettings + qdrant: QdrantSettings | None = None + postgres: PostgresSettings | None = None + clickhouse: ClickHouseSettings | None = None + milvus: MilvusSettings | None = None + + +""" +This is visible just for DI or testing purposes. + +Use dependency injection or `settings()` method instead. +""" +unsafe_settings = load_active_settings() + +""" +This is visible just for DI or testing purposes. + +Use dependency injection or `settings()` method instead. +""" +unsafe_typed_settings = Settings(**unsafe_settings) + + +def settings() -> Settings: + """Get the current loaded settings from the DI container. + + This method exists to keep compatibility with the existing code, + that require global access to the settings. + + For regular components use dependency injection instead. + """ + from private_gpt.di import global_injector + + return global_injector.get(Settings) diff --git a/pgpt/private_gpt/settings/settings_loader.py b/pgpt/private_gpt/settings/settings_loader.py new file mode 100644 index 0000000..2784cdf --- /dev/null +++ b/pgpt/private_gpt/settings/settings_loader.py @@ -0,0 +1,57 @@ +import functools +import logging +import os +import sys +from collections.abc import Iterable +from pathlib import Path +from typing import Any + +from pydantic.v1.utils import deep_update, unique_list + +from private_gpt.constants import PROJECT_ROOT_PATH +from private_gpt.settings.yaml import load_yaml_with_envvars + +logger = logging.getLogger(__name__) + +_settings_folder = os.environ.get("PGPT_SETTINGS_FOLDER", PROJECT_ROOT_PATH) + +# if running in unittest, use the test profile +_test_profile = ["test"] if "tests.fixtures" in sys.modules else [] + +active_profiles: list[str] = unique_list( + ["default"] + + [ + item.strip() + for item in os.environ.get("PGPT_PROFILES", "").split(",") + if item.strip() + ] + + _test_profile +) + + +def merge_settings(settings: Iterable[dict[str, Any]]) -> dict[str, Any]: + return functools.reduce(deep_update, settings, {}) + + +def load_settings_from_profile(profile: str) -> dict[str, Any]: + if profile == "default": + profile_file_name = "settings.yaml" + else: + profile_file_name = f"settings-{profile}.yaml" + + path = Path(_settings_folder) / profile_file_name + with Path(path).open("r") as f: + config = load_yaml_with_envvars(f) + if not isinstance(config, dict): + raise TypeError(f"Config file has no top-level mapping: {path}") + return config + + +def load_active_settings() -> dict[str, Any]: + """Load active profiles and merge them.""" + logger.info("Starting application with profiles=%s", active_profiles) + loaded_profiles = [ + load_settings_from_profile(profile) for profile in active_profiles + ] + merged: dict[str, Any] = merge_settings(loaded_profiles) + return merged diff --git a/pgpt/private_gpt/settings/yaml.py b/pgpt/private_gpt/settings/yaml.py new file mode 100644 index 0000000..a129ca4 --- /dev/null +++ b/pgpt/private_gpt/settings/yaml.py @@ -0,0 +1,41 @@ +import os +import re +import typing +from typing import Any, TextIO + +from yaml import SafeLoader + +_env_replace_matcher = re.compile(r"\$\{(\w|_)+:?.*}") + + +@typing.no_type_check # pyaml does not have good hints, everything is Any +def load_yaml_with_envvars( + stream: TextIO, environ: dict[str, Any] = os.environ +) -> dict[str, Any]: + """Load yaml file with environment variable expansion. + + The pattern ${VAR} or ${VAR:default} will be replaced with + the value of the environment variable. + """ + loader = SafeLoader(stream) + + def load_env_var(_, node) -> str: + """Extract the matched value, expand env variable, and replace the match.""" + value = str(node.value).removeprefix("${").removesuffix("}") + split = value.split(":", 1) + env_var = split[0] + value = environ.get(env_var) + default = None if len(split) == 1 else split[1] + if value is None and default is None: + raise ValueError( + f"Environment variable {env_var} is not set and not default was provided" + ) + return value or default + + loader.add_implicit_resolver("env_var_replacer", _env_replace_matcher, None) + loader.add_constructor("env_var_replacer", load_env_var) + + try: + return loader.get_single_data() + finally: + loader.dispose() diff --git a/pgpt/private_gpt/ui/__init__.py b/pgpt/private_gpt/ui/__init__.py new file mode 100644 index 0000000..8fe4022 --- /dev/null +++ b/pgpt/private_gpt/ui/__init__.py @@ -0,0 +1 @@ +"""Gradio based UI.""" diff --git a/pgpt/private_gpt/ui/avatar-bot.ico b/pgpt/private_gpt/ui/avatar-bot.ico new file mode 100644 index 0000000000000000000000000000000000000000..ab220adb2ff076d74bac45104f3c068b85e7aecd GIT binary patch literal 15406 zcmeHO4UAk>6@IhZs;Ee-yX>6r+&42jbMJk#V}X@LMG#quzY&B8O``@aiii;5Mj%bM@ad$Vs}XJ==2x<<)- z$z;0o?z=zdo_p>&-#HC}LNFYxSP=w;pc0(EEC^l~1VN?J{JXv~2)?x}2qq_!=ROEF zjs`(J2!a)C7c_r=0ie*$8m6bGmpRps){8|hy#=RwtTpE7SRC_X$OGp(;5o1Q>9pDU zAQ*DM4GwtB0dGlv&%>)8;zV}xeW!YQ`n=I-yvPApSdkekvN;+V**|@jg)`=>Y{Ryz zgJ3xOtTr}wpamYXB6}j$H)o%vaKKNj$n#$0)bzIu3X$qnd>*NOEc1H{uK9e%s-D&Q zaTY^P}_oXGvLY(%b_nwnakzLN*{*#Un~WWC#oJjwo&dEHsB@Z@wv!KqGi4;=7F zyq_ZXSk*6c4?4a%7sHLw(U(tEs|UQnnEe*eyZ>9z&QPG!FC05FzyQ2!7QAm(uUZ|7K$E$vjZJo%!oybjA=p;D>5#H)VF z0*@#62Jhglkue9i(}QWY2fkxPb|?4sb5`V9%4ELzc0%ko0Dn%$PWs&D!thNqGc$YX zq4%D$`T^ITzQcRU17F~s(`l?cV&B9V%Ke#4Uw?BVXHpmQ?TP6iSXK?g!<@*?$=%wV zv&h{}WTM?W^C21^k1X)pRzJWqUgUx<)-haDJ;sV`Pv>ocT@H9(FXyseDjny5ZJ9nE z;{8A0e6yf_TGj8i-WA#7fXk^**>;`aR8OsW+j|+`hv6Ff^{qVw?yDJN+bnf2SZiPGM7~J5CGYR?B0uUyem)FeOM9Cc z|8eTXR1nPT<0dHBQt8zm*p$8-%Mm!2XF6@P5GX6BdUa~cVjWif4d>P2UMbKvIMoX? zx;Nv2Z60`IJDb@71!v5O4%m@hhXZbmRF9dgR2as#Y#?QB!?5N=E}CPnc@F^J<~`D3 zthQjRLVKRkL&{;d10Hs&msr)4>tXozC=B0iMb5UswHA0foo}Ld_bg)VDGGIC4syUP znSEft>E}rYJmi4wj4d<2H^&BcFsAEe-}7MH?8ERV58RNkN9p+UeN*qZ@UCE-lkam8 z7^yXs@>36-(2Kq822m94AIn1Zvkth4>nC>Ue*v&G^leY^%xS_Oow#>dBa_B#1y0}L?0{}#xT(5YM-8;pf0 zr~w8TV1NMz=pD>g@zeX8!7$^1CG=?*!ZOAY6C&kJQ+-ztyzYXa zJeafKdCQGS4)s|la)$@bNOIPlY)Di7BJZaxm`6zWF!^SGRwz^qDObfymT|S)lH6Vh&+i`{eT5Fre`3tj$KaVPG2m(bFn$1EDZHt_4Fifklkycec|D4CQuUkUIlH}6=0nVts$P@lDQR<@>gN{20VgPokB^VVxvy+q zscF+#OY-YTY`CM=IHo-pUW%nW-cWHj_-GW z8hi?|_@Lxt-Vs0S`0p z-tYXKF|T*P9eMrdp0Kv87r(g;hFRz5M1J2!KkxOxn!e@3Tusydv~uRjKJR2cFbYH3 z{zaiwDy^gqn3I!E?2i}Oz&os8{iM(7fUhO~Ryy}Me{NN;qzzw8{KR=B`ONRzX5C$@ zZ=U5EaD_L9<*W1hQ{CX8QfZ|(<|A!fY3{x$E7mZmGRL$wsi9nO4UCgnFX6-R1gH9|oZXD?-Z-vP zJ+|9;Xd$es)k6C0R^-u~9ipsu#`92J%d?QgPwmz*#JjFCI$E?MCo|`p_#|1MD)v7e za1mqTetn-5oKp=r!UMPF{Rp17xTXNu(RWk9bw`6Myik_vd}g%-Z>H z>J96)FK4}R7xfg20(qMVIFjQyk*ztqvDciE*o1x=KY4kl?fGoG1%60hd|$?mZBU?I z(&n@7D%PdhdZctdU4O^x2WUSptrv^aNgY?e_HsecCutaC$(gVo&`Dgw)~b)o>Be4v z(`F`h6!F7VOVre^D;n*7dF$vW@1i2Y^O z#2iO%pr06`PZU@m$vOr4k=B?~SwlnHPTeBU#SghEFYeB@ literal 0 HcmV?d00001 diff --git a/pgpt/private_gpt/ui/images.py b/pgpt/private_gpt/ui/images.py new file mode 100644 index 0000000..6705b57 --- /dev/null +++ b/pgpt/private_gpt/ui/images.py @@ -0,0 +1 @@ +logo_svg = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODYxIiBoZWlnaHQ9Ijk4IiB2aWV3Qm94PSIwIDAgODYxIDk4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNNDguMTM0NSAwLjE1NzkxMUMzNi44Mjk5IDEuMDM2NTQgMjYuMTIwNSA1LjU1MzI4IDE3LjYyNTYgMTMuMDI1QzkuMTMwNDYgMjAuNDk2NyAzLjMxMTcgMzAuNTE2OSAxLjA0OTUyIDQxLjU3MDVDLTEuMjEyNzMgNTIuNjIzOCAwLjIwNDQxOSA2NC4xMDk0IDUuMDg2MiA3NC4yOTA1QzkuOTY4NjggODQuNDcxNiAxOC4wNTAzIDkyLjc5NDMgMjguMTA5OCA5OEwzMy43MDI2IDgyLjU5MDdMMzUuNDU0MiA3Ny43NjU2QzI5LjgzODcgNzQuMTY5MiAyNS41NDQ0IDY4Ljg2MDcgMjMuMjE0IDYyLjYzNDRDMjAuODgyMiA1Ni40MDg2IDIwLjYzOSA0OS41OTkxIDIyLjUyMDQgNDMuMjI0M0MyNC40MDI5IDM2Ljg0OTUgMjguMzA5NiAzMS4yNTI1IDMzLjY1NjEgMjcuMjcwNkMzOS4wMDIgMjMuMjg4MyA0NS41MDAzIDIxLjEzNSA1Mi4xNzg5IDIxLjEzM0M1OC44NTczIDIxLjEzMDMgNjUuMzU3MSAyMy4yNzgzIDcwLjcwNjUgMjcuMjU1OEM3Ni4wNTU0IDMxLjIzNCA3OS45NjY0IDM2LjgyNzcgODEuODU0MyA0My4yMDA2QzgzLjc0MjkgNDkuNTczNiA4My41MDYyIDU2LjM4MzYgODEuMTgwMSA2Mi42MTE3Qzc4Ljg1NDUgNjguODM5NiA3NC41NjUgNzQuMTUxNCA2OC45NTI5IDc3Ljc1MjhMNzAuNzA3NCA4Mi41OTA3TDc2LjMwMDIgOTcuOTk3MUM4Ni45Nzg4IDkyLjQ3MDUgOTUuNDA4OCA4My40NDE5IDEwMC4xNjMgNzIuNDQwNEMxMDQuOTE3IDYxLjQzOTQgMTA1LjcwNCA0OS4xNDE3IDEwMi4zODkgMzcuNjNDOTkuMDc0NiAyNi4xMTc5IDkxLjg2MjcgMTYuMDk5MyA4MS45NzQzIDkuMjcwNzlDNzIuMDg2MSAyLjQ0MTkxIDYwLjEyOTEgLTAuNzc3MDg2IDQ4LjEyODYgMC4xNTg5MzRMNDguMTM0NSAwLjE1NzkxMVoiIGZpbGw9IiMxRjFGMjkiLz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzVfMTkpIj4KPHBhdGggZD0iTTIyMC43NzIgMTIuNzUyNEgyNTIuNjM5QzI2Ny4yNjMgMTIuNzUyNCAyNzcuNzM5IDIxLjk2NzUgMjc3LjczOSAzNS40MDUyQzI3Ny43MzkgNDYuNzg3IDI2OS44ODEgNTUuMzUwOCAyNTguMzE0IDU3LjQxMDdMMjc4LjgzIDg1LjM3OTRIMjYxLjM3TDI0Mi4wNTQgNTcuOTUzM0gyMzUuNTA2Vjg1LjM3OTRIMjIwLjc3NEwyMjAuNzcyIDEyLjc1MjRaTTIzNS41MDQgMjYuMzAyOFY0NC40MDdIMjUyLjYzMkMyNTguOTYyIDQ0LjQwNyAyNjIuOTk5IDQwLjgyOTggMjYyLjk5OSAzNS40MTAyQzI2Mi45OTkgMjkuODgwOSAyNTguOTYyIDI2LjMwMjggMjUyLjYzMiAyNi4zMDI4SDIzNS41MDRaIiBmaWxsPSIjMUYxRjI5Ii8+CjxwYXRoIGQ9Ik0yOTUuMTc2IDg1LjM4NDRWMTIuNzUyNEgzMDkuOTA5Vjg1LjM4NDRIMjk1LjE3NloiIGZpbGw9IiMxRjFGMjkiLz4KPHBhdGggZD0iTTM2My43OTUgNjUuNzYzTDM4NS42MiAxMi43NTI0SDQwMS40NDRMMzcxLjIxNSA4NS4zODQ0SDM1Ni40ODNMMzI2LjI1NCAxMi43NTI0SDM0Mi4wNzhMMzYzLjc5NSA2NS43NjNaIiBmaWxsPSIjMUYxRjI5Ii8+CjxwYXRoIGQ9Ik00NDguMzI3IDcyLjA1MDRINDE1LjY5OEw0MTAuMjQxIDg1LjM4NDRIMzk0LjQxOEw0MjQuNjQ3IDEyLjc1MjRINDM5LjM3OUw0NjkuNjA4IDg1LjM4NDRINDUzLjc4M0w0NDguMzI3IDcyLjA1MDRaTTQ0Mi43NjEgNTguNUw0MzIuMDY2IDMyLjM3NDhMNDIxLjI2MiA1OC41SDQ0Mi43NjFaIiBmaWxsPSIjMUYxRjI5Ii8+CjxwYXRoIGQ9Ik00NjUuMjIxIDEyLjc1MjRINTMwLjU5MlYyNi4zMDI4SDUwNS4yNzVWODUuMzg0NEg0OTAuNTM5VjI2LjMwMjhINDY1LjIyMVYxMi43NTI0WiIgZmlsbD0iIzFGMUYyOSIvPgo8cGF0aCBkPSJNNTk1LjE5MyAxMi43NTI0VjI2LjMwMjhINTYyLjEyOFY0MS4xNTUxSDU5NS4xOTNWNTQuNzA2NUg1NjIuMTI4VjcxLjgzNEg1OTUuMTkzVjg1LjM4NDRINTQ3LjM5NVYxMi43NTI0SDU5NS4xOTNaIiBmaWxsPSIjMUYxRjI5Ii8+CjxwYXRoIGQ9Ik0xNjcuMjAxIDU3LjQxNThIMTg2LjUzNkMxOTAuODg2IDU3LjQ2NjIgMTk1LjE2OCA1Ni4zMzQ4IDE5OC45MTggNTQuMTQzN0MyMDIuMTc5IDUyLjIxOTkgMjA0Ljg2OSA0OS40NzM2IDIwNi43MTYgNDYuMTgzNUMyMDguNTYyIDQyLjg5MzQgMjA5LjUgMzkuMTc2NiAyMDkuNDMzIDM1LjQxMDJDMjA5LjQzMyAyMS45Njc1IDE5OC45NTggMTIuNzU3NCAxODQuMzM0IDEyLjc1NzRIMTUyLjQ2OFY4NS4zODk0SDE2Ny4yMDFWNTcuNDIwN1Y1Ny40MTU4Wk0xNjcuMjAxIDI2LjMwNThIMTg0LjMyOUMxOTAuNjU4IDI2LjMwNTggMTk0LjY5NiAyOS44ODQgMTk0LjY5NiAzNS40MTMzQzE5NC42OTYgNDAuODMyOSAxOTAuNjU4IDQ0LjQwOTkgMTg0LjMyOSA0NC40MDk5SDE2Ny4yMDFWMjYuMzA1OFoiIGZpbGw9IiMxRjFGMjkiLz4KPHBhdGggZD0iTTc5NC44MzUgMTIuNzUyNEg4NjAuMjA2VjI2LjMwMjhIODM0Ljg4OVY4NS4zODQ0SDgyMC4xNTZWMjYuMzAyOEg3OTQuODM1VjEyLjc1MjRaIiBmaWxsPSIjMUYxRjI5Ii8+CjxwYXRoIGQ9Ik03NDEuOTA3IDU3LjQxNThINzYxLjI0MUM3NjUuNTkyIDU3LjQ2NjEgNzY5Ljg3NCA1Ni4zMzQ3IDc3My42MjQgNTQuMTQzN0M3NzYuODg0IDUyLjIxOTkgNzc5LjU3NSA0OS40NzM2IDc4MS40MjEgNDYuMTgzNUM3ODMuMjY4IDQyLjg5MzQgNzg0LjIwNiAzOS4xNzY2IDc4NC4xMzkgMzUuNDEwMkM3ODQuMTM5IDIxLjk2NzUgNzczLjY2NCAxMi43NTc0IDc1OS4wMzkgMTIuNzU3NEg3MjcuMTc1Vjg1LjM4OTRINzQxLjkwN1Y1Ny40MjA3VjU3LjQxNThaTTc0MS45MDcgMjYuMzA1OEg3NTkuMDM1Qzc2NS4zNjUgMjYuMzA1OCA3NjkuNDAzIDI5Ljg4NCA3NjkuNDAzIDM1LjQxMzNDNzY5LjQwMyA0MC44MzI5IDc2NS4zNjUgNDQuNDA5OSA3NTkuMDM1IDQ0LjQwOTlINzQxLjkwN1YyNi4zMDU4WiIgZmlsbD0iIzFGMUYyOSIvPgo8cGF0aCBkPSJNNjgxLjA2OSA0Ny4wMTE1VjU5LjAxMjVINjk1LjM3OVY3MS42NzE5QzY5Mi41MjYgNzMuNDM2OCA2ODguNTI0IDc0LjMzMTkgNjgzLjQ3NyA3NC4zMzE5QzY2Ni4wMDMgNzQuMzMxOSA2NTguMDQ1IDYxLjgxMjQgNjU4LjA0NSA1MC4xOEM2NTguMDQ1IDMzLjk2MDUgNjcxLjAwOCAyNS40NzMyIDY4My44MTIgMjUuNDczMkM2OTAuNDI1IDI1LjQ2MjggNjk2LjkwOSAyNy4yODA0IDcwMi41NDEgMzAuNzIyNkw3MDMuMTU3IDMxLjEyNTRMNzA1Ljk1OCAxOC4xODZMNzA1LjY2MyAxNy45OTc3QzcwMC4wNDYgMTQuNDAwNCA2OTEuMjkxIDEyLjI1OSA2ODIuMjUxIDEyLjI1OUM2NjMuMTk3IDEyLjI1OSA2NDIuOTQ5IDI1LjM5NjcgNjQyLjk0OSA0OS43NDVDNjQyLjk0OSA2MS4wODQ1IDY0Ny4yOTMgNzAuNzE3NCA2NTUuNTExIDc3LjYwMjlDNjYzLjIyNCA4My44MjQ1IDY3Mi44NzQgODcuMTg5IDY4Mi44MDkgODcuMTIwMUM2OTQuMzYzIDg3LjEyMDEgNzAzLjA2MSA4NC42NDk1IDcwOS40MDIgNzkuNTY5Mkw3MDkuNTg5IDc5LjQxODFWNDcuMDExNUg2ODEuMDY5WiIgZmlsbD0iIzFGMUYyOSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzVfMTkiPgo8cmVjdCB3aWR0aD0iNzA3Ljc3OCIgaGVpZ2h0PSI3NC44NjExIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTUyLjQ0NCAxMi4yNSkiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K" diff --git a/pgpt/private_gpt/ui/ui.py b/pgpt/private_gpt/ui/ui.py new file mode 100644 index 0000000..bee4f5e --- /dev/null +++ b/pgpt/private_gpt/ui/ui.py @@ -0,0 +1,596 @@ +"""This file should be imported if and only if you want to run the UI locally.""" + +import base64 +import logging +import time +from collections.abc import Iterable +from enum import Enum +from pathlib import Path +from typing import Any + +import gradio as gr # type: ignore +from fastapi import FastAPI +from gradio.themes.utils.colors import slate # type: ignore +from injector import inject, singleton +from llama_index.core.llms import ChatMessage, ChatResponse, MessageRole +from llama_index.core.types import TokenGen +from pydantic import BaseModel + +from private_gpt.constants import PROJECT_ROOT_PATH +from private_gpt.di import global_injector +from private_gpt.open_ai.extensions.context_filter import ContextFilter +from private_gpt.server.chat.chat_service import ChatService, CompletionGen +from private_gpt.server.chunks.chunks_service import Chunk, ChunksService +from private_gpt.server.ingest.ingest_service import IngestService +from private_gpt.server.recipes.summarize.summarize_service import SummarizeService +from private_gpt.settings.settings import settings +from private_gpt.ui.images import logo_svg + +logger = logging.getLogger(__name__) + +THIS_DIRECTORY_RELATIVE = Path(__file__).parent.relative_to(PROJECT_ROOT_PATH) +# Should be "private_gpt/ui/avatar-bot.ico" +AVATAR_BOT = THIS_DIRECTORY_RELATIVE / "avatar-bot.ico" + +UI_TAB_TITLE = "My Private GPT" + +SOURCES_SEPARATOR = "
Sources: \n" + + +class Modes(str, Enum): + RAG_MODE = "RAG" + SEARCH_MODE = "Search" + BASIC_CHAT_MODE = "Basic" + SUMMARIZE_MODE = "Summarize" + + +MODES: list[Modes] = [ + Modes.RAG_MODE, + Modes.SEARCH_MODE, + Modes.BASIC_CHAT_MODE, + Modes.SUMMARIZE_MODE, +] + + +class Source(BaseModel): + file: str + page: str + text: str + + class Config: + frozen = True + + @staticmethod + def curate_sources(sources: list[Chunk]) -> list["Source"]: + curated_sources = [] + + for chunk in sources: + doc_metadata = chunk.document.doc_metadata + + file_name = doc_metadata.get("file_name", "-") if doc_metadata else "-" + page_label = doc_metadata.get("page_label", "-") if doc_metadata else "-" + + source = Source(file=file_name, page=page_label, text=chunk.text) + curated_sources.append(source) + curated_sources = list( + dict.fromkeys(curated_sources).keys() + ) # Unique sources only + + return curated_sources + + +@singleton +class PrivateGptUi: + @inject + def __init__( + self, + ingest_service: IngestService, + chat_service: ChatService, + chunks_service: ChunksService, + summarizeService: SummarizeService, + ) -> None: + self._ingest_service = ingest_service + self._chat_service = chat_service + self._chunks_service = chunks_service + self._summarize_service = summarizeService + + # Cache the UI blocks + self._ui_block = None + + self._selected_filename = None + + # Initialize system prompt based on default mode + default_mode_map = {mode.value: mode for mode in Modes} + self._default_mode = default_mode_map.get( + settings().ui.default_mode, Modes.RAG_MODE + ) + self._system_prompt = self._get_default_system_prompt(self._default_mode) + + def _chat( + self, message: str, history: list[list[str]], mode: Modes, *_: Any + ) -> Any: + def yield_deltas(completion_gen: CompletionGen) -> Iterable[str]: + full_response: str = "" + stream = completion_gen.response + for delta in stream: + if isinstance(delta, str): + full_response += str(delta) + elif isinstance(delta, ChatResponse): + full_response += delta.delta or "" + yield full_response + time.sleep(0.02) + + if completion_gen.sources: + full_response += SOURCES_SEPARATOR + cur_sources = Source.curate_sources(completion_gen.sources) + sources_text = "\n\n\n" + used_files = set() + for index, source in enumerate(cur_sources, start=1): + if f"{source.file}-{source.page}" not in used_files: + sources_text = ( + sources_text + + f"{index}. {source.file} (page {source.page}) \n\n" + ) + used_files.add(f"{source.file}-{source.page}") + sources_text += "
\n\n" + full_response += sources_text + yield full_response + + def yield_tokens(token_gen: TokenGen) -> Iterable[str]: + full_response: str = "" + for token in token_gen: + full_response += str(token) + yield full_response + + def build_history() -> list[ChatMessage]: + history_messages: list[ChatMessage] = [] + + for interaction in history: + history_messages.append( + ChatMessage(content=interaction[0], role=MessageRole.USER) + ) + if len(interaction) > 1 and interaction[1] is not None: + history_messages.append( + ChatMessage( + # Remove from history content the Sources information + content=interaction[1].split(SOURCES_SEPARATOR)[0], + role=MessageRole.ASSISTANT, + ) + ) + + # max 20 messages to try to avoid context overflow + return history_messages[:20] + + new_message = ChatMessage(content=message, role=MessageRole.USER) + all_messages = [*build_history(), new_message] + # If a system prompt is set, add it as a system message + if self._system_prompt: + all_messages.insert( + 0, + ChatMessage( + content=self._system_prompt, + role=MessageRole.SYSTEM, + ), + ) + match mode: + case Modes.RAG_MODE: + # Use only the selected file for the query + context_filter = None + if self._selected_filename is not None: + docs_ids = [] + for ingested_document in self._ingest_service.list_ingested(): + if ( + ingested_document.doc_metadata["file_name"] + == self._selected_filename + ): + docs_ids.append(ingested_document.doc_id) + context_filter = ContextFilter(docs_ids=docs_ids) + + query_stream = self._chat_service.stream_chat( + messages=all_messages, + use_context=True, + context_filter=context_filter, + ) + yield from yield_deltas(query_stream) + case Modes.BASIC_CHAT_MODE: + llm_stream = self._chat_service.stream_chat( + messages=all_messages, + use_context=False, + ) + yield from yield_deltas(llm_stream) + + case Modes.SEARCH_MODE: + response = self._chunks_service.retrieve_relevant( + text=message, limit=4, prev_next_chunks=0 + ) + + sources = Source.curate_sources(response) + + yield "\n\n\n".join( + f"{index}. **{source.file} " + f"(page {source.page})**\n " + f"{source.text}" + for index, source in enumerate(sources, start=1) + ) + case Modes.SUMMARIZE_MODE: + # Summarize the given message, optionally using selected files + context_filter = None + if self._selected_filename: + docs_ids = [] + for ingested_document in self._ingest_service.list_ingested(): + if ( + ingested_document.doc_metadata["file_name"] + == self._selected_filename + ): + docs_ids.append(ingested_document.doc_id) + context_filter = ContextFilter(docs_ids=docs_ids) + + summary_stream = self._summarize_service.stream_summarize( + use_context=True, + context_filter=context_filter, + instructions=message, + ) + yield from yield_tokens(summary_stream) + + # On initialization and on mode change, this function set the system prompt + # to the default prompt based on the mode (and user settings). + @staticmethod + def _get_default_system_prompt(mode: Modes) -> str: + p = "" + match mode: + # For query chat mode, obtain default system prompt from settings + case Modes.RAG_MODE: + p = settings().ui.default_query_system_prompt + # For chat mode, obtain default system prompt from settings + case Modes.BASIC_CHAT_MODE: + p = settings().ui.default_chat_system_prompt + # For summarization mode, obtain default system prompt from settings + case Modes.SUMMARIZE_MODE: + p = settings().ui.default_summarization_system_prompt + # For any other mode, clear the system prompt + case _: + p = "" + return p + + @staticmethod + def _get_default_mode_explanation(mode: Modes) -> str: + match mode: + case Modes.RAG_MODE: + return "Get contextualized answers from selected files." + case Modes.SEARCH_MODE: + return "Find relevant chunks of text in selected files." + case Modes.BASIC_CHAT_MODE: + return "Chat with the LLM using its training data. Files are ignored." + case Modes.SUMMARIZE_MODE: + ####### + # Modification par SPC + # + # return "Generate a summary of the selected files. Prompt to customize the result." + return "Générer l'analyse selon les conditions présentées dans le prompt système." + # + # Fin modification par SPC + ####### + case _: + return "" + + def _set_system_prompt(self, system_prompt_input: str) -> None: + logger.info(f"Setting system prompt to: {system_prompt_input}") + self._system_prompt = system_prompt_input + + def _set_explanatation_mode(self, explanation_mode: str) -> None: + self._explanation_mode = explanation_mode + + def _set_current_mode(self, mode: Modes) -> Any: + self.mode = mode + self._set_system_prompt(self._get_default_system_prompt(mode)) + self._set_explanatation_mode(self._get_default_mode_explanation(mode)) + interactive = self._system_prompt is not None + return [ + gr.update(placeholder=self._system_prompt, interactive=interactive), + gr.update(value=self._explanation_mode), + ] + + def _list_ingested_files(self) -> list[list[str]]: + files = set() + for ingested_document in self._ingest_service.list_ingested(): + if ingested_document.doc_metadata is None: + # Skipping documents without metadata + continue + file_name = ingested_document.doc_metadata.get( + "file_name", "[FILE NAME MISSING]" + ) + files.add(file_name) + return [[row] for row in files] + + def _upload_file(self, files: list[str]) -> None: + logger.debug("Loading count=%s files", len(files)) + paths = [Path(file) for file in files] + + # remove all existing Documents with name identical to a new file upload: + file_names = [path.name for path in paths] + doc_ids_to_delete = [] + for ingested_document in self._ingest_service.list_ingested(): + if ( + ingested_document.doc_metadata + and ingested_document.doc_metadata["file_name"] in file_names + ): + doc_ids_to_delete.append(ingested_document.doc_id) + if len(doc_ids_to_delete) > 0: + logger.info( + "Uploading file(s) which were already ingested: %s document(s) will be replaced.", + len(doc_ids_to_delete), + ) + for doc_id in doc_ids_to_delete: + self._ingest_service.delete(doc_id) + + self._ingest_service.bulk_ingest([(str(path.name), path) for path in paths]) + + def _delete_all_files(self) -> Any: + ingested_files = self._ingest_service.list_ingested() + logger.debug("Deleting count=%s files", len(ingested_files)) + for ingested_document in ingested_files: + self._ingest_service.delete(ingested_document.doc_id) + return [ + gr.List(self._list_ingested_files()), + gr.components.Button(interactive=False), + gr.components.Button(interactive=False), + gr.components.Textbox("All files"), + ] + + def _delete_selected_file(self) -> Any: + logger.debug("Deleting selected %s", self._selected_filename) + # Note: keep looping for pdf's (each page became a Document) + for ingested_document in self._ingest_service.list_ingested(): + if ( + ingested_document.doc_metadata + and ingested_document.doc_metadata["file_name"] + == self._selected_filename + ): + self._ingest_service.delete(ingested_document.doc_id) + return [ + gr.List(self._list_ingested_files()), + gr.components.Button(interactive=False), + gr.components.Button(interactive=False), + gr.components.Textbox("All files"), + ] + + def _deselect_selected_file(self) -> Any: + self._selected_filename = None + return [ + gr.components.Button(interactive=False), + gr.components.Button(interactive=False), + gr.components.Textbox("All files"), + ] + + def _selected_a_file(self, select_data: gr.SelectData) -> Any: + self._selected_filename = select_data.value + return [ + gr.components.Button(interactive=True), + gr.components.Button(interactive=True), + gr.components.Textbox(self._selected_filename), + ] + + def _build_ui_blocks(self) -> gr.Blocks: + logger.debug("Creating the UI blocks") + with gr.Blocks( + title=UI_TAB_TITLE, + theme=gr.themes.Soft(primary_hue=slate), + css=".logo { " + "display:flex;" + "background-color: #C7BAFF;" + "height: 80px;" + "border-radius: 8px;" + "align-content: center;" + "justify-content: center;" + "align-items: center;" + "}" + ".logo img { height: 25% }" + ".contain { display: flex !important; flex-direction: column !important; }" + "#component-0, #component-3, #component-10, #component-8 { height: 100% !important; }" + "#chatbot { flex-grow: 1 !important; overflow: auto !important;}" + "#col { height: calc(100vh - 112px - 16px) !important; }" + "hr { margin-top: 1em; margin-bottom: 1em; border: 0; border-top: 1px solid #FFF; }" + ".avatar-image { background-color: antiquewhite; border-radius: 2px; }" + ".footer { text-align: center; margin-top: 20px; font-size: 14px; display: flex; align-items: center; justify-content: center; }" + ".footer-zylon-link { display:flex; margin-left: 5px; text-decoration: auto; color: var(--body-text-color); }" + ".footer-zylon-link:hover { color: #C7BAFF; }" + ".footer-zylon-ico { height: 20px; margin-left: 5px; background-color: antiquewhite; border-radius: 2px; }", + ) as blocks: + with gr.Row(): + gr.HTML(f"