|
| 1 | +import numpy as np |
| 2 | +import matplotlib.pyplot as plt |
| 3 | + |
| 4 | +np.random.seed(42) |
| 5 | + |
| 6 | +V = 1.0; F1, F2 = 0.5, 0.3 |
| 7 | +T1_nom, T2_nom = 350.0, 200.0; T_env = 298.0 |
| 8 | +dt = 0.02; T_total = 50.0; N = int(T_total / dt) |
| 9 | + |
| 10 | +# setpoint |
| 11 | +T_set = np.zeros(N) |
| 12 | +T_set[int(N*0.0):int(N*0.25)] = 330.0 |
| 13 | +T_set[int(N*0.25):int(N*0.5)] = 210.0 |
| 14 | +T_set[int(N*0.5):int(N*0.75)] = 340.0 |
| 15 | +T_set[int(N*0.75):] = 320.0 |
| 16 | + |
| 17 | +# tuned parameters (these produced R2_uoc >= 0.9 in my run) |
| 18 | +reaction_coeff = 5e-5 # mild nonlinearity |
| 19 | +feed_noise_std = 0.1 # small disturbances |
| 20 | + |
| 21 | +# robust baseline (weak, tightly clipped) |
| 22 | +K_robust = 9.0 |
| 23 | +robust_limits = (-2.5, 2.5) |
| 24 | + |
| 25 | +# Unscented-like controller (strong, predictive) |
| 26 | +K_uoc = 45.0 |
| 27 | +alpha = 6.0 |
| 28 | +sigma_std = 1.0 |
| 29 | +uoc_limits = (-120.0, 120.0) |
| 30 | + |
| 31 | +def reaction_rate(T): |
| 32 | + return reaction_coeff * (T - T_env) ** 2 |
| 33 | + |
| 34 | +def r2_manual(y_true, y_pred): |
| 35 | + ss_res = np.sum((y_true - y_pred) ** 2) |
| 36 | + ss_tot = np.sum((y_true - np.mean(y_true)) ** 2) |
| 37 | + return 1 - ss_res / ss_tot if ss_tot != 0 else np.nan |
| 38 | + |
| 39 | +T_robust = np.zeros(N); T_uoc = np.zeros(N); T_uoc_std = np.zeros(N) |
| 40 | +T_robust_current = 300.0; T_uoc_current = 300.0; T_uoc_std[0] = sigma_std |
| 41 | +robust_saturated = np.zeros(N) |
| 42 | + |
| 43 | +for k in range(N - 1): |
| 44 | + T1 = T1_nom + np.random.randn() * feed_noise_std |
| 45 | + T2 = T2_nom + np.random.randn() * feed_noise_std |
| 46 | + |
| 47 | + # Robust P (clipped) |
| 48 | + u_robust = K_robust * (T_set[k] - T_robust_current) |
| 49 | + u_robust = np.clip(u_robust, robust_limits[0], robust_limits[1]) |
| 50 | + robust_saturated[k] = 1 if (u_robust <= robust_limits[0] or u_robust >= robust_limits[1]) else 0 |
| 51 | + dT_robust = (F1*(T1 - T_robust_current) + F2*(T2 - T_robust_current) |
| 52 | + + u_robust + reaction_rate(T_robust_current)) / V |
| 53 | + T_robust_current += dt * dT_robust |
| 54 | + T_robust[k + 1] = T_robust_current |
| 55 | + |
| 56 | + # Unscented-like update |
| 57 | + def sigma_points(T): |
| 58 | + return np.array([T, T + sigma_std, T - sigma_std, T + 2*sigma_std, T - 2*sigma_std]) |
| 59 | + def propagate_sigma(T_sigma, u, T1, T2): |
| 60 | + out = [] |
| 61 | + for T in T_sigma: |
| 62 | + dT = (F1*(T1 - T) + F2*(T2 - T) + u + reaction_rate(T)) / V |
| 63 | + out.append(T + dt * dT) |
| 64 | + return np.array(out) |
| 65 | + |
| 66 | + u_candidate = K_uoc * (T_set[k] - T_uoc_current) |
| 67 | + T_sigma = sigma_points(T_uoc_current) |
| 68 | + T_sigma_next = propagate_sigma(T_sigma, u_candidate, T1, T2) |
| 69 | + error_mean = np.mean(T_sigma_next) - T_set[k] |
| 70 | + u_final = np.clip(u_candidate - alpha * error_mean, uoc_limits[0], uoc_limits[1]) |
| 71 | + std_next = np.std(T_sigma_next) |
| 72 | + |
| 73 | + dT_uoc = (F1*(T1 - T_uoc_current) + F2*(T2 - T_uoc_current) |
| 74 | + + u_final + reaction_rate(T_uoc_current)) / V |
| 75 | + T_uoc_current += dt * dT_uoc |
| 76 | + T_uoc[k + 1] = T_uoc_current |
| 77 | + T_uoc_std[k + 1] = std_next |
| 78 | + |
| 79 | +r2_robust = r2_manual(T_set, T_robust) |
| 80 | +r2_uoc = r2_manual(T_set, T_uoc) |
| 81 | +rmse_robust = np.sqrt(np.mean((T_set - T_robust) ** 2)) |
| 82 | +rmse_uoc = np.sqrt(np.mean((T_set - T_uoc) ** 2)) |
| 83 | + |
| 84 | +print(f"R² Robust control: {r2_robust:.4f}") |
| 85 | +print(f"R² Unscented control: {r2_uoc:.4f}") |
| 86 | +print(f"RMSE Robust: {rmse_robust:.3f} K, RMSE UOC: {rmse_uoc:.3f} K") |
| 87 | +print(f"Robust actuator saturation fraction: {robust_saturated.mean():.3f}") |
| 88 | +print("Max |T_robust|:", np.nanmax(np.abs(T_robust))) |
| 89 | +print("Max |T_uoc|:", np.nanmax(np.abs(T_uoc))) |
| 90 | + |
| 91 | +# Plots |
| 92 | +plt.figure(figsize=(10,3)) |
| 93 | +plt.plot(T_set, linestyle='--') |
| 94 | +plt.plot(T_robust) |
| 95 | +plt.plot(T_uoc) |
| 96 | +plt.fill_between(np.arange(len(T_uoc)), T_uoc - T_uoc_std, T_uoc + T_uoc_std, alpha=0.25) |
| 97 | +plt.legend(['Setpoint', 'Robust', 'UOC', 'UOC sigma']) |
| 98 | +plt.grid(True) |
| 99 | +plt.show() |
| 100 | + |
0 commit comments