Coverage for fxdgm/Eq02.py: 96%

101 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-13 12:39 +0000

1''' 

2Jan Habscheid 

3Jan.Habscheid@rwth-aachen.de 

4 

5This file implements the fenics solver for the reduced systme of equations to two equations for the electric potential and the pressure. 

6''' 

7 

8import numpy as np 

9from mpi4py import MPI 

10from dolfinx import mesh, fem, log 

11from dolfinx.fem.petsc import NonlinearProblem 

12from dolfinx.nls.petsc import NewtonSolver 

13from ufl import TestFunctions, split, dot, grad, dx, inner, Mesh, exp 

14from basix.ufl import element, mixed_element 

15import matplotlib.pyplot as plt 

16from fxdgm.RefinedMesh1D import create_refined_mesh 

17 

18 

19def solve_System_2eq(phi_left:float, phi_right:float, p_right:float, z_A:float, z_C:float, y_A_R:float, y_C_R:float, K:float|str, Lambda2:float, a2:float, number_cells:int, solvation:float = 0, PoissonBoltzmann:bool=False, relax_param:float=None, x0:float=0, x1:float=1, refinement_style:str='uniform', return_type:str='Scalar', rtol:float=1e-8, max_iter:float=500): 

20 ''' 

21 Solve the simplified dimensionless system of equations presented in: Numerical Treatment of a Thermodynamically Consistent Electrolyte Model, B.Sc. Thesis Habscheid 2024 

22 

23 System of equations: 

24 λ²Δ φ =−L²n^F 

25  

26 a²∇p=−n^F∇ φ 

27  

28 with the space charge 

29 n^F = z_A y_A(φ, p) + z_C y_C(φ ,p) 

30 

31 if the mixture is compressible:  

32 y_alpha = C_alpha * (K+p−1)^(−κ+1)a²K exp(−z_α φ) 

33 

34 if the mixture is incompressible:  

35 y_alpha = D_alpha * exp(−(κ+1)a²p−z_α φ) 

36 

37 with φ the electric potential, p the pressure, n^F the total free charge density, J_α the diffusion fluxes of species α, λ² a dimensionless parameter, L²=1, a² a dimensionless parameter, N the number of species, and α the species index. 

38 

39 ! If the Newton solver diverges, you may try to reduce the relaxation parameter. 

40 

41 Parameters 

42 ---------- 

43 phi_left : float 

44 Value of φ at the left boundary 

45 phi_right : float 

46 Value of φ at the right boundary 

47 p_right : float 

48 Value of p at the right boundary 

49 z_A : float 

50 Charge number of species A 

51 z_C : float 

52 Charge number of species C 

53 y_A_R : float 

54 Atomic fractions of species A at right boundary 

55 y_C_R : float 

56 Atomic fractions of species C at right boundary 

57 K : float | str 

58 Dimensioness bulk modulus of the electrolyte. If 'incompressible', the system is solved for an incompressible electrolyte 

59 Lambda2 : float 

60 Dimensionless parameter 

61 a2 : float 

62 Dimensionless parameter 

63 number_cells : int 

64 Number of cells in the mesh 

65 solvation : float, optional 

66 solvation number, by default 0 

67 PoissonBoltzmann : bool, optional 

68 Solve classical Nernst-Planck model with the use of the Poisson-Boltzmann formulation if True, else solve the presented model by Dreyer, Guhlke, Müller, by default False 

69 relax_param : float, optional 

70 Relaxation parameter for the Newton solver 

71 xₙ₊₁ = γ xₙ f(xₙ)/f'(xₙ) with γ the relaxation parameter 

72 , by default None -> Determined automatically 

73 x0 : float, optional 

74 Left boundary of the domain, by default 0 

75 x1 : float, optional 

76 Right boundary of the domain, by default 1 

77 refinement_style : str, optional 

78 Specify for refinement towards zero 

79 Options are 'uniform', 'log', 'hard_log', 'hard_hard_log' by default 'uniform' 

80 return_type : str, optional 

81 'Vector' or 'Scalar', 'Scalar' returns dolfinx.fem type and 'Vector' numpy arrays of the solution, by default 'Scalar' 

82 rtol : float, optional 

83 Relative tolerance for Newton solver, by default 1e-8 

84 max_iter : float, optional 

85 Maximum number of Newton iterations, by default 500 

86 

87 Returns 

88 ------- 

89 y_A, y_C, phi, p, msh 

90 Returns atomic fractions for species A and C, electric potential, pressure, and the mesh 

91 If return_type is 'Vector', the solution is returned as numpy arrays 

92 ''' 

93 if return_type == 'Scalar': 

94 raise NotImplementedError('Scalar return type is not implemented yet') 

95 # Define boundaries of the domain 

96 x0 = 0 

97 x1 = 1 

98 

99 # Define boundaries for the boundary conditions 

100 def Left(x): 

101 return np.isclose(x[0], x0) 

102 

103 def Right(x): 

104 return np.isclose(x[0], x1) 

105 

106 # Create mesh 

107 if refinement_style == 'uniform': 

108 msh = mesh.create_unit_interval(MPI.COMM_WORLD, number_cells, dtype=np.float64) 

109 else: 

110 msh = create_refined_mesh(refinement_style, number_cells) 

111 

112 # Define Finite Elements 

113 CG1_elem = element('Lagrange', msh.basix_cell(), 1) 

114 

115 # Define Mixed Function Space 

116 W_elem = mixed_element([CG1_elem, CG1_elem])#, CG1_elem, CG1_elem]) 

117 W = fem.functionspace(msh, W_elem) 

118 

119 # Define Trial- and Testfunctions 

120 u = fem.Function(W) 

121 phi, p = split(u) 

122 (v_1, v_2) = TestFunctions(W) 

123 

124 # Collapse function space for bcs 

125 W0, _ = W.sub(0).collapse() 

126 W1, _ = W.sub(1).collapse() 

127 

128 # Define boundary conditions values 

129 def phi_left_(x): 

130 return np.full_like(x[0], phi_left) 

131 def phi_right_(x): 

132 return np.full_like(x[0], phi_right) 

133 def p_right_(x): 

134 return np.full_like(x[0], p_right) 

135 

136 # Interpolate bcs functions 

137 phi_left_bcs = fem.Function(W0) 

138 phi_left_bcs.interpolate(phi_left_) 

139 phi_right_bcs = fem.Function(W0) 

140 phi_right_bcs.interpolate(phi_right_) 

141 p_right_bcs = fem.Function(W1) 

142 p_right_bcs.interpolate(p_right_) 

143 

144 # Identify dofs for boundary conditions 

145 # Define boundary conditions 

146 facet_left_dofs = fem.locate_dofs_geometrical((W.sub(0), W.sub(0).collapse()[0]), Left) 

147 facet_right_dofs = fem.locate_dofs_geometrical((W.sub(0), W.sub(0).collapse()[0]), Right) 

148 bc_left_phi = fem.dirichletbc(phi_left_bcs, facet_left_dofs, W.sub(0)) 

149 bc_right_phi = fem.dirichletbc(phi_right_bcs, facet_right_dofs, W.sub(0)) 

150 

151 facet_right_dofs = fem.locate_dofs_geometrical((W.sub(1), W.sub(1).collapse()[0]), Right) 

152 bc_right_p = fem.dirichletbc(p_right_bcs, facet_right_dofs, W.sub(1)) 

153 

154 

155 # Combine boundary conditions into list 

156 bcs = [bc_left_phi, bc_right_phi, bc_right_p] 

157 

158 def y_A(phi, p): 

159 if PoissonBoltzmann == False: 

160 D_A = y_A_R / exp(-(solvation + 1) * a2 * p_right - z_A * phi_right) 

161 return D_A * exp(-(solvation + 1) * a2 * p - z_A * phi) 

162 elif PoissonBoltzmann == True: 

163 D_A = y_A_R / exp(- z_A * phi_right) 

164 return D_A * exp(- z_A * phi) 

165 

166 def y_C(phi, p): 

167 if PoissonBoltzmann == False: 

168 D_C = y_C_R / exp(-(solvation + 1) * a2 * - z_C * phi_right) 

169 return D_C * exp(-(solvation + 1) * a2 * p - z_C * phi) 

170 elif PoissonBoltzmann == True: 

171 D_C = y_C_R / exp(- z_C * phi_right) 

172 return D_C * exp(- z_C * phi) 

173 

174 

175 # Define variational problem 

176 if K == 'incompressible': 

177 # total free charge density 

178 def nF(y_A, y_C, p): 

179 return (z_C * y_C + z_A * y_A) 

180 else: 

181 # total number density 

182 def n(p): 

183 return (p-1)/K + 1 

184 

185 # total free charge density 

186 def nF(y_A, y_C, p): 

187 return (z_C * y_C + z_A * y_A) * n(p) 

188 # Variational Form 

189 A = ( 

190 inner(grad(phi), grad(v_1)) * dx 

191 - 1 / Lambda2 * nF(y_A(phi, p), y_C(phi, p), p) * v_1 * dx 

192 ) + ( 

193 inner(grad(p), grad(v_2)) * dx 

194 + 1 / a2 * nF(y_A(phi, p), y_C(phi, p), p) * dot(grad(phi), grad(v_2)) * dx 

195 ) 

196 F = A 

197 

198 # Define Nonlinear Problem 

199 problem = NonlinearProblem(F, u, bcs=bcs) 

200 

201 # Define Newton Solver and solver settings 

202 solver = NewtonSolver(MPI.COMM_WORLD, problem) 

203 solver.convergence_criterion = "incremental" 

204 solver.rtol = rtol 

205 if relax_param != None: 

206 solver.relaxation_parameter = relax_param 

207 else: 

208 if phi_right == phi_left: 

209 solver.relaxation_parameter = 1.0 

210 else: 

211 solver.relaxation_parameter = 1/(np.abs(phi_right-phi_left)**(5/4)) 

212 solver.max_it = max_iter 

213 solver.report = True 

214 

215 # Solve the problem 

216 log.set_log_level(log.LogLevel.INFO) 

217 n, converged = solver.solve(u) 

218 assert (converged) 

219 print(f"Number of interations: {n:d}") 

220 

221 # Split the mixed function space into the individual components  

222 phi, p = u.split() 

223 

224 # Return the solution 

225 if return_type=='Vector': 

226 x_vals = np.array(msh.geometry.x[:,0]) 

227 phi_vals = np.array(u.sub(0).collapse().x.array) 

228 p_vals = np.array(u.sub(1).collapse().x.array) 

229 

230 # Calculate the atomic fractions 

231 D_A = y_A_R / np.exp(-(solvation + 1) * a2 * p_right - z_A * phi_right) 

232 y_A_vals = D_A * np.exp(-(solvation + 1) * a2 * p_vals - z_A * phi_vals) 

233 

234 D_C = y_C_R / np.exp(-(solvation + 1) * a2 * p_right - z_C * phi_right) 

235 y_C_vals = D_C * np.exp(-(solvation + 1) * a2 * p_vals - z_C * phi_vals) 

236 

237 if PoissonBoltzmann: 

238 D_A = y_A_R / np.exp(- z_A * phi_right) 

239 y_A_vals = D_A * np.exp(- z_A * phi_vals) 

240 D_C = y_C_R / np.exp(- z_C * phi_right) 

241 y_C_vals = D_C * np.exp(- z_C * phi_vals) 

242 

243 return y_A_vals, y_C_vals, phi_vals, p_vals, x_vals 

244 

245if __name__ == '__main__': # pragma: no cover # dont cover main in coverage 

246 # Define the parameters 

247 phi_left = 5.0 

248 phi_right = 0.0 

249 p_right = 0.0 

250 y_A_R = 1/3 

251 y_C_R = 1/3 

252 z_A = -1.0 

253 z_C = 1.0 

254 K = 'incompressible' 

255 Lambda2 = 8.553e-6 

256 a2 = 7.5412e-4 

257 number_cells = 1024 

258 relax_param = .1 

259 rtol = 1e-4 

260 max_iter = 500 

261 

262 # Solve the system 

263 y_A, y_C, phi, p, x = solve_System_2eq(phi_left, phi_right, p_right, z_A, z_C, y_A_R, y_C_R, K, Lambda2, a2, number_cells, relax_param=relax_param, x0=0, x1=1, refinement_style='uniform', return_type='Vector', max_iter=max_iter, rtol=rtol) 

264 

265 # Plot the solution 

266 plt.plot(x, phi) 

267 plt.xlim(0,0.05) 

268 plt.grid() 

269 plt.xlabel('x [-]') 

270 plt.ylabel('$\\varphi$ [-]') 

271 plt.show() 

272 

273 plt.plot(x, y_A, '--', color='tab:blue', label='$y_A$') 

274 plt.plot(x, y_C, '-', color='tab:blue', label='$y_C$') 

275 plt.plot(x, 1 - y_A - y_C, ':', color='tab:blue', label='$y_S$') 

276 plt.xlim(0,0.05) 

277 plt.legend() 

278 plt.grid() 

279 plt.xlabel('x [-]') 

280 plt.ylabel('$y_\\alpha$ [-]') 

281 plt.show() 

282 

283 plt.plot(x, p) 

284 plt.xlim(0,0.05) 

285 plt.grid() 

286 plt.xlabel('x [-]') 

287 plt.ylabel('$p$ [-]') 

288 plt.show()