Navigation : Index # CC3 : SmartGrid - Ordonnancement de la production energetique sous incertitude
Étude de cas interdisciplinaire combinant programmation par contraintes (OR-Tools CP-SAT), inference probabiliste (modèle bayesien), optimisation multi-objectif et un jumeau numérique de reseau electrique.
Contexte metier
Un opérateur de reseau electrique doit, pour chaque heure, decider quelles centrales activer et a quel niveau de production (le unit commitment / dispatch) afin de satisfaire la demande tout en minimisant le cout economique et les emissions de CO2. Ce problème est rendu difficile par :
l’incertitude sur la production renouvelable (eolien, solaire) et sur la demande ;
les contraintes dures : capacites min/max des centrales, temps de montee/descente, reserve tournante (securite n-1) ;
les objectifs multiples conflictuels : cout, emissions, fiabilite.
Une seule technique ne suffit pas : un solveur déterministe ignore l’incertitude, un modèle probabiliste seul ne respecte pas les contraintes physiques. Cette étude de cas materialise leur composition ordonnee : filtrer les dispatches impossibles (contraintes), modeliser l’aleatoire (incertitude), puis optimiser (multi-objectif).
Architecture en 4 couches
Couche
Rôle
Technique
Partie
Contraintes dures
Filtrer les dispatches impossibles
OR-Tools CP-SAT
Partie 1
Incertitude
Modeliser l’aleatoire renouvelable
Modèle bayesien
Partie 2
Optimisation
Choisir le meilleur compromis
Multi-objectif pondere
Partie 3
Decision
Synthese + comparaison
Workflow + scoring
Partie 4
Ce notebook est une fondation : le jumeau numérique (couche 0) est operationnel et les 4 couches de solveurs (CP-SAT, bayésien, multi-objectif, analyse comparative) sont implémentees et exécutées (Parties 1 à 4). Les 3 exercices d’extension (Exercices 1 à 3) sont volontairement laissés en stub pour les étudiants.
0. Installation et jumeau numérique
Le jumeau numérique represente un reseau electrique : un ensemble de centrales (avec cout, capacites, facteur d’emission) et une demande horaire a satisfaire, avec une production renouvelable stochastique.
Partie 1 : Le Dispatcher sous Contraintes (OR-Tools CP-SAT)
Théorie : le unit commitment est un problème NP-difficile. On modelise, pour chaque heure et chaque centrale pilotable, une variable binaire (activee) et une variable continue (niveau de production), soumises aux contraintes :
somme des productions = demande nette (equilibre offre/demande) ;
pmin <= production <= pmax quand la centrale est activee ;
reserve tournante : capacite excédentaire disponible >= marge de securite.
L’objectif primaire : minimiser le cout total.
# Partie 1 : modele CP-SAT du dispatch (unit commitment)from ortools.sat.python import cp_modeldef solve_dispatch_cp(network, cost_weights=None):"""Resout le unit commitment par programmation par contraintes (CP-SAT). Pour chaque heure et centrale pilotable : binaire on (activee) + entier p (production MW). Contraintes : equilibre offre/demande nette + bornes de capacite (pmin <= p <= pmax quand active). Objectif pondere : on peut minimiser le cout (couts variables) ou le CO2 (facteurs d'emission). Renvoie un dict {hour: {plant_name: production_mw}}, '_cost', '_status'. cost_weights = (w_cost, w_co2) : poids de chaque terme dans l'objectif. """if cost_weights isNone: cost_weights = (1.0, 0.0) H =len(network.demand_mw) plants = network.plants model = cp_model.CpModel() on, p = {}, {}for h inrange(H):for i, plant inenumerate(plants): on[(h, i)] = model.NewBoolVar(f'on_{h}_{i}') p[(h, i)] = model.NewIntVar(0, int(plant.pmax), f'p_{h}_{i}')for h inrange(H): nd =int(round(network.net_demand(h))) model.Add(sum(p[(h, i)] for i inrange(len(plants))) == nd)for i, plant inenumerate(plants): model.Add(p[(h, i)] >=int(plant.pmin) * on[(h, i)]) model.Add(p[(h, i)] <=int(plant.pmax) * on[(h, i)]) w_cost, w_co2 = cost_weights# Objectif pondere cout+CO2. NB : on plie les poids DANS chaque terme et on somme# via cp_model.LinearExpr.Sum (jamais `poids * SumArray` ni le builtin sum()) : quand# un poids vaut 0, `0 * IntVar` produit un IntConstant(0) (objet) qu'on ne peut pas# additionner a une SumArray (__radd__ attend un float). Sum() accepte une liste# melangeant IntAffine et IntConstant sans probleme. wc, wco2 =int(round(w_cost)), int(round(w_co2)) objective_terms = []for h inrange(H):for i, plant inenumerate(plants): objective_terms.append(wc *int(round(plant.cost_per_mw)) * p[(h, i)]) objective_terms.append(wco2 *int(round(plant.co2_per_mw)) * p[(h, i)]) model.Minimize(cp_model.LinearExpr.Sum(objective_terms)) solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds =10.0 status = solver.Solve(model)if status notin (cp_model.OPTIMAL, cp_model.FEASIBLE):returnNone result = {}for h inrange(H): result[h] = {plants[i].name: solver.Value(p[(h, i)]) for i inrange(len(plants))} result['_cost'] =round(solver.ObjectiveValue(), 1) result['_status'] = solver.StatusName(status)return resultdispatch = solve_dispatch_cp(net)if dispatch:print('Dispatch CP-SAT (min cout) - statut:', dispatch['_status'])for h inrange(len(net.demand_mw)): prod =', '.join(f'{k}={v}MW'for k, v in dispatch[h].items() if v >0)print(f' H{h}: {prod}')print(f' Cout objectif: {dispatch["_cost"]}')else:print('Aucune solution faisable (capacite < demande)')
Interprétation : le dispatch min-coût et ses limites
Le statut OPTIMAL confirme que CP-SAT a trouvé le dispatch de coût minimal satisfaisant toutes les contraintes (équilibre offre/demande, capacités min/max). Lecture du résultat :
Le charbon porte l’essentiel de la charge (270–340 MW) et s’ajuste heure par heure pour suivre la demande ; l’hydroélectricité reste plate à 150 MW, vraisemblablement son plafond de capacité (pmax).
Le coût optimal de 63 600 € est le plancher économique : c’est la stratégie « Économique » que l’on retrouvera dans la comparaison finale (Partie 4).
Limite de cette stratégie : minimiser le coût seul pousse vers le charbon (le moins cher par MWh), mais ce sont aussi les émissions de CO2 les plus élevées. C’est précisément ce déséquilibre que les Parties 2 (risque) et 3 (multi-objectif) viennent corriger en intégrant d’autres axes que le coût.
Exercice 1 : Reserve tournante (securite n-1)
Ajoutez au modèle CP-SAT une contrainte de reserve tournante : a chaque heure, la somme des capacites disponibles (non utilisees) des centrales activees doit couvrir la perte de la plus grande centrale en service (critere n-1).
Indice : pour chaque heure, la reserve = sum(pmax_i - prod_i pour i active) doit etre >= max(pmax_i pour i active).
# Exercice 1 : contrainte de reserve tournante n-1def solve_dispatch_with_spinning_reserve(network: PowerNetwork):"""Dispatch CP-SAT avec reserve tournante n-1.""" result =None# TODO etudiant : etendre solve_dispatch_cp avec la reserve n-1return result
Partie 2 : Le Previsionniste Probabiliste (modèle bayesien)
Théorie : la production renouvelable est aleatoire. On modelise l’ecart demande - renouvelable comme une variable aleatoire et on infere sa distribution, pour quantifier le risque de defaillance (probabilite que la demande nette depasse la capacite pilotable disponible).
# Partie 2 : modele bayesien de l'incertitude renouvelablefrom math import sqrt, erfcdef failure_risk(network, hour):"""Probabilite que la demande nette reelle depasse la capacite pilotable. Modele : la production renouvelable reelle ~ N(mean, std). La demande nette reelle = demande - renouvelable_reelle ~ N(net_demand, renewable_std). Risque = P(demande nette reelle > capacite pilotable totale), calcule via la survivor function d'une gaussienne : SF(z) = 0.5 * erfc(z / sqrt(2)). """ mean_net = network.net_demand(hour) std = network.renewable_forecast_std[hour] capacity = network.total_capacity()if std <=0:return1.0if mean_net > capacity else0.0 z = (capacity - mean_net) / stdreturn0.5* erfc(z / sqrt(2))print('Risque de defaillance (capacite pilotable =', net.total_capacity(), 'MW) :')for h inrange(len(net.demand_mw)): r = failure_risk(net, h)print(f' H{h}: demande nette moyenne {net.net_demand(h):.0f} MW, 'f'incertitude +/-{net.renewable_forecast_std[h]} MW -> risque {r:.2e}')
Interprétation : un risque de queue négligeable, piloté par l’incertitude
Les risques de défaillance sont astronomiquement faibles (de l’ordre de 10⁻³⁵ à 10⁻²⁸⁴) : avec 800 MW de capacité pilotable pour une demande nette de 420–490 MW, la marge est si large que la probabilité d’un dépassement (queue de distribution gaussienne via erfc) est numériquement négligeable.
Le risque ne suit pas le niveau de demande mais l’incertitude de la prévision renouvelable : H5 (demande 430 MW, incertitude ±30 MW) affiche le risque le plus élevé (≈ 3·10⁻³⁵), très au-dessus de H0 (demande 420 MW, ±15 MW, ≈ 7·10⁻¹⁴²) — alors que la demande de H0 est plus faible. C’est l’écart-type renewable_forecast_std, pas la demande, qui pilote la probabilité de queue.
Enseignement : tant que la capacité pilotable surpasse largement la demande nette, la fiabilité est quasi gratuite. Le risque ne devient pertinent qu’en cas de marge tendue ou d’incertitude élevée — c’est ce que l’Exercice 2 explore en amplifiant l’incertitude renouvelable.
Exercice 2 : Sensibilite au prior sur la variabilite eolienne
Analysez comment le risque de defaillance (Partie 2) evolue quand on modifie l’incertitude du renouvelable (multipliez renewable_forecast_std par 0.5, 1.0, 2.0). Quelle heure devient la plus risqueuse ? Concluez sur la robustesse du dispatch determine en Partie 1.
# Exercice 2 : analyse de sensibilite au prior d'incertitudedef sensitivity_to_renewable_std(network: PowerNetwork, factors=(0.5, 1.0, 2.0)):"""Retourne le risque max par heure pour chaque facteur d'incertitude.""" result =None# TODO etudiant : boucler sur les facteurs, retourner {factor: max_risk}return result
Partie 3 : L’Optimiseur Multi-Objectif
Théorie : on veut minimiser simultanement le cout economique, les emissions CO2 et le risque de defaillance. Ces objectifs sont conflictuels (le charbon est bon marche mais très emissif). On construit un score pondere S = a*cout + b*co2 + c*risque et on cherche le dispatch minimisant S, en explorant le front de Pareto.
# Partie 3 : score multi-objectif (cout + CO2 + risque)def dispatch_metrics(dispatch, network):"""Calcule cout total, CO2 total et risque max d'un dispatch CP-SAT."""if dispatch isNone:return {'cost': float('inf'), 'co2': float('inf'), 'max_risk': 1.0} cost = co2 =0.0for h inrange(len(network.demand_mw)):for plant in network.plants: prod = dispatch[h].get(plant.name, 0) cost += prod * plant.cost_per_mw co2 += prod * plant.co2_per_mw risks = [failure_risk(network, h) for h inrange(len(network.demand_mw))]return {'cost': cost, 'co2': co2, 'max_risk': max(risks)}def multi_objective_score(dispatch, network, weights=(1.0, 1.0, 1.0), norms=None):"""Score = w_c*cout_norm + w_co2*co2_norm + w_r*risque_norm, normalise par les normes de reference (1.0 = composante a sa norme ; 0 = meilleur).""" m = dispatch_metrics(dispatch, network)if norms isNone: norms = {'cost': m['cost'] or1.0, 'co2': m['co2'] or1.0, 'max_risk': 1.0} wc, wco2, wr = weightsreturn (wc * (m['cost'] /max(norms['cost'], 1e-9))+ wco2 * (m['co2'] /max(norms['co2'], 1e-9))+ wr * (m['max_risk'] /max(norms['max_risk'], 1e-9)))score_min_cost = multi_objective_score(dispatch, net, weights=(1.0, 1.0, 1.0), norms={'cost': 200000, 'co2': 2000000, 'max_risk': 1.0})print(f'Score multi-objectif du dispatch min-cout (composantes normalisees par reference, 0 = meilleur): {score_min_cost:.3f}')
Score multi-objectif du dispatch min-cout (composantes normalisees par reference, 0 = meilleur): 1.137
Interprétation : décomposition du score multi-objectif
Le score 1,137 du dispatch min-coût se décompose, avec les poids (1, 1, 1) et les normes {cost: 200 000, co2: 2 000 000, max_risk: 1,0} :
risque : ≈ 0 (la capacité excède largement la demande, cf. Partie 2).
Le terme CO2 domine (0,82) : optimiser le coût seul produit un dispatch bon marché mais sale. C’est la traduction chiffrée du conflit d’objectifs. Le score multi-objectif rend ce compromis comparable entre stratégies (Partie 4) : un score plus bas indique un meilleur équilibre global coût/CO2/risque.
Exercice 3 : Ajouter un troisieme objectif (equite territoriale)
Ajoutez un troisieme objectif : equite territoriale (eviter qu’une même region subisse toutes les centrales les plus polluantes). Supposons que chaque centrale a un attribut region. Mesurez la concentration regionale de la pollution et ajoutez-la au score.
# Exercice 3 : objectif d'equite territorialedef territorial_equity_score(dispatch, network: PowerNetwork) ->float:"""Mesure de concentration de la pollution par region (0 = equitable).""" score =None# TODO etudiant : mesurer la concentration (ex: variance/entropy regionale des co2)return score
Partie 4 : Decision finale et analyse comparative
Synthese : on compare les stratégies (CP-SAT pur, multi-objectif weighted-sum, front de Pareto) sur les 3 axes cout/CO2/fiabilite + l’equite territoriale. Le tableau comparatif ci-dessous est produit a partir des Parties 1-3 deja executees.
Interprétation : le coût de la transition bas-carbone
Les trois stratégies sont ici scorées avec des poids égaux (1, 1, 1) et des normes tirées des extrêmes (coût et CO2 max). Décomposition des scores :
Économique (1,596) = coût 0,60 + CO2 1,00 + risque 0 : excellent sur le coût, maximal sur le CO2.
Écologique (1,560) = coût 1,00 + CO2 0,56 + risque 0 : inverse, bon sur le CO2, maximal sur le coût.
L’Écologique l’emporte (1,560 < 1,596) bien qu’elle coûte plus cher (106 800 € contre 63 600 €) : la réduction de CO2 (1 638 000 kg émis par l’Économique contre 918 000, soit 720 000 kg évités / −44 %) compense la perte économique. Compromis = Écologique : les deux stratégies convergent vers un dispatch identique (mêmes coût, CO2 et risque). Avec les poids de recherche (1, 3) donnant 3 fois plus de poids au CO2, la somme pondérée sélectionne en fait la solution min-CO2 — le compromis coïncide ici avec l’extrémité écologique du front.
Coût marginal d’évitement : passer du min-coût au min-CO2 coûte +43 200 € (+68 %) pour éviter 720 000 kg de CO2, soit environ 0,06 €/kg de CO2 évité. C’est le type d’arbitrage chiffré qu’un opérateur de réseau doit expliciter avant de décider.
Conclusion
Cette étude de cas illustre la composition ordonnee des paradigmes IA sur un problème metier reel (transition energetique) : on filtre les dispatches impossibles (CP-SAT) avant de modeliser l’incertitude (bayesien) avant d’optimiser (multi-objectif). Inverser l’ordre produit un système soit trop rigide (contraintes ignorent l’aleatoire), soit trop flou (decision sans contraintes physiques).
Etat de la fondation : jumeau numérique operationnel (couche 0) + 4 couches de solveurs implémentees (CP-SAT en Partie 1, bayésien en Partie 2, multi-objectif en Partie 3, analyse comparative en Partie 4 — toutes avec sorties réelles). Les 3 exercices d’extension (Exercices 1 à 3) restent en stub pour les étudiants.