voltage vs capacity discharge curves by C-rate
Battery Engineering
Example from the compendium of canonical charts
Python Code
"""Battery Engineering — voltage vs capacity discharge curves by C-rate."""
import numpy as np
import pandas as pd
import plotly.graph_objects as go
# C-rate comparison colors from slow (cool) to fast (warm)
C_RATES = [
("C/10", 0.1, TEAL),
("C/5", 0.2, "#52B3D0"),
("C/2", 0.5, GREEN),
("1C", 1.0, VIOLET),
("2C", 2.0, ORANGE),
("5C", 5.0, PINK),
]
# NMC cathode parameters
V_OCV = 4.18 # open-circuit voltage at full charge (V)
V_CUTOFF = 3.00 # discharge cutoff (V)
Q_NOM = 3.0 # nominal capacity at C/10 (Ah)
R_INT = 0.030 # internal resistance (Ω) — produces IR drop = I × R
def synthetic_discharge(c_rate, rng):
"""Simulate NMC discharge curve at a given C-rate using a simplified Peukert + IR model."""
I = c_rate * Q_NOM # current (A)
# Peukert: capacity available at c_rate
k = 1.045
Q = Q_NOM * (c_rate ** (1 - k)) if c_rate > 0.1 else Q_NOM
n = 300
q = np.linspace(0, Q, n) # discharged capacity (Ah)
soc = 1 - q / Q # state of charge 0→1
# Simplified OCV vs SOC (NMC-like sigmoid + plateau)
ocv = (V_OCV - 1.18
+ 1.18 / (1 + np.exp(-12 * (soc - 0.5)))
+ 0.12 * np.exp(-8 * (1 - soc)))
# IR drop + diffusion overpotential
v = ocv - I * R_INT - 0.02 * c_rate * (1 - soc ** 0.5)
noise = rng.normal(0, 0.002, n)
v = np.clip(v + noise, V_CUTOFF, V_OCV + 0.1)
# Truncate at cutoff
mask = v >= V_CUTOFF
return q[mask], v[mask]
def generate():
print("building battery C-rate discharge curves …")
rng = np.random.default_rng(48)
fig = go.Figure()
for label, c_rate, color in C_RATES:
q, v = synthetic_discharge(c_rate, rng)
fig.add_trace(go.Scatter(
x=q,
y=v,
mode="lines",
name=label,
line=dict(color=color, width=2.2),
hovertemplate=f"{label}<br>Q: %{{x:.3f}} Ah<br>V: %{{y:.3f}} V<extra></extra>",
))
fig.update_layout(
xaxis=dict(title="Discharged Capacity (Ah)", showgrid=False),
yaxis=dict(title="Voltage (V)", range=[2.85, 4.3], showgrid=False),
legend=dict(
title="C-rate",
orientation="v",
x=0.78, y=0.98, xanchor="left",
),
margin=dict(t=30, b=60, l=70, r=40),
height=460,
)
return fig
if __name__ == "__main__":
generate()
Made with Plotly