Coverage for gpaw/solvation/poisson.py: 91%

160 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-07-14 00:18 +0000

1import warnings 

2 

3import numpy as np 

4from scipy.special import erf 

5 

6from gpaw.poisson import BasePoissonSolver, PoissonSolver, FDPoissonSolver 

7from gpaw.fd_operators import Laplace, Gradient 

8from gpaw.wfd_operators import WeightedFDOperator 

9from gpaw.utilities.gauss import Gaussian 

10from gpaw import PoissonConvergenceError 

11from gpaw.utilities.timing import NullTimer 

12 

13 

14class SolvationPoissonSolver(FDPoissonSolver): 

15 """Base class for Poisson solvers with spatially varying dielectric. 

16 

17 The Poisson equation 

18 div(epsilon(r) grad phi(r)) = -4 pi rho(r) 

19 is solved. 

20 """ 

21 

22 def __init__(self, nn=3, relax='J', eps=2e-10, maxiter=1000, 

23 remove_moment=None, use_charge_center=False): 

24 if remove_moment is not None: 

25 raise NotImplementedError( 

26 'Removing arbitrary multipole moments ' 

27 'is not implemented for SolvationPoissonSolver!') 

28 if nn == 'M': 

29 raise NotImplementedError( 

30 'Mehrstellen stencil is not implemented ' 

31 'for SolvationPoissonSolver!') 

32 FDPoissonSolver.__init__(self, nn, relax, eps, maxiter, remove_moment, 

33 use_charge_center=use_charge_center) 

34 

35 def set_dielectric(self, dielectric): 

36 """Set the dielectric. 

37 

38 Arguments: 

39 dielectric -- A Dielectric instance. 

40 """ 

41 self.dielectric = dielectric 

42 

43 def load_gauss(self, center=None): 

44 """Load compensating charge distribution for charged systems. 

45 

46 See Appendix B of 

47 A. Held and M. Walter, J. Chem. Phys. 141, 174108 (2014). 

48 """ 

49 # XXX Check if update is needed (dielectric changed)? 

50 epsr, dx_epsr, dy_epsr, dz_epsr = self.dielectric.eps_gradeps 

51 gauss = Gaussian(self.gd, center=center) 

52 rho_g = gauss.get_gauss(0) 

53 phi_g = gauss.get_gauss_pot(0) 

54 x, y, z = gauss.xyz 

55 fac = 2. * np.sqrt(gauss.a) * np.exp(-gauss.a * gauss.r2) 

56 fac /= np.sqrt(np.pi) * gauss.r2 

57 fac -= erf(np.sqrt(gauss.a) * gauss.r) / (gauss.r2 * gauss.r) 

58 fac *= 2.0 * 1.7724538509055159 

59 dx_phi_g = fac * x 

60 dy_phi_g = fac * y 

61 dz_phi_g = fac * z 

62 sp = dx_phi_g * dx_epsr + dy_phi_g * dy_epsr + dz_phi_g * dz_epsr 

63 rho = epsr * rho_g - 1. / (4. * np.pi) * sp 

64 invnorm = np.sqrt(4. * np.pi) / self.gd.integrate(rho) 

65 self.phi_gauss = phi_g * invnorm 

66 self.rho_gauss = rho * invnorm 

67 

68 

69class WeightedFDPoissonSolver(SolvationPoissonSolver): 

70 """Weighted finite difference Poisson solver with dielectric. 

71 

72 Following V. M. Sanchez, M. Sued and D. A. Scherlis, 

73 J. Chem. Phys. 131, 174108 (2009). 

74 """ 

75 

76 def create_laplace(self, gd, scale=1.0, n=1, dtype=float): 

77 operators = [Laplace(gd, scale, n, dtype)] 

78 operators += [Gradient(gd, j, scale, n, dtype) for j in (0, 1, 2)] 

79 return WeightedFDOperator(operators) 

80 

81 def solve(self, phi, rho, charge=None, 

82 maxcharge=1e-6, 

83 zero_initial_phi=False, timer=None): 

84 self._init() 

85 if self.gd.pbc_c.all(): 

86 actual_charge = self.gd.integrate(rho) 

87 if abs(actual_charge) > maxcharge: 

88 raise NotImplementedError( 

89 'charged periodic systems are not implemented') 

90 self.restrict_op_weights() 

91 ret = FDPoissonSolver.solve(self, phi, rho, charge, maxcharge, 

92 zero_initial_phi) 

93 return ret 

94 

95 def restrict_op_weights(self): 

96 """Restric operator weights to coarse grids.""" 

97 self._init() 

98 weights = [self.dielectric.eps_gradeps] + self.op_coarse_weights 

99 for i, res in enumerate(self.restrictors): 

100 for j in range(4): 

101 res.apply(weights[i][j], weights[i + 1][j]) 

102 self.step = 0.66666666 / self.operators[0].get_diagonal_element() 

103 

104 def get_description(self): 

105 description = SolvationPoissonSolver.get_description(self) 

106 return description.replace( 

107 'solver with', 

108 'weighted FD solver with dielectric and') 

109 

110 def _init(self): 

111 if self._initialized: 

112 return 

113 self.operators[0].set_weights(self.dielectric.eps_gradeps) 

114 self.op_coarse_weights = [] 

115 for operator in self.operators[1:]: 

116 weights = [gd.empty() for gd in (operator.gd, ) * 4] 

117 self.op_coarse_weights.append(weights) 

118 operator.set_weights(weights) 

119 return SolvationPoissonSolver._init(self) 

120 

121 

122class PolarizationPoissonSolver(BasePoissonSolver): 

123 """Poisson solver with dielectric. 

124 

125 Calculates the polarization charges first using only the 

126 vacuum poisson equation, then solves the vacuum equation 

127 with polarization charges. 

128 """ 

129 

130 def __init__(self, nn=3, relax='J', eps=2e-10, maxiter=1000, 

131 remove_moment=None, use_charge_center=False, 

132 gas_phase_poisson='fast'): 

133 self.nn = nn 

134 self.eps = eps 

135 self.maxiter = maxiter 

136 self.phi_tilde = None 

137 

138 self.gas_phase_poisson = PoissonSolver( 

139 name=gas_phase_poisson, nn=nn, eps=eps, 

140 remove_moment=remove_moment, 

141 use_charge_center=use_charge_center) 

142 

143 def set_dielectric(self, dielectric): 

144 """Set the dielectric. 

145 

146 Arguments: 

147 dielectric -- A Dielectric instance. 

148 """ 

149 self.dielectric = dielectric 

150 

151 def get_description(self): 

152 return ('PolarizationPoissonSolver based on ' 

153 + self.gas_phase_poisson.get_description()) 

154 

155 def set_grid_descriptor(self, gd): 

156 self.gd = gd 

157 self.gas_phase_poisson.set_grid_descriptor(gd) 

158 

159 def solve(self, phi, rho, charge=None, 

160 maxcharge=1e-6, 

161 zero_initial_phi=False, timer=NullTimer()): 

162 # get initial meaningful phi -> only do this if necessary 

163 if zero_initial_phi: 

164 niter = self.gas_phase_poisson.solve( 

165 phi, rho, charge=None, maxcharge=maxcharge, 

166 zero_initial_phi=zero_initial_phi, timer=timer) 

167 else: 

168 niter = 0 

169 

170 phi_old = phi.copy() 

171 while niter < self.maxiter: 

172 rho_mod = self.rho_with_polarization_charge(phi, rho) 

173 niter += self.gas_phase_poisson.solve( 

174 phi, rho_mod, charge=None, maxcharge=maxcharge, 

175 zero_initial_phi=zero_initial_phi, timer=timer) 

176 residual = phi - phi_old 

177 error = self.gd.comm.sum_scalar( 

178 np.dot(residual.ravel(), residual.ravel())) * self.gd.dv 

179 if error < self.eps: 

180 return niter 

181 phi_old = phi.copy() 

182 

183 raise PoissonConvergenceError( 

184 'PolarizationPoisson solver did not converge in ' 

185 + f'{niter} iterations!') 

186 

187 def rho_with_polarization_charge(self, phi, rho): 

188 epsr, dx_epsr, dy_epsr, dz_epsr = self.dielectric.eps_gradeps 

189 dx_phi = self.gd.empty() 

190 dy_phi = self.gd.empty() 

191 dz_phi = self.gd.empty() 

192 Gradient(self.gd, 0, 1.0, self.nn).apply(phi, dx_phi) 

193 Gradient(self.gd, 1, 1.0, self.nn).apply(phi, dy_phi) 

194 Gradient(self.gd, 2, 1.0, self.nn).apply(phi, dz_phi) 

195 

196 scalar_product = ( 

197 dx_epsr * dx_phi + 

198 dy_epsr * dy_phi + 

199 dz_epsr * dz_phi) 

200 

201 return (rho + scalar_product / (4. * np.pi)) / epsr 

202 

203 def estimate_memory(self, mem): 

204 # XXX estimate your own contribution 

205 return self.gas_phase_poisson.estimate_memory(mem) 

206 

207 def todict(self): 

208 my_dict = self.gas_phase_poisson.todict() 

209 my_dict['name'] = f"polarization-{my_dict['name']}" 

210 return my_dict 

211 

212 

213class ADM12PoissonSolver(SolvationPoissonSolver): 

214 """Poisson solver with dielectric. 

215 

216 Following O. Andreussi, I. Dabo, and N. Marzari, 

217 J. Chem. Phys. 136, 064102 (2012). 

218 

219 Warning: Not intended for production use, as it is not tested 

220 thouroughly! 

221 

222 XXX TODO : * Correction for charged systems??? 

223 * Check: Can the polarization charge introduce a monopole? 

224 * Convergence problems depending on eta. Apparently this 

225 method works best with FFT as in the original Paper. 

226 * Optimize numerics. 

227 """ 

228 

229 def __init__(self, nn=3, relax='J', eps=2e-10, maxiter=1000, 

230 remove_moment=None, eta=.6, use_charge_center=False): 

231 """Constructor for ADM12PoissonSolver. 

232 

233 Additional arguments not present in SolvationPoissonSolver: 

234 eta -- linear mixing parameter 

235 """ 

236 adm12_warning = UserWarning( 

237 'ADM12PoissonSolver is not tested thoroughly' 

238 ' and therefore not recommended for production code!') 

239 warnings.warn(adm12_warning) 

240 self.eta = eta 

241 SolvationPoissonSolver.__init__( 

242 self, nn, relax, eps, maxiter, remove_moment, 

243 use_charge_center=use_charge_center) 

244 

245 def set_grid_descriptor(self, gd): 

246 SolvationPoissonSolver.set_grid_descriptor(self, gd) 

247 self.gradx = Gradient(gd, 0, 1.0, self.nn) 

248 self.grady = Gradient(gd, 1, 1.0, self.nn) 

249 self.gradz = Gradient(gd, 2, 1.0, self.nn) 

250 

251 def get_description(self): 

252 if len(self.operators) == 0: 

253 return 'uninitialized ADM12PoissonSolver' 

254 else: 

255 description = SolvationPoissonSolver.get_description(self) 

256 return description.replace( 

257 'solver with', 

258 'ADM12 solver with dielectric and') 

259 

260 def _init(self): 

261 if self._initialized: 

262 return 

263 self.rho_iter = self.gd.zeros() 

264 self.d_phi = self.gd.empty() 

265 return SolvationPoissonSolver._init(self) 

266 

267 def solve(self, phi, rho, charge=None, 

268 maxcharge=1e-6, 

269 zero_initial_phi=False, timer=None): 

270 self._init() 

271 if self.gd.pbc_c.all(): 

272 actual_charge = self.gd.integrate(rho) 

273 if abs(actual_charge) > maxcharge: 

274 raise NotImplementedError( 

275 'charged periodic systems are not implemented') 

276 return FDPoissonSolver.solve( 

277 self, phi, rho, charge, maxcharge, zero_initial_phi, 

278 timer=timer) 

279 

280 def solve_neutral(self, phi, rho, timer=None): 

281 self._init() 

282 self.rho = rho 

283 return SolvationPoissonSolver.solve_neutral(self, phi, rho) 

284 

285 def iterate2(self, step, level=0): 

286 self._init() 

287 if level == 0: 

288 epsr, dx_epsr, dy_epsr, dz_epsr = self.dielectric.eps_gradeps 

289 self.gradx.apply(self.phis[0], self.d_phi) 

290 sp = dx_epsr * self.d_phi 

291 self.grady.apply(self.phis[0], self.d_phi) 

292 sp += dy_epsr * self.d_phi 

293 self.gradz.apply(self.phis[0], self.d_phi) 

294 sp += dz_epsr * self.d_phi 

295 self.rho_iter = self.eta / (4. * np.pi) * sp + \ 

296 (1. - self.eta) * self.rho_iter 

297 self.rhos[0][:] = (self.rho_iter + self.rho) / epsr 

298 return SolvationPoissonSolver.iterate2(self, step, level)