Initial commit
This commit is contained in:
130
A题/ZJ_v2/fig14_lifecycle_degradation.py
Normal file
130
A题/ZJ_v2/fig14_lifecycle_degradation.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user