VoronoiFVMPoisson2.jl
Realization of Poisson-Boltzmann and Poisson-Bikerman equations based on plain VoronoiFVM.jl (without LiquidElectrolytes). This notebook is meant for demonstration purposes and may be moved to VoronoiFVM.jl
It demonstrates a way to calculate the double layer charge as a unknown of the system.
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))
const nv = ones(num_nodes(grid))
52-element Vector{Float64}:
1.0
1.0
1.0
1.0
1.0
1.0
1.0
⋮
1.0
1.0
1.0
1.0
1.0
1.0
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 - q= 0\\ q&=F\sum_{i=1}^n z_i \bar c y_i\\ Q&=\int_\Omega q d\omega \end{aligned}$$
In this version of the implementation, we introduce three species: Electrostatic potential \(\phi\), charge density \(q\) and double layer charge \(Q\).
begin
const iφ = 1
const iq = 2
const iQ = 3
end
3
In VoronoiFVM, the left hand side is expressed via:
function pb_flux(y, u, edge, data)
return y[iφ] = ε * ε_0 * (u[iφ, 1] - u[iφ, 2])
end
pb_flux (generic function with 1 method)
It remains to state the dependency 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 = iφ, region = 2, value = bcvals[2])
boundary_dirichlet!(y, u, bnode, species = iφ, region = 1, value = bcvals[1])
return
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_spacechargedensity(ϕ)
sumyz = zero(ϕ)
for i in 1:length(z)
sumyz += z[i] * pbo_molfrac(ϕ, i)
end
return F * c̄ * sumyz
end
pbo_spacechargedensity (generic function with 1 method)
function pbo_reaction(y, u, node, data)
y[iφ] = u[iq] # implements the `-q` part of of the lhs of the poission equation
y[iq] = u[iq] - pbo_spacechargedensity(u[iφ]) # implements q=F\sum z_i c_i
return
end
pbo_reaction (generic function with 1 method)
Implement the calculation of the double layer charge. This just adds one additional equation to calculate the value of the integral. VoronoiFVM uses sparsity detection in order to obtain add the sparsity pattern of this operator to the overall one.
function pb_generic(y0, u0, sys, data)
y = reshape(y0, sys)
u = reshape(u0, sys)
y[iQ, 1] = u[iQ, 1] - u[iq, :] ⋅ nv # implements Q=\int_\Omega q
return
end
pb_generic (generic function with 1 method)
begin
pbo_system = VoronoiFVM.System(
grid;
reaction = pbo_reaction,
flux = pb_flux,
bcondition = pb_bc,
generic = pb_generic,
unknown_storage = :sparse # use sparse storage of the unknown vector
)
enable_species!(pbo_system, species = [iφ, iq], regions = [1])
# add the double layer charge as additional species at boundary 1:
enable_boundary_species!(pbo_system, iQ, [1])
nv .= nodevolumes(pbo_system)
end;
VoronoiFVM.has_generic_operator(pbo_system)
true
Calculate the double layer capacitance vs voltage curve
function dlcapsweep(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 = unknowns(system, inival = 0)
else
inival = solutions[end]
end
push!(solutions, solve(system; inival))
# Here, we can directly access the double layer charge as
# solution component
Q = solutions[end][iQ, 1]
bcvals[1] = v + δ
solδ = solve(system; inival = solutions[end])
Qδ = solδ[iQ, 1]
push!(dlcaps, (Qδ[1] - Q[1]) / δ)
end
return voltages, dlcaps, TransientSolution(solutions, voltages)
end
dlcapsweep (generic function with 1 method)
pbo_volts, pbo_caps, pbo_sols = dlcapsweep(pbo_system)
(0.0:0.01:1.0, [0.2279211284512252, 0.2323215509488505, 0.24560434836520817, 0.2682802752170324, 0.3012241276640487, 0.34571356466214787, 0.40348767573309546, 0.4768306796052932, 0.5686896505780261, 0.6828408873538888 … 4.535790741564331e12, 6.694035309081421e12, 9.879227520046996e12, 1.4580015175693969e13, 2.1517557125893555e13, 3.1756157938746336e13, 4.686654535776123e13, 6.916683932620117e13, 1.020781802002832e14, 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.0 0.0 … 0.0 0.0; 0.0 NaN … NaN NaN], [0.01 0.009674563602456738 … 9.237370354343367e-6 6.465143843346327e-36; 770183.0785876448 743926.521956174 … 693.796260457962 -5.572267160095641e-42; 0.002293713725788274 NaN … NaN NaN], [0.02 0.01933737655569516 … 1.830282568784546e-5 1.280996606961598e-35; 1.658521748094714e6 1.5934423500260697e6 … 1374.680491459436 1.0980080326360017e-41; 0.004675118475748602 NaN … NaN NaN], [0.03 0.028976332547113244 … 2.703703974868103e-5 1.8922955816241115e-35; 2.801298125527285e6 2.6679554281862536e6 … 2030.6861595805808 -1.873828355746101e-40; 0.007235274526353839 NaN … NaN NaN], [0.04 0.03857861313360881 … 3.530395636764442e-5 2.4708888720557127e-35; 4.3738282129090745e6 4.115911824820652e6 … 2651.594463344157 -1.503801473677301e-39; 0.0100721266209927 NaN … NaN NaN], [0.05 0.04813033116640495 … 4.299813647931688e-5 3.009396902700902e-35; 6.617357535984294e6 6.132836264874609e6 … 3229.4861453607864 -6.394687358043636e-39; 0.013294321683177773 NaN … NaN NaN], [0.06 0.05761617795125789 … 5.004812708334442e-5 3.502818748971761e-35; 9.87607118565252e6 8.983616422027325e6 … 3758.9945290832356 -1.8422140289333155e-38; 0.017025509074486918 NaN … NaN NaN], [0.07 0.06701908305595497 … 5.6416027295178016e-5 3.9485017655699003e-35; 1.4649896059320828e7 1.3030361033735603e7 … 4237.272933352258 -4.0190527334113987e-38; 0.021409345229854687 NaN … NaN NaN], [0.08 0.07631990299437343 … 6.209404779787786e-5 4.345900785932484e-35; 2.1671195817754738e7 1.87668447237299e7 … 4663.7362709194185 -7.141991323295859e-38; 0.02661549435087813 NaN … NaN NaN], [0.09 0.08549716482188524 … 6.709916609103969e-5 4.696204048408479e-35; 3.2017124510484297e7 2.685982648686085e7 … 5039.659626573801 -1.0858635591392667e-37; 0.03284703175053636 NaN … NaN NaN] … [0.91 0.2723334591068045 … 9.830385042594758e-5 6.880188938832809e-35; 2.3261904016877727e21 3.8712356463144646e10 … 7383.37926678981 -6.389658871551468e-33; 1.1630952008882016e11 NaN … NaN NaN], [0.92 0.2727500284677846 … 9.831524079548552e-5 6.880986139493458e-35; 3.433050943435032e21 3.934513937035178e10 … 7384.234775789538 -6.165399150565574e-33; 1.7165254717624973e11 NaN … NaN NaN], [0.93 0.27316037783279323 … 9.832640201129167e-5 6.881767301911823e-35; 5.066583875365027e21 3.9978585882881004e10 … 7385.073073489546 -5.952038688677802e-33; 2.533291937728161e11 NaN … NaN NaN], [0.94 0.2735646843799637 … 9.83373419311011e-5 6.882532976042983e-35; 7.477393312557031e21 4.0612677614489006e10 … 7385.894750069348 -5.748908232557507e-33; 3.73869665632483e11 NaN … NaN NaN], [0.95 0.27396311798639467 … 9.834806803918683e-5 6.88328368570378e-35; 1.1035327180218686e22 4.124739691918747e10 … 7386.7003676582835 -5.555387825881395e-33; 5.517663590156324e11 NaN … NaN NaN], [0.96 0.2743558416170826 … 9.835858746880248e-5 6.884019930143553e-35; 1.6286216450586584e22 4.188272685263071e10 … 7387.490462021435 -5.370902597815067e-33; 8.143108225340942e11 NaN … NaN NaN], [0.97 0.2747430116886644 … 9.836890702300541e-5 6.884742185501539e-35; 2.4035612351468206e22 4.2518651135954315e10 … 7388.265544123866 -5.194918918836546e-33; 1.201780617578242e12 NaN … NaN NaN], [0.98 0.2751247784098808 … 9.83790331939987e-5 6.885450906160586e-35; 3.5472367867813983e22 4.3155154121883064e10 … 7389.026101581772 -5.026940945425737e-33; 1.7736183933955977e12 NaN … NaN NaN], [0.99 0.2755012861005022 … 9.838897218110978e-5 6.88614652600547e-35; 5.235102246407546e22 4.37922207629412e10 … 7389.772600015129 -4.866507654782349e-33; 2.6175511232087393e12 NaN … NaN NaN], [1.0 0.2758726734903106 … 9.839872990752456e-5 6.886829459594095e-35; 7.726097009500335e22 4.4429836581612366e10 … 7390.50548430307 -4.7131898214291714e-33; 3.8630485047552017e12 NaN … NaN NaN]], 0.0:0.01:1.0, NewtonSolverHistory[]))
pbo_sols[end]
3×52 VoronoiFVM.SparseSolutionArray{Float64, 2, Int32}:
1.0 0.275873 0.202794 0.169966 … 9.83987e-5 6.88683e-35
7.7261e22 4.44298e10 2.58457e9 7.20254e8 7390.51 -4.71319e-33
3.86305e12 NaN NaN NaN NaN NaN
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_spacechargedensity(ϕ)
sumyz = zero(ϕ)
for i in 1:length(z)
sumyz += z[i] * pbi_molfrac(ϕ, i)
end
return F * c̄ * sumyz
end
pbi_spacechargedensity (generic function with 1 method)
function pbi_reaction(y, u, node, data)
y[iφ] = u[iq]
y[iq] = u[iq] - pbi_spacechargedensity(u[iφ])
return
end
pbi_reaction (generic function with 1 method)
begin
pbi_system = VoronoiFVM.System(
grid;
reaction = pbi_reaction,
flux = pb_flux,
bcondition = pb_bc,
generic = pb_generic,
unkown_storage = :sparse
)
enable_species!(pbi_system, species = [iφ, iq], regions = [1])
enable_boundary_species!(pbi_system, iQ, [1])
end
pbi_volts, pbi_caps, pbi_sols = dlcapsweep(pbi_system)
(0.0:0.01:1.0, [0.22792112829474656, 0.2323166562568724, 0.24558296238939673, 0.2682244335553471, 0.3011027537086486, 0.34547100866758745, 0.40302337127411797, 0.4759624294985512, 0.5670867874531943, 0.6799016647915379 … 1.6615077567161407, 1.648492447738903, 1.6351761185662284, 1.6216101924282356, 1.6078596289714753, 1.5940011670112497, 1.5801209693444562, 1.5663116858499748, 1.5526690268430343, 1.5392880156372968], TransientSolution{Any, 3, Vector{Any}, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}(Any[[0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0], [0.01 0.009674565740806013 … 9.237401913675065e-6 6.4651659314094e-36; 770161.7929485585 743907.4659817506 … 693.7986307859829 4.887886501479563e-42; 0.002293697778634902 0.0 … 0.0 0.0], [0.02 0.01933739458710759 … 1.8303078174837674e-5 1.281014278275626e-35; 1.6583313857287976e6 1.5932736862664495e6 … 1374.6994550178708 -2.4669743573360575e-41; 0.004674983468875458 0.0 … 0.0 0.0], [0.03 0.02897639891080578 … 2.703789214876588e-5 1.8923552402601653e-35; 2.8005284525481714e6 2.6672851317663076e6 … 2030.7501808917302 -1.7603939703404386e-40; 0.007234774241549017 0.0 … 0.0 0.0], [0.04 0.03857879032931354 … 3.530597861058704e-5 2.471030406832481e-35; 4.371501221533081e6 4.1139336116121896e6 … 2651.7463482960966 -1.4740458217579362e-39; 0.010070777407589814 0.0 … 0.0 0.0], [0.05 0.04813073300819722 … 4.3002093324967484e-5 3.009673838401386e-35; 6.611230735607479e6 6.127788495814615e6 … 3229.783333125046 -6.309165037159845e-39; 0.013291219913180824 0.0 … 0.0 0.0], [0.06 0.05761700690912229 … 5.005498639671611e-5 3.503298825507761e-35; 9.861096225129828e6 8.971752594172677e6 … 3759.5097137046223 -1.816785039015882e-38; 0.017018994557003328 0.0 … 0.0 0.0], [0.07 0.06702069411926119 … 5.642697455746506e-5 3.9492679535937835e-35; 1.4614839448078213e7 1.3003887141167352e7 … 4238.095153689524 -3.950104810928035e-38; 0.021396388408061188 0.0 … 0.0 0.0], [0.08 0.07632291087241937 … 6.211050841066459e-5 4.3470528478866273e-35; 2.159126333078234e7 1.870988050535633e7 … 4664.972585899112 -6.993374179358427e-38; 0.02659058250018928 0.0 … 0.0 0.0], [0.09 0.08550262057379068 … 6.712283647877935e-5 4.697860715356911e-35; 3.1837919333442215e7 2.6740696820269518e7 … 5041.43745124439 -1.0564943530408122e-37; 0.0328001393477958 0.0 … 0.0 0.0] … [0.91 0.6216348682552673 … 0.00010614269340449226 7.428821779973225e-35; 5.355707815488365e9 5.355706892479685e9 … 7972.140186865812 -7.390556397265991e-42; 2.271748421230688 0.0 … 0.0 0.0], [0.92 0.6292533108610063 … 0.00010624974683503027 7.436314343341398e-35; 5.355707815492339e9 5.355707129328256e9 … 7980.1807762440185 4.9776824259402936e-43; 2.2882993414473707 0.0 … 0.0 0.0], [0.93 0.6368907011963554 … 0.0001063566063892804 7.443793337500575e-35; 5.355707815495032e9 5.355707305777183e9 … 7988.206804063241 -2.564120636256701e-41; 2.3047185816724305 0.0 … 0.0 0.0], [0.94 0.6445474374833199 … 0.00010646329044417426 7.451260048633966e-35; 5.355707815496857e9 5.35570743713736e9 … 7996.219650580693 1.1930529491956295e-41; 2.321003374388665 0.0 … 0.0 0.0], [0.95 0.6522238341336632 … 0.00010656980411580759 7.458714834812033e-35; 5.355707815498093e9 5.355707534859713e9 … 8004.219700056957 -8.41145412002479e-42; 2.3371515345401632 0.0 … 0.0 0.0], [0.96 0.659920103452666 … 0.00010667613931128037 7.466157129620735e-35; 5.355707815498931e9 5.355707607503623e9 … 8012.20634464818 -3.818329108636591e-42; 2.353161586684778 0.0 … 0.0 0.0], [0.97 0.6676363402950177 … 0.00010678227517388473 7.473585473319784e-35; 5.355707815499498e9 5.35570766146342e9 … 8020.178017844658 -1.5372162151885555e-41; 2.369032871630028 0.0 … 0.0 0.0], [0.98 0.6753725105302449 … 0.00010688817895392736 7.480997573790647e-35; 5.355707815499884e9 5.355707701513126e9 … 8028.132259874226 -4.617711785315587e-42; 2.384765626593753 0.0 … 0.0 0.0], [0.99 0.683128444098655 … 0.00010699380731248987 7.488390397785434e-35; 5.355707815500143e9 5.355707731214766e9 … 8036.065815628824 2.73877951142391e-42; 2.4003610334619734 0.0 … 0.0 0.0], [1.0 0.6909038332509377 … 0.00010709910803411353 7.495760290796003e-35; 5.355707815500321e9 5.355707753224251e9 … 8043.974763298014 9.625616705354513e-42; 2.41582123102185 0.0 … 0.0 0.0]], 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)[iφ, :]
cp = c̄ * pb_molfrac.(ϕ, 1) / ufac"mol/dm^3"
cm = c̄ * pb_molfrac.(ϕ, 2) / ufac"mol/dm^3"
q = pbsols(pbv)[iq, :] / (F * 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)
scalarplot!(vis[1, 2], X / nm, q, color = :green, clear = false)
return reveal(vis)
end
plotsols (generic function with 1 method)