Fast 1-D finite-difference cyclic voltammetry (CV) simulator in Python.
pip install git+https://github.com/hgstei/cvsim.gitCyclic voltammetry is a central electrochemical technique: the electrode potential is swept back and forth while recording the current, which reflects the interplay of diffusion, adsorption, and charge-transfer kinetics at the electrode–electrolyte interface.
This package implements a 1-D explicit finite-difference scheme for the corresponding diffusion–reaction problem. At each time step the Butler–Volmer expression determines the electron-transfer rate at the surface, while Fick's second law propagates the concentration profile into solution. An optional passivation model attenuates the current through a growing film using a WKB tunneling factor, with the film thickness growing proportionally to the accumulated charge density.
The implementation is optimised for speed:
| L (steps) | naive NumPy | cvsim | speed-up |
|---|---|---|---|
| 1 500 | ~1 300 ms | ~20 ms | ~65× |
| 10 000 | ~60 000 ms | ~600 ms | ~100× |
Key optimisations:
- Butler–Volmer arrays precomputed once (avoids O(L²) exp calls).
- Spatial diffusion update vectorised with NumPy slice operations.
- Rolling 2-row concentration buffers instead of an (L+1)×j matrix.
- Passivation=0 path skips all charge-density tracking.
- O(1) incremental trapezoid replaces O(L)
cumtrapzcalled each step. - WKB sqrt factors precomputed; only a scalar exp evaluated per step.
from cvsim import simulate_cv
import matplotlib.pyplot as plt
eta, J = simulate_cv(
L = 1500, # finite-difference steps (higher = more accurate)
DM = 0.45, # dimensionless model diffusion coefficient (≤ 0.5)
C = 1e-6, # initial concentration [mol/cm³]
D = 1e-5, # diffusion coefficient [cm²/s]
etai = +1.0, # initial overpotential [V]
etaf = -1.0, # final overpotential [V]
v = 0.001, # scan rate [V/s]
n = 1.0, # electrons transferred
alpha = 0.5, # charge-transfer coefficient
k0 = 1.0, # standard rate constant [cm/s]
eta0 = 0.0, # formal potential [V]
)
plt.plot(eta, J)
plt.xlabel("Overpotential (V)")
plt.ylabel("Current density (mA/cm²)")
plt.show()simulate_cv also accepts an lmfit.Parameters object directly, so it is a
drop-in replacement for the notebook functions calcCV_calc_noUnits and
calcCV_calc_fast:
from lmfit import Parameters
from cvsim import simulate_cv
params = Parameters()
params.add("L", value=1500)
# ... (same parameter set as the simulation notebooks)
eta, J = simulate_cv(params)| Parameter | Unit | Description |
|---|---|---|
L |
— | Finite-difference steps (trade accuracy vs. speed) |
DM |
— | Model diffusion coefficient (stability: DM ≤ 0.5) |
C |
mol/cm³ | Initial concentration of oxidised species |
D |
cm²/s | Diffusion coefficient (same for O and R) |
etai |
V | Initial overpotential |
etaf |
V | Final (turn-around) overpotential |
v |
V/s | Potential sweep rate |
n |
— | Electrons transferred |
alpha |
— | Charge-transfer coefficient |
k0 |
cm/s | Standard heterogeneous rate constant |
eta0 |
V | Formal potential (output axis shift) |
bb |
cm²·Å/(mA·s) | Passivation thickness proportionality constant |
passivation |
— | 0 = none (default), 1 = WKB tunneling model |
v0 |
eV | Tunneling barrier height (V_B − ΔE) |
chargeDens_init |
C/cm² | Initial charge density |
The simulation framework was originally published in:
H.-G. Steinrück, J. Chem. Phys. 154, 174703 (2021).
DOI: 10.1063/5.0049591
The key novelty of cvsim compared to other CV simulation tools is the passivation scenario: a dynamically evolving tunneling barrier arising from a homogeneous single-phase insulating film. The WKB tunneling probability is evaluated at each time step using the instantaneous film thickness, which grows proportionally to the accumulated charge density — making it possible to simulate how passivation progressively suppresses faradaic current during a potential sweep.
Hans-Georg Steinrück
Based on the simulation framework by Peter Attia.