100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
"""
|
|
Figure: Battery SOC Trajectory under Different Usage Scenarios
|
|
MCM/ICM 2026 - Problem A
|
|
"""
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
|
|
# === MCM O-Award Style Configuration ===
|
|
plt.rcParams.update({
|
|
'font.family': 'serif',
|
|
'font.serif': ['Times New Roman'],
|
|
'mathtext.fontset': 'stix',
|
|
'axes.labelsize': 12,
|
|
'axes.titlesize': 14,
|
|
'xtick.labelsize': 11,
|
|
'ytick.labelsize': 11,
|
|
'legend.fontsize': 9,
|
|
'axes.linewidth': 1.0,
|
|
'axes.unicode_minus': False,
|
|
'figure.dpi': 300,
|
|
})
|
|
|
|
# === Data Configuration ===
|
|
scenarios = ['Gaming', 'Navigation', 'Movie', 'Chatting', 'Screen Off']
|
|
power_values = [3.551, 2.954, 2.235, 1.481, 0.517] # W
|
|
|
|
# Time-to-empty from different starting SOC [100%, 75%, 50%, 25%]
|
|
data_matrix = np.array([
|
|
[4.11, 3.05, 2.01, 0.97], # Gaming
|
|
[5.01, 3.72, 2.45, 1.18], # Navigation
|
|
[6.63, 4.92, 3.24, 1.56], # Movie
|
|
[10.02, 7.43, 4.89, 2.36], # Chatting
|
|
[29.45, 21.85, 14.39, 6.95] # Screen Off
|
|
])
|
|
start_soc = [100, 75, 50, 25]
|
|
z_min = 0.02 # SOC threshold (2%)
|
|
|
|
# Professional color palette (colorblind-friendly, Nature-style)
|
|
colors = ['#E64B35', '#4DBBD5', '#00A087', '#3C5488', '#F39B7F']
|
|
markers = ['o', 's', '^', 'D', 'v']
|
|
|
|
# === Figure Setup ===
|
|
fig, ax = plt.subplots(figsize=(8, 5))
|
|
|
|
# Mixed power-law model parameters
|
|
n1, n2, w1, w2 = 1.2, 5.0, 0.6, 0.4
|
|
|
|
for i, scenario in enumerate(scenarios):
|
|
t_end = data_matrix[i, 0] # Time from 100% to z_min
|
|
color = colors[i]
|
|
marker = markers[i]
|
|
|
|
# Mixed power-law SOC decay model
|
|
t = np.linspace(0, t_end, 200)
|
|
tau = t / t_end
|
|
shape_func = w1 * (tau ** n1) + w2 * (tau ** n2)
|
|
z = 100 - 98 * shape_func # 100% -> 2%
|
|
|
|
# Plot trajectory
|
|
label = f'{scenario} ({power_values[i]:.3f} W)'
|
|
ax.plot(t, z, color=color, linewidth=1.8, label=label, zorder=3)
|
|
|
|
# Add markers along curve
|
|
mark_indices = np.linspace(0, 199, 8, dtype=int)
|
|
ax.scatter(t[mark_indices], z[mark_indices], color=color, marker=marker,
|
|
s=35, edgecolors='white', linewidths=0.5, zorder=4)
|
|
|
|
# Threshold line
|
|
ax.axhline(y=z_min*100, color='#B71C1C', linestyle='--', linewidth=1.5,
|
|
label=f'Cutoff Threshold ({z_min*100:.0f}%)', zorder=2)
|
|
|
|
# === Axis Configuration ===
|
|
ax.set_xlabel('Time $t$ (hours)', fontweight='bold')
|
|
ax.set_ylabel('State of Charge $z(t)$ (%)', fontweight='bold')
|
|
ax.set_xlim(0, 32)
|
|
ax.set_ylim(0, 105)
|
|
ax.set_xticks(np.arange(0, 35, 5))
|
|
ax.set_yticks(np.arange(0, 120, 20))
|
|
|
|
# Grid styling
|
|
ax.grid(True, linestyle='-', alpha=0.3, linewidth=0.5, color='gray')
|
|
ax.set_axisbelow(True)
|
|
|
|
# Legend (outside plot area for clarity)
|
|
ax.legend(loc='upper right', frameon=True, fancybox=False,
|
|
edgecolor='black', framealpha=0.95, ncol=1)
|
|
|
|
# Minor ticks
|
|
ax.minorticks_on()
|
|
ax.tick_params(which='minor', length=2, width=0.5)
|
|
ax.tick_params(which='major', length=4, width=1.0)
|
|
|
|
# === Output ===
|
|
plt.tight_layout()
|
|
plt.savefig('combined_soc_trajectory.png', dpi=300, bbox_inches='tight',
|
|
facecolor='white', edgecolor='none')
|
|
plt.savefig('combined_soc_trajectory.pdf', bbox_inches='tight',
|
|
facecolor='white', edgecolor='none')
|
|
print("Figure saved: combined_soc_trajectory.png / .pdf")
|