Coverage for gpaw/solvation/hamiltonian.py: 98%
170 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-12 00:18 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-12 00:18 +0000
1from gpaw.hamiltonian import RealSpaceHamiltonian
2from gpaw.solvation.poisson import WeightedFDPoissonSolver
3from gpaw.fd_operators import Gradient
4from gpaw.io.logger import indent
5from ase.units import Ha
6import numpy as np
9class SolvationRealSpaceHamiltonian(RealSpaceHamiltonian):
10 """Realspace Hamiltonian with continuum solvent model.
12 See also Section III of
13 A. Held and M. Walter, J. Chem. Phys. 141, 174108 (2014).
14 """
16 def __init__(
17 self,
18 # solvation related arguments:
19 cavity, dielectric, interactions,
20 # RealSpaceHamiltonian arguments:
21 gd, finegd, nspins, collinear, setups, timer, xc, world,
22 redistributor, vext=None, psolver=None,
23 stencil=3):
24 """Constructor of SolvationRealSpaceHamiltonian class.
26 Additional arguments not present in RealSpaceHamiltonian:
27 cavity -- A Cavity instance.
28 dielectric -- A Dielectric instance.
29 interactions -- A list of Interaction instances.
30 """
31 self.cavity = cavity
32 self.dielectric = dielectric
33 self.interactions = interactions
34 cavity.set_grid_descriptor(finegd)
35 dielectric.set_grid_descriptor(finegd)
36 for ia in interactions:
37 ia.set_grid_descriptor(finegd)
38 if psolver is None:
39 psolver = WeightedFDPoissonSolver()
40 psolver.set_dielectric(self.dielectric)
41 self.gradient = None
42 RealSpaceHamiltonian.__init__(
43 self,
44 gd, finegd, nspins, collinear, setups, timer, xc, world,
45 vext=vext, psolver=psolver,
46 stencil=stencil, redistributor=redistributor)
48 for ia in interactions:
49 setattr(self, 'e_' + ia.subscript, None)
50 self.new_atoms = None
51 self.vt_ia_g = None
52 self.e_el_free = None
53 self.e_el_extrapolated = None
55 def __str__(self):
56 s = RealSpaceHamiltonian.__str__(self) + '\n'
57 s += ' Solvation:\n'
58 components = [self.cavity, self.dielectric] + self.interactions
59 s += ''.join([indent(str(c), 2) for c in components])
60 return s
62 def estimate_memory(self, mem):
63 RealSpaceHamiltonian.estimate_memory(self, mem)
64 solvation = mem.subnode('Solvation')
65 for name, obj in [
66 ('Cavity', self.cavity),
67 ('Dielectric', self.dielectric),
68 ] + [('Interaction: ' + ia.subscript, ia) for ia in self.interactions]:
69 obj.estimate_memory(solvation.subnode(name))
71 def update_atoms(self, atoms, log):
72 self.new_atoms = atoms.copy()
73 log('Solvation position-dependent initialization:')
74 self.cavity.update_atoms(atoms, log)
75 for ia in self.interactions:
76 ia.update_atoms(atoms, log)
78 def initialize(self):
79 self.gradient = [
80 Gradient(self.finegd, i, 1.0, self.poisson.nn) for i in (0, 1, 2)
81 ]
82 self.vt_ia_g = self.finegd.zeros()
83 self.cavity.allocate()
84 self.dielectric.allocate()
85 for ia in self.interactions:
86 ia.allocate()
87 RealSpaceHamiltonian.initialize(self)
89 def update(self, density, wfs=None, kin_en_using_band=True):
90 self.timer.start('Hamiltonian')
91 if self.vt_sg is None:
92 self.timer.start('Initialize Hamiltonian')
93 self.initialize()
94 self.timer.stop('Initialize Hamiltonian')
96 cavity_changed = self.cavity.update(self.new_atoms, density)
97 if cavity_changed:
98 self.cavity.update_vol_surf()
99 self.dielectric.update(self.cavity)
101 # e_coulomb, Ebar, Eext, Exc =
102 finegd_energies = self.update_pseudo_potential(density)
103 self.finegd.comm.sum(finegd_energies)
104 ia_changed = [
105 ia.update(
106 self.new_atoms,
107 density,
108 self.cavity if cavity_changed else None)
109 for ia in self.interactions]
110 if np.any(ia_changed):
111 self.vt_ia_g.fill(.0)
112 for ia in self.interactions:
113 if ia.depends_on_el_density:
114 self.vt_ia_g += ia.delta_E_delta_n_g
115 if self.cavity.depends_on_el_density:
116 self.vt_ia_g += (ia.delta_E_delta_g_g *
117 self.cavity.del_g_del_n_g)
118 if len(self.interactions) > 0:
119 for vt_g in self.vt_sg:
120 vt_g += self.vt_ia_g
121 Eias = np.array([ia.E for ia in self.interactions])
123 Ekin1 = self.gd.comm.sum_scalar(self.calculate_kinetic_energy(density))
124 W_aL = self.calculate_atomic_hamiltonians(density)
125 atomic_energies = self.update_corrections(density, W_aL)
126 self.world.sum(atomic_energies)
128 energies = atomic_energies
129 energies[1:] += finegd_energies
130 energies[0] += Ekin1
132 if not kin_en_using_band:
133 assert wfs is not None
134 with self.timer('New Kinetic Energy'):
135 energies[0] = \
136 self.calculate_kinetic_energy_directly(density,
137 wfs)
139 (self.e_kinetic0, self.e_coulomb, self.e_zero,
140 self.e_external, self.e_xc) = energies
142 self.finegd.comm.sum(Eias)
144 self.cavity.communicate_vol_surf(self.world)
145 for E, ia in zip(Eias, self.interactions):
146 setattr(self, 'e_' + ia.subscript, E)
148 self.new_atoms = None
149 self.timer.stop('Hamiltonian')
151 def update_pseudo_potential(self, density):
152 ret = RealSpaceHamiltonian.update_pseudo_potential(self, density)
153 if not self.cavity.depends_on_el_density:
154 return ret
155 del_g_del_n_g = self.cavity.del_g_del_n_g
156 # XXX optimize numerics
157 del_eps_del_g_g = self.dielectric.del_eps_del_g_g
158 Veps = -1. / (8. * np.pi) * del_eps_del_g_g * del_g_del_n_g
159 Veps *= self.grad_squared(self.vHt_g)
160 for vt_g in self.vt_sg:
161 vt_g += Veps
162 return ret
164 def calculate_forces(self, dens, F_av):
165 # XXX reorganize
166 self.el_force_correction(dens, F_av)
167 for ia in self.interactions:
168 if self.cavity.depends_on_atomic_positions:
169 for a, F_v in enumerate(F_av):
170 del_g_del_r_vg = self.cavity.get_del_r_vg(a, dens)
171 for v in (0, 1, 2):
172 F_v[v] -= self.finegd.integrate(
173 ia.delta_E_delta_g_g * del_g_del_r_vg[v],
174 global_integral=False)
175 if ia.depends_on_atomic_positions:
176 for a, F_v in enumerate(F_av):
177 del_E_del_r_vg = ia.get_del_r_vg(a, dens)
178 for v in (0, 1, 2):
179 F_v[v] -= self.finegd.integrate(
180 del_E_del_r_vg[v],
181 global_integral=False)
182 return RealSpaceHamiltonian.calculate_forces(self, dens, F_av)
184 def el_force_correction(self, dens, F_av):
185 if not self.cavity.depends_on_atomic_positions:
186 return
187 del_eps_del_g_g = self.dielectric.del_eps_del_g_g
188 fixed = 1 / (8 * np.pi) * del_eps_del_g_g * \
189 self.grad_squared(self.vHt_g) # XXX grad_vHt_g inexact in bmgs
190 for a, F_v in enumerate(F_av):
191 del_g_del_r_vg = self.cavity.get_del_r_vg(a, dens)
192 for v in (0, 1, 2):
193 F_v[v] += self.finegd.integrate(
194 fixed * del_g_del_r_vg[v],
195 global_integral=False)
197 def get_energy(self, e_entropy, wfs, kin_en_using_band=True, e_sic=None):
198 RealSpaceHamiltonian.get_energy(self, e_entropy, wfs,
199 kin_en_using_band, e_sic)
200 # The total energy calculated by the parent class includes the
201 # solvent electrostatic contributions but not the interaction
202 # energies. We add those here and store the electrostatic energies.
203 self.e_el_free = self.e_total_free
204 self.e_el_extrapolated = self.e_total_extrapolated
205 for ia in self.interactions:
206 self.e_total_free += getattr(self, 'e_' + ia.subscript)
207 self.e_total_extrapolated = (self.e_total_free +
208 wfs.occupations.extrapolate_factor *
209 e_entropy)
210 return self.e_total_free
212 def grad_squared(self, x):
213 # XXX ugly
214 gs = np.empty_like(x)
215 tmp = np.empty_like(x)
216 self.gradient[0].apply(x, gs)
217 np.square(gs, gs)
218 self.gradient[1].apply(x, tmp)
219 np.square(tmp, tmp)
220 gs += tmp
221 self.gradient[2].apply(x, tmp)
222 np.square(tmp, tmp)
223 gs += tmp
224 return gs
226 def summary(self, wfs, log):
227 # This is almost duplicate code to gpaw/hamiltonian's
228 # Hamiltonian.summary, but with the cavity and interactions added.
230 log('Energy contributions relative to reference atoms:',
231 f'(reference = {self.setups.Eref * Ha:.6f})\n')
233 energies = [('Kinetic: ', self.e_kinetic),
234 ('Potential: ', self.e_coulomb),
235 ('External: ', self.e_external),
236 ('XC: ', self.e_xc),
237 ('Entropy (-ST):', self.e_entropy),
238 ('Local: ', self.e_zero)]
240 if len(self.interactions) > 0:
241 energies += [('Interactions', None)]
242 for ia in self.interactions:
243 energies += [(f' {ia.subscript:s}:',
244 getattr(self, 'e_' + ia.subscript))]
246 for name, e in energies:
247 if e is not None:
248 log('%-14s %+11.6f' % (name, Ha * e))
249 else:
250 log('%-14s' % (name))
252 log('--------------------------')
253 log('Free energy: %+11.6f' % (Ha * self.e_total_free))
254 log('Extrapolated: %+11.6f' % (Ha * self.e_total_extrapolated))
255 log()
256 self.xc.summary(log)
258 try:
259 workfunctions = self.get_workfunctions(wfs)
260 except ValueError:
261 pass
262 else:
263 log('Dipole-layer corrected work functions: {:.6f}, {:.6f} eV'
264 .format(*np.array(workfunctions) * Ha))
265 log()
267 self.cavity.summary(log)