Coverage for fxdgm/EqN.py: 94%

110 statements  

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

1''' 

2Jan Habscheid 

3Jan.Habscheid@rwth-aachen.de 

4 

5This script implements the fenics solver for the generic system of equations for N species 

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, ln, Mesh 

14from basix.ufl import element, mixed_element 

15import matplotlib.pyplot as plt 

16from fxdgm.RefinedMesh1D import create_refined_mesh 

17 

18def solve_System_Neq(phi_left:float, phi_right:float, p_right:float, z_alpha:list, y_R:list, 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='Vector', rtol:float=1e-8, max_iter:float=500): 

19 ''' 

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

21 

22 System of equations: 

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

24 

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

26  

27 div(J_α)=0  α∈ {1,...,N−1} 

28 

29 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. 

30 

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

32 

33 Parameters 

34 ---------- 

35 phi_left : float 

36 Value of φ at the left boundary 

37 phi_right : float 

38 Value of φ at the right boundary 

39 p_right : float 

40 Value of p at the right boundary 

41 z_alpha : list 

42 Charge numbers for species α = 1,...,N-1 

43 y_R : list 

44 Atomic fractions at right boundary for species α = 1,...,N-1 

45 K : float | str 

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

47 Lambda2 : float 

48 Dimensionless parameter 

49 a2 : float 

50 Dimensionless parameter 

51 number_cells : int 

52 Number of cells in the mesh 

53 solvation : float, optional 

54 solvation number, not implemented yet, by default 0 

55 PoissonBoltzmann : bool, optional 

56 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, Not implemented yet, by default False 

57 relax_param : float, optional 

58 Relaxation parameter for the Newton solver 

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

60 , by default None -> Determined automatically 

61 x0 : float, optional 

62 Left boundary of the domain, by default 0 

63 x1 : float, optional 

64 Right boundary of the domain, by default 1 

65 refinement_style : str, optional 

66 Specify for refinement towards zero 

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

68 return_type : str, optional 

69 'Vector' or 'Scalar' (not implemented yet, should be implemented in a later version), 'Scalar' returns dolfinx.fem type and 'Vector' numpy arrays of the solution, by default 'Vector' 

70 rtol : float, optional 

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

72 max_iter : float, optional 

73 Maximum number of Newton iterations, by default 500 

74 

75 Returns 

76 ------- 

77 y_A, y_C, phi, p, msh 

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

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

80 Only return_type 'Vector' is implemented yet 

81 ''' 

82 if solvation != 0: 

83 raise NotImplementedError('Solvation number not implemented yet') 

84 if PoissonBoltzmann: 

85 raise NotImplementedError('Poisson-Boltzmann not implemented yet') 

86 # Define boundaries of the domain 

87 x0 = 0 

88 x1 = 1 

89 

90 # Define boundaries 

91 def Left(x): 

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

93 

94 def Right(x): 

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

96 

97 # Create mesh 

98 if refinement_style == 'uniform': 

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

100 else: 

101 msh = create_refined_mesh(refinement_style, number_cells) 

102 

103 # Define Finite Elements 

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

105 

106 # Define Mixed Function Space 

107 Elem_list = [CG1_elem, CG1_elem] 

108 [Elem_list.append(CG1_elem) for _ in range(len(z_alpha))] 

109 W_elem = mixed_element(Elem_list) 

110 W = fem.functionspace(msh, W_elem) 

111 

112 # Define Trial- and Testfunctions 

113 u = fem.Function(W) 

114 my_TrialFunctions = split(u) 

115 my_TestFunctions = TestFunctions(W) 

116 

117 phi, p = my_TrialFunctions[0], my_TrialFunctions[1] 

118 v_1, v_2 = my_TestFunctions[0], my_TestFunctions[1] 

119 y_alpha = my_TrialFunctions[2:] 

120 v_alpha = my_TestFunctions[2:] 

121 

122 # Collapse function space for bcs 

123 W_ = [] 

124 [W_.append(W.sub(i).collapse()[0]) for i in range(len(z_alpha)+2)] 

125 

126 # Define boundary conditions values 

127 def phi_left_(x): 

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

129 def phi_right_(x): 

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

131 def p_right_(x): 

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

133 

134 # Interpolate bcs functions 

135 phi_left_bcs = fem.Function(W_[0]) 

136 phi_left_bcs.interpolate(phi_left_) 

137 phi_right_bcs = fem.Function(W_[0]) 

138 phi_right_bcs.interpolate(phi_right_) 

139 p_right_bcs = fem.Function(W_[1]) 

140 p_right_bcs.interpolate(p_right_) 

141 

142 # Identify dofs for boundary conditions 

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

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

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

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

147 

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

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

150 

151 # Combine boundary conditions for electric potential and pressure into list 

152 bcs = [bc_left_phi, bc_right_phi, bc_right_p] 

153 

154 # Repeat the same for the boundary conditoins for the atomic fractions 

155 for i in range(len(z_alpha)): 

156 y_right_bcs = fem.Function(W_[i+2]) 

157 def y_right_(x): 

158 return np.full_like(x[0], y_R[i]) 

159 y_right_bcs.interpolate(y_right_) 

160 facet_right_dofs = fem.locate_dofs_geometrical((W.sub(i+2), W.sub(i+2).collapse()[0]), Right) 

161 bc_right_y = fem.dirichletbc(y_right_bcs, facet_right_dofs, W.sub(i+2)) 

162 bcs.append(bc_right_y) 

163 

164 # Define variational problem 

165 if K == 'incompressible': 

166 # total free charge density 

167 def nF(y_alpha): 

168 nF = 0 

169 for i in range(len(z_alpha)): 

170 nF += z_alpha[i] * y_alpha[i] 

171 return nF 

172 

173 # Diffusion fluxes for species A and C 

174 def J_alpha(y_alpha, alpha, phi, p): 

175 mu_alpha = ln(y_alpha[alpha]) 

176 mu_S = ln(1 - sum(y_alpha)) 

177 return grad(mu_alpha - mu_S + z_alpha[alpha] * phi) 

178 

179 # Variational Form 

180 A = ( 

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

182 - 1 / Lambda2 * nF(y_alpha) * v_1 * dx 

183 ) + ( 

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

185 + 1 / a2 * nF(y_alpha) * dot(grad(phi), grad(v_2)) * dx 

186 ) 

187 for alpha in range(len(z_alpha)): 

188 A += ( 

189 inner(J_alpha(y_alpha, alpha, phi, p), grad(v_alpha[alpha])) * dx 

190 ) 

191 if PoissonBoltzmann: 

192 raise ValueError('Poisson-Boltzmann not implemented for incompressible systems') 

193 else: 

194 raise ValueError('Only incompressible systems are implemented') 

195 F = A 

196 

197 # Initialize initial guess for u 

198 with u.vector.localForm() as u_loc: 

199 u_loc.set(0) 

200 

201 # Initialize initial guess for u 

202 for alpha in range(len(z_alpha)): 

203 y_alpha_init = fem.Function(W_[alpha+2]) 

204 y_alpha_init.interpolate(lambda x: np.full_like(x[0], y_R[alpha])) 

205 u.sub(alpha+2).interpolate(y_alpha_init) 

206 

207 # Define Nonlinear Problem 

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

209 

210 # Define Newton Solver 

211 solver = NewtonSolver(MPI.COMM_WORLD, problem) 

212 solver.convergence_criterion = "incremental" 

213 solver.rtol = rtol 

214 if relax_param != None: 

215 solver.relaxation_parameter = relax_param 

216 else: 

217 if phi_right == phi_left: 

218 solver.relaxation_parameter = 1.0 

219 else: 

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

221 solver.max_it = max_iter 

222 solver.report = True 

223 

224 

225 log.set_log_level(log.LogLevel.INFO) 

226 n, converged = solver.solve(u) 

227 assert (converged) 

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

229 

230 # Return the solution  

231 if return_type=='Vector': 

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

233 phi = np.array(u.sub(0).collapse().x.array) 

234 p = np.array(u.sub(1).collapse().x.array) 

235 y = [] 

236 [y.append(u.sub(i+2).collapse().x.array) for i in range(len(z_alpha))] 

237 y = np.array(y) 

238 return y, phi, p, x 

239 

240 

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

242 # Define the parameters 

243 phi_left = 8.0 

244 phi_right = 0.0 

245 p_right = 0.0 

246 y_R = [3/6, 1/6, 1/6] 

247 z_alpha = [-1.0, 1.0, 2.0] 

248 K = 'incompressible' 

249 Lambda2 = 8.553e-6 

250 a2 = 7.5412e-4 

251 number_cells = 1024 

252 relax_param = .05 

253 rtol = 1e-4 

254 max_iter = 2_500 

255 refinement_style = 'hard_log' 

256 return_type = 'Vector' 

257 

258 # Solve the system 

259 y, phi, p, x = solve_System_Neq(phi_left, phi_right, p_right, z_alpha, y_R, K, Lambda2, a2, number_cells, relax_param=relax_param, refinement_style=refinement_style, return_type=return_type, max_iter=max_iter, rtol=rtol) 

260 

261 # Plot the solution 

262 plt.figure() 

263 plt.plot(x, phi) 

264 plt.xlim(0,0.05) 

265 plt.grid() 

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

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

268 plt.show() 

269 

270 plt.figure() 

271 plt.xlim(0,0.05) 

272 plt.plot(x, p) 

273 plt.grid() 

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

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

276 plt.show() 

277 

278 plt.figure() 

279 for i in range(len(z_alpha)): 

280 plt.plot(x, y[i], label=f'$y_{i}$') 

281 plt.xlim(0,0.05) 

282 plt.legend() 

283 plt.grid() 

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

285 plt.ylabel('$y_i$ [-]') 

286 plt.show()