Coverage for gpaw/test/response/test_coulomb.py: 98%
104 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.grid_descriptor import GridDescriptor
3from gpaw import PoissonSolver
4from gpaw.spline import Spline
5from gpaw.lfc import LocalizedFunctionsCollection as LFC
6from gpaw.response.qpd import SingleQPWDescriptor
7import numpy as np
8from gpaw.response.coulomb_kernels import (get_coulomb_kernel,
9 get_integrated_kernel)
10from gpaw.kpt_descriptor import KPointDescriptor
11from gpaw.poisson_extravacuum import ExtraVacuumPoissonSolver
12from gpaw.poisson_moment import MomentCorrectionPoissonSolver
15class ExtraVacuum2DPoisson:
16 def __init__(self, poisson):
17 self.poisson = poisson
19 def set_grid_descriptor(self, gd):
20 self.N_c = gd.N_c
21 self.gd = GridDescriptor(gd.N_c * np.array([1, 1, 9]),
22 gd.cell_cv @ np.diag([1, 1, 9]),
23 pbc_c=gd.pbc_c)
24 self.poisson.set_grid_descriptor(self.gd)
26 def solve(self, v, n):
27 myv = self.gd.zeros()
28 myn = self.gd.zeros()
29 beg, end = self.N_c[2] * 4, self.N_c[2] * 5 - 1
30 myn[:, :, beg:end] = n
31 self.poisson.solve(myv, myn)
32 v[:] = myv[:, :, beg:end]
35class Grid:
36 def __init__(self, L, N, q, truncation):
37 if q == 0:
38 supercell = 1
39 else:
40 supercell = round(1 / q)
41 assert np.allclose(supercell * q, 1.0)
43 # First we create a unit cell quantities
44 pbc_c = [True, True, True]
45 if truncation is None:
46 pass
47 elif truncation == '0D':
48 pbc_c = [False, False, False]
49 elif truncation == '2D':
50 pbc_c = [True, True, False]
51 else:
52 raise NotImplementedError
53 pbc_c = np.array(pbc_c)
54 self.pbc_c = pbc_c
55 gd = GridDescriptor((N, N, N), [L, L, L], pbc_c=pbc_c)
56 self.periodicgd = GridDescriptor((N, N, N), [L, L, L])
57 kd = KPointDescriptor([[q, 0, 0]])
58 # Create some charge distribution to the unit cell
59 if truncation == '0D':
60 cut = L / 9
61 else:
62 cut = L / 4
63 spline = Spline.from_data(l=0, rmax=cut, f_g=np.array([1, 0.5, 0.0]))
64 c = LFC(gd, [[spline]] * 2, kd=kd, dtype=complex)
66 if truncation == '0D':
67 c.set_positions([[0.5 - 0.1, 0.5, 0.5], [0.5 + 0.1, 0.5, 0.5]])
68 else:
69 c.set_positions([[0.11, 0.0, 0.5], [0.5, 0.39, 0.5]])
70 n_R = gd.zeros(dtype=complex)
71 c.add(n_R, {0: np.array([-1], dtype=complex),
72 1: np.array([1], dtype=complex)}, 0)
74 # Now we replicate to supercell, to calculate using grid based Poisson
75 # solvers which cannot handle Bloch-phases at the moment
76 supergd = GridDescriptor((N * supercell, N, N),
77 [L * supercell, L, L],
78 pbc_c=pbc_c)
80 # Avoid going over 79 characters
81 EVPS = ExtraVacuumPoissonSolver
82 MCPS = MomentCorrectionPoissonSolver
83 PS = PoissonSolver
84 if truncation is None:
85 superpoisson = PS(nn=3)
86 elif truncation == '0D':
87 mcps = MCPS(PS(nn=3), moment_corrections=9)
88 superpoisson = EVPS(gpts=(N * 4, N * 4, N * 4),
89 poissonsolver_large=mcps,
90 coarses=1,
91 poissonsolver_small=PS(nn=3))
92 elif truncation == '2D':
93 superpoisson = ExtraVacuum2DPoisson(PS(nn=3))
94 else:
95 raise NotImplementedError(truncation)
96 superpoisson.set_grid_descriptor(supergd)
98 if q != 0:
99 supern_R = supergd.zeros(dtype=complex)
100 for i in range(supercell):
101 pn_R = n_R * np.exp(2j * np.pi * i / supercell)
102 supern_R[(i * N):((i + 1) * N), :, :] = pn_R
104 else:
105 supergd = gd
106 supern_R = n_R
108 superv_R = supergd.zeros(dtype=complex)
109 superpoisson.solve(superv_R.real, supern_R.real)
110 superpoisson.solve(superv_R.imag, supern_R.imag)
112 self.Ec = supergd.integrate(supern_R, superv_R) / supercell
114 self.gd = gd
115 self.n_R = gd.zero_pad(n_R)
118@pytest.mark.serial
119@pytest.mark.response
120@pytest.mark.parametrize('gridparam', [(32, 1e-5), (64, 1e-7), (96, 1e-8)])
121@pytest.mark.parametrize('qtrunc', [
122 (0, '2D', 20.7454594963, 506.01293778),
123 (1 / 3, '2D', 13.174804153, 190.916278817),
124 (0, None, 20.228908696, 578.42826785),
125 (1 / 3, None, 13.5467930334, 214.823201910),
126 (0, '0D', 14.3596834829, 206.74182299)])
127def test_coulomb(gridparam, qtrunc):
128 N, maxdev = gridparam
129 q, truncation, sqrtV0_dev, V0_dev = qtrunc
130 L = 10
131 print()
132 # Slightly lower tolerance for 0D system, because it uses so localized
133 # charges to avoid crosstalk
134 if truncation == '0D':
135 maxdev *= 1e2
136 grid = Grid(L, N, q, truncation=truncation)
137 # Use maximum ecut
138 ecut0 = 0.5 * np.pi**2 / ((L / N)**2)
139 qpd = SingleQPWDescriptor.from_q([q, 0, 0], ecut0, grid.periodicgd,
140 gammacentered=False)
141 # XXX It is silly that get_coulomb_kernel uses k-point numbers
142 # per dim to determine non-periodic direction. It should just get
143 # non_per_dir=2, etc.
144 N_c = np.array([2, 2, 1])
145 v_G = get_coulomb_kernel(qpd, N_c, truncation=truncation, pbc_c=grid.pbc_c)
146 V0, sqrtV0 = get_integrated_kernel(
147 qpd=qpd, N_c=N_c, truncation=truncation, pbc_c=grid.pbc_c, N=100)
148 assert V0 == pytest.approx(V0_dev, maxdev)
149 assert sqrtV0 == pytest.approx(sqrtV0_dev, maxdev)
151 n_G = qpd.fft(grid.n_R * grid.periodicgd.plane_wave([-q, 0, 0]))
152 Ec2 = n_G.conj() @ (v_G * n_G) / N**6 * L**3
153 dev = np.abs(grid.Ec / Ec2 - 1)
154 print(f'N={N:4d} q=[{q:.3f},0,0] truncation: {str(truncation):5s}'
155 f'rel. log10 deviation: {np.log10(dev):7.4f}'
156 f'coulomb:{Ec2:9.5f}')
157 assert np.abs(dev) < maxdev