Coverage for gpaw/solvation/interactions.py: 83%
113 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-20 00:19 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-20 00:19 +0000
1import numpy as np
2from ase.units import Bohr, Hartree
4from gpaw.solvation.gridmem import NeedsGD
7class Interaction(NeedsGD):
8 """Base class for non-electrostatic solvent solute interactions.
10 See also Section III of
11 A. Held and M. Walter, J. Chem. Phys. 141, 174108 (2014).
13 Attributes:
14 subscript -- Short label used to reference the interaction.
15 E -- Local interaction energy in Hartree
16 delta_E_delta_n_g -- Functional derivative of the interaction energy
17 with respect to the electron density.
18 delta_E_delta_n_g -- Functional derivative of the interaction energy
19 with respect to the cavity.
20 """
22 subscript = 'unnamed'
24 def __init__(self):
25 NeedsGD.__init__(self)
26 self.E = None
27 self.delta_E_delta_n_g = None
28 self.delta_E_delta_g_g = None
30 @classmethod
31 def from_dict(self, dct):
32 if not isinstance(dct, dict):
33 return dct
34 dct = dct.copy()
35 name = dct.pop('name')
36 if name == 'SurfaceInteraction':
37 return SurfaceInteraction(**dct)
38 if name == 'VolumeInteraction':
39 return VolumeInteraction(**dct)
40 if name == 'LeakedDensityInteraction':
41 return LeakedDensityInteraction(**dct)
42 raise ValueError(name)
44 def write(self, writer):
45 pass
47 def update(self, atoms, density, cavity):
48 """Update the Kohn-Sham potential and the energy.
50 atoms and/or cavity are None iff they have not changed
51 since the last call
53 Return whether the interaction has changed.
54 """
55 raise NotImplementedError
57 def estimate_memory(self, mem):
58 ngrids = 1 + self.depends_on_el_density
59 mem.subnode('Functional Derivatives', ngrids * self.gd.bytecount())
61 def allocate(self):
62 NeedsGD.allocate(self)
63 self.delta_E_delta_g_g = self.gd.empty()
64 if self.depends_on_el_density:
65 self.delta_E_delta_n_g = self.gd.empty()
67 @property
68 def depends_on_atomic_positions(self):
69 """Return whether the ia depends explicitly on atomic positions."""
70 raise NotImplementedError
72 @property
73 def depends_on_el_density(self):
74 """Return whether the ia depends explicitly on the electron density."""
75 raise NotImplementedError
77 def get_del_r_vg(self, atomic_index, density):
78 """Return spatial derivatives with respect to atomic position."""
79 raise NotImplementedError
81 def __str__(self):
82 s = f"Interaction: {self.__class__.__name__}\n"
83 s += f" subscript: {self.subscript}\n"
84 return s
86 def update_atoms(self, atoms, log):
87 """Inexpensive initialization when atoms change."""
88 pass
91class SurfaceInteraction(Interaction):
92 """An interaction with energy proportional to the cavity surface area."""
94 subscript = 'surf'
95 depends_on_el_density = False
96 depends_on_atomic_positions = False
98 def __init__(self, surface_tension):
99 """Constructor for SurfaceInteraction class.
101 Arguments:
102 surface_tension -- Proportionality factor to calculate
103 energy from surface area in eV / Angstrom ** 2.
104 """
105 Interaction.__init__(self)
106 self.surface_tension = float(surface_tension)
108 def todict(self):
109 return {'surface_tension': self.surface_tension}
111 def write(self, writer):
112 writer.write(name='SurfaceInteraction',
113 surface_tension=self.surface_tension)
115 def update(self, atoms, density, cavity):
116 if cavity is None:
117 return False
118 acalc = cavity.surface_calculator
119 st = self.surface_tension * Bohr ** 2 / Hartree
120 self.E = st * acalc.A
121 np.multiply(st, acalc.delta_A_delta_g_g, self.delta_E_delta_g_g)
122 return True
124 def __str__(self):
125 s = Interaction.__str__(self)
126 s += f' Surface_tension: {self.surface_tension} eV/(Angstrom^2)'
127 return s
130class VolumeInteraction(Interaction):
131 """An interaction with energy proportional to the cavity volume"""
133 subscript = 'vol'
134 depends_on_el_density = False
135 depends_on_atomic_positions = False
137 def __init__(self, pressure):
138 """Constructor for VolumeInteraction class.
140 Arguments:
141 pressure -- Proportionality factor to calculate
142 energy from volume in eV / Angstrom ** 3.
143 """
144 Interaction.__init__(self)
145 self.pressure = float(pressure)
147 def todict(self):
148 return {'pressure': self.pressure}
150 def update(self, atoms, density, cavity):
151 if cavity is None:
152 return False
153 vcalc = cavity.volume_calculator
154 pressure = self.pressure * Bohr ** 3 / Hartree
155 self.E = pressure * vcalc.V
156 np.multiply(pressure, vcalc.delta_V_delta_g_g, self.delta_E_delta_g_g)
157 return True
159 def __str__(self):
160 s = Interaction.__str__(self)
161 s += f' Pressure: {self.pressure}'
162 return s
165class LeakedDensityInteraction(Interaction):
166 """Interaction proportional to charge leaking outside cavity.
168 The charge outside the cavity is calculated as
169 """
171 subscript = 'leak'
172 depends_on_el_density = True
173 depends_on_atomic_positions = False
175 def __init__(self, voltage):
176 """Constructor for LeakedDensityInteraction class.
178 Arguments:
179 voltage -- Proportionality factor to calculate
180 energy from integrated electron density in Volts.
181 A positive value of the voltage leads to a
182 positive interaction energy.
183 """
184 Interaction.__init__(self)
185 self.voltage = float(voltage)
187 def todict(self):
188 return {'voltage': self.voltage}
190 def update(self, atoms, density, cavity):
191 E0 = self.voltage / Hartree
192 if cavity is not None:
193 np.multiply(E0, cavity.g_g, self.delta_E_delta_n_g)
194 np.multiply(E0, density.nt_g, self.delta_E_delta_g_g)
195 self.E = self.gd.integrate(
196 density.nt_g * self.delta_E_delta_n_g,
197 global_integral=False
198 )
199 return True
201 def __str__(self):
202 s = Interaction.__str__(self)
203 s += f' Voltage: {self.voltage:.4f}'
204 return s