Coverage for gpaw/response/pair_functions.py: 98%
206 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
1from pathlib import Path
2import numpy as np
4from ase.units import Hartree
6from gpaw.response.qpd import SingleQPWDescriptor
8from gpaw.response.frequencies import ComplexFrequencyDescriptor
9from gpaw.response.pair_integrator import DynamicPairFunction
10from gpaw.response.pw_parallelization import (Blocks1D,
11 PlaneWaveBlockDistributor)
14class LatticePeriodicPairFunction(DynamicPairFunction):
15 r"""Data object for lattice periodic pair functions.
17 Any spatial dependent pair function is considered to be lattice periodic,
18 if it is invariant under translations of Bravais lattice vectors R:
20 pf(r, r', z) = pf(r + R, r' + R, z).
22 The Bloch lattice Fourier transform of a lattice periodic pair function,
23 __
24 \
25 pf(r, r', q, z) = / e^(-iq.[r-r'-R']) pf(r, r' + R', z)
26 ‾‾
27 R'
29 is then periodic in both r and r' independently and can be expressed in an
30 arbitrary lattice periodic basis.
32 In the GPAW response code, lattice periodic pair functions are expanded in
33 plane waves,
35 1 //
36 pf_GG'(q, z) = ‾‾ || drdr' e^(-iG.r) pf(r, r', q, z) e^(iG'.r')
37 V0 //
38 V0
40 which are encoded in the SingleQPWDescriptor along with the wave vector q.
41 """
43 def __init__(self,
44 qpd: SingleQPWDescriptor,
45 zd: ComplexFrequencyDescriptor,
46 blockdist: PlaneWaveBlockDistributor,
47 distribution='ZgG'):
48 """Contruct the LatticePeriodicPairFunction.
50 Parameters
51 ----------
52 distribution : str
53 Memory distribution of the pair function array.
54 Choices: 'ZgG', 'GZg' and 'zGG'.
55 """
56 self.qpd = qpd
57 self.blockdist = blockdist
58 self.distribution = distribution
60 self.blocks1d = None
61 self.shape = None
62 super().__init__(qpd.q_c, zd)
64 def zeros(self):
65 if self.shape is None:
66 self._initialize_block_distribution()
67 return np.zeros(self.shape, complex)
69 def _initialize_block_distribution(self):
70 """Initialize 1D block distribution and corresponding array shape."""
71 nz = len(self.zd)
72 nG = self.qpd.ngmax
73 blockdist = self.blockdist
74 distribution = self.distribution
76 if distribution == 'ZgG':
77 blocks1d = Blocks1D(blockdist.blockcomm, nG)
78 shape = (nz, blocks1d.nlocal, nG)
79 elif distribution == 'GZg':
80 blocks1d = Blocks1D(blockdist.blockcomm, nG)
81 shape = (nG, nz, blocks1d.nlocal)
82 elif distribution == 'zGG':
83 blocks1d = Blocks1D(blockdist.blockcomm, nz)
84 shape = (blocks1d.nlocal, nG, nG)
85 else:
86 raise NotImplementedError(f'Distribution: {distribution}')
88 self.blocks1d = blocks1d
89 self.shape = shape
91 def array_with_view(self, view):
92 """Access a given view into the pair function array."""
93 if view == 'ZgG' and self.distribution in ['ZgG', 'GZg']:
94 if self.distribution == 'GZg':
95 pf_GZg = self.array
96 pf_ZgG = pf_GZg.transpose((1, 2, 0))
97 else:
98 pf_ZgG = self.array
100 pf_x = pf_ZgG
101 else:
102 raise ValueError(f'{view} is not a valid view, when array is of '
103 f'distribution {self.distribution}')
105 return pf_x
107 def copy_with_distribution(self, distribution='ZgG'):
108 """Copy the pair function to a specified memory distribution."""
109 new_pf = self._new(*self.my_args(), distribution=distribution)
110 new_pf.array[:] = self.array_with_view(distribution)
112 return new_pf
114 @classmethod
115 def _new(cls, *args, **kwargs):
116 return cls(*args, **kwargs)
118 def my_args(self, qpd=None, zd=None, blockdist=None):
119 """Return the positional construction arguments of the
120 LatticePeriodicPairFunction."""
121 if qpd is None:
122 qpd = self.qpd
123 if zd is None:
124 zd = self.zd
125 if blockdist is None:
126 blockdist = self.blockdist
128 return qpd, zd, blockdist
130 def copy_with_reduced_pd(self, qpd):
131 """Copy the pair function, but within a reduced plane-wave basis."""
132 new_pf = self._new(*self.my_args(qpd=qpd),
133 distribution=self.distribution)
134 if self.distribution == 'zGG':
135 new_pf.array[:] = map_zGG_array_to_reduced_pd(self.qpd, qpd,
136 self.array)
137 elif self.distribution == 'ZgG':
138 new_pf.array[:] = map_ZgG_array_to_reduced_pd(self.qpd, qpd,
139 self.blockdist,
140 self.array)
141 else:
142 raise NotImplementedError('Chi.copy_with_reduced_pd has not been '
143 'implemented for distribution '
144 f'{self.distribution}')
145 return new_pf
147 def copy_with_global_frequency_distribution(self):
148 """Copy the pair function, but with distribution zGG over world."""
149 # Make a copy, which is globally block distributed
150 blockdist = self.blockdist.new_distributor(nblocks='max')
151 new_pf = self._new(*self.my_args(blockdist=blockdist),
152 distribution='zGG')
154 # Redistribute the data, distributing the frequencies over world
155 assert self.distribution == 'ZgG'
156 new_pf.array[:] = self.blockdist.distribute_frequencies(self.array,
157 len(self.zd))
159 return new_pf
162def map_ZgG_array_to_reduced_pd(qpdi, qpd, blockdist, in_ZgG):
163 """Map the array in_ZgG from the qpdi to the qpd plane-wave basis."""
164 # Distribute over frequencies
165 nw = in_ZgG.shape[0]
166 tmp_zGG = blockdist.distribute_as(in_ZgG, nw, 'zGG')
168 # Reduce the plane-wave basis
169 tmp_zGG = map_zGG_array_to_reduced_pd(qpdi, qpd, tmp_zGG)
171 # Distribute over plane waves
172 out_ZgG = blockdist.distribute_as(tmp_zGG, nw, 'ZgG')
174 return out_ZgG
177def map_zGG_array_to_reduced_pd(qpdi, qpd, in_zGG):
178 """Map the array in_zGG from the qpdi to the qpd plane-wave basis."""
179 from gpaw.pw.descriptor import PWMapping
181 # Initialize the basis mapping
182 pwmapping = PWMapping(qpdi, qpd)
183 G2_GG = tuple(np.meshgrid(pwmapping.G2_G1, pwmapping.G2_G1,
184 indexing='ij'))
185 G1_GG = tuple(np.meshgrid(pwmapping.G1, pwmapping.G1,
186 indexing='ij'))
188 # Allocate array in the new basis
189 nG = qpd.ngmax
190 out_zGG_shape = (in_zGG.shape[0], nG, nG)
191 out_zGG = np.zeros(out_zGG_shape, complex)
193 # Extract values
194 for z, in_GG in enumerate(in_zGG):
195 out_zGG[z][G2_GG] = in_GG[G1_GG]
197 return out_zGG
200class Chi(LatticePeriodicPairFunction):
201 r"""Data object for the four-component susceptibility tensor χ_GG'^μν(q,z).
202 """
204 def __init__(self, spincomponent, qpd, zd,
205 blockdist, distribution='ZgG'):
206 r"""Construct a susceptibility of a given spin-component (μν)."""
207 self.spincomponent = spincomponent
208 super().__init__(qpd, zd, blockdist, distribution=distribution)
210 def new(self, **kwargs):
211 return self._new(*self.my_args_and_kwargs(**kwargs))
213 def my_args(self, spincomponent=None, qpd=None, zd=None, blockdist=None):
214 """Return positional construction arguments of the Chi object."""
215 if spincomponent is None:
216 spincomponent = self.spincomponent
217 qpd, zd, blockdist = super().my_args(qpd=qpd, zd=zd,
218 blockdist=blockdist)
220 return spincomponent, qpd, zd, blockdist
222 def my_args_and_kwargs(self, distribution=None, **args):
223 """Return all the construction arguments of Chi, in order."""
224 args = self.my_args(**args)
225 if distribution is None:
226 distribution = self.distribution
227 return args + (distribution,)
229 def copy_with_reduced_ecut(self, ecut):
230 """Copy the susceptibility, but with a reduced ecut."""
231 ecut = ecut / Hartree # eV -> Hartree
232 assert ecut <= self.qpd.ecut
233 qpd = self.qpd.copy_with(ecut=ecut)
234 return self.copy_with_reduced_pd(qpd)
236 def copy_reactive_part(self):
237 r"""Return a copy of the reactive part of the susceptibility.
239 The reactive part of the susceptibility is defined as (see
240 [PRB 103, 245110 (2021)]):
242 1
243 χ_GG'^(μν')(q,z) = ‾ [χ_GG'^μν(q,z) + χ_(-G'-G)^νμ(-q,-z*)].
244 2
246 However if the density operators n^μ(r) and n^ν(r) are each others
247 Hermitian conjugates, the reactive part simply becomes the Hermitian
248 part in terms of the plane-wave basis:
250 1
251 χ_GG'^(μν')(q,z) = ‾ [χ_GG'^μν(q,z) + χ_G'G^(μν*)(q,z)],
252 2
254 which is trivial to evaluate.
255 """
256 assert self.distribution == 'zGG' or \
257 (self.distribution == 'ZgG' and self.blockdist.blockcomm.size == 1)
258 assert self.spincomponent in ['00', 'uu', 'dd', '+-', '-+'], \
259 'Spin-density operators has to be each others hermitian conjugates'
260 chiksr = self.new(distribution='zGG')
261 chiks_zGG = self.array
262 chiksr.array += chiks_zGG
263 chiksr.array += np.conj(np.transpose(chiks_zGG, (0, 2, 1)))
264 chiksr.array /= 2.
266 return chiksr
268 def copy_dissipative_part(self):
269 r"""Return a copy of the dissipative part of the susceptibility.
271 The dissipative part of the susceptibility is defined as (see
272 [PRB 103, 245110 (2021)]):
274 1
275 χ_GG'^(μν")(q,z) = ‾‾ [χ_GG'^μν(q,z) - χ_(-G'-G)^νμ(-q,-z*)].
276 2i
278 Similar to the reactive part, this expression reduces to the
279 anti-Hermitian part of the susceptibility in terms of the plane-wave
280 basis, whenever the density operators n^μ(r) and n^ν(r) are each others
281 Hermitian conjugates:
283 1
284 χ_GG'^(μν")(q,z) = ‾‾ [χ_GG'^μν(q,z) - χ_G'G^(μν*)(q,z)].
285 2i
286 """
287 assert self.distribution == 'zGG' or \
288 (self.distribution == 'ZgG' and self.blockdist.blockcomm.size == 1)
289 assert self.spincomponent in ['00', 'uu', 'dd', '+-', '-+'], \
290 'Spin-density operators has to be each others hermitian conjugates'
291 chiksd = self.new(distribution='zGG')
292 chiks_zGG = self.array
293 chiksd.array += chiks_zGG
294 chiksd.array -= np.conj(np.transpose(chiks_zGG, (0, 2, 1)))
295 chiksd.array /= 2.j
297 return chiksd
299 def symmetrize_reciprocity(self):
300 r"""Symmetrize the reciprocity of the susceptibility (for q=0).
302 In collinear systems without spin-orbit coupling, the plane-wave
303 susceptibility is reciprocal in the sense that
305 χ_GG'^(μν)(q, ω) = χ_(-G'-G)^(μν)(-q, ω)
307 for all μν ∈ {00, uu, dd, +-, -+}, see [PRB 106, 085131 (2022)].
308 For q=0, we may symmetrize the susceptibility in this sense for free.
309 """
310 assert np.allclose(self.q_c, 0.)
311 assert self.distribution == 'zGG' or \
312 (self.distribution == 'ZgG' and self.blockdist.blockcomm.size == 1)
313 assert self.spincomponent in ['00', 'uu', 'dd', '+-', '-+'], \
314 'Spin-density operators has to be each others hermitian conjugates'
315 invmap_GG = get_inverted_pw_mapping(self.qpd, self.qpd)
316 for chi_GG in self.array:
317 # Symmetrize [χ_(GG')(q, ω) + χ_(-G'-G)(-q, ω)] / 2
318 chi_GG[:] = (chi_GG + chi_GG[invmap_GG].T) / 2.
320 def write_macroscopic_component(self, filename):
321 """Write the spatially averaged (macroscopic) component of the
322 susceptibility to a file along with the frequency grid."""
323 chi_Z = self.get_macroscopic_component()
324 if self.blocks1d.blockcomm.rank == 0:
325 write_pair_function(filename, self.zd, chi_Z)
327 def get_macroscopic_component(self):
328 """Get the macroscopic (G=0) component, all-gathered."""
329 assert self.distribution == 'zGG'
330 chi_zGG = self.array
331 chi_z = chi_zGG[:, 0, 0] # Macroscopic component
332 chi_Z = self.blocks1d.all_gather(chi_z)
333 return chi_Z
335 def write_array(self, filename):
336 """Write the full susceptibility array to a file along with the
337 frequency grid and plane-wave components."""
338 assert self.distribution == 'zGG'
339 chi_ZGG = self.blocks1d.gather(self.array)
340 if self.blocks1d.blockcomm.rank == 0:
341 write_susceptibility_array(filename, self.zd, self.qpd, chi_ZGG)
343 def write_diagonal(self, filename):
344 """Write the diagonal of the many-body susceptibility within a reduced
345 plane-wave basis to a file along with the frequency grid."""
346 assert self.distribution == 'zGG'
347 chi_zGG = self.array
348 chi_zG = np.diagonal(chi_zGG, axis1=1, axis2=2)
349 chi_ZG = self.blocks1d.gather(chi_zG)
350 if self.blocks1d.blockcomm.rank == 0:
351 write_susceptibility_array(filename, self.zd, self.qpd, chi_ZG)
354def get_inverted_pw_mapping(qpd1, qpd2):
355 """Get the planewave coefficients mapping GG' of qpd1 into -G-G' of qpd2"""
356 G1_Gc = get_pw_coordinates(qpd1)
357 G2_Gc = get_pw_coordinates(qpd2)
359 mG2_G1 = []
360 for G1_c in G1_Gc:
361 found_match = False
362 for G2, G2_c in enumerate(G2_Gc):
363 if np.all(G2_c == -G1_c):
364 mG2_G1.append(G2)
365 found_match = True
366 break
367 if not found_match:
368 raise ValueError('Could not match qpd1 and qpd2')
370 # Set up mapping from GG' to -G-G'
371 invmap_GG = tuple(np.meshgrid(mG2_G1, mG2_G1, indexing='ij'))
373 return invmap_GG
376def get_pw_coordinates(qpd):
377 """Get the reciprocal lattice vector coordinates corresponding to a
378 givne plane wave basis.
380 Please remark, that the response code currently works with one q-vector
381 at a time, at thus only a single plane wave representation at a time.
383 Returns
384 -------
385 G_Gc : nd.array (dtype=int)
386 Coordinates on the reciprocal lattice
387 """
388 # List of all plane waves
389 G_Gv = np.array([qpd.G_Qv[Q] for Q in qpd.Q_qG[0]])
391 # Use cell to get coordinates
392 B_cv = 2.0 * np.pi * qpd.gd.icell_cv
393 return np.round(np.dot(G_Gv, np.linalg.inv(B_cv))).astype(int)
396def write_pair_function(filename, zd, pf_z):
397 """Write a pair function pf(q,z) for a specific q."""
398 # For now, we assume that the complex frequencies lie on a horizontal
399 # contour and that the pair function is complex. This could be easily
400 # generalized at a later stage.
401 assert zd.horizontal_contour
402 omega_w = zd.omega_w * Hartree # Ha -> eV
403 pf_w = pf_z # Atomic units
404 assert pf_w.dtype == complex
406 # Write results file
407 with open(filename, 'w') as fd:
408 print('# {:>11}, {:>11}, {:>11}'.format(
409 'omega [eV]', 'pf_w.real', 'pf_w.imag'), file=fd)
410 for omega, pf in zip(omega_w, pf_w):
411 print(' {:11.6f}, {:11.6f}, {:11.6f}'.format(
412 omega, pf.real, pf.imag), file=fd)
415def read_pair_function(filename):
416 """Read a stored pair function file."""
417 d = np.loadtxt(filename, delimiter=',')
419 if d.shape[1] == 3:
420 # Complex pair function on a horizontal contour
421 omega_w = np.array(d[:, 0], float)
422 pf_w = np.array(d[:, 1], complex)
423 pf_w.imag = d[:, 2]
424 else:
425 raise ValueError(f'Unexpected array dimension {d.shape}')
427 return omega_w, pf_w
430def write_susceptibility_array(filename, zd, qpd, chi_zx):
431 """Write dynamic susceptibility array to a .npz file."""
432 assert Path(filename).suffix == '.npz', filename
433 # For now, we assume that the complex frequencies lie on a horizontal
434 # contour
435 assert zd.horizontal_contour
436 omega_w = zd.omega_w * Hartree # Ha -> eV
437 G_Gc = get_pw_coordinates(qpd)
438 chi_wx = chi_zx
439 np.savez(filename, omega_w=omega_w, G_Gc=G_Gc, chi_wx=chi_wx)
442def read_susceptibility_array(filename):
443 """Read a stored dynamic susceptibility array file."""
444 assert Path(filename).suffix == '.npz', filename
445 npzfile = np.load(filename)
446 return npzfile['omega_w'], npzfile['G_Gc'], npzfile['chi_wx']