"""Frontière de Pareto multi-objectifs (Sprint 19). Algorithme générique sur N objectifs (CER, coût, vitesse, CO₂…). Renvoie les noms des points non-dominés. """ from __future__ import annotations from typing import Optional def compute_pareto_front( points: list[dict], objectives: tuple[str, ...] = ("cer", "cost"), name_key: str = "engine", minimize: Optional[tuple[bool, ...]] = None, ) -> list[str]: """Calcule la frontière de Pareto sur ``len(objectives)`` dimensions. Un point ``p`` est Pareto-dominant si aucun autre point n'a, pour TOUS les objectifs, une valeur au moins aussi bonne ET au moins une valeur strictement meilleure. Parameters ---------- points: Liste de dicts. Chaque dict doit contenir ``name_key`` et toutes les clés de ``objectives``. Les points dont une valeur d'objectif est ``None`` sont ignorés (pas de comparaison possible). objectives: Clés des objectifs à minimiser/maximiser. name_key: Clé identifiant le point (par défaut ``"engine"``). minimize: Pour chaque objectif, ``True`` = minimiser (ex. CER, coût), ``False`` = maximiser (ex. ancrage). Doit avoir la même longueur que ``objectives``. Returns ------- Liste des ``name`` des points sur le front Pareto, ordre stable depuis ``points``. """ if minimize is None: minimize = tuple(True for _ in objectives) if len(minimize) != len(objectives): raise ValueError("`minimize` doit avoir la même longueur que `objectives`") valid = [] for p in points: try: vals = tuple(float(p[k]) for k in objectives) except (KeyError, TypeError, ValueError): continue valid.append((p[name_key], vals)) front: list[str] = [] for name_a, vals_a in valid: dominated = False for name_b, vals_b in valid: if name_a == name_b: continue # B domine A si B est ≥ aussi bon partout ET strictement meilleur quelque part better_or_equal_everywhere = True strictly_better_somewhere = False for va, vb, mini in zip(vals_a, vals_b, minimize): if mini: if vb > va: better_or_equal_everywhere = False break if vb < va: strictly_better_somewhere = True else: # maximiser if vb < va: better_or_equal_everywhere = False break if vb > va: strictly_better_somewhere = True if better_or_equal_everywhere and strictly_better_somewhere: dominated = True break if not dominated: front.append(name_a) return front __all__ = ["compute_pareto_front"]