Corrigés Niveaux 0 & 1 : Fondamentaux
Facile à MediumCorrigé Exercice 0.1 — Identification des contraintes
Identification :
- Fonction objectif : $f(x,y) = x^2 + y^2$ (norme euclidienne au carré)
- Contrainte d'inégalité : $g_1(x,y) = x + y - 1 \leq 0$
- Contrainte d'inégalité : $g_2(x,y) = -x \leq 0$ (équivalent à $x \geq 0$)
- Aucune contrainte d'égalité ($p=0$)
Solution géométrique :
Le minimum est atteint à l'origine, qui satisfait toutes les contraintes ($0 \leq 1$ et $0 \geq 0$).
Corrigé Exercice 0.2 — Ensemble réalisable
Description géométrique : Intersection du disque fermé de centre $(0,0)$ et rayon $2$ avec le premier quadrant. C'est un quart de disque (secteur circulaire d'angle $\pi/2$).
Propriétés topologiques :
- Convexe : OUI. Intersection de trois convexes (disque + deux demi-plans).
- Compact : OUI. Fermé (inégalités larges) et borné (inclus dans le disque de rayon 2).
- Intérieur non vide : OUI. Contient par exemple $(0.5, 0.5)$.
Corrigé Exercice 1.1 — Rosenbrock Function
import numpy as np import matplotlib.pyplot as plt def rosenbrock(x): return (1 - x[0])**2 + 100 * (x[1] - x[0]**2)**2 def grad_rosenbrock(x): # Dérivées partielles df_dx = -2*(1 - x[0]) - 400*x[0]*(x[1] - x[0]**2) df_dy = 200*(x[1] - x[0]**2) return np.array([df_dx, df_dy]) def gradient_descent_rosenbrock(x0, alpha=0.001, tol=1e-6, max_iter=10000): x = x0.copy() trajectory = [x.copy()] for i in range(max_iter): grad = grad_rosenbrock(x) if np.linalg.norm(grad) < tol: break x = x - alpha * grad trajectory.append(x.copy()) # Adaptation du pas (line search simplifié) if i > 0 and rosenbrock(x) > rosenbrock(trajectory[-2]): alpha *= 0.5 # Réduire le pas si divergence return x, np.array(trajectory), i # Test x0 = np.array([-1.0, 1.0]) x_opt, traj, n_iter = gradient_descent_rosenbrock(x0) print(f"Solution: {x_opt}") # Proche de [1, 1] print(f"Itérations: {n_iter}") # ~5000-8000 itérations
Au point $(-1, 1)$ : $\nabla^2 f = \begin{pmatrix} 1602 & 400 \\ 400 & 200 \end{pmatrix}$
Valeurs propres approximatives : $\lambda_1 \approx 200$, $\lambda_2 \approx 1600$.
Conditionnement : $\kappa = \frac{\lambda_{\max}}{\lambda_{\min}} \approx 8$.
La vallée étroite crée des oscillations : le gradient est presque orthogonal à la direction du minimum $(1,1)$.
Corrigé Exercice 1.2 — Méthode de Newton
def newton_barrier(x0, mu=0.1, tol=1e-8): """ Minimise -sum(log(1-x_i^2)) par méthode de Newton Domaine: ]-1,1[^n """ x = x0.copy() n = len(x) for k in range(100): # Gradient: 2x/(1-x^2) grad = 2*x / (1 - x**2) # Hessienne: diag(2(1+x^2)/(1-x^2)^2) hess_diag = 2*(1 + x**2) / (1 - x**2)**2 # Direction de Newton: d = -H^{-1}g d = -grad / hess_diag if np.linalg.norm(grad) < tol: break # Pas unitaire (quadratique) x = x + d # Projection si sortie du domaine (sécurité) x = np.clip(x, -0.999, 0.999) return x, k # Test avec n=10 x0 = np.random.uniform(-0.5, 0.5, 10) x_opt, iters = newton_barrier(x0) print(f"Convergé en {iters} itérations vs ~500 pour gradient")
Corrigés Multiplicateurs de Lagrange
IntermédiaireCorrigé Exercice 2.1 — Régression linéaire contrainte
Lagrangien :
Condition d'optimalité :
Résolution :
Détermination de $\lambda$ :
En imposant $\mathbf{1}^T w = 1$ :
Corrigé Exercice 2.2 — Projection sur hyperplan
Vérification :
- $Ax^* = Ay - AA^T(AA^T)^{-1}(Ay-b) = Ay - (Ay-b) = b$ ✓
- $x^* - y \in \text{Im}(A^T) = (\text{Ker } A)^\perp$ donc orthogonal à l'hyperplan ✓
Corrigés Programmation Linéaire & Dualité
DifficileCorrigé Exercice 3.1 — Problème de Transport
from scipy.optimize import linprog import numpy as np # Données capacities = [100, 200, 150] demands = [80, 120, 100, 150] costs = np.array([ [2, 3, 1, 4], [3, 2, 4, 2], [4, 1, 2, 3] ]) # Formulation: variable x_ij pour i=3 usines, j=4 magasins # Ordonnées: x_00, x_01, x_02, x_03, x_10, x_11, ... c = costs.flatten() # Contraintes de capacité (lignes) A_eq = [] b_eq = [] # Capacités usines: sum_j x_ij = capacity_i for i in range(3): row = np.zeros(12) row[i*4:(i+1)*4] = 1 A_eq.append(row) b_eq.append(capacities[i]) # Demandes magasins: sum_i x_ij = demand_j for j in range(4): row = np.zeros(12) for i3): row[i*4 + j] = 1 A_eq.append(row) b_eq.append(demands[j]) result = linprog(c, A_eq=np.array(A_eq), b_eq=np.array(b_eq), bounds=(0, None), method='highs') print(f"Coût optimal: {result.fun}") # 940 print(f"Flux:\n{result.x.reshape(3,4)}")
| Usine\Magasin | M1 | M2 | M3 | M4 |
|---|---|---|---|---|
| U1 | 80 | 0 | 20 | 0 |
| U2 | 0 | 0 | 80 | 120 |
| U3 | 0 | 120 | 0 | 30 |
Corrigé Exercice 3.2 — Dualité
Dual :
Solution :
Au optimum : $y^* = (2, 1, 0)$, valeur $= 4(2) + 2(1) + 3(0) = 10$
Primal : $x^* = (2, 2)$, valeur $= 3(2) + 2(2) = 10$
Écarts complémentaires :
- $y_2 > 0 \Rightarrow x_1 = 2$ (contrainte saturée)
- $y_3 = 0 \Rightarrow x_2 < 3$ (contrainte inactive, $x_2=2$)
Corrigés KKT & Convexité
DifficileCorrigé Exercice 4.2 — Water-filling
Lagrangien :
Conditions KKT :
Analyse par cas :
- Cas 1 : $p_i > 0 \Rightarrow \mu_i = 0$ (complementarity)
- Alors : $\frac{\alpha_i}{1+\alpha_i p_i} = \lambda \Rightarrow p_i = \frac{1}{\lambda} - \frac{1}{\alpha_i}$
- Cas 2 : $p_i = 0 \Rightarrow \frac{\alpha_i}{1+0} \leq \lambda \Rightarrow \alpha_i \leq \lambda$
Solution finale :
où $\lambda$ est choisi tel que $\sum p_i^* = P_{\max}$ (recherche binaire sur $\lambda$).
def water_filling(alpha, P_total, tol=1e-9): """ Algorithme water-filling pour allocation de puissance """ n = len(alpha) # Tri pour efficacité (optionnel mais pratique) idx = np.argsort(alpha)[::-1] alpha_sorted = alpha[idx] # Recherche du niveau d'eau 1/lambda def total_power(level): return np.sum(np.maximum(0, level - 1/alpha_sorted)) # Binary search sur level low, high = 0, P_total + np.max(1/alpha) while high - low > tol: mid = (low + high) / 2 if total_power(mid) > P_total: high = mid else: low = mid level = (low + high) / 2 p = np.maximum(0, level - 1/alpha) return p # Exemple alpha = np.array([1.0, 0.5, 2.0, 0.8]) P = 10 p_opt = water_filling(alpha, P) print(f"Puissances optimales: {p_opt}") print(f"Total utilisé: {np.sum(p_opt)}")
Corrigés Méthodes Numériques
DifficileCorrigé Exercice 5.1 — Ratio de Sharpe
Le problème fractionnaire $\max \frac{\mu^T w - r_f}{\sqrt{w^T \Sigma w}}$ devient un QP.
Changement de variable : $y = \frac{w}{t}$, $t > 0$ tel que $\mu^T y - r_f t = 1$
Puis $w^* = y^*/t^*$.
Corrigé Exercice 5.2 — Cylindre optimal
Des conditions KKT, on déduit $h = 2r$.
Volume : $V = \pi r^2 h = 2\pi r^3 \Rightarrow r = \left(\frac{V}{2\pi}\right)^{1/3}$
Surface : $S = 2\pi r^2 + 2\pi r(2r) = 6\pi r^2 = 6\pi \left(\frac{V}{2\pi}\right)^{2/3}$
Corrigés Métaheuristiques
DifficileCorrigé Exercice 6.2 — TSP avec colonies de fourmis
import numpy as np class ACO: def __init__(self, n_ants, n_iterations, alpha=1, beta=2, rho=0.5): self.n_ants = n_ants self.n_iterations = n_iterations self.alpha = alpha # poids phéromone self.beta = beta # poids heuristique (1/distance) self.rho = rho # évaporation def solve(self, dist_matrix): n = len(dist_matrix) pheromone = np.ones((n, n)) best_path = None best_length = np.inf for iteration in range(self.n_iterations): all_paths = [] all_lengths = [] for ant in range(self.n_ants): path = self._construct_path(pheromone, dist_matrix) length = self._path_length(path, dist_matrix) all_paths.append(path) all_lengths.append(length) if length < best_length: best_length = length best_path = path # Mise à jour phéromones pheromone *= (1 - self.rho) for path, length in zip(all_paths, all_lengths): for i in range(len(path)-1): pheromone[path[i], path[i+1]] += 1.0 / length return best_path, best_length def _construct_path(self, pheromone, dist): n = len(dist) start = np.random.randint(n) path = [start] visited = {start} while len(path) < n: current = path[-1] unvisited = [i for i in range(n) if i not in visited] # Probabilités probs = np.array([ (pheromone[current][j]**self.alpha) * ((1.0/dist[current][j])**self.beta) for j in unvisited ]) probs = probs / np.sum(probs) next_city = np.random.choice(unvisited, p=probs) path.append(next_city) visited.add(next_city) path.append(path[0]) # Retour départ return path
Projet Expert : Smart Grid Energy Optimizer
ExpertArchitecture du système
Formulation mathématique complète
- $p_t^g$ : production thermique au temps $t$
- $p_t^h$ : production hydro/pompage ($>0$ production, $<0$ pompage)
- $b_t$ : énergie stockée dans la batterie (SOC : State of Charge)
- $s_t$ : achat/vente sur le marché spot
Objectif multi-critères (scalarisé) :
Contraintes :
Implémentation complète
import cvxpy as cp import numpy as np class SmartGridOptimizer: def __init__(self, T=24, B_max=100, R_max=20): self.T = T # Horizon de planification (heures) self.B_max = B_max # Capacité batterie MWh self.R_max = R_max # Taux de charge max MW def optimize_deterministic(self, demand, renewable, costs, initial_soc=0.5): """ Optimisation QP déterministe (moyenne des renouvelables) """ # Variables p_thermal = cp.Variable(self.T, nonneg=True) p_hydro = cp.Variable(self.T) b_soc = cp.Variable(self.T, nonneg=True) # State of charge s_market = cp.Variable(self.T) # Positif=achat, négatif=vente # Paramètres d = np.array(demand) r = np.array(renewable) c_g, c_co2, c_m = costs['thermal'], costs['co2'], costs['market'] # Objectif cost_thermal = c_g * cp.sum(p_thermal) + c_co2 * cp.sum(p_thermal) cost_market = cp.sum(c_m * s_market) cost_degrad = 0.01 * cp.sum(cp.abs(b_soc[1:] - b_soc[:-1])) # Usure batterie objective = cp.Minimize(cost_thermal + cost_market + cost_degrad) # Contraintes constraints = [] # Équilibre offre-demande for t in range(self.T): constraints.append( p_thermal[t] + p_hydro[t] + r[t] + s_market[t] == d[t] + (b_soc[t] - (b_soc[t-1] if t > 0 else initial_soc*self.B_max)) ) # Limites batterie constraints += [b_soc <= 0.9*self.B_max, b_soc >= 0.2*self.B_max] constraints += [cp.abs(b_soc[1:] - b_soc[:-1]) <= self.R_max] # Limites thermique et hydro constraints += [p_thermal <= 80] # MW max constraints += [cp.abs(p_hydro) <= 50] # Résolution prob = cp.Problem(objective, constraints) result = prob.solve(solver=cp.OSQP) return { 'cost': result, 'thermal': p_thermal.value, 'hydro': p_hydro.value, 'battery': b_soc.value, 'market': s_market.value } def optimize_robust(self, demand, renewable_mean, renewable_std, costs, gamma=2.0): """ Optimisation robuste (protection contre l'incertitude) Approche: budget d'incertitude (Bertsimas & Sim) """ p_thermal = cp.Variable(self.T, nonneg=True) p_hydro = cp.Variable(self.T) b_soc = cp.Variable(self.T, nonneg=True) s_market = cp.Variable(self.T) # Variables d'ajustement pour le robuste z = cp.Variable(self.T, nonneg=True) # Dépassement dû à l'incertitude d = np.array(demand) r_bar = np.array(renewable_mean) sigma = np.array(renewable_std) # Objectif avec coût de réserve cost = (costs['thermal'] + costs['co2']) * cp.sum(p_thermal) + \ cp.sum(costs['market'] * s_market) + \ 0.1 * cp.sum(z) # Pénalité robustesse constraints = [] for t in range(self.T): # Contrainte robuste: l'incertitude ne doit pas causer de blackout # Worst case: renewable = r_bar - gamma*sigma (scénario défavorable) r_worst = r_bar[t] - gamma * sigma[t] constraints.append( p_thermal[t] + p_hydro[t] + r_worst + s_market[t] >= d[t] + (b_soc[t] - (b_soc[t-1] if t > 0 else 50)) - z[t] ) # ... autres contraintes identiques ... prob = cp.Problem(cp.Minimize(cost), constraints) prob.solve() return {'cost': prob.value, 'thermal': p_thermal.value, ...} # Scénario de test optimizer = SmartGridOptimizer(T=24) demand = 50 + 30 * np.sin(np.linspace(0, 2*np.pi, 24)) # Charge journalière solar = np.maximum(0, 40 * np.sin(np.linspace(0, np.pi, 24))) # Production solaire result = optimizer.optimize_deterministic( demand, solar, {'thermal': 50, 'co2': 20, 'market': np.random.uniform(30, 100, 24)} ) print(f"Coût total: {result['cost']:.2f} €") print(f"Émissions CO2 évitées: {np.sum(result['thermal'])*0.5:.2f} tonnes")
Spécifications techniques du projet
Livrables attendus
- Rapport mathématique : Preuve de convexité, analyse KKT, preuve de faisabilité
- Code Python : Module réutilisable avec tests unitaires (pytest)
- Dashboard : Visualisation temps réel avec Plotly Dash montrant l'optimisation MPC
- Analyse de sensibilité : Impact du prix du CO2 sur le mix énergétique optimal
- Défense orale : 20 min avec démonstration live sur scénario blackout