"""
config_loader.py
----------------
Module centralisé pour charger et valider les fichiers de configuration
(YAML ou JSON) utilisés dans VISWIR_vQuasar.
Ce fichier est conçu pour être extensible :
- On peux ajouter de nouveaux fichiers de config (ex: logging_config.yaml).
- On peux définir des clés obligatoires par contexte (fixed, sql, optuna...).
- On peux fusionner plusieurs fichiers en un seul dictionnaire global.
Auteur : Alexandre Riffard
"""
import json
import yaml
from pathlib import Path
from typing import Any, Dict
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
CONFIG_DIR = PROJECT_ROOT / "config"
[docs]
def resolve_config_path(path: str | Path) -> Path:
"""
Resolve a configuration file path relative to current directory,
project root, or config directory.
"""
p = Path(path)
if p.exists():
return p
# Try relative to project root
candidate_root = (PROJECT_ROOT / p).resolve()
if candidate_root.exists():
return candidate_root
# Try removing leading ".." components
clean_parts = [part for part in p.parts if part != ".."]
if clean_parts:
candidate_clean = (PROJECT_ROOT / Path(*clean_parts)).resolve()
if candidate_clean.exists():
return candidate_clean
# Try directly in CONFIG_DIR
candidate_config = (CONFIG_DIR / p.name).resolve()
if candidate_config.exists():
return candidate_config
return p
# ============================================================
# Exceptions personnalisées
# ============================================================
[docs]
class ConfigError(Exception):
"""Custom exception for configuration-related errors."""
# ============================================================
# Fonctions utilitaires
# ============================================================
[docs]
def load_config(path: str | Path, defaults: Dict[str, Any] | None = None) -> Dict[str, Any]:
"""
Load a YAML or JSON configuration file and merge it with default values.
Parameters
----------
path : str or Path
Path to the configuration file (.yaml/.yml or .json).
defaults : dict, optional
Dictionary of default values to merge with the loaded configuration.
Returns
-------
dict
Final configuration dictionary (file values + defaults).
Raises
------
ConfigError
If the file does not exist or has an unsupported format.
"""
path = resolve_config_path(path)
if not path.exists():
raise ConfigError(f"❌ Fichier de configuration introuvable : {path}")
# Lecture du fichier
if path.suffix in [".yaml", ".yml"]:
with open(path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f) or {}
elif path.suffix == ".json":
with open(path, "r", encoding="utf-8") as f:
config = json.load(f)
else:
raise ConfigError(f"❌ Format non supporté : {path.suffix}")
# Fusion avec les valeurs par défaut
if defaults:
merged = defaults.copy()
merged.update(config)
return merged
return config
[docs]
def validate_config(config: Dict[str, Any], required_keys: list[str], context: str = "") -> None:
"""
Validate that required keys are present in the configuration.
Parameters
----------
config : dict
Loaded configuration dictionary.
required_keys : list of str
List of mandatory keys that must be present.
context : str, optional
Context name (e.g., "optuna", "sql") used in error messages.
Raises
------
ConfigError
If one or more required keys are missing.
"""
missing = [key for key in required_keys if key not in config]
if missing:
raise ConfigError(
f"❌ Clés manquantes dans la configuration {context or ''} : {missing}"
)
# ============================================================
# Chargement global (fusion de me plusieurs fichiers)
# ============================================================
[docs]
def load_all_configs(
base_path: str | Path = "config/config_viswir.yaml",
params_path: str | Path = "config/parameters.json",
yolo_path: str | Path = "config/yolo_config.json",
optuna_path: str | Path = "config/optuna_config.yaml",
logging_path: str | Path = "config/logging_config.yaml"
) -> Dict[str, Dict[str, Any]]:
"""
Load and validate all configuration files required for the VISWIR_vQuasar project.
Parameters
----------
base_path : str or Path, optional
General configuration (paths, mode, etc.).
params_path : str or Path, optional
Fusion parameters (fixed values or ranges).
yolo_path : str or Path, optional
YOLO configuration (model, thresholds, classes).
optuna_path : str or Path, optional
Optuna configuration (HPC optimization).
logging_path : str or Path, optional
Logger configuration.
Returns
-------
dict of dict
Structured dictionary containing all loaded configurations.
"""
configs = {}
# Config générale
configs["base"] = load_config(base_path)
validate_config(
configs["base"],
["visible_folder", "swir_folder", "output_folder", "mode", "run_detection", "save_output"],
context="base"
)
# Paramètres de fusion
configs["params"] = load_config(params_path)
validate_config(
configs["params"],
["facteur_swir", "beta", "level", "apply_gamma", "gamma_value"],
context="parameters"
)
# YOLO
configs["yolo"] = load_config(yolo_path)
validate_config(
configs["yolo"],
["model_path", "confidence_threshold", "iou_threshold", "device"],
context="yolo"
)
# Optuna (optionnel → valeurs par défaut si absent)
try:
configs["optuna"] = load_config(optuna_path, defaults={
"n_trials": 100,
"n_jobs": 4,
"sampler": "TPE",
"pruner": "MedianPruner",
"timeout": None,
"storage": None
})
except ConfigError:
configs["optuna"] = {
"n_trials": 100,
"n_jobs": 4,
"sampler": "TPE",
"pruner": "MedianPruner",
"timeout": None,
"storage": None
}
# Logging (optionnel)
try:
configs["logging"] = load_config(logging_path, defaults={
"level": "INFO",
"log_to_file": True,
"log_file": "../logs/viswir.log",
"log_to_console": True,
"format": "[%(asctime)s] [%(levelname)s] %(message)s"
})
except ConfigError:
configs["logging"] = {
"level": "INFO",
"log_to_file": True,
"log_file": "../logs/viswir.log",
"log_to_console": True,
"format": "[%(asctime)s] [%(levelname)s] %(message)s"
}
return configs
# ============================================================
# Chargement spécial pour Optuna (espace de recherche des paramètres)
# ============================================================
[docs]
def load_optuna_search_space(path: str | Path = "config/optuna_search_space.yaml") -> Dict[str, Any]:
"""
Load the YAML file defining the Optuna search space.
Parameters
----------
path : str or Path, optional
Path to the YAML file describing the search space.
Returns
-------
dict
Dictionary describing the Optuna search space.
"""
return load_config(path)