VoronoiFVMPoisson.jl
Realization of Poisson-Boltzmann and Poisson-Bikerman equations based on plain VoronoiFVM.jl (without LiquidElectrolytes). This notebook is meant for demonstation purposes and may be moved to VoronoiFVM.jl.
begin
using PlutoUI, HypertextLiteral, UUIDs
using LinearAlgebra
using Interpolations
using VoronoiFVM, GridVisualize, ExtendableGrids
using LaTeXStrings
using LessUnitful, Unitful
if isdefined(Main, :PlutoRunner)
using CairoMakie
default_plotter!(CairoMakie)
CairoMakie.activate!(type = "svg")
end
end
Constants, Units & Parameters
See LessUnitful.jl ph and ufac and Unitful.jl for u
begin
const ε = 78.49
const ε_0 = ph"ε_0"
const F = ph"N_A" * ph"e"
const K = ufac"K"
const nm = ufac"nm"
const dm = ufac"dm"
const V = ufac"V"
const mol = ufac"mol"
end;
Parameters:
begin
const molarity = 0.01
const c_bulk = [molarity, molarity] * mol / dm^3 # bulk ion molar concentrations
const z = [-1, 1] # species charge numbers
const c̄ = 55.508mol / dm^3 # summary molar concentration
const nref = 0 # grid refinement level
const T = 298.15K # temperature
const L = 20.0nm # computational domain size
const vmax = 1.0 * V # maximum voltage
end;
Check for bulk electroneutrality:
@assert dot(c_bulk, z) == 0
Derived data:
begin
const RT = ph"R" * T
const c0_bulk = c̄ - sum(c_bulk) # solvent bulk molar concentration
const hmin = 1.0e-1 * nm * 2.0^(-nref) # grid size at working electrode
const hmax = 1.0 * nm * 2.0^(-nref) # grid size at bulk
const l_debye = sqrt(ε * ε_0 * RT / (F^2 * c_bulk[1])) # Debye length
const dlcap0 = sqrt(2 * ε * ε_0 * F^2 * c_bulk[1] / RT) # Double layer capacitance at point of zero charge (0V)
end;
Debye length= 4.3018 nm
Double layer capacitance at zero voltage for symmetric binary electrolyte = 22.847 μF cm^-2
X = geomspace(0, L, hmin, hmax)
52-element Vector{Float64}:
0.0
1.0000000000000002e-10
2.0469806721888107e-10
3.1431492001257436e-10
4.290816462337372e-10
5.492401903976795e-10
6.750438637356726e-10
⋮
1.5461201801124036e-8
1.6287579454587685e-8
1.715278088569287e-8
1.8058630061610094e-8
1.9007036640713593e-8
2.0e-8
grid = simplexgrid(X)
ExtendableGrids.ExtendableGrid{Float64, Int32}
dim = 1
nnodes = 52
ncells = 51
nbfaces = 2
gridplot(grid, size = (600, 200))
Nonlinear Poisson equation
Let \(\bar c\) be the summary concentration, and \(y_i\) be the solute molar fractions, such that for the species molar concentrations one has \(c_i=\bar c y_i\). Then the Poisson equation states:
$$\begin{aligned} -\nabla \cdot εε_0 \nabla \phi &= F\sum_{i=1}^n z_i \bar c y_i\\ \end{aligned}$$
Un VoronoiFVM, the left hand side is expressed via:
function pb_flux(y, u, edge, data)
return y[1] = ε * ε_0 * (u[1, 1] - u[1, 2])
end
pb_flux (generic function with 1 method)
It remains to state the dependecy of the mole fractions on the potential.
We choose Dirichlet boundary conditions at the electrode (index 1) and the bulk (index 2):
const bcvals = [0.0, 0.0]
2-element Vector{Float64}:
0.0
0.0
function pb_bc(y, u, bnode, data)
boundary_dirichlet!(y, u, bnode, species = 1, region = 2, value = bcvals[2])
return boundary_dirichlet!(y, u, bnode, species = 1, region = 1, value = bcvals[1])
end
pb_bc (generic function with 1 method)
Classical Poisson-Boltzmann
$$\begin{aligned} y_i&= \frac{c_{i,bulk}}{\bar c}\exp\left(z_i\phi \frac{F}{RT}\right) \end{aligned}$$
pbo_molfrac(ϕ, i) = (c_bulk[i] / c̄) * exp(z[i] * ϕ * F / RT)
pbo_molfrac (generic function with 1 method)
function pbo_spacecharge(ϕ)
sumyz = zero(ϕ)
for i in 1:length(z)
sumyz += z[i] * pbo_molfrac(ϕ, i)
end
return F * c̄ * sumyz
end
pbo_spacecharge (generic function with 1 method)
function pbo_reaction(y, u, node, data)
return y[1] = pbo_spacecharge(u[1])
end
pbo_reaction (generic function with 1 method)
pbo_system = VoronoiFVM.System(
grid;
reaction = pbo_reaction,
flux = pb_flux,
bcondition = pb_bc,
species = [1]
)
VoronoiFVM.System{Float64, Float64, Int32, Int64, Matrix{Int32}}(
grid = ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=52, ncells=51,
nbfaces=2),
physics = Physics(flux=pb_flux, storage=default_storage, reaction=pbo_reaction,
breaction=pb_bc, ),
num_species = 1)
function sweep(system; nsweep = 100, δ = 1.0e-4)
voltages = range(0, vmax, length = nsweep + 1)
solutions = []
dlcaps = zeros(0)
for v in voltages
bcvals[1] = v
if length(solutions) == 0
inival = 0.0
else
inival = solutions[end]
end
push!(solutions, solve(system; inival))
q = integrate(system, system.physics.reaction, solutions[end])
bcvals[1] = v + δ
solδ = solve(system; inival = solutions[end])
qδ = integrate(system, system.physics.reaction, solδ)
push!(dlcaps, (qδ[1] - q[1]) / δ)
end
return voltages, dlcaps, TransientSolution(solutions, voltages)
end
sweep (generic function with 1 method)
pbo_volts, pbo_caps, pbo_sols = sweep(pbo_system)
(0.0:0.01:1.0, [0.2279211284512289, 0.23232155094885917, 0.24560434836521683, 0.26828027521705844, 0.301224127664014, 0.34571356466221725, 0.40348767573306077, 0.47683067960557074, 0.5686896505781303, 0.6828408873541664 … 4.535790741564179e12, 6.694035309081421e12, 9.879227520047607e12, 1.458001517569336e13, 2.1517557125893555e13, 3.175615793874756e13, 4.686654535775879e13, 6.916683932619873e13, 1.0207818020028808e14, 1.506495739066797e14], TransientSolution{Any, 3, Vector{Any}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}(Any[[0.0 0.0 … 0.0 0.0], [0.01 0.009674563602456738 … 9.237370354343381e-6 6.465143843346336e-36], [0.02 0.01933737655569516 … 1.8302825687845453e-5 1.2809966069615975e-35], [0.03 0.028976332547113244 … 2.7037039748681027e-5 1.8922955816241113e-35], [0.04 0.03857861313360881 … 3.530395636764442e-5 2.470888872055713e-35], [0.05 0.04813033116640495 … 4.299813647931687e-5 3.0093969027009016e-35], [0.06 0.05761617795125789 … 5.0048127083344423e-5 3.502818748971762e-35], [0.07 0.06701908305595497 … 5.641602729517804e-5 3.948501765569903e-35], [0.08 0.07631990299437341 … 6.209404779787784e-5 4.3459007859324827e-35], [0.09 0.08549716482188524 … 6.709916609103969e-5 4.696204048408479e-35] … [0.91 0.2723334589126947 … 9.830385041770582e-5 6.880188938255974e-35], [0.92 0.2727500282846376 … 9.831524078776162e-5 6.880986138952869e-35], [0.93 0.2731603776598562 … 9.83264020040468e-5 6.881767301404764e-35], [0.94 0.27356468421654406 … 9.83373419242993e-5 6.882532975566934e-35], [0.95 0.27396311783185523 … 9.834806803279557e-5 6.883283685256463e-35], [0.96 0.2743558414708359 … 9.835858746279237e-5 6.884019929722911e-35], [0.97 0.27474301155016845 … 9.836890701734921e-5 6.884742185105666e-35], [0.98 0.27512477827863524 … 9.837903318867109e-5 6.885450905787713e-35], [0.99 0.2755012859760444 … 9.838897217608784e-5 6.886146525653989e-35], [1.0 0.2758726733722127 … 9.839872990278725e-5 6.886829459262535e-35]], 0.0:0.01:1.0, NewtonSolverHistory[]))
plotcdl(pbo_volts, pbo_caps, pb_v)
plotsols(pbo_sols, pb_v, pbo_molfrac)
"Poisson-Bikerman"
Here we use the expression
$$\begin{aligned} y_i&= \frac{\frac{c_{i,bulk}}{c_{0,bulk}}\exp\left(z_i\phi \frac{F}{RT}\right)}{1+\sum_{j=1}^n \frac{c_{j,bulk}}{c_{0,bulk}}\exp\left(z_i\phi \frac{F}{RT}\right)} \end{aligned}$$
function pbi_molfrac(ϕ, j)
N = length(z)
denom = one(ϕ)
for i in 1:length(z)
denom += exp(z[i] * ϕ * F / RT) * c_bulk[i] / c0_bulk
end
return exp(z[j] * ϕ * F / RT) * c_bulk[j] / (c0_bulk * denom)
end
pbi_molfrac (generic function with 1 method)
function pbi_spacecharge(ϕ)
sumyz = zero(ϕ)
for i in 1:length(z)
sumyz += z[i] * pbi_molfrac(ϕ, i)
end
return F * c̄ * sumyz
end
pbi_spacecharge (generic function with 1 method)
function pbi_reaction(y, u, node, data)
return y[1] = pbi_spacecharge(u[1])
end
pbi_reaction (generic function with 1 method)
pbi_system = VoronoiFVM.System(grid; reaction = pbi_reaction, flux = pb_flux, bcondition = pb_bc, species = [1])
VoronoiFVM.System{Float64, Float64, Int32, Int64, Matrix{Int32}}(
grid = ExtendableGrids.ExtendableGrid{Float64, Int32}(dim=1, nnodes=52, ncells=51,
nbfaces=2),
physics = Physics(flux=pb_flux, storage=default_storage, reaction=pbi_reaction,
breaction=pb_bc, ),
num_species = 1)
pbi_volts, pbi_caps, pbi_sols = sweep(pbi_system)
(0.0:0.01:1.0, [0.227921128294752, 0.23231665625685072, 0.24558296238934468, 0.26822443355533837, 0.3011027537085965, 0.34547100866769154, 0.4030233712739098, 0.47596242949837775, 0.5670867874531943, 0.6799016647912604 … 1.6615077566939362, 1.6484924477255802, 1.6351761185573466, 1.621610192410472, 1.6078596289670344, 1.5940011669801635, 1.5801209693666607, 1.5663116858721793, 1.5526690268252707, 1.5392880156506195], TransientSolution{Any, 3, Vector{Any}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}(Any[[0.0 0.0 … 0.0 0.0], [0.01 0.009674565740806013 … 9.237401913675033e-6 6.46516593140938e-36], [0.02 0.01933739458710759 … 1.8303078174837718e-5 1.281014278275629e-35], [0.03 0.02897639891080578 … 2.7037892148765896e-5 1.892355240260166e-35], [0.04 0.03857879032931354 … 3.530597861058703e-5 2.4710304068324803e-35], [0.05 0.04813073300819722 … 4.3002093324967484e-5 3.009673838401386e-35], [0.06 0.05761700690912229 … 5.0054986396716e-5 3.5032988255077535e-35], [0.07 0.06702069411926119 … 5.642697455746507e-5 3.9492679535937835e-35], [0.08 0.07632291087241937 … 6.211050841066457e-5 4.3470528478866273e-35], [0.09 0.08550262057379068 … 6.712283647877935e-5 4.697860715356911e-35] … [0.91 0.6216348682552673 … 0.00010614269340449226 7.428821779973225e-35], [0.92 0.6292533108610063 … 0.00010624974683503027 7.436314343341397e-35], [0.93 0.6368907011963554 … 0.00010635660638928038 7.443793337500572e-35], [0.94 0.6445474374833199 … 0.00010646329044417423 7.451260048633965e-35], [0.95 0.6522238341336632 … 0.00010656980411580769 7.458714834812039e-35], [0.96 0.6599201034526659 … 0.00010667613931128036 7.466157129620734e-35], [0.97 0.6676363402950177 … 0.00010678227517388479 7.473585473319788e-35], [0.98 0.6753725105302449 … 0.00010688817895392738 7.48099757379065e-35], [0.99 0.683128444098655 … 0.00010699380731248986 7.488390397785434e-35], [1.0 0.6909038332509377 … 0.0001070991080341135 7.495760290796002e-35]], 0.0:0.01:1.0, NewtonSolverHistory[]))
plotcdl (generic function with 1 method)
function plotsols(pbsols, pbv, pb_molfrac)
vis = GridVisualizer(size = (700, 200), layout = (1, 2), xlabel = "x/nm")
ϕ = pbsols(pbv)[1, :]
cp = c̄ * pb_molfrac.(ϕ, 1) / ufac"mol/dm^3"
cm = c̄ * pb_molfrac.(ϕ, 2) / ufac"mol/dm^3"
scalarplot!(vis[1, 1], X / nm, ϕ, color = :green, ylabel = "ϕ/V")
scalarplot!(vis[1, 2], X / nm, cp, color = :red, ylabel = "c/(mol/L)")
scalarplot!(vis[1, 2], X / nm, cm, color = :blue, clear = false)
return reveal(vis)
end
plotsols (generic function with 1 method)