""" 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 }