Coverage for gpaw/response/paw.py: 99%

210 statements  

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

1from __future__ import annotations 

2 

3import numpy as np 

4from scipy.special import spherical_jn 

5from dataclasses import dataclass 

6 

7from gpaw.spline import Spline 

8from gpaw.ffbt import rescaled_fourier_bessel_transform 

9from gpaw.gaunt import gaunt, super_gaunt 

10from gpaw.spherical_harmonics import Y 

11from gpaw.atom.radialgd import RadialGridDescriptor 

12from gpaw.sphere.rshe import RealSphericalHarmonicsExpansion 

13from gpaw.response.pw_parallelization import Blocks1D 

14 

15 

16# Important note: The test suite monkeypatches this value to 2**10 so 

17# you may get different results in tests and production until we 

18# implement a better solution (e.g. generating setup-dependent radial_points). 

19# 

20# The motivation for lowering to 2**10 in tests is that many tests 

21# take 3-4 times longer if we do not. 

22# 

23# See https://gitlab.com/gpaw/gpaw/-/issues/984 

24DEFAULT_RADIAL_POINTS = 2**12 

25 

26 

27@dataclass 

28class LeanPAWDataset: 

29 rgd: RadialGridDescriptor 

30 l_j: np.ndarray 

31 rcut_j: np.ndarray 

32 phit_jg: np.ndarray 

33 phi_jg: np.ndarray 

34 # Number of radial points in spline interpolation 

35 radial_points: int | None = None 

36 

37 def __post_init__(self): 

38 if self.radial_points is None: 

39 # We assign this late due to monkeypatch in testing 

40 self.radial_points = DEFAULT_RADIAL_POINTS 

41 

42 # Number of basis functions 

43 self.ni = np.sum([2 * l + 1 for l in self.l_j]) 

44 # Maximum angular momentum index l 

45 self.lmax = np.max(self.l_j) 

46 # Grid cutoff to create spline representation 

47 self.gcut2 = self.rgd.ceil(2 * max(self.rcut_j)) 

48 

49 # Set up cache 

50 self.dn_g_cache = {} 

51 self.dn_kspline_cache = {} 

52 

53 def dn_kspline(self, j1: int, j2: int, l: int): 

54 """Get spline representation of Δn_jj'l(k).""" 

55 if (j1, j2, l) not in self.dn_kspline_cache: 

56 dn_g = self.get_pair_density_correction(j1, j2) 

57 self.dn_kspline_cache[j1, j2, l] = \ 

58 self.rescaled_fourier_bessel_transform(dn_g, l) 

59 return self.dn_kspline_cache[j1, j2, l] 

60 

61 def get_pair_density_correction(self, j1: int, j2: int): 

62 """Get Δn_jj'(r), while keeping the newest correction cached.""" 

63 if (j1, j2) not in self.dn_g_cache: 

64 self.dn_g_cache = {} # keep only one density in cache 

65 self.dn_g_cache[j1, j2] = self.calculate_pair_density_correction( 

66 j1, j2) 

67 return self.dn_g_cache[j1, j2] 

68 

69 def calculate_pair_density_correction(self, j1, j2): 

70 """Calculate the pair density PAW correction for two partial waves. 

71 ˷ ˷ 

72 Δn (r) = φ (r) φ (r) - φ (r) φ (r) 

73 jj' j j' j j' 

74 """ 

75 # (Real) radial functions for the partial waves 

76 phi_jg = self.phi_jg 

77 phit_jg = self.phit_jg 

78 return phi_jg[j1] * phi_jg[j2] - phit_jg[j1] * phit_jg[j2] 

79 

80 def rescaled_fourier_bessel_transform(self, f_g, l): 

81 """Calculate the rescaled Fourier Bessel transform f_l(k) 

82 

83 rc 

84 4π ⌠ 2 

85 f (k) = ‾‾‾ ⎪ r dr j (kr) f(r) 

86 l k^l ⌡ l 

87 0 

88 """ 

89 # First, we make a spline representation of the radial function f(r), 

90 # which is rescaled by a factor of r^-l by the radial grid descriptor: 

91 spline = self.rgd.spline( 

92 f_g[:self.gcut2], l=l, points=self.radial_points) 

93 # Once rescaled by r^-l, we can use the gpaw.ffbt module to Fast 

94 # Fourier Bessel Transform the radial spline r^-l f(r) and obtain 

95 # f_l(k) in a reciprocal spline representation 

96 kspline = rescaled_fourier_bessel_transform( 

97 spline, N=4 * self.radial_points) 

98 # Since this procedures hinges on a series of hardcoded parameters, we 

99 # return a self-testing version of the spline. If someone wants to run 

100 # calculations without dynamic testing of the FFBT methodology (i.e. 

101 # in an "unprotected" mode), simply return the bare `kspline` here. 

102 return SelfTestingKSpline(self.rgd, f_g, kspline) 

103 

104 

105class SelfTestingKSpline(Spline): 

106 """Self-testing reciprocal spline representation, f_l(k).""" 

107 

108 def __init__(self, rgd, f_g, spline: Spline): 

109 # Store original real-space representation of the radial function f(r) 

110 self.rgd = rgd 

111 self.f_g = f_g 

112 super().__init__(spline.spline) 

113 

114 def map(self, k_G): 

115 self.test_spline_representation(k_G) 

116 return super().map(k_G) 

117 

118 def test_spline_representation(self, k_G): 

119 """Test validity of the FFBT implementation on input domain. 

120 

121 At present, LeanPAWDataset's FFBT implementation relies on a range of 

122 hardcoded parameters, which are not guaranteed to work for all cases. 

123 

124 In particular, the uniform radial grid used for the FFBT is defined 

125 through the `rcut` and `N` parameters in 

126 `rescaled_fourier_bessel_transform()` 

127 where the former is hardcoded inside the function itself. 

128 

129 Furthermore, the `points` parameter to `rgd.spline()` controls the 

130 fidelity of the interpolation between nonlinear and equidistant radial 

131 grids needed to make use of the FFBT algorithm. 

132 

133 To make a generally reliable implementation, one would need to control 

134 all of these parameters based on the setup, e.g. the nonlinear radial 

135 grid spacing. In doing so, one should be mindful that the `rcut` 

136 parameter defines the reciprocal grid spacing of the kspline and that 

137 `N` controls the range of the reciprocal space domain. 

138 

139 For now, we simply check that the requested plane waves are within the 

140 computed k-range of the FFBT and check that the resulting transforms 

141 match a manual calculation at a few selected K-vectors. 

142 """ 

143 kmax = np.max(k_G) 

144 assert kmax <= self.get_cutoff() 

145 

146 # Manual calculation at finite k 

147 k_k = np.array([kmax, np.average(k_G)]) 

148 f_k = 4 * np.pi * fourier_bessel_transform( 

149 np.array(k_k), self.l, self.rgd, self.f_g) 

150 # Manual calculation at k=0 

151 if self.l == 0: # Vanishes for l>0 

152 k_k = np.append(k_k, [0.]) 

153 f_k = np.append(f_k, [self.rgd.integrate(self.f_g)]) 

154 # FFBT calculation 

155 myf_k = k_k**self.l * super().map(k_k) 

156 assert np.allclose(myf_k, f_k, rtol=1e-2, atol=1e-3), \ 

157 f'FFBT mismatch: {myf_k}, {f_k}' 

158 

159 

160def calculate_pair_density_correction(qG_Gv: np.ndarray, *, 

161 pawdata: LeanPAWDataset): 

162 r"""Calculate the atom-centered PAW correction to the pair density. 

163 ˍ 

164 The atom-centered pair density correction tensor, Q_aii', is defined as the 

165 atom-centered Fourier transform 

166 

167 ˍ / ˷ ˷ 

168 Q_aii'(G+q) = | dr e^-i(G+q)r [φ_ai^*(r) φ_ai'(r) - φ_ai^*(r) φ_ai'(r)] 

169 / 

170 

171 evaluated with the augmentation sphere center at the origin. The full pair 

172 density correction tensor is then given by 

173 ˍ 

174 Q_aii'(G+q) = e^(-i[G+q].R_a) Q_aii'(G+q) 

175 

176 Expanding the plane wave coefficient into real spherical harmonics and 

177 spherical Bessel functions, the correction can split into angular and 

178 radial contributions 

179 

180 l 

181 __ __ 

182 \ \ l m ˰ m,m_i,m_i' 

183 Q_aii'(K) = / / (-i) Y (K) g 

184 ‾‾ ‾‾ l l,l_i,l_i' 

185 l m=-l 

186 rc 

187 / a a ˷a ˷a 

188 × 4π | r^2 dr j_l(|K|r) [φ (r) φ (r) - φ (r) φ (r)] 

189 / j_i j_i' j_i j_i' 

190 0 

191 

192 where K = G+q and g denotes the Gaunt coefficients. 

193 

194 For more information, see [PRB 103, 245110 (2021)]. In particular, it 

195 should be noted that the partial waves themselves are defined via real 

196 spherical harmonics and radial functions φ_j from the PAW setups: 

197 

198 a m_i ˰ a 

199 φ (r) = Y (r) φ (r) 

200 i l_i j_i 

201 """ 

202 ni = pawdata.ni # Number of partial waves 

203 l_j = pawdata.l_j # l-index for each radial function index j 

204 G_LLL = gaunt(pawdata.lmax) 

205 

206 # Initialize correction tensor 

207 npw = qG_Gv.shape[0] 

208 Qbar_Gii = np.zeros((npw, ni, ni), dtype=complex) 

209 

210 # K-vector norm 

211 k_G = np.linalg.norm(qG_Gv, axis=1) 

212 

213 # Loop of radial function indices for partial waves i and i' 

214 i1_counter = 0 

215 for j1, l1 in enumerate(l_j): 

216 i2_counter = 0 

217 for j2, l2 in enumerate(l_j): 

218 # Sample l according to the Gaunt coefficient selection rules, see 

219 # e.g. gpaw.test.test_gaunt 

220 for l in range(abs(l1 - l2), l1 + l2 + 1, 2): 

221 # Calculate the spherical Fourier-Bessel transform 

222 # rc 

223 # 4π / 

224 # Δn_jj'l(k) = ‾‾‾ | r^2 dr j_l(kr) Δn_jj'(r) 

225 # k^l / 

226 # 0 

227 # To evaluate the radial integral efficiently, we rely on the 

228 # Fast Fourier Bessel Transform (FFBT) algorithm, see gpaw.ffbt 

229 # The FFBT produces a radial spline representation of 

230 # Δn_jj'l(k), which we map to the K-vector norms in question: 

231 dn_G = pawdata.dn_kspline(j1, j2, l).map(k_G) 

232 

233 # Angular part of the integral 

234 f_G = (-1j)**l * dn_G 

235 for m in range(l**2, (l + 1)**2): 

236 # Calculate the solid harmonic 

237 # m ˰ 

238 # |K|^l Y (K) 

239 # l 

240 klY_G = Y(m, *qG_Gv.T) 

241 

242 # Generate m-indices for each radial function 

243 for m1 in range(2 * l1 + 1): 

244 for m2 in range(2 * l2 + 1): 

245 # Set up the i=(l,m) index for each partial wave 

246 i1 = i1_counter + m1 

247 i2 = i2_counter + m2 

248 # Extract Gaunt coefficients 

249 gaunt_coeff = G_LLL[l1**2 + m1, l2**2 + m2, m] 

250 if (gaunt_coeff == 0): 

251 continue 

252 

253 # Add contribution to the PAW correction 

254 Qbar_Gii[:, i1, i2] += gaunt_coeff * klY_G * f_G 

255 

256 # Add to i and i' counters 

257 i2_counter += 2 * l2 + 1 

258 i1_counter += 2 * l1 + 1 

259 return Qbar_Gii 

260 

261 

262def calculate_matrix_element_correction(qG_Gv, pawdata, 

263 rshe: RealSphericalHarmonicsExpansion): 

264 r"""Calculate the atom-centered correction to a generalized matrix element. 

265 

266 For matrix elements corresponding to the expectation value of a plane wave 

267 coefficient e^-i(G+q)r and a known functional of the (spin-)density 

268 f[n](r), the PAW correction tensor is given by 

269 

270 F_aii'(G+q) = <φ_ai| e^-i(G+q)r f[n](r) |φ_ai'> 

271 ˷ ˷ 

272 - <φ_ai| e^-i(G+q)r f[n](r) |φ_ai'> 

273 ˍ 

274 = e^(-i[G+q].R_a) F_aii'(G+q) 

275 ˍ 

276 where F_aii'(G+q) is the atom-centered PAW correction tensor. 

277 

278 Expanding the functional f[n](r) in the atom-centered frame in real 

279 spherical harmonics (corresponding to the input rshe), 

280 

281 l 

282 __ __ 

283 → \ \ m ˰ m 

284 f[n](r) = / / Y (r) f (r) 

285 ‾‾ ‾‾ l l 

286 l m=-l 

287 

288 expansion of the plane-wave coefficient in real spherical harmonics and 

289 spherical Bessel functions j_l(Kr) yields the following expression for the 

290 atom-centered correction tensor [publication in preparation]: 

291 

292 l l' 

293 __ __ __ __ 

294 ˍ \ \ \ \ l' m'˰ m_i,m_i',m,m' 

295 F_aii'(K) = 4π / / / / (-i) Y (K) G 

296 ‾‾ ‾‾ ‾‾ ‾‾ l' l_i,l_i',l,l' 

297 l m=-l l' m'=-l' 

298 

299 rc 

300 / 2 a a ˷a ˷a m 

301 × | r dr j (Kr) [φ (r) φ (r) - φ (r) φ (r)] f (r) 

302 / l' j_i j_i' j_i j_i' l 

303 0 

304 

305 where K=G+q and G_LLLL denotes the super Gaunt coefficients, which yield 

306 the integrals over four spherical harmonics. 

307 """ 

308 rgd = rshe.rgd 

309 assert rgd is pawdata.xc_correction.rgd 

310 ni = pawdata.ni # Number of partial waves 

311 l_j = pawdata.l_j # l-index for each radial function index j 

312 lmax = max(l_j) 

313 assert max(rshe.l_M) <= 2 * lmax 

314 G_LLLL = super_gaunt(lmax) 

315 # (Real) radial functions for the partial waves 

316 phi_jg = pawdata.phi_jg 

317 phit_jg = pawdata.phit_jg 

318 # Truncate the radial functions to span only the radial grid coordinates 

319 # which need correction 

320 assert np.allclose(rgd.r_g, pawdata.rgd.r_g[:rgd.N]) 

321 phi_jg = np.array(phi_jg)[:, :rgd.N] 

322 phit_jg = np.array(phit_jg)[:, :rgd.N] 

323 

324 # Initialize correction tensor 

325 npw = qG_Gv.shape[0] 

326 Fbar_Gii = np.zeros((npw, ni, ni), dtype=complex) 

327 

328 # K-vector norm and direction 

329 k_G = np.linalg.norm(qG_Gv, axis=1) 

330 Kd_Gv = qG_Gv.copy() 

331 Kd_Gv[k_G > 1e-10] /= k_G[k_G > 1e-10, np.newaxis] 

332 

333 # Loop of radial function indices for partial waves i and i' 

334 i1_counter = 0 

335 for j1, l1 in enumerate(l_j): 

336 i2_counter = 0 

337 for j2, l2 in enumerate(l_j): 

338 # Calculate the radial partial wave correction 

339 # ˷ ˷ 

340 # Δn_jj'(r) = φ_j(r) φ_j'(r) - φ_j(r) φ_j'(r) 

341 dn_g = phi_jg[j1] * phi_jg[j2] - phit_jg[j1] * phit_jg[j2] 

342 

343 # Loop through the angular components in the real spherical 

344 # harmonics expansion of f[n](r) 

345 for l, L, f_g in zip(rshe.l_M, rshe.L_M, rshe.f_gM.T): 

346 dnf_g = dn_g * f_g 

347 # Apply Gaunt coefficient selection rules to loop through 

348 # the l' coefficients of the plane-wave expansion 

349 lpmin = np.min(abs( 

350 np.arange(abs(l1 - l2), l1 + l2 + 1) - l)) 

351 for lp in range(lpmin, l1 + l2 + l + 1): 

352 if not (l1 + l2 + l + lp) % 2 == 0: 

353 continue 

354 # Calculate radial part of the correction 

355 dnf_G = parallel_fourier_bessel_transform( 

356 k_G, lp, rgd, dnf_g) 

357 

358 # Calculate angular part of the correction 

359 x_G = 4 * np.pi * (-1j)**lp * dnf_G 

360 # Loop through available m-indices for the partial waves 

361 # and generate the composite L=(l,m) index as well as the 

362 # partial wave index i 

363 for m1 in range(2 * l1 + 1): 

364 L1 = l1**2 + m1 

365 i1 = i1_counter + m1 

366 for m2 in range(2 * l2 + 1): 

367 L2 = l2**2 + m2 

368 i2 = i2_counter + m2 

369 # Loop through m' indices of the plane-wave 

370 # expansion and generate the L' composite index 

371 for mp in range(2 * lp + 1): 

372 Lp = lp**2 + mp 

373 # If the angular integral (super gaunt 

374 # coefficient) is finite, 

375 coeff = G_LLLL[L1, L2, L, Lp] 

376 if abs(coeff) > 1e-10: 

377 # Calculate spherical harmonic and add 

378 # contribution to the PAW correction 

379 Y_G = Y(Lp, *Kd_Gv.T) 

380 Fbar_Gii[:, i1, i2] += coeff * Y_G * x_G 

381 

382 # Add to i and i' counters 

383 i2_counter += 2 * l2 + 1 

384 i1_counter += 2 * l1 + 1 

385 return Fbar_Gii 

386 

387 

388def parallel_fourier_bessel_transform(k_G, *args, comm=None): 

389 """Distribute FBT plane-wave components over a given communicator.""" 

390 # NB: If we need to do something similar elsewhere, we can generalize this 

391 # function to a decorator! 

392 if comm is None: 

393 from gpaw.mpi import world as comm 

394 Gblocks = Blocks1D(comm, len(k_G)) 

395 f_myG = fourier_bessel_transform(k_G[Gblocks.myslice], *args) 

396 return Gblocks.all_gather(f_myG) 

397 

398 

399def fourier_bessel_transform(k_G, l, rgd, f_g): 

400 """Perform a spherical Fourier-Bessel transform of a radial function f(r). 

401 

402 Computes the transform 

403 

404 max 

405 r 

406 ⌠ 2 

407 f(k) = ⎪ r dr j (kr) f(r) 

408 ⌡ l 

409 0 

410 

411 on the supplied radial grid. 

412 """ 

413 # Vectorize calculation of spherical Bessel functions 

414 l_Gg = l * np.ones((len(k_G), rgd.N), dtype=int) 

415 kr_Gg = k_G[:, np.newaxis] * rgd.r_g[np.newaxis] 

416 jl_Gg = spherical_jn(l_Gg, kr_Gg) # so slow... 

417 # Integrate the radial grid using linear interpolation 

418 f_G = rgd.integrate_trapz(jl_Gg * f_g[np.newaxis]) 

419 return f_G 

420 

421 

422class PWPAWCorrectionData: 

423 def __init__(self, Q_aGii, qpd, pawdatasets, pos_av, atomrotations): 

424 # Sometimes we loop over these in ways that are very dangerous. 

425 # It must be list, not dictionary. 

426 assert isinstance(Q_aGii, list) 

427 assert len(Q_aGii) == len(pos_av) == len(pawdatasets.by_atom) 

428 

429 self.Q_aGii = Q_aGii 

430 

431 self.qpd = qpd 

432 self.pawdatasets = pawdatasets 

433 self.pos_av = pos_av 

434 self.atomrotations = atomrotations 

435 

436 def _new(self, Q_aGii): 

437 return PWPAWCorrectionData(Q_aGii, qpd=self.qpd, 

438 pawdatasets=self.pawdatasets, 

439 pos_av=self.pos_av, 

440 atomrotations=self.atomrotations) 

441 

442 def remap(self, M_vv, G_Gv, sym, sign): 

443 Q_aGii = [] 

444 for a, Q_Gii in enumerate(self.Q_aGii): 

445 x_G = self._get_x_G(G_Gv, M_vv, self.pos_av[a]) 

446 U_ii = self.atomrotations.get_by_a(a).R_sii[sym] 

447 

448 Q_Gii = np.einsum('ij,kjl,ml->kim', 

449 U_ii, 

450 Q_Gii * x_G[:, None, None], 

451 U_ii, 

452 optimize='optimal') 

453 if sign == -1: 

454 Q_Gii = Q_Gii.conj() 

455 Q_aGii.append(Q_Gii) 

456 

457 return self._new(Q_aGii) 

458 

459 def _get_x_G(self, G_Gv, M_vv, pos_v): 

460 # This doesn't really belong here. Or does it? Maybe this formula 

461 # is only used with PAW corrections. 

462 return np.exp(1j * (G_Gv @ (pos_v - M_vv @ pos_v))) 

463 

464 def remap_by_symop(self, symop, G_Gv, M_vv): 

465 return self.remap(M_vv, G_Gv, symop.symno, symop.sign) 

466 

467 def multiply(self, P_ani, band): 

468 assert isinstance(P_ani, list) 

469 assert len(P_ani) == len(self.Q_aGii) 

470 

471 C1_aGi = [Qa_Gii @ P1_ni[band].conj() 

472 for Qa_Gii, P1_ni in zip(self.Q_aGii, P_ani)] 

473 return C1_aGi 

474 

475 def reduce_ecut(self, G2G): 

476 # XXX actually we should return this with another PW descriptor. 

477 return self._new([Q_Gii.take(G2G, axis=0) for Q_Gii in self.Q_aGii]) 

478 

479 def almost_equal(self, otherpawcorr, G_G): 

480 for a, Q_Gii in enumerate(otherpawcorr.Q_aGii): 

481 e = abs(self.Q_aGii[a] - Q_Gii[G_G]).max() 

482 if e > 1e-12: 

483 return False 

484 return True 

485 

486 

487def get_pair_density_paw_corrections(pawdatasets, qpd, spos_ac, atomrotations): 

488 r"""Calculate and bundle paw corrections to the pair densities as a 

489 PWPAWCorrectionData object. 

490 

491 The pair density PAW correction tensor is given by: 

492 

493 / 

494 Q_aii'(G+q) = | dr e^(-i[G+q].r) [φ_ai^*(r-R_a) φ_ai'(r-R_a) 

495 / ˷ ˷ 

496 - φ_ai^*(r-R_a) φ_ai'(r-R_a)] 

497 """ 

498 qG_Gv = qpd.get_reciprocal_vectors(add_q=True) 

499 pos_av = spos_ac @ qpd.gd.cell_cv 

500 

501 # Calculate pair density PAW correction tensor 

502 Qbar_xGii = {} 

503 for species_index, pawdata in pawdatasets.by_species.items(): 

504 # Calculate atom-centered correction tensor 

505 Qbar_Gii = calculate_pair_density_correction(qG_Gv, pawdata=pawdata) 

506 # Add dependency on the atomic position (phase factor) 

507 Qbar_xGii[species_index] = Qbar_Gii 

508 

509 Q_aGii = [] 

510 for a, (pos_v, pawdata) in enumerate(zip(pos_av, pawdatasets.by_atom)): 

511 x_G = np.exp(-1j * (qG_Gv @ pos_v)) 

512 species_index = pawdatasets.id_by_atom[a] 

513 Qbar_Gii = Qbar_xGii[species_index] 

514 Q_aGii.append(x_G[:, np.newaxis, np.newaxis] * Qbar_Gii) 

515 

516 return PWPAWCorrectionData(Q_aGii, qpd=qpd, 

517 pawdatasets=pawdatasets, 

518 pos_av=pos_av, 

519 atomrotations=atomrotations) 

520 

521 

522def get_matrix_element_paw_corrections(qpd, pawdata_a, rshe_a, spos_ac): 

523 r"""Calculate the PAW correction to a generalized matrix element. 

524 

525 For a given functional of the electron (spin-)density f[n](r), the PAW 

526 correction is given by 

527 ˍ 

528 F_aii'(G+q) = e^(-i[G+q].R_a) F_aii'(G+q) 

529 ˍ 

530 where F_aii'(G+q) is the atom-centered correction (see above). 

531 """ 

532 qG_Gv = qpd.get_reciprocal_vectors(add_q=True) 

533 

534 F_aGii = [] 

535 for pawdata, rshe, spos_c in zip(pawdata_a.by_atom, rshe_a, spos_ac): 

536 # Calculate atom-centered PAW correction 

537 Fbar_Gii = calculate_matrix_element_correction( 

538 qG_Gv, pawdata, rshe) 

539 

540 # XXX Can time be saved by doing some of the processing per species 

541 # rather than per atom? 

542 

543 # Add dependency on the atomic position (phase factor) 

544 pos_v = spos_c @ qpd.gd.cell_cv 

545 x_G = np.exp(-1j * (qG_Gv @ pos_v)) 

546 F_aGii.append(x_G[:, np.newaxis, np.newaxis] * Fbar_Gii) 

547 

548 return F_aGii