Skip to content

Commit

Permalink
first try at flow battery example
Browse files Browse the repository at this point in the history
  • Loading branch information
tlroy committed Jan 30, 2024
1 parent ec06b33 commit 74ed15b
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 48 deletions.
14 changes: 11 additions & 3 deletions echemfem/flow_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,11 @@ def setup_problem(self):
# dimensional Navier-Stokes
else:
pprint("Using dimensional Navier-Stokes")
rho = Constant(params["density"])
nu = Constant(params["kinematic viscosity"])
rho = params["density"]
if params.get("kinematic viscosity"):
nu = params["kinematic viscosity"]
else:
nu = params["dynamic viscosity"] / rho
F = nu * inner(grad(u), grad(v)) * dx \
+ inner(dot(grad(u), u), v) * dx \
- 1.0/rho * p * div(v) * dx \
Expand Down Expand Up @@ -233,7 +236,10 @@ def setup_problem(self):
else:
pprint("Using dimensional Navier-Stokes-Brinkman")
rho = params["density"]
nu = params["kinematic viscosity"]
if params.get("kinematic viscosity"):
nu = params["kinematic viscosity"]
else:
nu = params["dynamic viscosity"] / rho
# inverse permeability: scalar field only for now
if params.get("permeability"):
inv_K = 1 / params["permeability"]
Expand All @@ -248,6 +254,8 @@ def setup_problem(self):
- 1.0/rho * p * div(v) * dx \
+ nu * inv_K * inner(u, v) * dx \
+ div(u) * q * dx
#- inner(u, grad(q)) * dx
# this sets u.n = 0 on Neumann BCs
if self.boundary_markers.get("inlet pressure"):
n = FacetNormal(self.mesh)
in_id = self.boundary_markers["inlet pressure"]
Expand Down
113 changes: 68 additions & 45 deletions echemfem/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ def print_solver_info(self):
print("Species information")
for c in self.conc_params:
print("> {}".format(c["name"]))
if self.flow["electroneutrality"] or self.flow["poisson"] or self.flow["electroneutrality full"]:
if self.flow["electroneutrality"] or (self.flow["poisson"] and self.flow["migration"]) or self.flow["electroneutrality full"]:
print(" z = {}".format(c["z"]))

print("Solver information")
Expand Down Expand Up @@ -815,6 +815,9 @@ def output_state(self, u, prefix="results/", initiate=True, **kwargs):
if self.flow["poisson"] or self.flow["electroneutrality"] or self.flow["electroneutrality full"]:
collection.append(Function(self.Vu,
name="Liquid Potential").assign(uviz[self.num_mass]))
if self.flow["porous"]:
collection.append(Function(self.Vu,
name="Solid Potential").assign(uviz[self.i_Us]))

if self.flow["electroneutrality"]:
C_el = 0.0
Expand Down Expand Up @@ -1425,8 +1428,9 @@ def mass_conservation_form(

if self.flow["poisson"] or self.flow["electroneutrality"] or self.flow["electroneutrality full"]:
U = u[self.i_Ul]
z = conc_params["z"]
if self.flow["migration"]:
F = self.physical_params["F"]
z = conc_params["z"]
R = self.physical_params["R"]
T = self.physical_params["T"]
K = D * z * F / R / T
Expand Down Expand Up @@ -2010,6 +2014,10 @@ def potential_poisson_form(self, u, test_fn, conc_params, solid=False, W=None, i
if not solid:
if eps_r is not None and eps_0 is not None:
K_U = eps_r * eps_0
elif self.physical_params.get("liquid conductivity"):
K_U = self.physical_params["liquid conductivity"]
if self.flow["porous"]:
K_U = self.effective_diffusion(K_U, phase="liquid")
else:
K_U = 0.0

Expand All @@ -2029,54 +2037,63 @@ def potential_poisson_form(self, u, test_fn, conc_params, solid=False, W=None, i
i_el = None
rang = range(self.num_liquid)
# Do eliminated concentration last
for i in rang:
z = conc_params[i]["z"]
if not i == i_el:
C = u[conc_params[i]["i_c"]]
C_n += z * C # electroneutrality constraint
else:
C = -C_n / z
if (bulk_reaction is not None) and (
not solid) and self.flow["electroneutrality"]:
if reactions[i] != 0.0:
a -= z * F * reactions[i] * \
test_fn * self.dx(domain=self.mesh)

neumann = self.boundary_markers.get("neumann")
if (neumann is not None) and (z != 0.0) and eps_r is None and eps_0 is None:
a -= z * F * test_fn * \
self.neumann(C, conc_params[i], u) * self.ds(neumann)
if not solid and z != 0:
D = conc_params[i]["diffusion coefficient"]
if self.flow["porous"]:
D = self.effective_diffusion(D)
if eps_r is None or eps_0 is None:
K_U += z**2 * F**2 * D * C / R / T
elif z != 0.0:
a -= F * z * C * test_fn * self.dx()

# Echem reaction
name = conc_params[i]["name"]
if not self.flow["porous"]:
for echem in self.echem_params:
electrode = self.boundary_markers.get(
echem["boundary"])
n_ = echem["electrons"]
if name in echem["stoichiometry"]:
nu = echem["stoichiometry"][name]
a -= test_fn * z * nu / n_ * \
echem["reaction"](u) * self.ds(electrode)
if not solid and self.flow["migration"]:
# If electroneutral, this one is used for potential initial guess
# If using Poisson, this includes some things for the "true" Poisson equation
for i in rang:
z = conc_params[i]["z"]
if not i == i_el:
C = u[conc_params[i]["i_c"]]
C_n += z * C # electroneutrality constraint
else:
for echem in self.echem_params:
n_ = echem["electrons"]
if name in echem["stoichiometry"]:
nu = echem["stoichiometry"][name]
a -= av * test_fn * z * nu / n_ * \
echem["reaction"](u) * self.dx()
C = -C_n / z
if (bulk_reaction is not None) and (
not solid) and self.flow["electroneutrality"]:
if reactions[i] != 0.0:
a -= z * F * reactions[i] * \
test_fn * self.dx(domain=self.mesh)

neumann = self.boundary_markers.get("neumann")
if (neumann is not None) and (z != 0.0) and eps_r is None and eps_0 is None:
a -= z * F * test_fn * \
self.neumann(C, conc_params[i], u) * self.ds(neumann)
if not solid and z != 0:
D = conc_params[i]["diffusion coefficient"]
if self.flow["porous"]:
D = self.effective_diffusion(D)
if (eps_r is None or eps_0 is None) and \
self.physical_params.get("liquid conductivity") is None:
K_U += z**2 * F**2 * D * C / R / T
elif z != 0.0:
# right-hand side of true Poisson equation
a -= F * z * C * test_fn * self.dx()

# Echem reaction for charge-conservation eqn
if not self.flow["poisson"]:
name = conc_params[i]["name"]
if not self.flow["porous"]:
for echem in self.echem_params:
electrode = self.boundary_markers.get(
echem["boundary"])
n_ = echem["electrons"]
if name in echem["stoichiometry"]:
nu = echem["stoichiometry"][name]
a -= test_fn * z * nu / n_ * \
echem["reaction"](u) * self.ds(electrode)
else:
for echem in self.echem_params:
n_ = echem["electrons"]
if name in echem["stoichiometry"]:
nu = echem["stoichiometry"][name]
a -= av * test_fn * z * nu / n_ * \
echem["reaction"](u) * self.dx()
if solid:
for echem in self.echem_params:
a += av * test_fn * echem["reaction"](u) * self.dx()
elif not solid and self.flow["poisson"]:
for echem in self.echem_params:
a -= av * test_fn * echem["reaction"](u) * self.dx()


# diffusion of potential
a += inner(K_U * grad(U), grad(test_fn)) * self.dx()
Expand Down Expand Up @@ -2127,6 +2144,12 @@ def potential_poisson_form(self, u, test_fn, conc_params, solid=False, W=None, i
else:
bcs.append(DirichletBC(W.sub(i_bc), U_0, dirichlet)) # for initial guess

I_app = self.physical_params.get("applied current density")
if self.boundary_markers.get("applied solid current") and solid:
a -= I_app * test_fn * self.ds(self.boundary_markers["applied solid current"])
if self.boundary_markers.get("applied liquid current") and not solid:
a -= I_app * test_fn * self.ds(self.boundary_markers["applied liquid current"])

if robin is not None:
U_0 = self.U_app
a -= self.physical_params["gap capacitance"] * \
Expand Down
168 changes: 168 additions & 0 deletions examples/simple_flow_battery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
from firedrake import *
from echemfem import EchemSolver, NavierStokesBrinkmanFlowSolver, NavierStokesFlowSolver
import argparse
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--family", type=str, default='CG')
parser.add_argument("--vel_file", type=str, default=None)
args, _ = parser.parse_known_args()
if args.family == "CG":
SUPG = True
else:
SUPG = False

#flow_rate = 2.4 * 5/3 * 1e-8 # m3/s -> x ml/min
#area = 4e-4 # m2 -> 4 cm2
#v_avg = flow_rate/area
v_avg = 1e-10#
print(v_avg)

current_density = 50 * 10 # A/m2 -> 50 mA/cm2
current_density = 1e-1 # A/m2 -> 50 mA/cm2

electrode_thickness = 500e-6 # m
electrode_length = 500e-6#2e-3 # m
inlet_length = electrode_length/4
outlet_length = electrode_length/4
mesh = RectangleMesh(50, 50, electrode_length, electrode_thickness, quadrilateral=True)
if False:
x, y = SpatialCoordinate(mesh)
V1 = FunctionSpace(mesh, "HDiv Trace", 0)
f3 = Function(V1).interpolate(conditional(And(And(y < 1e-16,
x <= electrode_length - outlet_length),
x >= inlet_length), 1., 0.))
f5 = Function(V1).interpolate(conditional(And(y < 1e-16, x < inlet_length), 1., 0.))
f6 = Function(V1).interpolate(conditional(And(y < 1e-16, x > electrode_length - outlet_length), 1., 0.))
mesh = RelabeledMesh(mesh,
[f3, f5, f6],
[3, 5, 6])

"""
Schematic of the domain:
_____4_____
| |
1 2
| |
|_5_._3_._6_|
"""

class BortelsSolver(EchemSolver):
def __init__(self):
conc_params = []

conc_params.append({"name": "V2",
"diffusion coefficient": 2.4e-10, # m^2/s
"bulk": 1000., # mol/m^3
})

conc_params.append({"name": "V3",
"diffusion coefficient": 2.4e-10, # m^2/s
"bulk": 1000., # mol/m^3
})

#physical_params = {"flow": ["advection", "diffusion", "porous"],
physical_params = {"flow": ["advection", "diffusion", "poisson", "porous"],
"F": 96485.3329, # C/mol
"R": 8.3144598, # J/K/mol
"T": 273.15 + 25., # K
"solid conductivity": 1e4, # S/m
"liquid conductivity": 40, # S/m
"specific surface area": 8e4, # 1/m
"porosity": 0.68,
"U_app": 0., # ground U_solid = 0
"applied current density": Constant(current_density),
}

def reaction(u):
# Butler-Volmer
V2 = u[0]
V3 = u[1]
Phi2 = u[2]
Phi1 = u[3]
#Phi2 = -0.2499
#Phi1 = 0
Cref = 1. # mol/m3
J0 = 0.016 # A/m^2
U0 = -0.25
F = physical_params["F"]
R = physical_params["R"]
T = physical_params["T"]
beta = 0.5 * F / R / T
eta = Phi1 - Phi2 - U0
return J0 / Cref * (V2 * exp(beta * eta)
- V3 * exp(-beta * eta))
#def reaction(u):
# return 1e-1



echem_params = []
echem_params.append({"reaction": reaction,
"electrons": 1,
"stoichiometry": {"V2": 1,
"V3": -1},
})
#echem_params = []
super().__init__(
conc_params,
physical_params,
mesh,
echem_params=echem_params,
family=args.family,
SUPG=SUPG)

def set_boundary_markers(self):
self.boundary_markers = {"inlet": (1,),
"outlet": (2,),
"applied": (3,), # U_solid = 0
"applied liquid current": (4,),
}

def set_velocity(self):
boundary_markers = {"no slip": (3,4,1,2,),
#"inlet velocity": (5,),
#"outlet velocity": (6,),
"outlet pressure": (6,),
"inlet pressure": (5,),
}
boundary_markers = {"no slip": (3,4,),
#"inlet velocity": (5,),
#"outlet velocity": (6,),
"outlet pressure": (2,),
"inlet pressure": (1,),
}

#vel = as_vector([Constant(0), Constant(v_avg)])
#vel_out = as_vector([Constant(0), Constant(-v_avg)])
x, y = SpatialCoordinate(self.mesh)
vel = as_vector([Constant(0), 4 * Constant(v_avg) * x * (Constant(inlet_length) - x)/Constant(inlet_length**2)])
vel_out = as_vector([Constant(0), 4 * Constant(-v_avg) * (x - Constant(electrode_length-outlet_length)) * (Constant(electrode_length) - x)/Constant(outlet_length**2)])
flow_params = {"inlet velocity": vel,
"outlet pressure": 0.,
"inlet pressure": 4e-3,
"outlet velocity": vel_out,
"density": 1e3, # kg/m3
"dynamic viscosity": 8.9e-4, # Pa s
"permeability": 5.53e-11 # m2
}
NS_solver = NavierStokesBrinkmanFlowSolver(mesh, flow_params, boundary_markers)
#NS_solver = NavierStokesFlowSolver(mesh, flow_params, boundary_markers)
NS_solver.setup_solver()
NS_solver.solve()
self.vel = NS_solver.vel


solver = BortelsSolver()
solver.setup_solver(initial_solve=False)
solver.solve()
us = solver.u.subfunctions
i_n = Function(solver.V, name="Current Density").interpolate(solver.echem_params[0]["reaction"](solver.u))
File("results/current_density.pvd").write(i_n)
Vec = VectorFunctionSpace(solver.mesh, "CG", 1)
kappa_0 = solver.physical_params["liquid conductivity"]
kappa = solver.physical_params["porosity"]**1.5 * kappa_0
i_l = Function(Vec, name="ionic current").interpolate(kappa * grad(us[solver.i_Ul]))
File("results/current.pvd").write(i_l)
av = solver.physical_params["specific surface area"]
print(assemble(av * i_n * dx)/assemble(Constant(1) * dx(domain=solver.mesh)))
print(current_density/electrode_length)

0 comments on commit 74ed15b

Please sign in to comment.