132 lines
4.9 KiB
Python
132 lines
4.9 KiB
Python
"""
|
|
Fig 13: Survival/Reliability Curve
|
|
Shows probability of battery lasting beyond time t
|
|
"""
|
|
|
|
import os
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
from plot_style import set_oprice_style, save_figure
|
|
from validation import check_monotonic_decreasing
|
|
|
|
def make_figure(config):
|
|
"""Generate Fig 13: Survival Curve"""
|
|
|
|
set_oprice_style()
|
|
|
|
seed = config.get('global', {}).get('seed', 42)
|
|
np.random.seed(seed)
|
|
|
|
# Generate TTE distribution (log-normal)
|
|
n_samples = 300
|
|
tte_mean = 2.5 # hours
|
|
tte_std = 0.4
|
|
|
|
# Log-normal parameters
|
|
mu = np.log(tte_mean**2 / np.sqrt(tte_mean**2 + tte_std**2))
|
|
sigma = np.sqrt(np.log(1 + tte_std**2 / tte_mean**2))
|
|
|
|
tte_samples = np.random.lognormal(mu, sigma, n_samples)
|
|
tte_samples = np.clip(tte_samples, 1.0, 5.0)
|
|
|
|
# Sort for survival function
|
|
tte_sorted = np.sort(tte_samples)
|
|
|
|
# Compute survival function: S(t) = P(TTE > t)
|
|
t_values = np.linspace(0, 4.5, 200)
|
|
survival_prob = np.zeros(len(t_values))
|
|
|
|
for i, t in enumerate(t_values):
|
|
survival_prob[i] = np.sum(tte_sorted > t) / n_samples
|
|
|
|
# Find confidence levels
|
|
tte_50 = np.percentile(tte_sorted, 50) # Median
|
|
tte_95 = np.percentile(tte_sorted, 5) # 95% confidence (5th percentile)
|
|
|
|
# Create figure
|
|
fig, ax = plt.subplots(figsize=(12, 8))
|
|
|
|
# Plot survival curve
|
|
ax.plot(t_values, survival_prob * 100, 'b-', linewidth=3, label='Survival Function')
|
|
|
|
# Fill area under curve
|
|
ax.fill_between(t_values, 0, survival_prob * 100, alpha=0.2, color='lightblue')
|
|
|
|
# Mark key percentiles
|
|
ax.axhline(50, color='orange', linestyle='--', linewidth=1.5, alpha=0.7, label='50% Survival')
|
|
ax.axvline(tte_50, color='orange', linestyle=':', linewidth=1.5, alpha=0.7)
|
|
ax.plot(tte_50, 50, 'o', markersize=10, color='orange', markeredgecolor='white',
|
|
markeredgewidth=2, zorder=5)
|
|
ax.text(tte_50 + 0.1, 50, f'Median TTE = {tte_50:.2f}h', fontsize=10,
|
|
verticalalignment='center')
|
|
|
|
ax.axhline(95, color='green', linestyle='--', linewidth=1.5, alpha=0.7, label='95% Confidence')
|
|
ax.axvline(tte_95, color='green', linestyle=':', linewidth=1.5, alpha=0.7)
|
|
ax.plot(tte_95, 95, 's', markersize=10, color='green', markeredgecolor='white',
|
|
markeredgewidth=2, zorder=5)
|
|
ax.text(tte_95 + 0.1, 95, f'95% Confident TTE = {tte_95:.2f}h', fontsize=10,
|
|
verticalalignment='center')
|
|
|
|
# Annotate interpretation
|
|
ax.annotate('95% of devices will\nlast at least this long',
|
|
xy=(tte_95, 95), xytext=(tte_95 - 0.5, 75),
|
|
arrowprops=dict(arrowstyle='->', color='green', lw=2),
|
|
fontsize=10, color='darkgreen',
|
|
bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))
|
|
|
|
# Labels and styling
|
|
ax.set_xlabel('Time (hours)', fontsize=11)
|
|
ax.set_ylabel('Survival Probability (%)', fontsize=11)
|
|
ax.set_title('Battery Reliability: Survival Function Analysis',
|
|
fontsize=12, fontweight='bold')
|
|
ax.set_xlim(0, 4.5)
|
|
ax.set_ylim(0, 105)
|
|
ax.grid(True, alpha=0.3)
|
|
ax.legend(loc='upper right', framealpha=0.9, fontsize=10)
|
|
|
|
# Add distribution statistics
|
|
stats_text = f'TTE Distribution:\\n'
|
|
stats_text += f'Mean: {tte_mean:.2f}h\\n'
|
|
stats_text += f'Std: {tte_std:.2f}h\\n'
|
|
stats_text += f'Median: {tte_50:.2f}h\\n'
|
|
stats_text += f'95% CI: {tte_95:.2f}h\\n'
|
|
stats_text += f'Sample Size: {n_samples}'
|
|
|
|
ax.text(0.02, 0.35, stats_text, transform=ax.transAxes,
|
|
fontsize=9, verticalalignment='top',
|
|
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8),
|
|
family='monospace')
|
|
|
|
# Add interpretation guide
|
|
guide_text = 'Interpretation:\\n' + \
|
|
'• S(t) = P(TTE > t)\\n' + \
|
|
'• Steep drop = high variability\\n' + \
|
|
'• Use 95% value for conservative estimates'
|
|
|
|
ax.text(0.98, 0.02, guide_text, transform=ax.transAxes,
|
|
fontsize=9, verticalalignment='bottom', horizontalalignment='right',
|
|
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))
|
|
|
|
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, 'Fig13_Survival_Curve')
|
|
save_figure(fig, output_base, dpi=config.get('global', {}).get('dpi', 300))
|
|
plt.close()
|
|
|
|
# Validation
|
|
is_monotonic = check_monotonic_decreasing(survival_prob)
|
|
|
|
return {
|
|
"output_files": [f"{output_base}.pdf", f"{output_base}.png"],
|
|
"computed_metrics": {
|
|
"median_tte_h": float(tte_50),
|
|
"tte_95_confidence_h": float(tte_95),
|
|
"n_samples": n_samples
|
|
},
|
|
"validation_flags": {"survival_monotonic": is_monotonic},
|
|
"pass": is_monotonic
|
|
}
|