131 lines
5.0 KiB
Python
131 lines
5.0 KiB
Python
"""
|
|
Fig 14: Lifecycle Degradation (Aging)
|
|
Shows SOH and TTE evolution over multiple charge cycles
|
|
"""
|
|
|
|
import os
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
from plot_style import set_oprice_style, save_figure
|
|
|
|
def make_figure(config):
|
|
"""Generate Fig 14: Lifecycle Degradation"""
|
|
|
|
set_oprice_style()
|
|
|
|
# Aging parameters
|
|
lambda_fade = config.get('battery_params', {}).get('lambda_fade', 0.0001)
|
|
n_cycles = 1000
|
|
|
|
# Cycle numbers
|
|
cycles = np.arange(0, n_cycles + 1)
|
|
|
|
# SOH degradation (exponential decay with square root of cycles term)
|
|
# More realistic aging: fast initial drop, then slower
|
|
SOH = 1.0 - lambda_fade * cycles - 0.00005 * np.sqrt(cycles)
|
|
SOH = np.clip(SOH, 0.7, 1.0) # SOH doesn't go below 70%
|
|
|
|
# TTE degradation (proportional to SOH but with additional resistance increase)
|
|
# As battery ages, internal resistance increases, reducing effective capacity
|
|
TTE_fresh = 2.5 # hours at SOH=1.0
|
|
resistance_factor = 1.0 + 0.5 * (1 - SOH) # Resistance increases as SOH decreases
|
|
TTE = TTE_fresh * SOH / resistance_factor
|
|
|
|
# Create figure with dual y-axis
|
|
fig, ax1 = plt.subplots(figsize=(12, 8))
|
|
|
|
# Plot SOH on left axis
|
|
color1 = '#1f77b4'
|
|
ax1.set_xlabel('Charge-Discharge Cycles', fontsize=11)
|
|
ax1.set_ylabel('State of Health (SOH)', fontsize=11, color=color1)
|
|
line1 = ax1.plot(cycles, SOH * 100, color=color1, linewidth=2.5,
|
|
label='SOH', marker='o', markevery=100, markersize=6)
|
|
ax1.tick_params(axis='y', labelcolor=color1)
|
|
ax1.set_ylim(65, 105)
|
|
ax1.grid(True, alpha=0.3, axis='x')
|
|
|
|
# Mark critical SOH thresholds
|
|
ax1.axhline(80, color='orange', linestyle='--', linewidth=1.5, alpha=0.6,
|
|
label='80% SOH (End of Life)')
|
|
|
|
# Second y-axis for TTE
|
|
ax2 = ax1.twinx()
|
|
color2 = '#d62728'
|
|
ax2.set_ylabel('Time to Empty (hours)', fontsize=11, color=color2)
|
|
line2 = ax2.plot(cycles, TTE, color=color2, linewidth=2.5,
|
|
label='TTE at Full Charge', marker='s', markevery=100, markersize=6)
|
|
ax2.tick_params(axis='y', labelcolor=color2)
|
|
ax2.set_ylim(1.0, 2.8)
|
|
|
|
# Title
|
|
ax1.set_title('Battery Lifecycle Degradation: SOH and TTE Evolution',
|
|
fontsize=12, fontweight='bold')
|
|
|
|
# Combined legend
|
|
lines = line1 + line2
|
|
labels = [l.get_label() for l in lines]
|
|
ax1.legend(lines, labels, loc='upper right', framealpha=0.9, fontsize=10)
|
|
|
|
# Find cycle at 80% SOH
|
|
idx_80 = np.where(SOH <= 0.80)[0]
|
|
if len(idx_80) > 0:
|
|
cycle_80 = cycles[idx_80[0]]
|
|
soh_80 = SOH[idx_80[0]]
|
|
tte_80 = TTE[idx_80[0]]
|
|
|
|
ax1.axvline(cycle_80, color='gray', linestyle=':', linewidth=1.5, alpha=0.5)
|
|
ax1.annotate(f'End of Life\\nCycle {cycle_80}\\nSOH={soh_80*100:.1f}%\\nTTE={tte_80:.2f}h',
|
|
xy=(cycle_80, 80), xytext=(cycle_80 + 150, 85),
|
|
arrowprops=dict(arrowstyle='->', color='darkred', lw=1.5),
|
|
fontsize=9, color='darkred',
|
|
bbox=dict(boxstyle='round', facecolor='mistyrose', alpha=0.8))
|
|
|
|
# Add aging mechanism annotation
|
|
mechanism_text = 'Aging Mechanisms:\\n' + \
|
|
'• Capacity fade\\n' + \
|
|
'• Resistance increase\\n' + \
|
|
'• Accelerated at extremes\\n' + \
|
|
f' (λ_fade = {lambda_fade:.6f})'
|
|
|
|
ax1.text(0.02, 0.30, mechanism_text, transform=ax1.transAxes,
|
|
fontsize=9, verticalalignment='top',
|
|
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
|
|
|
|
# Add degradation statistics
|
|
initial_tte = TTE[0]
|
|
final_tte = TTE[-1]
|
|
tte_loss = initial_tte - final_tte
|
|
tte_loss_pct = tte_loss / initial_tte * 100
|
|
|
|
stats_text = f'Performance Metrics:\\n'
|
|
stats_text += f'Initial TTE: {initial_tte:.2f}h\\n'
|
|
stats_text += f'After {n_cycles} cycles: {final_tte:.2f}h\\n'
|
|
stats_text += f'Total loss: {tte_loss:.2f}h ({tte_loss_pct:.1f}%)\\n'
|
|
stats_text += f'Avg loss per 100 cycles: {tte_loss/(n_cycles/100):.3f}h'
|
|
|
|
ax1.text(0.98, 0.97, stats_text, transform=ax1.transAxes,
|
|
fontsize=9, verticalalignment='top', horizontalalignment='right',
|
|
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8),
|
|
family='monospace')
|
|
|
|
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, 'Fig14_Lifecycle_Degradation')
|
|
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": {
|
|
"initial_tte_h": float(initial_tte),
|
|
"final_tte_h": float(final_tte),
|
|
"tte_loss_pct": float(tte_loss_pct),
|
|
"eol_cycle": int(cycle_80) if len(idx_80) > 0 else n_cycles
|
|
},
|
|
"validation_flags": {},
|
|
"pass": True
|
|
}
|