模型复查

This commit is contained in:
ChuXun
2026-01-30 17:33:29 +08:00
parent 727e19bee1
commit cb7f55fa21
30 changed files with 4431 additions and 0 deletions

View File

@@ -0,0 +1,254 @@
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
import pandas as pd
import os
# ==========================================
# 1. Configuration & Plotting Style Setup
# ==========================================
def configure_plots():
"""Configure Matplotlib to meet academic standards (Times New Roman)."""
plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.serif'] = ['Times New Roman']
plt.rcParams['axes.labelsize'] = 12
plt.rcParams['xtick.labelsize'] = 10
plt.rcParams['ytick.labelsize'] = 10
plt.rcParams['legend.fontsize'] = 10
plt.rcParams['figure.dpi'] = 150
plt.rcParams['savefig.dpi'] = 300
# Ensure output directory exists
if not os.path.exists('output'):
os.makedirs('output')
# ==========================================
# 2. Physical Model Class
# ==========================================
class SmartphoneBatteryModel:
def __init__(self, capacity_mah=4000, temp_c=25, r_int=0.15):
"""
Initialize the battery model.
Args:
capacity_mah (float): Design capacity in mAh.
temp_c (float): Ambient temperature in Celsius.
r_int (float): Internal resistance in Ohms.
"""
self.q_design = capacity_mah / 1000.0 # Convert to Ah
self.temp_k = temp_c + 273.15
self.r_int = r_int
# Temperature correction for capacity (Arrhenius-like approximation)
# Reference T = 298.15K (25C). Lower temp -> Lower capacity.
self.temp_factor = np.exp(0.5 * (1 - 298.15 / self.temp_k))
# Clamp factor to reasonable bounds (e.g., 0.5 to 1.1)
self.temp_factor = np.clip(self.temp_factor, 0.1, 1.2)
self.q_eff = self.q_design * self.temp_factor
# Shepherd Model Parameters for OCV (Open Circuit Voltage)
# V = E0 - K/SOC + A*exp(-B*(1-SOC))
# Tuned for a typical Li-ion 3.7V/4.2V cell
self.E0 = 3.4
self.K = 0.02 # Polarization constant
self.A = 0.6 # Exponential zone amplitude
self.B = 20.0 # Exponential zone time constant
def get_ocv(self, soc):
"""
Calculate Open Circuit Voltage based on SOC.
Includes a safety clamp for SOC to avoid division by zero.
"""
soc = np.clip(soc, 0.01, 1.0) # Avoid singularity at SOC=0
# Simplified Shepherd Model + Linear term for better fit
# V_ocv = Constant + Linear*SOC - Polarization + Exponential_Drop
v_ocv = 3.2 + 0.6 * soc - (0.05 / soc) + 0.5 * np.exp(-20 * (1 - soc))
return np.clip(v_ocv, 2.5, 4.3)
def calculate_current(self, power_watts, soc):
"""
Calculate current I given Power P and SOC.
Solves the quadratic equation: P = (V_ocv - I * R_int) * I
=> R_int * I^2 - V_ocv * I + P = 0
"""
v_ocv = self.get_ocv(soc)
# Quadratic coefficients: a*I^2 + b*I + c = 0
a = self.r_int
b = -v_ocv
c = power_watts
delta = b**2 - 4*a*c
if delta < 0:
# Power demand exceeds battery capability (voltage collapse)
# Return a very high current to simulate crash or max out
return v_ocv / (2 * self.r_int)
# We want the smaller root (stable operating point)
# I = (-b - sqrt(delta)) / 2a
current = (-b - np.sqrt(delta)) / (2 * a)
return current
def derivative(self, t, y, power_func):
"""
The ODE function dy/dt = f(t, y).
y[0] = SOC (0.0 to 1.0)
"""
soc = y[0]
if soc <= 0:
return [0.0] # Battery empty
# Get instantaneous power demand from the scenario function
p_load = power_func(t)
# Calculate current required to support this power
i_load = self.calculate_current(p_load, soc)
# d(SOC)/dt = -I / Q_eff
# Units: I in Amps, Q in Ah, t in Seconds.
# We need to convert Q to Amp-seconds (Coulombs) -> Q * 3600
d_soc_dt = -i_load / (self.q_eff * 3600.0)
return [d_soc_dt]
# ==========================================
# 3. Scenario Definitions
# ==========================================
def scenario_idle(t):
"""Scenario A: Idle (Background tasks only). Constant low power."""
return 0.2 # 200 mW
def scenario_social(t):
"""Scenario B: Social Media (Screen on, fluctuating network)."""
base = 1.5 # Screen + CPU
noise = 0.5 * np.sin(t / 60.0) # Fluctuating network usage every minute
return base + max(0, noise)
def scenario_gaming(t):
"""Scenario C: Heavy Gaming (High CPU/GPU, High Screen)."""
base = 4.5 # High power
# Power increases slightly over time due to thermal throttling inefficiency or complexity
trend = 0.0001 * t
return base + trend
# ==========================================
# 4. Simulation & Visualization Logic
# ==========================================
def run_simulation():
configure_plots()
# Initialize Model
battery = SmartphoneBatteryModel(capacity_mah=4000, temp_c=25, r_int=0.15)
# Time span: 0 to 24 hours (in seconds)
t_span = (0, 24 * 3600)
t_eval = np.linspace(0, 24 * 3600, 1000) # Evaluation points
scenarios = {
"Idle (Background)": scenario_idle,
"Social Media": scenario_social,
"Heavy Gaming": scenario_gaming
}
results = {}
print(f"{'='*60}")
print(f"{'Simulation Start':^60}")
print(f"{'='*60}")
print(f"Battery Capacity: {battery.q_design*1000:.0f} mAh")
print(f"Temperature: {battery.temp_k - 273.15:.1f} C")
print("-" * 60)
# Solve ODE for each scenario
for name, p_func in scenarios.items():
# Solve IVP
# Stop event: SOC reaches 0
def battery_empty(t, y): return y[0]
battery_empty.terminal = True
sol = solve_ivp(
fun=lambda t, y: battery.derivative(t, y, p_func),
t_span=t_span,
y0=[1.0], # Start at 100% SOC
t_eval=t_eval,
events=battery_empty,
method='RK45'
)
# Post-process to get Voltage and Power for plotting
soc_vals = sol.y[0]
time_vals = sol.t
# Re-calculate V and P for visualization
voltages = []
powers = []
for t, s in zip(time_vals, soc_vals):
p = p_func(t)
i = battery.calculate_current(p, s)
v = battery.get_ocv(s) - i * battery.r_int
voltages.append(v)
powers.append(p)
results[name] = {
"time_h": time_vals / 3600.0, # Convert to hours
"soc": soc_vals * 100.0, # Convert to %
"voltage": voltages,
"power": powers,
"tte": time_vals[-1] / 3600.0 # Time to empty
}
print(f"Scenario: {name:<20} | Time-to-Empty: {results[name]['tte']:.2f} hours")
print(f"{'='*60}")
# ==========================================
# 5. Plotting
# ==========================================
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
colors = ['#2ca02c', '#1f77b4', '#d62728'] # Green, Blue, Red
# Plot 1: SOC vs Time
ax1 = axes[0]
for (name, data), color in zip(results.items(), colors):
ax1.plot(data["time_h"], data["soc"], label=f"{name} (TTE={data['tte']:.1f}h)", linewidth=2, color=color)
ax1.set_title("State of Charge (SOC) vs. Time", fontsize=14, fontweight='bold')
ax1.set_xlabel("Time (Hours)")
ax1.set_ylabel("SOC (%)")
ax1.set_ylim(0, 105)
ax1.grid(True, linestyle='--', alpha=0.6)
ax1.legend()
# Plot 2: Terminal Voltage vs SOC
ax2 = axes[1]
for (name, data), color in zip(results.items(), colors):
# Plot Voltage against SOC (reversed x-axis usually)
ax2.plot(data["soc"], data["voltage"], label=name, linewidth=2, color=color)
ax2.set_title("Terminal Voltage vs. SOC", fontsize=14, fontweight='bold')
ax2.set_xlabel("SOC (%)")
ax2.set_ylabel("Voltage (V)")
ax2.invert_xaxis() # Standard battery curve convention
ax2.grid(True, linestyle='--', alpha=0.6)
# Plot 3: Power Consumption Profile
ax3 = axes[2]
for (name, data), color in zip(results.items(), colors):
ax3.plot(data["time_h"], data["power"], label=name, linewidth=2, color=color, alpha=0.8)
ax3.set_title("Power Consumption Profile", fontsize=14, fontweight='bold')
ax3.set_xlabel("Time (Hours)")
ax3.set_ylabel("Power (Watts)")
ax3.grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
# Save and Show
save_path = os.path.join('output', 'battery_simulation_results.png')
plt.savefig(save_path)
print(f"Plot saved to: {save_path}")
plt.show()
if __name__ == "__main__":
run_simulation()