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

634 statements  

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

1import numpy as np 

2from ase.units import kB, Hartree, Bohr 

3from ase.data.vdw import vdw_radii 

4 

5from gpaw.solvation.gridmem import NeedsGD 

6from gpaw.fd_operators import Gradient 

7from gpaw.io.logger import indent 

8 

9BAD_RADIUS_MESSAGE = 'All atomic radii have to be finite and >= zero.' 

10 

11 

12def set_log_and_check_radii(obj, atoms, log): 

13 radii = np.array( 

14 [obj.atomic_radii.get(symbol, vdw_radii[Z]) 

15 for symbol, Z in zip(atoms.symbols, atoms.numbers)], dtype=float) 

16 obj.atomic_radii_output = radii.copy() 

17 obj.symbols = atoms.get_chemical_symbols() 

18 log_radii, a_index, na = np.unique(radii, return_index=True, 

19 return_counts=True) 

20 log_symbols = [atoms.get_chemical_symbols()[a] for a in a_index] 

21 log(f' Atomic radii for {obj.__class__.__name__}:') 

22 log(' ' * 4 + 'Type' + ' ' * 4 + 'Radius' + ' ' * 3 + 'No. of atoms') 

23 outstring = '' 

24 for ia in range(len(log_radii)): 

25 for col in [f'{log_symbols[ia]:>3}', f'{log_radii[ia]:.3f}', 

26 f'{na[ia]}\n']: 

27 outstring += ' ' * 5 + col 

28 log(outstring) 

29 if not np.isfinite(radii).all() or (radii < 0).any(): 

30 raise ValueError(BAD_RADIUS_MESSAGE) 

31 

32 

33def get_pbc_positions(atoms, r_max): 

34 """Return dict mapping atom index to positions in Bohr. 

35 

36 With periodic boundary conditions, it also includes neighbouring 

37 cells up to a distance of r_max (in Bohr). 

38 """ 

39 # code snippet taken from ase/calculators/vdwcorrection.py 

40 pbc_c = atoms.get_pbc() 

41 cell_cv = atoms.get_cell() / Bohr 

42 Rcell_c = np.sqrt(np.sum(cell_cv ** 2, axis=1)) 

43 ncells_c = np.ceil(np.where(pbc_c, 1. + r_max / Rcell_c, 1)).astype(int) 

44 pos_aav = {} 

45 # loop over all atoms in the cell (and neighbour cells for PBC) 

46 for index1, atom in enumerate(atoms): 

47 pos = atom.position / Bohr 

48 pos_aav[index1] = np.empty((np.prod(ncells_c * 2 - 1), 3)) 

49 # loops over neighbour cells 

50 index2 = 0 

51 for ix in range(-ncells_c[0] + 1, ncells_c[0]): 

52 for iy in range(-ncells_c[1] + 1, ncells_c[1]): 

53 for iz in range(-ncells_c[2] + 1, ncells_c[2]): 

54 i_c = np.array([ix, iy, iz]) 

55 pos_aav[index1][index2, :] = pos + np.dot(i_c, cell_cv) 

56 index2 += 1 

57 return pos_aav 

58 

59 

60class Cavity(NeedsGD): 

61 """Base class for representing a cavity in the solvent. 

62 

63 Attributes: 

64 g_g -- The cavity on the fine grid. It varies from zero at the 

65 solute location to one in the bulk solvent. 

66 del_g_del_n_g -- The partial derivative of the cavity with respect to 

67 the electron density on the fine grid. 

68 grad_g_vg -- The gradient of the cavity on the fine grid. 

69 V -- global Volume in Bohr ** 3 or None 

70 A -- global Area in Bohr ** 2 or None 

71 """ 

72 

73 def __init__(self, surface_calculator=None, volume_calculator=None): 

74 """Constructor for the Cavity class. 

75 

76 Arguments: 

77 surface_calculator -- A SurfaceCalculator instance or None 

78 volume_calculator -- A VolumeCalculator instance or None 

79 """ 

80 NeedsGD.__init__(self) 

81 self.g_g = None 

82 self.del_g_del_n_g = None 

83 self.grad_g_vg = None 

84 if isinstance(surface_calculator, dict): 

85 surface_calculator = GradientSurface(**surface_calculator) 

86 if isinstance(volume_calculator, dict): 

87 volume_calculator = KB51Volume(**volume_calculator) 

88 self.surface_calculator = surface_calculator 

89 self.volume_calculator = volume_calculator 

90 self.V = None # global Volume 

91 self.A = None # global Surface 

92 

93 def todict(self): 

94 dct = {} 

95 if self.surface_calculator is not None: 

96 dct['surface_calculator'] = self.surface_calculator.todict() 

97 if self.volume_calculator is not None: 

98 dct['volume_calculator'] = self.volume_calculator.todict() 

99 return dct 

100 

101 @classmethod 

102 def from_dict(cls, dct): 

103 if not isinstance(dct, dict): 

104 return dct 

105 return EffectivePotentialCavity(**dct) 

106 

107 def write(self, writer): 

108 pass 

109 

110 def read(self, reader): 

111 pass 

112 

113 def estimate_memory(self, mem): 

114 ngrids = 1 + self.depends_on_el_density 

115 mem.subnode('Distribution Function', ngrids * self.gd.bytecount()) 

116 mem.subnode( 

117 'Gradient of Distribution Function', 3 * self.gd.bytecount() 

118 ) 

119 if self.surface_calculator is not None: 

120 self.surface_calculator.estimate_memory( 

121 mem.subnode('Surface Calculator') 

122 ) 

123 if self.volume_calculator is not None: 

124 self.volume_calculator.estimate_memory( 

125 mem.subnode('Volume Calculator') 

126 ) 

127 

128 def update(self, atoms, density): 

129 """Update the cavity and its gradient. 

130 

131 atoms are None, iff they have not changed. 

132 

133 Return whether the cavity has changed. 

134 """ 

135 raise NotImplementedError() 

136 

137 def update_vol_surf(self): 

138 """Update volume and surface.""" 

139 if self.surface_calculator is not None: 

140 self.surface_calculator.update(self) 

141 if self.volume_calculator is not None: 

142 self.volume_calculator.update(self) 

143 

144 def communicate_vol_surf(self, world): 

145 """Communicate global volume and surface.""" 

146 if self.surface_calculator is not None: 

147 A = np.array([self.surface_calculator.A]) 

148 self.gd.comm.sum(A) 

149 world.broadcast(A, 0) 

150 self.A = A[0] 

151 else: 

152 self.A = None 

153 if self.volume_calculator is not None: 

154 V = np.array([self.volume_calculator.V]) 

155 self.gd.comm.sum(V) 

156 world.broadcast(V, 0) 

157 self.V = V[0] 

158 else: 

159 self.V = None 

160 

161 def allocate(self): 

162 NeedsGD.allocate(self) 

163 self.g_g = self.gd.empty() 

164 self.grad_g_vg = self.gd.empty(3) 

165 if self.depends_on_el_density: 

166 self.del_g_del_n_g = self.gd.empty() 

167 if self.surface_calculator is not None: 

168 self.surface_calculator.allocate() 

169 if self.volume_calculator is not None: 

170 self.volume_calculator.allocate() 

171 

172 def set_grid_descriptor(self, gd): 

173 NeedsGD.set_grid_descriptor(self, gd) 

174 if self.surface_calculator is not None: 

175 self.surface_calculator.set_grid_descriptor(gd) 

176 if self.volume_calculator is not None: 

177 self.volume_calculator.set_grid_descriptor(gd) 

178 

179 def get_del_r_vg(self, atom_index, density): 

180 """Return spatial derivatives with respect to atomic position.""" 

181 raise NotImplementedError() 

182 

183 @property 

184 def depends_on_el_density(self): 

185 """Return whether the cavity depends on the electron density.""" 

186 raise NotImplementedError() 

187 

188 @property 

189 def depends_on_atomic_positions(self): 

190 """Return whether the cavity depends explicitly on atomic positions.""" 

191 raise NotImplementedError() 

192 

193 def __str__(self): 

194 s = f'Cavity: {self.__class__.__name__}\n' 

195 for calc, calcname in ((self.surface_calculator, 'Surface'), 

196 (self.volume_calculator, 'Volume')): 

197 s += f' {calcname} Calculator: ' 

198 if calc is None: 

199 s += 'None\n' 

200 else: 

201 s += str(calc) 

202 return s 

203 

204 def update_atoms(self, atoms, log): 

205 """Inexpensive initialization when atoms change.""" 

206 pass 

207 

208 def summary(self, log): 

209 """Log cavity surface area and volume.""" 

210 A = (f'{self.A * Bohr ** 2:.5f}' if self.A is not None 

211 else 'not calculated (no calculator defined)') 

212 V = (f'{self.V * Bohr ** 3:.5f}' if self.V is not None 

213 else 'not calculated (no calculator defined)') 

214 log(f'Solvation cavity surface area: {A}') 

215 log(f'Solvation cavity volume: {V}') 

216 

217 

218class EffectivePotentialCavity(Cavity): 

219 """Cavity built from effective potential and Boltzmann distribution. 

220 

221 See also 

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

223 """ 

224 

225 def __init__(self, 

226 effective_potential, 

227 temperature, 

228 surface_calculator=None, 

229 volume_calculator=None): 

230 """Constructor for the EffectivePotentialCavity class. 

231 

232 Additional arguments not present in base Cavity class: 

233 effective_potential -- A Potential instance. 

234 temperature -- Temperature for the Boltzmann distribution 

235 in Kelvin. 

236 """ 

237 Cavity.__init__(self, surface_calculator, volume_calculator) 

238 self.effective_potential = Potential.from_dict(effective_potential) 

239 self.temperature = float(temperature) 

240 self.minus_beta = -1. / (kB * temperature / Hartree) 

241 

242 def todict(self): 

243 return { 

244 'effective_potential': { 

245 'name': self.effective_potential.__class__.__name__, 

246 **self.effective_potential.todict()}, 

247 'temperature': self.temperature, 

248 **super().todict()} 

249 

250 def write(self, writer): 

251 writer.write(effective_potential=self.effective_potential, 

252 temperature=self.temperature, 

253 surface_calculator=self.surface_calculator, 

254 volume_calculator=self.volume_calculator) 

255 

256 # For future, not used at the moment 

257 def read(self, reader): 

258 c = reader.parameters.cavity 

259 self.effective_potential = c.effective_potential 

260 self.temperature = c.temperature 

261 self.surface_calculator = c.surface_calculator 

262 self.volume_calculator = c.volume_calculator 

263 

264 def estimate_memory(self, mem): 

265 Cavity.estimate_memory(self, mem) 

266 self.effective_potential.estimate_memory( 

267 mem.subnode('Effective Potential') 

268 ) 

269 

270 def set_grid_descriptor(self, gd): 

271 Cavity.set_grid_descriptor(self, gd) 

272 self.effective_potential.set_grid_descriptor(gd) 

273 

274 def allocate(self): 

275 Cavity.allocate(self) 

276 self.effective_potential.allocate() 

277 

278 def update(self, atoms, density): 

279 if not self.effective_potential.update(atoms, density): 

280 return False 

281 u_g = self.effective_potential.u_g 

282 np.exp(u_g * self.minus_beta, out=self.g_g) 

283 if self.depends_on_el_density: 

284 self.del_g_del_n_g.fill(self.minus_beta) 

285 self.del_g_del_n_g *= self.g_g 

286 self.del_g_del_n_g *= self.effective_potential.del_u_del_n_g 

287 # This class supports the GradientSurface API, 

288 # so we can use it for the gradient also 

289 grad_inner_vg = self.get_grad_inner() 

290 del_outer_del_inner = self.get_del_outer_del_inner() 

291 for i in (0, 1, 2): 

292 np.multiply( 

293 grad_inner_vg[i], 

294 del_outer_del_inner, 

295 self.grad_g_vg[i] 

296 ) 

297 return True 

298 

299 def get_del_r_vg(self, atom_index, density): 

300 u = self.effective_potential 

301 del_u_del_r_vg = u.get_del_r_vg(atom_index, density) 

302 # there should be no more NaNs now, but let's keep the hint 

303 # asserts lim_(||r - r_atom|| -> 0) dg/du * du/dr_atom = 0 

304 # del_u_del_r_vg[np.isnan(del_u_del_r_vg)] = .0 

305 return self.minus_beta * self.g_g * del_u_del_r_vg 

306 

307 @property 

308 def depends_on_el_density(self): 

309 return self.effective_potential.depends_on_el_density 

310 

311 @property 

312 def depends_on_atomic_positions(self): 

313 return self.effective_potential.depends_on_atomic_positions 

314 

315 def __str__(self): 

316 s = Cavity.__str__(self) 

317 s += f' {self.__class__.__name__}\n' 

318 s += indent(f'{self.effective_potential}') 

319 s += indent(f' temperature: {self.temperature}K') 

320 return s 

321 

322 def update_atoms(self, atoms, log): 

323 self.effective_potential.update_atoms(atoms, log) 

324 

325 # --- BEGIN GradientSurface API --- 

326 

327 def get_grad_inner(self): 

328 return self.effective_potential.grad_u_vg 

329 

330 def get_del_outer_del_inner(self): 

331 return self.minus_beta * self.g_g 

332 

333 # --- END GradientSurface API --- 

334 

335 

336class Potential(NeedsGD): 

337 """Base class for describing an effective potential. 

338 

339 Attributes: 

340 u_g -- The potential on the fine grid in Hartree. 

341 grad_u_vg -- The gradient of the potential of the fine grid. 

342 del_u_del_n_g -- Partial derivative with respect to the electron density. 

343 """ 

344 

345 def __init__(self): 

346 NeedsGD.__init__(self) 

347 self.u_g = None 

348 self.del_u_del_n_g = None 

349 self.grad_u_vg = None 

350 

351 @classmethod 

352 def from_dict(self, dct): 

353 if not isinstance(dct, dict): 

354 return dct 

355 dct = dct.copy() 

356 name = dct.pop('name') 

357 if name == 'Power12Potential': 

358 return Power12Potential(**dct) 

359 assert name == 'SJMPower12Potential' 

360 from gpaw.solvation.sjm import SJMPower12Potential 

361 return SJMPower12Potential(**dct) 

362 

363 @property 

364 def depends_on_el_density(self): 

365 """Return whether the cavity depends on the electron density.""" 

366 raise NotImplementedError() 

367 

368 @property 

369 def depends_on_atomic_positions(self): 

370 """returns whether the cavity depends explicitly on atomic positions""" 

371 raise NotImplementedError() 

372 

373 def estimate_memory(self, mem): 

374 ngrids = 1 + self.depends_on_el_density 

375 mem.subnode('Potential', ngrids * self.gd.bytecount()) 

376 mem.subnode('Gradient of Potential', 3 * self.gd.bytecount()) 

377 

378 def allocate(self): 

379 NeedsGD.allocate(self) 

380 self.u_g = self.gd.empty() 

381 if self.depends_on_el_density: 

382 self.del_u_del_n_g = self.gd.empty() 

383 self.grad_u_vg = self.gd.empty(3) 

384 

385 def update(self, atoms, density): 

386 """Update the potential and its gradient. 

387 

388 atoms are None, iff they have not changed. 

389 

390 Return whether the potential has changed. 

391 """ 

392 raise NotImplementedError() 

393 

394 def get_del_r_vg(self, atom_index, density): 

395 """Return spatial derivatives with respect to atomic position.""" 

396 raise NotImplementedError() 

397 

398 def __str__(self): 

399 return f' Potential: {self.__class__.__name__}\n' 

400 

401 def update_atoms(self, atoms, log): 

402 """Inexpensive initialization when atoms change.""" 

403 pass 

404 

405 

406def get_vdw_radii(atoms): 

407 """Returns a list of van der Waals radii for a given atoms object.""" 

408 return [vdw_radii[n] for n in atoms.numbers] 

409 

410 

411class Power12Potential(Potential): 

412 """Inverse power law potential. 

413 

414 A 1 / r ** 12 repulsive potential taking the value u0 at the atomic radius. 

415 

416 See also 

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

418 

419 Parameters: 

420 

421 atomic_radii: function 

422 Callable mapping an ase.Atoms object to an iterable of atomic radii 

423 in Angstroms. If not provided, defaults to van der Waals radii. 

424 u0: float 

425 Strength of the potential at the atomic radius in eV. 

426 Defaults to 0.18 eV, the best-fit value for water from Held & 

427 Walter. 

428 pbc_cutoff: float 

429 Cutoff in eV for including neighbor cells in a calculation with 

430 periodic boundary conditions. 

431 """ 

432 depends_on_el_density = False 

433 depends_on_atomic_positions = True 

434 

435 def __init__(self, atomic_radii=None, u0=0.180, pbc_cutoff=1e-6, 

436 tiny=1e-10): 

437 Potential.__init__(self) 

438 self.atomic_radii = atomic_radii or {} 

439 self.u0 = float(u0) 

440 self.pbc_cutoff = float(pbc_cutoff) 

441 self.tiny = float(tiny) 

442 self.r12_a = None 

443 self.r_vg = None 

444 self.pos_aav = None 

445 self.del_u_del_r_vg = None 

446 self.atomic_radii_output = None 

447 self.symbols = None 

448 

449 def todict(self): 

450 return { 

451 'atomic_radii': self.atomic_radii, 

452 'u0': self.u0, 

453 'pbc_cutoff': self.pbc_cutoff, 

454 'tiny': self.tiny} 

455 

456 def estimate_memory(self, mem): 

457 Potential.estimate_memory(self, mem) 

458 nbytes = self.gd.bytecount() 

459 mem.subnode('Coordinates', 3 * nbytes) 

460 mem.subnode('Atomic Position Derivative', 3 * nbytes) 

461 

462 def allocate(self): 

463 Potential.allocate(self) 

464 self.r_vg = self.gd.get_grid_point_coordinates() 

465 self.del_u_del_r_vg = self.gd.empty(3) 

466 

467 def update(self, atoms, density): 

468 if atoms is None: 

469 return False 

470 self.r12_a = (self.atomic_radii_output / Bohr) ** 12 

471 r_cutoff = (self.r12_a.max() * self.u0 / self.pbc_cutoff) ** (1. / 12.) 

472 self.pos_aav = get_pbc_positions(atoms, r_cutoff) 

473 self.u_g.fill(.0) 

474 self.grad_u_vg.fill(.0) 

475 na = np.newaxis 

476 for index, pos_av in self.pos_aav.items(): 

477 r12 = self.r12_a[index] 

478 for pos_v in pos_av: 

479 origin_vg = pos_v[:, na, na, na] 

480 r_diff_vg = self.r_vg - origin_vg 

481 r_diff2_g = (r_diff_vg ** 2).sum(0) 

482 r_diff2_g[r_diff2_g < self.tiny] = self.tiny 

483 u_g = r12 / r_diff2_g ** 6 

484 self.u_g += u_g 

485 u_g /= r_diff2_g 

486 r_diff_vg *= u_g[na, ...] 

487 self.grad_u_vg += r_diff_vg 

488 self.u_g *= self.u0 / Hartree 

489 self.grad_u_vg *= -12. * self.u0 / Hartree 

490 # avoid overflow in norm calculation: 

491 self.grad_u_vg[self.grad_u_vg < -1e20] = -1e20 

492 self.grad_u_vg[self.grad_u_vg > 1e20] = 1e20 

493 return True 

494 

495 def get_del_r_vg(self, atom_index, density): 

496 u0 = self.u0 / Hartree 

497 r12 = self.r12_a[atom_index] 

498 na = np.newaxis 

499 self.del_u_del_r_vg.fill(.0) 

500 for pos_v in self.pos_aav[atom_index]: 

501 origin_vg = pos_v[:, na, na, na] 

502 diff_vg = self.r_vg - origin_vg 

503 diff2_g = (diff_vg ** 2).sum(0) 

504 diff2_g[diff2_g < self.tiny] = self.tiny 

505 diff2_g **= 7 

506 diff_vg /= diff2_g[na, ...] 

507 self.del_u_del_r_vg += diff_vg 

508 self.del_u_del_r_vg *= (12. * u0 * r12) 

509 return self.del_u_del_r_vg 

510 

511 def __str__(self): 

512 s = Potential.__str__(self) 

513 s += indent(f' u0: {self.u0}eV\n') 

514 s += indent(f' pbc_cutoff: {self.pbc_cutoff}\n') 

515 s += indent(f' tiny: {self.tiny}\n') 

516 return s 

517 

518 def update_atoms(self, atoms, log): 

519 set_log_and_check_radii(self, atoms, log) 

520 

521 def write(self, writer): 

522 writer.write( 

523 name=self.__class__.__name__, 

524 atomic_radii=self.atomic_radii_output, 

525 u0=self.u0, 

526 pbc_cutoff=self.pbc_cutoff, 

527 tiny=self.tiny) 

528 

529 

530class SmoothStepCavity(Cavity): 

531 """Base class for cavities based on a smooth step function and a density. 

532 

533 Attributes: 

534 del_g_del_rho_g -- Partial derivative with respect to the density. 

535 """ 

536 

537 def __init__( 

538 self, 

539 density, 

540 surface_calculator=None, volume_calculator=None, 

541 ): 

542 """Constructor for the SmoothStepCavity class. 

543 

544 Additional arguments not present in the base Cavity class: 

545 density -- A Density instance 

546 """ 

547 Cavity.__init__(self, surface_calculator, volume_calculator) 

548 self.del_g_del_rho_g = None 

549 self.density = density 

550 

551 @property 

552 def depends_on_el_density(self): 

553 return self.density.depends_on_el_density 

554 

555 @property 

556 def depends_on_atomic_positions(self): 

557 return self.density.depends_on_atomic_positions 

558 

559 def set_grid_descriptor(self, gd): 

560 Cavity.set_grid_descriptor(self, gd) 

561 self.density.set_grid_descriptor(gd) 

562 

563 def estimate_memory(self, mem): 

564 Cavity.estimate_memory(self, mem) 

565 mem.subnode('Cavity Derivative', self.gd.bytecount()) 

566 self.density.estimate_memory(mem.subnode('Density')) 

567 

568 def allocate(self): 

569 Cavity.allocate(self) 

570 self.del_g_del_rho_g = self.gd.empty() 

571 self.density.allocate() 

572 

573 def update(self, atoms, density): 

574 if not self.density.update(atoms, density): 

575 return False 

576 self.update_smooth_step(self.density.rho_g) 

577 if self.depends_on_el_density: 

578 np.multiply( 

579 self.del_g_del_rho_g, 

580 self.density.del_rho_del_n_g, 

581 self.del_g_del_n_g 

582 ) 

583 # This class supports the GradientSurface API, 

584 # so we can use it for the gradient also 

585 grad_inner_vg = self.get_grad_inner() 

586 del_outer_del_inner = self.get_del_outer_del_inner() 

587 for i in (0, 1, 2): 

588 np.multiply( 

589 grad_inner_vg[i], 

590 del_outer_del_inner, 

591 self.grad_g_vg[i]) 

592 return True 

593 

594 def update_smooth_step(self, rho_g): 

595 """Calculate self.g_g and self.del_g_del_rho_g.""" 

596 raise NotImplementedError() 

597 

598 def get_del_r_vg(self, atom_index, density): 

599 return self.del_g_del_rho_g * self.density.get_del_r_vg( 

600 atom_index, density 

601 ) 

602 

603 def __str__(self): 

604 s = Cavity.__str__(self) 

605 s += indent(str(self.density)) 

606 return s 

607 

608 def update_atoms(self, atoms, log): 

609 self.density.update_atoms(atoms, log) 

610 

611 # --- BEGIN GradientSurface API --- 

612 def get_grad_inner(self): 

613 return self.density.grad_rho_vg 

614 

615 def get_del_outer_del_inner(self): 

616 return self.del_g_del_rho_g 

617 

618 # --- END GradientSurface API --- 

619 

620 

621class Density(NeedsGD): 

622 """Class representing a density for the use with a SmoothStepCavity. 

623 

624 Attributes: 

625 rho_g -- The density on the fine grid in 1 / Bohr ** 3. 

626 del_rho_del_n_g -- Partial derivative with respect to the electron density. 

627 grad_rho_vg -- The gradient of the density on the fine grid. 

628 """ 

629 

630 def __init__(self): 

631 NeedsGD.__init__(self) 

632 self.rho_g = None 

633 self.del_rho_del_n_g = None 

634 self.grad_rho_vg = None 

635 

636 def estimate_memory(self, mem): 

637 nbytes = self.gd.bytecount() 

638 mem.subnode('Density', nbytes) 

639 if self.depends_on_el_density: 

640 mem.subnode('Density Derivative', nbytes) 

641 mem.subnode('Gradient of Density', 3 * nbytes) 

642 

643 def allocate(self): 

644 NeedsGD.allocate(self) 

645 self.rho_g = self.gd.empty() 

646 if self.depends_on_el_density: 

647 self.del_rho_del_n_g = self.gd.empty() 

648 self.grad_rho_vg = self.gd.empty(3) 

649 

650 def update(self, atoms, density): 

651 raise NotImplementedError() 

652 

653 @property 

654 def depends_on_el_density(self): 

655 raise NotImplementedError() 

656 

657 @property 

658 def depends_on_atomic_positions(self): 

659 raise NotImplementedError() 

660 

661 def __str__(self): 

662 return f"Density: {self.__class__.__name__}\n" 

663 

664 def update_atoms(self, atoms, log): 

665 """Inexpensive initialization when atoms change.""" 

666 pass 

667 

668 

669class FDGradientDensity(Density): 

670 """Base class for all Density classes with finite difference gradient""" 

671 

672 def __init__(self, boundary_value, nn=3): 

673 """Constructur for the FDGradientDensity class. 

674 

675 Arguments: 

676 boundary_value -- Boundary value of rho_g 

677 (for non-periodic directions). 

678 nn -- Stencil size for the finite difference gradient. 

679 """ 

680 Density.__init__(self) 

681 self.boundary_value = boundary_value 

682 self.nn = nn 

683 self.gradient = None 

684 

685 def allocate(self): 

686 Density.allocate(self) 

687 self.gradient = [ 

688 Gradient(self.gd, i, 1.0, self.nn) for i in (0, 1, 2) 

689 ] 

690 

691 def update(self, atoms, density): 

692 changed = self.update_only_density(atoms, density) 

693 if changed: 

694 self.update_gradient() 

695 return changed 

696 

697 def update_gradient(self): 

698 if self.boundary_value != 0: 

699 in_g = self.rho_g - self.boundary_value 

700 else: 

701 in_g = self.rho_g 

702 for i in (0, 1, 2): 

703 self.gradient[i].apply(in_g, self.grad_rho_vg[i]) 

704 

705 

706class ElDensity(FDGradientDensity): 

707 """Wrapper class for using the electron density in a SmoothStepCavity. 

708 

709 (Hopefully small) negative values of the electron density are set to zero. 

710 """ 

711 

712 depends_on_el_density = True 

713 depends_on_atomic_positions = False 

714 

715 def __init__(self, nn=3): 

716 """Constructor for the ElDensity class. 

717 

718 Arguments: 

719 nn -- Stencil size for the finite difference gradient. 

720 """ 

721 FDGradientDensity.__init__(self, boundary_value=.0, nn=nn) 

722 

723 def allocate(self): 

724 FDGradientDensity.allocate(self) 

725 self.del_rho_del_n_g = 1. # free array 

726 

727 def update_only_density(self, atoms, density): 

728 self.rho_g[:] = density.nt_g 

729 self.rho_g[self.rho_g < .0] = .0 

730 return True 

731 

732 

733# TODO: implement analytic gradient for SSS09Density 

734class SSS09Density(FDGradientDensity): 

735 """Fake density from atomic radii for the use in a SmoothStepCavity. 

736 

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

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

739 """ 

740 

741 depends_on_el_density = False 

742 depends_on_atomic_positions = True 

743 

744 def __init__(self, atomic_radii, pbc_cutoff=1e-3, nn=3, tiny=1e-100): 

745 """Constructor for the SSS09Density class. 

746 

747 Arguments: 

748 atomic_radii -- Callable mapping an ase.Atoms object 

749 to an iterable of atomic radii in Angstroms. 

750 pbc_cutoff -- Cutoff in eV for including neighbor cells in 

751 a calculation with periodic boundary conditions. 

752 nn -- Stencil size for the finite difference gradient. 

753 """ 

754 FDGradientDensity.__init__(self, boundary_value=.0, nn=nn) 

755 self.atomic_radii = atomic_radii 

756 self.atomic_radii_output = None 

757 self.symbols = None 

758 self.pbc_cutoff = float(pbc_cutoff) 

759 self.tiny = float(tiny) 

760 self.pos_aav = None 

761 self.r_vg = None 

762 self.del_rho_del_r_vg = None 

763 

764 def estimate_memory(self, mem): 

765 FDGradientDensity.estimate_memory(self, mem) 

766 nbytes = self.gd.bytecount() 

767 mem.subnode('Coordinates', 3 * nbytes) 

768 mem.subnode('Atomic Position Derivative', 3 * nbytes) 

769 

770 def allocate(self): 

771 FDGradientDensity.allocate(self) 

772 self.r_vg = self.gd.get_grid_point_coordinates() 

773 self.del_rho_del_r_vg = self.gd.empty(3) 

774 

775 def update_only_density(self, atoms, density): 

776 if atoms is None: 

777 return False 

778 r_a = self.atomic_radii_output / Bohr 

779 r_cutoff = r_a.max() - np.log(self.pbc_cutoff) 

780 self.pos_aav = get_pbc_positions(atoms, r_cutoff) 

781 self.rho_g.fill(.0) 

782 na = np.newaxis 

783 for index, pos_av in self.pos_aav.items(): 

784 for pos_v in pos_av: 

785 origin_vg = pos_v[:, na, na, na] 

786 r_diff_vg = self.r_vg - origin_vg 

787 norm_r_diff_g = (r_diff_vg ** 2).sum(0) ** .5 

788 self.rho_g += np.exp(r_a[index] - norm_r_diff_g) 

789 return True 

790 

791 def get_del_r_vg(self, atom_index, density): 

792 r_a = self.atomic_radii_output[atom_index] / Bohr 

793 na = np.newaxis 

794 self.del_rho_del_r_vg.fill(.0) 

795 for pos_v in self.pos_aav[atom_index]: 

796 origin_vg = pos_v[:, na, na, na] 

797 r_diff_vg = self.r_vg - origin_vg 

798 norm_r_diff_g = np.sum(r_diff_vg ** 2, axis=0) ** .5 

799 norm_r_diff_g[norm_r_diff_g < self.tiny] = self.tiny 

800 exponential = np.exp(r_a - norm_r_diff_g) 

801 exponential /= norm_r_diff_g 

802 r_diff_vg *= exponential[na, ...] 

803 self.del_rho_del_r_vg += r_diff_vg 

804 return self.del_rho_del_r_vg 

805 

806 def __str__(self): 

807 s = FDGradientDensity.__str__(self) 

808 s += indent(f' pbc_cutoff: {self.pbc_cutoff}\n') 

809 s += indent(f' tiny: {self.tiny}\n') 

810 return s 

811 

812 def update_atoms(self, atoms, log): 

813 set_log_and_check_radii(self, atoms, log) 

814 

815 

816class ADM12SmoothStepCavity(SmoothStepCavity): 

817 """Cavity from smooth step function. 

818 

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

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

821 """ 

822 

823 def __init__( 

824 self, 

825 rhomin, rhomax, epsinf, 

826 density, 

827 surface_calculator=None, volume_calculator=None 

828 ): 

829 """Constructor for the ADM12SmoothStepCavity class. 

830 

831 Additional arguments not present in the SmoothStepCavity class: 

832 rhomin -- Lower density isovalue in 1 / Angstrom ** 3. 

833 rhomax -- Upper density isovalue in 1 / Angstrom ** 3. 

834 epsinf -- Static dielectric constant of the solvent. 

835 """ 

836 SmoothStepCavity.__init__( 

837 self, density, surface_calculator, volume_calculator 

838 ) 

839 self.rhomin = float(rhomin) 

840 self.rhomax = float(rhomax) 

841 self.epsinf = float(epsinf) 

842 

843 def update_smooth_step(self, rho_g): 

844 eps = self.epsinf 

845 inside = rho_g > self.rhomax * Bohr ** 3 

846 outside = rho_g < self.rhomin * Bohr ** 3 

847 transition = np.logical_not( 

848 np.logical_or(inside, outside) 

849 ) 

850 self.g_g[inside] = .0 

851 self.g_g[outside] = 1. 

852 self.del_g_del_rho_g.fill(.0) 

853 t, dt = self._get_t_dt(np.log(rho_g[transition])) 

854 if eps == 1.0: 

855 # lim_{eps -> 1} (eps - eps ** t) / (eps - 1) = 1 - t 

856 self.g_g[transition] = t 

857 self.del_g_del_rho_g[transition] = dt / rho_g[transition] 

858 else: 

859 eps_to_t = eps ** t 

860 self.g_g[transition] = (eps_to_t - 1.) / (eps - 1.) 

861 self.del_g_del_rho_g[transition] = ( 

862 eps_to_t * np.log(eps) * dt 

863 ) / ( 

864 rho_g[transition] * (eps - 1.) 

865 ) 

866 

867 def _get_t_dt(self, x): 

868 lnmax = np.log(self.rhomax * Bohr ** 3) 

869 lnmin = np.log(self.rhomin * Bohr ** 3) 

870 twopi = 2. * np.pi 

871 arg = twopi * (lnmax - x) / (lnmax - lnmin) 

872 t = (arg - np.sin(arg)) / twopi 

873 dt = -2. * np.sin(arg / 2.) ** 2 / (lnmax - lnmin) 

874 return (t, dt) 

875 

876 def __str__(self): 

877 s = SmoothStepCavity.__str__(self) 

878 s += indent(f' rhomin: {self.rhomin}\n') 

879 s += indent(f' rhomax: {self.rhomax}\n') 

880 s += indent(f' epsinf: {self.epsinf}') 

881 return s 

882 

883 

884class FG02SmoothStepCavity(SmoothStepCavity): 

885 """Cavity from smooth step function. 

886 

887 Following J. Fattebert and F. Gygi, J. Comput. Chem. 23, 662 (2002). 

888 """ 

889 

890 def __init__( 

891 self, 

892 rho0, 

893 beta, 

894 density, 

895 surface_calculator=None, volume_calculator=None 

896 ): 

897 """Constructor for the FG02SmoothStepCavity class. 

898 

899 Additional arguments not present in the SmoothStepCavity class: 

900 rho0 -- Density isovalue in 1 / Angstrom ** 3. 

901 beta -- Parameter controlling the steepness of the transition. 

902 """ 

903 SmoothStepCavity.__init__( 

904 self, density, surface_calculator, volume_calculator 

905 ) 

906 self.rho0 = float(rho0) 

907 self.beta = float(beta) 

908 

909 def update_smooth_step(self, rho_g): 

910 rho0 = self.rho0 / (1. / Bohr ** 3) 

911 rho_scaled_g = rho_g / rho0 

912 exponent = 2. * self.beta 

913 np.divide(1., rho_scaled_g ** exponent + 1., self.g_g) 

914 np.multiply( 

915 (-exponent / rho0) * rho_scaled_g ** (exponent - 1.), 

916 self.g_g ** 2, 

917 self.del_g_del_rho_g 

918 ) 

919 

920 def __str__(self): 

921 s = SmoothStepCavity.__str__(self) 

922 s += indent(f' rho0: {self.rho0}\n') 

923 s += indent(f' beta: {self.beta}') 

924 return s 

925 

926 

927class SurfaceCalculator(NeedsGD): 

928 """Base class for surface calculators. 

929 

930 Attributes: 

931 A -- Local area in Bohr ** 2. 

932 delta_A_delta_g_g -- Functional derivative with respect to the cavity 

933 on the fine grid. 

934 """ 

935 

936 def __init__(self): 

937 NeedsGD.__init__(self) 

938 self.A = None 

939 self.delta_A_delta_g_g = None 

940 

941 def write(self, writer): 

942 pass 

943 

944 def read(self, reader): 

945 pass 

946 

947 def estimate_memory(self, mem): 

948 mem.subnode('Functional Derivative', self.gd.bytecount()) 

949 

950 def allocate(self): 

951 NeedsGD.allocate(self) 

952 self.delta_A_delta_g_g = self.gd.empty() 

953 

954 def __str__(self): 

955 return f'Surface Calculator: {self.__class__.__name__}\n' 

956 

957 def update(self, cavity): 

958 """Calculate A and delta_A_delta_g_g.""" 

959 raise NotImplementedError() 

960 

961 

962class GradientSurface(SurfaceCalculator): 

963 """Class for getting the surface area from the gradient of the cavity. 

964 

965 See also W. Im, D. Beglov and B. Roux, 

966 Comput. Phys. Commun. 111, 59 (1998) 

967 and 

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

969 """ 

970 

971 def __init__(self, nn=3): 

972 SurfaceCalculator.__init__(self) 

973 self.nn = nn 

974 self.gradient = None 

975 self.gradient_out = None 

976 self.norm_grad_out = None 

977 self.div_tmp = None 

978 

979 def todict(self): 

980 return {'nn': self.nn} 

981 

982 def write(self, writer): 

983 writer.write( 

984 name='GradientSurface', 

985 nn=self.nn) 

986 

987 def __str__(self): 

988 return f'GradientSurface with {self.nn} nn\n' 

989 

990 def read(self, reader): 

991 self.nn = reader.parameters.cavity.nn 

992 

993 def estimate_memory(self, mem): 

994 SurfaceCalculator.estimate_memory(self, mem) 

995 nbytes = self.gd.bytecount() 

996 mem.subnode('Gradient', 4 * nbytes) 

997 mem.subnode('Divergence', nbytes) 

998 

999 def allocate(self): 

1000 SurfaceCalculator.allocate(self) 

1001 self.gradient = [ 

1002 Gradient(self.gd, i, 1.0, self.nn) for i in (0, 1, 2) 

1003 ] 

1004 self.gradient_out = self.gd.empty(3) 

1005 self.norm_grad_out = self.gd.empty() 

1006 self.div_tmp = self.gd.empty() 

1007 

1008 def update(self, cavity): 

1009 del_outer_del_inner = cavity.get_del_outer_del_inner() 

1010 sign = np.sign(del_outer_del_inner.max() + del_outer_del_inner.min()) 

1011 self.gradient_out[...] = cavity.get_grad_inner() 

1012 self.norm_grad_out = (self.gradient_out ** 2).sum(0) ** .5 

1013 self.A = sign * self.gd.integrate( 

1014 del_outer_del_inner * self.norm_grad_out, global_integral=False 

1015 ) 

1016 mask = self.norm_grad_out > 1e-12 # avoid division by zero or overflow 

1017 imask = np.logical_not(mask) 

1018 masked_norm_grad = self.norm_grad_out[mask] 

1019 for i in (0, 1, 2): 

1020 self.gradient_out[i][mask] /= masked_norm_grad 

1021 # set limit for later calculations: 

1022 self.gradient_out[i][imask] = .0 

1023 self.calc_div(self.gradient_out, self.delta_A_delta_g_g) 

1024 if sign == 1: 

1025 self.delta_A_delta_g_g *= -1. 

1026 

1027 def calc_div(self, vec, out): 

1028 self.gradient[0].apply(vec[0], out) 

1029 self.gradient[1].apply(vec[1], self.div_tmp) 

1030 out += self.div_tmp 

1031 self.gradient[2].apply(vec[2], self.div_tmp) 

1032 out += self.div_tmp 

1033 

1034 

1035class VolumeCalculator(NeedsGD): 

1036 """Base class for volume calculators. 

1037 

1038 Attributes: 

1039 V -- Local volume in Bohr ** 3. 

1040 delta_V_delta_g_g -- Functional derivative with respect to the cavity 

1041 on the fine grid. 

1042 """ 

1043 

1044 def __init__(self): 

1045 NeedsGD.__init__(self) 

1046 self.V = None 

1047 self.delta_V_delta_g_g = None 

1048 

1049 def estimate_memory(self, mem): 

1050 mem.subnode('Functional Derivative', self.gd.bytecount()) 

1051 

1052 def allocate(self): 

1053 NeedsGD.allocate(self) 

1054 self.delta_V_delta_g_g = self.gd.empty() 

1055 

1056 def __str__(self): 

1057 return f"{self.__class__.__name__}\n" 

1058 

1059 def update(self, cavity): 

1060 """Calculate V and delta_V_delta_g_g""" 

1061 raise NotImplementedError() 

1062 

1063 

1064class KB51Volume(VolumeCalculator): 

1065 """KB51 Volume Calculator. 

1066 

1067 V = Integral(1 - g) + kappa_T * k_B * T 

1068 

1069 Following J. G. Kirkwood and F. P. Buff, J. Chem. Phys. 19, 6, 774 (1951). 

1070 """ 

1071 

1072 def __init__(self, compressibility=.0, temperature=.0): 

1073 """Constructor for KB51Volume class. 

1074 

1075 Arguments: 

1076 compressibility -- Isothermal compressibility of the solvent 

1077 in Angstrom ** 3 / eV. 

1078 temperature -- Temperature in Kelvin. 

1079 """ 

1080 VolumeCalculator.__init__(self) 

1081 self.compressibility = float(compressibility) 

1082 self.temperature = float(temperature) 

1083 

1084 def todict(self): 

1085 return {'compressibility': self.compressibility, 

1086 'temperature': self.temperature} 

1087 

1088 def __str__(self): 

1089 s = VolumeCalculator.__str__(self) 

1090 s += indent(f' compressibility: {self.compressibility}\n') 

1091 s += indent(f' temperature: {self.temperature}\n') 

1092 return s 

1093 

1094 def allocate(self): 

1095 VolumeCalculator.allocate(self) 

1096 self.delta_V_delta_g_g = -1. # frees array 

1097 

1098 def update(self, cavity): 

1099 self.V = self.gd.integrate(1. - cavity.g_g, global_integral=False) 

1100 V_compress = self.compressibility * kB * self.temperature / Bohr ** 3 

1101 self.V += V_compress / self.gd.comm.size