Claude
fix(audit): 5 corrections suite à l'audit complet de mes derniers sprints
1e8b84c unverified
Raw
History Blame
5.54 kB
"""Front Pareto coût/qualité (Sprint 19).
Construit trois fronts Pareto avec des axes alternatifs :
- ``cost`` — CER vs coût € / 1000 pages.
- ``speed`` — CER vs durée moyenne par page.
- ``co2`` — CER vs empreinte carbone (g CO₂ / 1000 pages, expérimental).
API
---
Deux fonctions séparées pour rendre le contrat explicite :
1. :func:`attach_engine_costs` — **mute en place** ``engines_summary``
en y ajoutant ``mean_duration_seconds`` et ``cost`` (extraits du
benchmark et de la table de pricing). Le nom dit clairement qu'il
y a mutation.
2. :func:`build_pareto_section` — **fonction pure**, lit les coûts
déjà attachés à ``engines_summary``. Retourne le dict ``pareto``
prêt pour le template.
L'orchestrateur (``__init__.py``) appelle les deux dans l'ordre.
Cette séparation rend possible :
- Tester :func:`build_pareto_section` indépendamment avec un
``engines_summary`` pré-fabriqué.
- Réutiliser les coûts attachés sans recalculer Pareto.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from picarones.measurements.pricing import (
build_costs_for_benchmark,
load_pricing_database,
)
from picarones.measurements.statistics import compute_pareto_front
if TYPE_CHECKING:
from picarones.core.results import BenchmarkResult
def attach_engine_costs(
engines_summary: list[dict], benchmark: "BenchmarkResult",
) -> None:
"""Annote chaque entrée de ``engines_summary`` avec son coût.
**Mute en place** : ajoute deux champs à chaque dict moteur :
- ``mean_duration_seconds`` (float ou ``None`` si pas de durée).
- ``cost`` : dict de la forme ``{cost_per_1k_pages_eur: ...,
co2_per_1k_pages_g: ..., ...}`` ou ``None`` si pricing
indisponible.
Doit être appelée AVANT :func:`build_pareto_section`, qui lit
ces deux champs.
"""
durations_by_engine: dict[str, float] = {}
for report in benchmark.engine_reports:
durs = [
dr.duration_seconds
for dr in report.document_results
if dr.duration_seconds is not None
]
if durs:
durations_by_engine[report.engine_name] = sum(durs) / len(durs)
costs_by_engine = build_costs_for_benchmark(
engines_summary, durations_by_engine,
)
for entry in engines_summary:
name = entry["name"]
entry["mean_duration_seconds"] = (
round(durations_by_engine.get(name, 0.0), 4)
if name in durations_by_engine else None
)
entry["cost"] = costs_by_engine.get(name)
def build_pareto_section(engines_summary: list[dict]) -> dict:
"""Construit le bloc ``pareto`` du dict de rapport.
**Fonction pure** : ne mute rien. Lit ``mean_duration_seconds``
et ``cost`` qui doivent avoir été attachés en amont par
:func:`attach_engine_costs`. Si ces champs sont absents, le
moteur est silencieusement omis du front (cohérent avec un
moteur qui n'a pas de prix connu).
Retour
------
dict
Trois fronts Pareto (``cost``, ``speed``, ``co2``) plus
``pricing_meta`` (table de pricing utilisée).
"""
pricing_defaults, _ = load_pricing_database()
pareto_points = []
for entry in engines_summary:
cer = entry.get("cer")
cost = (entry.get("cost") or {}).get("cost_per_1k_pages_eur")
if cer is None or cost is None:
continue
pareto_points.append({"engine": entry["name"], "cer": cer, "cost": cost})
pareto_front_engines = compute_pareto_front(
pareto_points, objectives=("cer", "cost"),
)
pareto_speed_points = []
for entry in engines_summary:
cer = entry.get("cer")
dur = entry.get("mean_duration_seconds")
if cer is None or dur is None:
continue
pareto_speed_points.append({"engine": entry["name"], "cer": cer, "dur": dur})
pareto_front_speed = compute_pareto_front(
pareto_speed_points, objectives=("cer", "dur"),
)
pareto_co2_points = []
for entry in engines_summary:
cer = entry.get("cer")
co2 = (entry.get("cost") or {}).get("co2_per_1k_pages_g")
if cer is None or co2 is None:
continue
pareto_co2_points.append({"engine": entry["name"], "cer": cer, "co2": co2})
pareto_front_co2 = compute_pareto_front(
pareto_co2_points, objectives=("cer", "co2"),
)
return {
"cost": {
"points": pareto_points,
"front": pareto_front_engines,
"axis_label": "Coût (€ / 1000 pages)",
},
"speed": {
"points": pareto_speed_points,
"front": pareto_front_speed,
"axis_label": "Temps moyen (s / page)",
},
"co2": {
"points": pareto_co2_points,
"front": pareto_front_co2,
"axis_label": (
"Empreinte carbone (g CO₂ / 1000 pages, expérimental)"
),
},
"pricing_meta": {
"last_updated": pricing_defaults.last_updated,
"currency": pricing_defaults.currency,
"hourly_rate_local_cpu_eur": pricing_defaults.hourly_rate_local_cpu_eur,
"hourly_rate_local_gpu_eur": pricing_defaults.hourly_rate_local_gpu_eur,
"grid_intensity_local": pricing_defaults.grid_intensity_local,
"grid_intensity_cloud": pricing_defaults.grid_intensity_cloud,
},
}
__all__ = ["attach_engine_costs", "build_pareto_section"]