Coverage for gpaw/test/test_extensions.py: 13%
111 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
1import pytest
2from gpaw.new.extensions import Extension
3from ase.units import Hartree, Bohr
4import numpy as np
7class Spring:
8 name = 'spring'
10 def __init__(self, *, a1, a2, l, k):
11 self.a1, self.a2, self.l, self.k = a1, a2, l, k
13 def build(self, atoms, domain_comm, log):
14 atoms = atoms.copy()
15 log('Building Spring')
17 class EnergyAdder(Extension):
18 name = 'spring'
20 @property
21 def _name(_self):
22 return f'Spring k={self.k}'
24 def __init__(self):
25 self._calculate(atoms)
27 def _calculate(_self, atoms):
28 D = atoms.get_distance(self.a1, self.a2) - self.l
29 v = atoms.positions[self.a1] - atoms.positions[self.a2]
30 v /= np.linalg.norm(v)
31 F = self.k * D / Hartree * Bohr
32 _self.E = 1 / 2 * self.k * D**2 / Hartree
33 _self.F_av = np.zeros((len(atoms), 3))
34 _self.F_av[self.a1, :] = -v * F
35 _self.F_av[self.a2, :] = v * F
37 def force_contribution(self):
38 return self.F_av
40 def get_energy_contributions(self):
41 return {self._name: self.E}
43 def move_atoms(self, relpos_ac):
44 atoms.set_scaled_positions(relpos_ac)
45 self._calculate(atoms)
47 return EnergyAdder()
49 def todict(self):
50 return dict(a1=self.a1, a2=self.a2, l=self.l, k=self.k)
53@pytest.mark.parametrize('parallel', [(1, 1), (1, 2), (2, 1)])
54@pytest.mark.parametrize('mode', [{'name': 'pw', 'ecut': 300}, 'lcao'])
55def test_extensions(mode, parallel, in_tmp_dir, gpaw_new):
56 if not gpaw_new:
57 pytest.skip('Only GPAW new')
58 ktot = 20
60 from gpaw.new.ase_interface import GPAW
61 from gpaw import restart
62 from gpaw.mpi import world
63 domain, band = parallel
64 if world.size < domain * band:
65 pytest.skip('Not enough cores for this test.')
66 if world.size > domain * band * 2:
67 pytest.skip('Too many cores for this test.')
69 # 1. Create a calculation with a particular list of extensions.
70 def get_atoms():
71 from ase.build import molecule
72 atoms = molecule('H2')
73 atoms.center(vacuum=3)
74 atoms.set_pbc((True, True, True))
75 return atoms
77 atoms = get_atoms()
79 def get_calc(atoms):
80 # To test multiple extensions, create two sprigs which add
81 # up to k=ktot, which is what is tested in this test
82 calc = GPAW(extensions=[Spring(a1=0, a2=1, l=2, k=4),
83 Spring(a1=0, a2=1, l=2, k=ktot - 4)],
84 symmetry='off',
85 parallel={'band': band, 'domain': domain},
86 kpts=(2, 1, 1),
87 convergence={'density': 1e-6},
88 mode=mode)
89 atoms.calc = calc
90 return calc
92 calc = get_calc(atoms)
94 E, F = atoms.get_potential_energy(), atoms.get_forces()
96 # Write the GPW file for the restart test later on (4.)
97 print('Wrote the potential energy', E)
98 calc.write('calc.gpw')
100 # 2. Test that moving the atoms works after an SFC convergence
101 atoms.positions[0, 2] -= 0.1
102 movedE, movedF = atoms.get_potential_energy(), atoms.get_forces()
104 # Reset atoms to their original positions
105 atoms.positions[0, 2] += 0.1
107 # 3. Calculate a reference result without extensions
108 calc = GPAW(mode=mode,
109 kpts=(2, 1, 1),
110 convergence={'density': 1e-6},
111 symmetry='off')
112 atoms.calc = calc
114 E0, F0 = atoms.get_potential_energy(), atoms.get_forces()
116 # Manually evaluate the spring energy, and compare forces
117 l = atoms.get_distance(0, 1)
118 assert E == pytest.approx(E0 + 1 / 2 * ktot * (l - 2)**2)
119 assert F[0, 2] == pytest.approx(F0[0, 2] - ktot * (l - 2))
121 # Evaluate the reference energy and forces also for the moved atoms
122 atoms.positions[0, 2] -= 0.1
123 movedE0, movedF0 = atoms.get_potential_energy(), atoms.get_forces()
124 l = atoms.get_distance(0, 1)
125 assert movedE == pytest.approx(movedE0 + 1 / 2 * ktot * (l - 2)**2)
126 assert movedF[0, 2] == pytest.approx(movedF0[0, 2] - ktot * (l - 2))
128 def hook(extensions):
129 return [Spring(**{k: v for k, v in dct.items() if k != 'name'})
130 if dct['name'] == 'spring'
131 else dct
132 for dct in extensions]
134 # 4. Test restarting from a file
135 atoms, calc = restart(
136 'calc.gpw',
137 Class=GPAW,
138 object_hooks={'extensions': hook})
140 # Make sure the cached energies and forces are correct
141 # without a new calculation
142 assert E == pytest.approx(atoms.get_potential_energy())
143 assert F == pytest.approx(atoms.get_forces())
145 if mode == 'lcao':
146 # See issue #1369
147 return
149 # Make sure the recalculated energies are forces are correct
150 atoms.set_positions(atoms.get_positions() + 1e-10)
151 assert E == pytest.approx(atoms.get_potential_energy(), abs=1e-5)
152 assert F == pytest.approx(atoms.get_forces(), abs=1e-5)
154 # 5. Test full blown relaxation.
155 from ase.optimize import BFGS
156 atoms = get_atoms()
157 calc = get_calc(atoms)
158 relax = BFGS(atoms)
159 relax.run()
160 nsteps = relax.nsteps
161 assert atoms.get_distance(0, 1) == pytest.approx(1.8483, abs=1e-2)
162 Egs = atoms.get_potential_energy()
163 L = atoms.get_distance(0, 1)
165 # 6. Test restarting from a relaxation.
166 atoms = get_atoms()
167 calc = get_calc(atoms)
168 relax = BFGS(atoms, restart='relax_restart')
169 for _, _ in zip(relax.irun(), range(3)):
170 pass
171 calc.write('restart_relax.gpw')
172 atoms, calc = restart('restart_relax.gpw',
173 Class=GPAW,
174 object_hooks={'extensions': hook})
175 relax = BFGS(atoms, restart='relax_restart')
176 relax.run()
178 assert relax.nsteps + 3 == nsteps
179 assert atoms.get_distance(0, 1) == pytest.approx(L, abs=1e-2)
180 assert atoms.get_potential_energy() == pytest.approx(Egs, abs=1e-4)