Initial commit

This commit is contained in:
ChuXun
2026-02-16 21:52:26 +08:00
commit 18ce59bec7
334 changed files with 35333 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
"""
Fig 12: Monte Carlo Spaghetti Plot
Shows uncertainty in TTE predictions with random perturbations
"""
import os
import numpy as np
import matplotlib.pyplot as plt
from plot_style import set_oprice_style, save_figure
def simulate_stochastic_trajectory(base_power, duration_hours, n_points,
sigma_P, theta_P, dt, seed_offset=0):
"""
Simulate one stochastic SOC trajectory with OU process for power
Args:
base_power: Base power consumption (W)
duration_hours: Simulation duration
n_points: Number of time points
sigma_P: OU process volatility
theta_P: OU process mean reversion rate
dt: Time step (hours)
seed_offset: Random seed offset
"""
np.random.seed(42 + seed_offset)
# Time axis
t_h = np.linspace(0, duration_hours, n_points)
# Ornstein-Uhlenbeck process for power fluctuation
dW = np.random.normal(0, np.sqrt(dt), n_points)
P_perturbation = np.zeros(n_points)
for i in range(1, n_points):
P_perturbation[i] = P_perturbation[i-1] * (1 - theta_P * dt) + sigma_P * dW[i]
P_total = base_power + P_perturbation
P_total = np.clip(P_total, base_power * 0.5, base_power * 1.5)
# Compute SOC trajectory
Q_full = 2.78 # Ah
V_avg = 3.8 # V
E_total = Q_full * V_avg # Wh
z = np.zeros(n_points)
z[0] = 1.0
for i in range(1, n_points):
# Energy consumed in this step
dE = P_total[i] * dt
dz = -dE / E_total
z[i] = z[i-1] + dz
if z[i] < 0.05:
z[i] = 0.05
break
# Fill remaining with cutoff value
z[z < 0.05] = 0.05
return t_h, z
def make_figure(config):
"""Generate Fig 12: Monte Carlo Spaghetti Plot"""
set_oprice_style()
# Simulation parameters
n_paths = 100
duration_hours = 4.0
n_points = 200
dt = duration_hours / n_points
# Baseline power
base_power = 15.0 # W
# OU process parameters for power uncertainty
sigma_P = 2.0 # Volatility
theta_P = 0.5 # Mean reversion rate
# Create figure
fig, ax = plt.subplots(figsize=(12, 8))
# Store all paths for statistics
all_z = []
all_tte = []
# Simulate and plot paths
for i in range(n_paths):
t_h, z = simulate_stochastic_trajectory(base_power, duration_hours, n_points,
sigma_P, theta_P, dt, seed_offset=i)
# Plot path
if i < 50: # Only plot half to avoid overcrowding
ax.plot(t_h, z * 100, color='gray', alpha=0.15, linewidth=0.8, zorder=1)
all_z.append(z)
# Find TTE (first time z <= 0.05)
cutoff_idx = np.where(z <= 0.05)[0]
if len(cutoff_idx) > 0:
tte = t_h[cutoff_idx[0]]
else:
tte = duration_hours
all_tte.append(tte)
# Compute statistics
all_z = np.array(all_z)
z_mean = np.mean(all_z, axis=0)
z_std = np.std(all_z, axis=0)
z_p5 = np.percentile(all_z, 5, axis=0)
z_p95 = np.percentile(all_z, 95, axis=0)
t_h_ref = np.linspace(0, duration_hours, n_points)
# Plot confidence band
ax.fill_between(t_h_ref, z_p5 * 100, z_p95 * 100,
alpha=0.3, color='lightblue', label='90% Confidence Band', zorder=2)
# Plot mean
ax.plot(t_h_ref, z_mean * 100, 'b-', linewidth=3, label='Mean Trajectory', zorder=3)
# Plot ±1 std
ax.plot(t_h_ref, (z_mean + z_std) * 100, 'b--', linewidth=1.5, alpha=0.6,
label='Mean ± 1σ', zorder=2)
ax.plot(t_h_ref, (z_mean - z_std) * 100, 'b--', linewidth=1.5, alpha=0.6, zorder=2)
# Cutoff line
ax.axhline(5, color='r', linestyle='--', linewidth=1.5, alpha=0.7, label='Cutoff (5%)')
# Labels and styling
ax.set_xlabel('Time (hours)', fontsize=11)
ax.set_ylabel('State of Charge (%)', fontsize=11)
ax.set_title('Monte Carlo Uncertainty Quantification (N=100 paths)',
fontsize=12, fontweight='bold')
ax.set_xlim(0, duration_hours)
ax.set_ylim(0, 105)
ax.grid(True, alpha=0.3)
ax.legend(loc='upper right', framealpha=0.9, fontsize=10)
# Add statistics box
tte_mean = np.mean(all_tte)
tte_std = np.std(all_tte)
tte_p5 = np.percentile(all_tte, 5)
tte_p95 = np.percentile(all_tte, 95)
stats_text = f'TTE Statistics (hours):\\n'
stats_text += f'Mean: {tte_mean:.2f} ± {tte_std:.2f}\\n'
stats_text += f'5th %ile: {tte_p5:.2f}\\n'
stats_text += f'95th %ile: {tte_p95:.2f}\\n'
stats_text += f'Range: [{tte_p5:.2f}, {tte_p95:.2f}]\\n'
stats_text += f'Coefficient of Variation: {tte_std/tte_mean*100:.1f}%'
ax.text(0.02, 0.35, stats_text, transform=ax.transAxes,
fontsize=9, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8),
family='monospace')
# Add annotation explaining OU process
ax.text(0.98, 0.97, 'Uncertainty Model:\\nOrnstein-Uhlenbeck\\nprocess for power\\nfluctuations',
transform=ax.transAxes, fontsize=9,
verticalalignment='top', horizontalalignment='right',
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))
plt.tight_layout()
# Save
figure_dir = config.get('global', {}).get('figure_dir', 'figures')
os.makedirs(figure_dir, exist_ok=True)
output_base = os.path.join(figure_dir, 'Fig12_Monte_Carlo')
save_figure(fig, output_base, dpi=config.get('global', {}).get('dpi', 300))
plt.close()
return {
"output_files": [f"{output_base}.pdf", f"{output_base}.png"],
"computed_metrics": {
"tte_mean_h": float(tte_mean),
"tte_std_h": float(tte_std),
"tte_cv_pct": float(tte_std/tte_mean * 100),
"n_paths": n_paths
},
"validation_flags": {},
"pass": True
}