Files
MCM/A题/AAA常用/最终内容/fig_generation_p3.py
2026-02-16 21:52:26 +08:00

698 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Problem 3: Sensitivity Analysis and Assumption Testing Figures
All data sourced from 整合输出.md (frozen model specification)
"""
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from math import pi
import os
# ==========================================
# Style Configuration
# ==========================================
plt.rcParams['font.family'] = 'Arial'
plt.rcParams['font.size'] = 11
plt.rcParams['axes.labelsize'] = 12
plt.rcParams['axes.titlesize'] = 13
plt.rcParams['figure.dpi'] = 150
colors = ['#2ecc71', '#3498db', '#9b59b6', '#e74c3c', '#f39c12', '#1abc9c', '#34495e']
os.makedirs('figures', exist_ok=True)
def save_fig(name):
plt.tight_layout()
plt.savefig(f'figures/{name}.png', dpi=300, bbox_inches='tight')
plt.close()
print(f"Saved {name}.png")
# ==========================================
# Data from 整合输出.md (FROZEN)
# ==========================================
# SOBOL_TABLE_v1
SOBOL_TABLE = {
'k_L': {'S_i': 0.412, 'ST_i': 0.445},
'k_C': {'S_i': 0.285, 'ST_i': 0.312},
'kappa': {'S_i': 0.164, 'ST_i': 0.198},
'k_N': {'S_i': 0.042, 'ST_i': 0.065},
'R_ref': {'S_i': 0.021, 'ST_i': 0.048},
'alpha_Q': {'S_i': 0.011, 'ST_i': 0.032},
}
# COMPUTE_LOG_v1
SOBOL_COMPUTE = {
'N_base': 512,
'D': 6,
'N_evals_total': 4096,
'seed': 20260201,
'sampling_scheme': 'Saltelli',
}
# SCENARIO_TTE_TABLE_v1
SCENARIO_TABLE = {
'S0': {'desc': 'Baseline', 'TTE': 4.60, 'delta': 0.00, 'reason': 'SOC_ZERO'},
'S1': {'desc': 'Brightness Reduced (0.5x)', 'TTE': 5.82, 'delta': 1.22, 'reason': 'SOC_ZERO'},
'S2': {'desc': 'CPU Reduced (0.5x)', 'TTE': 5.45, 'delta': 0.85, 'reason': 'SOC_ZERO'},
'S3': {'desc': 'Network Reduced (0.5x)', 'TTE': 4.92, 'delta': 0.32, 'reason': 'SOC_ZERO'},
'S4': {'desc': 'Poor Signal (Psi=0.2)', 'TTE': 2.78, 'delta': -1.82, 'reason': 'SOC_ZERO'},
'S5': {'desc': 'Cold Ambient (0C)', 'TTE': 3.15, 'delta': -1.45, 'reason': 'V_CUTOFF'},
'S6': {'desc': 'Hot Ambient (40C)', 'TTE': 4.98, 'delta': 0.38, 'reason': 'SOC_ZERO'},
'S7': {'desc': 'Background Cut (0.5x)', 'TTE': 4.74, 'delta': 0.14, 'reason': 'SOC_ZERO'},
}
# MECH_SIGNATURES_v1
MECH_SIGNATURES = {
'S0': {'avg_P': 3.22, 'max_I': 1.54, 'min_Delta': 8.15, 'avg_R0': 0.108, 'avg_Qeff': 4.00},
'S4': {'avg_P': 5.32, 'max_I': 2.45, 'min_Delta': 3.82, 'avg_R0': 0.112, 'avg_Qeff': 4.00},
'S5': {'avg_P': 3.28, 'max_I': 1.92, 'min_Delta': 0.85, 'avg_R0': 0.235, 'avg_Qeff': 3.52},
}
# UQ_SUMMARY_v1
UQ_SUMMARY = {
'mean': 4.6021,
'std': 0.0542,
'p10': 4.5314,
'p50': 4.6018,
'p90': 4.6725,
'CI95_low': 4.5959,
'CI95_high': 4.6083,
}
# REPRODUCIBILITY_v1
REPRODUCIBILITY = {
'seed': 20260201,
'M': 300,
'theta': 1/600,
'sigma': 0.02,
'dt': 1.0,
}
# Assumption Robustness Data (from document analysis)
ASSUMPTION_ROBUSTNESS = {
'Baseline': {'TTE': 4.60, 'delta': 0.0, 'delta_pct': 0.0},
'OCV Linear→Poly': {'TTE': 4.68, 'delta': 0.08, 'delta_pct': 1.7},
'CPL→CC': {'TTE': 5.12, 'delta': 0.52, 'delta_pct': 11.3},
'Lumped→FEM': {'TTE': 4.48, 'delta': -0.12, 'delta_pct': -2.6},
'Signal Exp→Lin': {'TTE': 5.49, 'delta': 0.89, 'delta_pct': 19.3},
'OU θ Change': {'TTE': 4.63, 'delta': 0.03, 'delta_pct': 0.7},
}
# Physical Decoupling Data
DECOUPLING_DATA = {
'Full Model (All Coupling)': {'TTE': 4.60, 'delta_pct': 0.0},
'No Thermal (dT/dt=0)': {'TTE': 4.85, 'delta_pct': 5.4},
'No CPL (I=const)': {'TTE': 5.12, 'delta_pct': 11.3},
'No Signal (Psi=0.9)': {'TTE': 6.42, 'delta_pct': 39.6},
'No R(T) (R0=const)': {'TTE': 4.73, 'delta_pct': 2.8},
'No Coupling (Linear)': {'TTE': 7.21, 'delta_pct': 56.7},
}
# Extreme Scenario Data
EXTREME_SCENARIOS = {
'E0: Baseline': {'TTE': 4.60, 'delta_pct': 0, 'confidence': 5},
'E1: Arctic (-10°C)': {'TTE': 1.68, 'delta_pct': -63.5, 'confidence': 3},
'E2: Basement (Weak Signal)': {'TTE': 1.85, 'delta_pct': -59.8, 'confidence': 4},
'E3: Desert (50°C)': {'TTE': 3.78, 'delta_pct': -17.8, 'confidence': 3},
'E4: Perfect Storm': {'TTE': 0.92, 'delta_pct': -80.0, 'confidence': 2},
'E5: Aged Battery (SOH=70%)': {'TTE': 3.22, 'delta_pct': -30.0, 'confidence': 4},
'E6: Gaming Max': {'TTE': 1.54, 'delta_pct': -66.5, 'confidence': 3},
}
# Usage Fluctuation Data (from OU process analysis)
FLUCTUATION_DATA = {
'Low (σ=0.01)': {'mean': 4.60, 'std': 0.027, 'p10': 4.57, 'p90': 4.63, 'cv': 0.59},
'Baseline (σ=0.02)': {'mean': 4.60, 'std': 0.054, 'p10': 4.53, 'p90': 4.67, 'cv': 1.18},
'High (σ=0.04)': {'mean': 4.60, 'std': 0.108, 'p10': 4.46, 'p90': 4.74, 'cv': 2.35},
'Extreme (σ=0.08)': {'mean': 4.60, 'std': 0.215, 'p10': 4.32, 'p90': 4.88, 'cv': 4.67},
}
# Load Model Comparison (CPL vs CC vs CR)
LOAD_MODEL_COMPARISON = {
'CPL (Ours)': {'TTE': 4.60, 'end_current': 1.01, 'current_increase': 46},
'CC (Constant Current)': {'TTE': 5.12, 'end_current': 0.69, 'current_increase': 0},
'CR (Constant Resistance)': {'TTE': 5.38, 'end_current': 0.59, 'current_increase': -14},
}
# Signal Mapping Validation Data
SIGNAL_MAPPING = {
0.9: {'measured': 0.78, 'exp_model': 0.80, 'lin_model': 0.82},
0.5: {'measured': 2.15, 'exp_model': 2.18, 'lin_model': 1.52},
0.2: {'measured': 5.32, 'exp_model': 5.28, 'lin_model': 2.22},
0.1: {'measured': 8.15, 'exp_model': 8.21, 'lin_model': 2.92},
}
# Amplification Factor by SOC
AMPLIFICATION_BY_SOC = {
'1.0-0.7 (Early)': 1.8,
'0.7-0.4 (Mid)': 2.7,
'0.4-0.2 (Late)': 3.5,
'0.2-0 (Critical)': 4.2,
}
# ==========================================
# Fig 14: Sobol Sensitivity Indices
# ==========================================
def plot_fig14():
"""
Sobol sensitivity indices from SOBOL_TABLE_v1
Shows first-order (S_i) and total-order (ST_i) indices
"""
params = list(SOBOL_TABLE.keys())
param_labels = [r'$k_L$ (Screen)', r'$k_C$ (CPU)', r'$\kappa$ (Signal)',
r'$k_N$ (Network)', r'$R_{ref}$ (Resistance)', r'$\alpha_Q$ (Capacity)']
S1 = [SOBOL_TABLE[p]['S_i'] for p in params]
ST = [SOBOL_TABLE[p]['ST_i'] for p in params]
interaction = [st - s for st, s in zip(ST, S1)]
fig, ax = plt.subplots(figsize=(11, 6))
x = np.arange(len(params))
width = 0.35
bars1 = ax.bar(x - width/2, S1, width, label=r'First-order $S_i$', color=colors[1], alpha=0.8, edgecolor='black')
bars2 = ax.bar(x + width/2, ST, width, label=r'Total-order $ST_i$', color=colors[4], alpha=0.8, edgecolor='black')
# Add interaction values as text
for i, (xi, sti, inter) in enumerate(zip(x, ST, interaction)):
ax.annotate(f'Int={inter:.3f}', xy=(xi + width/2, sti + 0.01),
ha='center', fontsize=9, color='gray')
ax.set_ylabel('Sobol Index')
ax.set_title(f'Global Sensitivity Analysis: Sobol Indices (N={SOBOL_COMPUTE["N_evals_total"]})')
ax.set_xticks(x)
ax.set_xticklabels(param_labels, rotation=15, ha='right')
ax.legend(loc='upper right')
ax.set_ylim(0, 0.55)
# Add cumulative line on secondary axis
ax2 = ax.twinx()
cumsum = np.cumsum(ST) / np.sum(ST) * 100
ax2.plot(x, cumsum, 'o--', color='red', alpha=0.7, linewidth=2, markersize=8, label='Cumulative %')
ax2.set_ylabel('Cumulative Contribution (%)', color='red')
ax2.tick_params(axis='y', labelcolor='red')
ax2.set_ylim(0, 110)
# Mark 75% threshold
ax2.axhline(75, color='red', linestyle=':', alpha=0.5)
ax2.text(2.5, 77, '75% (Top 3 params)', fontsize=9, color='red')
# Stats box
stats_text = (f"SOBOL_TABLE_v1:\n"
f"Top contributor: k_L (ST={SOBOL_TABLE['k_L']['ST_i']:.3f})\n"
f"Top 2 total: {SOBOL_TABLE['k_L']['ST_i'] + SOBOL_TABLE['k_C']['ST_i']:.1%}\n"
f"Max interaction: kappa ({max(interaction):.3f})")
ax.text(0.02, 0.98, stats_text, transform=ax.transAxes, fontsize=9,
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.9))
save_fig('fig14_sobol_indices')
# ==========================================
# Fig 15: Assumption Robustness Waterfall
# ==========================================
def plot_fig15():
"""
Bar chart showing impact of changing modeling assumptions on TTE.
MCM O-Award style: clean, professional, data-driven visualization.
Data from ASSUMPTION_ROBUSTNESS
"""
# Extract data (skip baseline for comparison)
assumptions_full = list(ASSUMPTION_ROBUSTNESS.keys())
assumptions = assumptions_full[1:] # Exclude 'Baseline'
tte_vals = [ASSUMPTION_ROBUSTNESS[a]['TTE'] for a in assumptions]
delta_pcts = [ASSUMPTION_ROBUSTNESS[a]['delta_pct'] for a in assumptions]
baseline = 4.60
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(assumptions))
# Vibrant color coding: bright green for robust, orange for moderate, bright red for critical
bar_colors = []
for dp in delta_pcts:
if abs(dp) > 10:
bar_colors.append('#e74c3c') # Red - Critical
elif abs(dp) > 5:
bar_colors.append('#f39c12') # Orange - Moderate
else:
bar_colors.append('#27ae60') # Green - Robust
# Create bars
bars = ax.bar(x, tte_vals, width=0.6, color=bar_colors, alpha=0.9,
edgecolor='#2C3E50', linewidth=1.5)
# Baseline reference line
ax.axhline(baseline, color='#2c3e50', linestyle='--', linewidth=2.5,
label=f'Baseline CPL Model ({baseline:.2f}h)', zorder=1)
# Add value labels ABOVE bars (not overlapping)
for i, (bar, tte, dp) in enumerate(zip(bars, tte_vals, delta_pcts)):
height = bar.get_height()
# Position label above the bar
y_pos = height + 0.12
sign = '+' if dp > 0 else ''
label = f'{tte:.2f}h\n({sign}{dp:.1f}%)'
ax.text(bar.get_x() + bar.get_width()/2, y_pos, label,
ha='center', va='bottom', fontsize=10, fontweight='bold')
# Styling
ax.set_xticks(x)
ax.set_xticklabels(assumptions, rotation=20, ha='right', fontsize=10)
ax.set_ylabel('Time to Exhaustion (hours)', fontsize=11, fontweight='bold')
ax.set_xlabel('Modeling Assumption Change', fontsize=11, fontweight='bold')
ax.set_title('Assumption Robustness Analysis: Impact on TTE Prediction',
fontsize=13, fontweight='bold', pad=15)
ax.set_ylim(0, 6.8)
# Add legend with color explanation - positioned at upper right, outside plot area
from matplotlib.patches import Patch
legend_elements = [
plt.Line2D([0], [0], color='#2c3e50', linestyle='--', linewidth=2.5, label=f'Baseline ({baseline:.2f}h)'),
Patch(facecolor='#27ae60', edgecolor='#2C3E50', label='Robust (|Δ| < 5%)'),
Patch(facecolor='#f39c12', edgecolor='#2C3E50', label='Moderate (5-10%)'),
Patch(facecolor='#e74c3c', edgecolor='#2C3E50', label='Critical (|Δ| > 10%)')
]
ax.legend(handles=legend_elements, loc='upper right', fontsize=9,
framealpha=0.95, edgecolor='gray', bbox_to_anchor=(0.99, 0.99))
# Grid for readability
ax.yaxis.grid(True, linestyle=':', alpha=0.6)
ax.set_axisbelow(True)
plt.tight_layout()
save_fig('fig15_assumption_robustness')
# ==========================================
# Fig 16: Physical Coupling Decoupling Analysis
# ==========================================
def plot_fig16():
"""
Shows impact of disabling each physical coupling mechanism.
Data from DECOUPLING_DATA
"""
experiments = list(DECOUPLING_DATA.keys())
tte_vals = [DECOUPLING_DATA[e]['TTE'] for e in experiments]
delta_pcts = [DECOUPLING_DATA[e]['delta_pct'] for e in experiments]
fig, ax = plt.subplots(figsize=(10, 6))
# Color by magnitude
bar_colors = [colors[0] if d == 0 else (colors[3] if d > 10 else colors[1]) for d in delta_pcts]
bars = ax.barh(experiments, tte_vals, color=bar_colors, alpha=0.8, edgecolor='black', height=0.6)
ax.axvline(4.60, color='red', linestyle='--', linewidth=2, label='Full Model (4.60h)')
# Add percentage labels
for bar, pct in zip(bars, delta_pcts):
width = bar.get_width()
label = f'+{pct:.1f}%' if pct > 0 else ('Baseline' if pct == 0 else f'{pct:.1f}%')
ax.text(width + 0.15, bar.get_y() + bar.get_height()/2, label,
va='center', fontsize=10, fontweight='bold')
ax.set_xlabel('TTE (hours)')
ax.set_title('Physical Coupling Decoupling Analysis: TTE Overestimation When Ignoring Feedback')
ax.set_xlim(0, 8)
ax.legend(loc='lower right')
ax.grid(axis='x', alpha=0.3)
# Key insight box
insight_text = ("Key Insight:\n"
"Signal-Power coupling: +39.6%\n"
"CPL feedback: +11.3%\n"
"All coupling off: +56.7%")
ax.text(0.98, 0.02, insight_text, transform=ax.transAxes, fontsize=9,
ha='right', va='bottom', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9))
save_fig('fig16_decoupling')
# ==========================================
# Fig 17: Extreme Scenario Stress Testing
# ==========================================
def plot_fig17():
"""
Bar chart showing TTE under extreme conditions.
Data from EXTREME_SCENARIOS
"""
scenarios = list(EXTREME_SCENARIOS.keys())
tte_vals = [EXTREME_SCENARIOS[s]['TTE'] for s in scenarios]
delta_pcts = [EXTREME_SCENARIOS[s]['delta_pct'] for s in scenarios]
confidences = [EXTREME_SCENARIOS[s]['confidence'] for s in scenarios]
fig, ax = plt.subplots(figsize=(11, 6))
# Color by severity (green to red)
norm_delta = [(d - min(delta_pcts)) / (max(delta_pcts) - min(delta_pcts) + 1e-6) for d in delta_pcts]
bar_colors = plt.cm.RdYlGn_r(norm_delta)
bars = ax.bar(scenarios, tte_vals, color=bar_colors, edgecolor='black', alpha=0.85)
ax.axhline(4.60, color='gray', linestyle='--', alpha=0.7, linewidth=2, label='Baseline (4.60h)')
# Add labels with confidence stars
for bar, d, c in zip(bars, delta_pcts, confidences):
height = bar.get_height()
stars = '*' * c
label = f'{d:+.0f}%\n[{stars}]' if d != 0 else f'Baseline\n[{stars}]'
ax.text(bar.get_x() + bar.get_width()/2, height + 0.15, label,
ha='center', va='bottom', fontsize=9)
ax.set_ylabel('TTE (hours)')
ax.set_title('Extreme Scenario Stress Testing (Confidence: * to *****)')
ax.set_ylim(0, 6)
plt.xticks(rotation=30, ha='right')
ax.legend(loc='upper right')
# Highlight perfect storm
ax.annotate('Perfect Storm\n(DANGER)', xy=(4, 0.92), xytext=(5, 2),
arrowprops=dict(arrowstyle='->', color='red', lw=2),
fontsize=10, color='red', fontweight='bold')
save_fig('fig17_extreme_scenarios')
# ==========================================
# Fig 18: Usage Pattern Fluctuation Impact
# ==========================================
def plot_fig18():
"""
Shows how usage pattern volatility affects TTE uncertainty.
Data from FLUCTUATION_DATA
"""
labels = list(FLUCTUATION_DATA.keys())
means = [FLUCTUATION_DATA[l]['mean'] for l in labels]
stds = [FLUCTUATION_DATA[l]['std'] for l in labels]
p10s = [FLUCTUATION_DATA[l]['p10'] for l in labels]
p90s = [FLUCTUATION_DATA[l]['p90'] for l in labels]
cvs = [FLUCTUATION_DATA[l]['cv'] for l in labels]
fig, ax = plt.subplots(figsize=(9, 6))
x = np.arange(len(labels))
bar_colors = [colors[0], colors[1], colors[4], colors[3]]
# Plot 90% confidence intervals
for i, (m, low, high, c) in enumerate(zip(means, p10s, p90s, bar_colors)):
ax.plot([i, i], [low, high], color='black', linewidth=3, alpha=0.4)
ax.plot(i, m, 'o', markersize=14, color=c, markeredgecolor='black', markeredgewidth=1.5)
ax.plot([i-0.15, i+0.15], [low, low], 'k-', linewidth=2)
ax.plot([i-0.15, i+0.15], [high, high], 'k-', linewidth=2)
# Add width and CV annotations
for i, (low, high, cv) in enumerate(zip(p10s, p90s, cvs)):
width = high - low
ax.annotate(f'Width: {width:.2f}h\nCV: {cv:.1f}%',
xy=(i, high + 0.03), ha='center', fontsize=9,
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
ax.set_xticks(x)
ax.set_xticklabels(labels)
ax.set_ylabel('TTE (hours)')
ax.set_title('Usage Pattern Fluctuation: Impact on TTE Uncertainty (90% CI)')
ax.set_ylim(4.1, 5.1)
ax.axhline(4.60, color='gray', linestyle='--', alpha=0.5, label='Mean TTE')
ax.legend(loc='upper left')
ax.grid(axis='y', alpha=0.3)
# Robustness conclusion
ax.text(0.98, 0.02, 'Robust: CV < 2.5% for reasonable volatility',
transform=ax.transAxes, ha='right', va='bottom', fontsize=10, fontweight='bold',
bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8))
save_fig('fig18_fluctuation')
# ==========================================
# Fig 19: CPL vs CC vs CR Load Model Comparison
# ==========================================
def plot_fig19():
"""
Compares three load models: CPL, CC, CR.
Shows TTE prediction differences and end-of-life current behavior.
MCM O-Award style: clear comparison with proper annotations.
"""
models = list(LOAD_MODEL_COMPARISON.keys())
tte_vals = [LOAD_MODEL_COMPARISON[m]['TTE'] for m in models]
currents = [LOAD_MODEL_COMPARISON[m]['end_current'] for m in models]
# Baseline values
baseline_tte = 4.60 # CPL model TTE
nominal_current = 0.69 # Nominal operating current
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5.5))
# Distinct colors for each model
bar_colors = ['#27ae60', '#3498db', '#9b59b6'] # Green, Blue, Purple
# ===== Left Panel: TTE Comparison =====
x1 = np.arange(len(models))
bars1 = ax1.bar(x1, tte_vals, width=0.6, color=bar_colors, alpha=0.85,
edgecolor='#2c3e50', linewidth=1.5)
# Baseline reference line (CPL model)
ax1.axhline(baseline_tte, color='#e74c3c', linestyle='--', linewidth=2,
label=f'CPL Baseline ({baseline_tte:.2f}h)')
# Add value labels ABOVE bars
for i, (bar, t) in enumerate(zip(bars1, tte_vals)):
height = bar.get_height()
delta_pct = (t - baseline_tte) / baseline_tte * 100
if abs(delta_pct) < 0.1:
label = f'{t:.2f}h\n(Baseline)'
else:
label = f'{t:.2f}h\n({delta_pct:+.1f}%)'
ax1.text(bar.get_x() + bar.get_width()/2, height + 0.12, label,
ha='center', va='bottom', fontsize=10, fontweight='bold')
ax1.set_xticks(x1)
ax1.set_xticklabels(models, fontsize=10)
ax1.set_ylabel('Time to Exhaustion (hours)', fontsize=11, fontweight='bold')
ax1.set_title('(a) TTE Prediction by Load Model', fontsize=12, fontweight='bold')
ax1.set_ylim(0, 6.5)
ax1.legend(loc='upper left', fontsize=9)
ax1.yaxis.grid(True, linestyle=':', alpha=0.5)
ax1.set_axisbelow(True)
# ===== Right Panel: End-of-Life Current =====
x2 = np.arange(len(models))
bars2 = ax2.bar(x2, currents, width=0.6, color=bar_colors, alpha=0.85,
edgecolor='#2c3e50', linewidth=1.5)
# Nominal current reference line
ax2.axhline(nominal_current, color='#7f8c8d', linestyle='--', linewidth=2,
label=f'Nominal Current ({nominal_current:.2f}A)')
# Measured range shading (28-45% increase from literature)
measured_low = nominal_current * 1.28
measured_high = nominal_current * 1.45
ax2.axhspan(measured_low, measured_high, alpha=0.25, color='#27ae60',
label=f'Measured Range ({measured_low:.2f}-{measured_high:.2f}A)')
# Add value labels ABOVE bars
for i, (bar, c) in enumerate(zip(bars2, currents)):
height = bar.get_height()
delta_pct = (c - nominal_current) / nominal_current * 100
label = f'{c:.2f}A\n({delta_pct:+.0f}%)'
ax2.text(bar.get_x() + bar.get_width()/2, height + 0.03, label,
ha='center', va='bottom', fontsize=10, fontweight='bold')
ax2.set_xticks(x2)
ax2.set_xticklabels(models, fontsize=10)
ax2.set_ylabel('Current at SOC=0.1 (A)', fontsize=11, fontweight='bold')
ax2.set_title('(b) End-of-Life Current Surge', fontsize=12, fontweight='bold')
ax2.set_ylim(0, 1.3)
ax2.legend(loc='upper right', fontsize=9)
ax2.yaxis.grid(True, linestyle=':', alpha=0.5)
ax2.set_axisbelow(True)
# Add key insight annotation on right panel
ax2.annotate('CPL captures\nreal-world surge',
xy=(0, 1.01), xytext=(0.8, 1.15),
fontsize=9, ha='center',
arrowprops=dict(arrowstyle='->', color='#27ae60', lw=1.5),
bbox=dict(boxstyle='round,pad=0.3', facecolor='#d5f5e3', edgecolor='#27ae60'))
plt.tight_layout()
save_fig('fig19_cpl_comparison')
# ==========================================
# Fig 20: Signal-Power Mapping Validation
# ==========================================
def plot_fig20():
"""
Validates exponential signal-power mapping against literature.
Shows linear model failure at low signal quality.
"""
psi_vals = list(SIGNAL_MAPPING.keys())
measured = [SIGNAL_MAPPING[p]['measured'] for p in psi_vals]
exp_model = [SIGNAL_MAPPING[p]['exp_model'] for p in psi_vals]
lin_model = [SIGNAL_MAPPING[p]['lin_model'] for p in psi_vals]
fig, ax = plt.subplots(figsize=(9, 6))
x = np.arange(len(psi_vals))
width = 0.25
ax.bar(x - width, measured, width, label='Measured (Literature)', color='gray', alpha=0.7, edgecolor='black')
ax.bar(x, exp_model, width, label='Exponential Model (Ours)', color=colors[0], alpha=0.8, edgecolor='black')
ax.bar(x + width, lin_model, width, label='Linear Model', color=colors[3], alpha=0.8, edgecolor='black')
ax.set_xlabel('Signal Quality Ψ')
ax.set_ylabel('Network Power (W)')
ax.set_title('Signal-Power Mapping: Model Validation')
ax.set_xticks(x)
ax.set_xticklabels([f'Ψ={p}' for p in psi_vals])
ax.legend(loc='upper left')
# Calculate and annotate errors
for i, (m, l) in enumerate(zip(measured, lin_model)):
err = (l - m) / m * 100
if abs(err) > 20:
ax.annotate(f'Error: {err:.0f}%',
xy=(i + width, l), xytext=(i + width + 0.3, l + 0.8),
arrowprops=dict(arrowstyle='->', color='red', lw=1.5),
color='red', fontsize=9, fontweight='bold')
# Add conclusion box
ax.text(0.98, 0.98, 'Linear model fails at Ψ < 0.3\n(Error > 50%)',
transform=ax.transAxes, ha='right', va='top', fontsize=10,
bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9))
save_fig('fig20_signal_validation')
# ==========================================
# Fig 21: 3D Sensitivity Framework Radar
# ==========================================
def plot_fig21():
"""
Radar chart summarizing sensitivity across six dimensions.
Higher = more sensitive / less robust (risk indicator)
"""
categories = ['Temperature\nSensitivity', 'Signal\nSensitivity', 'Load\nSensitivity',
'Assumption\nRobustness', 'Fluctuation\nRobustness', 'Extreme\nResilience']
# Values: higher = worse (more sensitive or less robust)
# Based on analysis: Temp=4.8 (63.5% impact), Signal=4.5 (59.8%), Load=3.2,
# Assumption=4.0 (robust to most), Fluctuation=4.5 (CV<2.5%), Extreme=2.5 (E4 problematic)
values = [4.8, 4.5, 3.2, 4.0, 4.5, 2.5]
N = len(categories)
angles = [n / float(N) * 2 * pi for n in range(N)]
values_plot = values + values[:1]
angles += angles[:1]
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
ax.plot(angles, values_plot, 'o-', linewidth=2, color=colors[2], markersize=8)
ax.fill(angles, values_plot, alpha=0.25, color=colors[2])
# Risk zones
theta = np.linspace(0, 2*pi, 100)
ax.fill_between(theta, 4, 5, alpha=0.15, color='red')
ax.fill_between(theta, 0, 2, alpha=0.15, color='green')
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories, size=10)
ax.set_ylim(0, 5)
ax.set_yticks([1, 2, 3, 4, 5])
ax.set_title('3D Sensitivity Framework Summary\n(Higher = More Sensitive / Less Robust)', pad=20)
# Add legend
from matplotlib.patches import Patch
legend_elements = [Patch(facecolor='red', alpha=0.3, label='High Risk Zone (>4)'),
Patch(facecolor='green', alpha=0.3, label='Safe Zone (<2)')]
ax.legend(handles=legend_elements, loc='upper right', bbox_to_anchor=(1.3, 1.0))
save_fig('fig21_framework_radar')
# ==========================================
# Fig 22: Fluctuation Amplification by SOC
# ==========================================
def plot_fig22():
"""
Shows how fluctuation amplification increases at low SOC.
CPL feedback causes "last 20% drops fast" phenomenon.
"""
soc_ranges = list(AMPLIFICATION_BY_SOC.keys())
beta_vals = list(AMPLIFICATION_BY_SOC.values())
fig, ax = plt.subplots(figsize=(9, 5))
# Color gradient (green to red)
colors_grad = plt.cm.Reds(np.linspace(0.3, 0.9, len(beta_vals)))
bars = ax.bar(soc_ranges, beta_vals, color=colors_grad, alpha=0.85, edgecolor='black')
ax.axhline(1.0, color='gray', linestyle='--', alpha=0.6, label='No Amplification (β=1)')
ax.set_ylabel('Amplification Factor β')
ax.set_xlabel('SOC Range')
ax.set_title('Fluctuation Amplification by Battery State: Why "Last 20% Drops Fast"')
ax.set_ylim(0, 5)
ax.legend(loc='upper left')
for bar, b in zip(bars, beta_vals):
ax.text(bar.get_x() + bar.get_width()/2, b + 0.15, f'{b:.1f}×',
ha='center', fontweight='bold', fontsize=11)
# Physical explanation
ax.text(0.98, 0.98, 'Physics: I=P/V causes\npositive feedback at low V',
transform=ax.transAxes, ha='right', va='top', fontsize=10,
bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.9))
ax.grid(axis='y', alpha=0.3)
save_fig('fig22_amplification')
# ==========================================
# Main Execution
# ==========================================
if __name__ == "__main__":
print("=" * 60)
print("Problem 3 Figure Generation (Data from 整合输出.md)")
print("=" * 60)
print("\n[1/9] Fig 14: Sobol Sensitivity Indices...")
plot_fig14()
print("[2/9] Fig 15: Assumption Robustness Waterfall...")
plot_fig15()
print("[3/9] Fig 16: Physical Decoupling Analysis...")
plot_fig16()
print("[4/9] Fig 17: Extreme Scenario Testing...")
plot_fig17()
print("[5/9] Fig 18: Usage Pattern Fluctuation...")
plot_fig18()
print("[6/9] Fig 19: CPL vs CC vs CR Comparison...")
plot_fig19()
print("[7/9] Fig 20: Signal Mapping Validation...")
plot_fig20()
print("[8/9] Fig 21: 3D Framework Radar...")
plot_fig21()
print("[9/9] Fig 22: SOC Amplification...")
plot_fig22()
print("\n" + "=" * 60)
print("All Problem 3 figures generated successfully!")
print("Output directory: figures/")
print("=" * 60)