Coverage for gpaw/densities.py: 51%

136 statements  

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

1from __future__ import annotations 

2 

3from math import pi 

4from typing import TYPE_CHECKING 

5 

6import numpy as np 

7from ase.units import Bohr 

8 

9from gpaw.core.atom_arrays import AtomArrays, AtomArraysLayout 

10from gpaw.core.uniform_grid import UGArray 

11from gpaw.setup import Setups 

12from gpaw.spherical_harmonics import Y 

13from gpaw.spline import Spline 

14from gpaw.typing import Array1D, Array3D, Vector, Array2D 

15from gpaw.new import zips as zip 

16 

17if TYPE_CHECKING: 

18 from gpaw.new.calculation import DFTCalculation 

19 

20 

21class Densities: 

22 def __init__(self, 

23 nt_sR: UGArray, 

24 D_asii: AtomArrays, 

25 relpos_ac: Array2D, 

26 setups: Setups): 

27 self.nt_sR = nt_sR 

28 self.D_asii = D_asii 

29 self.relpos_ac = relpos_ac 

30 self.setups = setups 

31 

32 @classmethod 

33 def from_calculation(cls, calculation: DFTCalculation): 

34 density = calculation.density 

35 return cls(density.nt_sR, 

36 density.D_asii, 

37 calculation.relpos_ac, 

38 calculation.setups) 

39 

40 def pseudo_densities(self, 

41 grid_spacing: float = None, # Ang 

42 grid_refinement: int = None, 

43 add_compensation_charges: bool = True 

44 ) -> UGArray: 

45 nt_sR = self._pseudo_densities(grid_spacing, grid_refinement) 

46 

47 ncomponents = nt_sR.dims[0] 

48 ndensities = ncomponents % 3 

49 

50 if add_compensation_charges: 

51 cc_asL = AtomArraysLayout( 

52 [(ncomponents, setup.Delta_iiL.shape[2]) 

53 for setup in self.setups], 

54 atomdist=self.D_asii.layout.atomdist).empty() 

55 

56 for a, D_sii in self.D_asii.items(): 

57 Q_sL = np.einsum('sij, ijL -> sL', 

58 D_sii.real, self.setups[a].Delta_iiL) 

59 delta = (self.setups[a].Delta0 + 

60 self.setups[a].Nv / (4 * pi)**0.5) 

61 Q_sL[:ndensities, 0] += delta / ndensities 

62 cc_asL[a] = Q_sL 

63 

64 ghat_aLR = self.setups.create_compensation_charges( 

65 nt_sR.desc, 

66 self.relpos_ac, 

67 self.D_asii.layout.atomdist) 

68 ghat_aLR.add_to(nt_sR, cc_asL) 

69 

70 return nt_sR.scaled(Bohr, Bohr**-3) 

71 

72 def _pseudo_densities(self, 

73 grid_spacing: float = None, # Ang 

74 grid_refinement: int = None, 

75 ) -> UGArray: 

76 nt_sR = self.nt_sR.to_pbc_grid() 

77 grid = nt_sR.desc 

78 if grid_spacing is not None: 

79 assert grid_refinement is None 

80 grid = grid.uniform_grid_with_grid_spacing( 

81 grid_spacing / Bohr) 

82 elif grid_refinement is not None and grid_refinement > 1: 

83 grid = grid.new(size=grid.size * grid_refinement) 

84 else: 

85 return nt_sR.copy() 

86 

87 return nt_sR.interpolate(grid=grid) 

88 

89 def all_electron_densities(self, 

90 *, 

91 grid_spacing: float = None, # Ang 

92 grid_refinement: int = None, 

93 skip_core: bool = False, 

94 ) -> UGArray: 

95 n_sR = self._pseudo_densities(grid_spacing, grid_refinement) 

96 ncomponents = n_sR.dims[0] 

97 nspins = ncomponents % 3 

98 grid = n_sR.desc 

99 

100 electrons_as = np.zeros((len(self.relpos_ac), ncomponents)) 

101 splines = {} 

102 for a, D_sii in self.D_asii.items(): 

103 D_sii = D_sii.real 

104 relpos_c = self.relpos_ac[a] 

105 setup = self.setups[a] 

106 if setup not in splines: 

107 phi_j, phit_j, nc, nct = setup.get_partial_waves()[:4] 

108 if skip_core: 

109 nc = Spline.from_data(0, 10.0, [0.0, 0.0]) 

110 rcut = max(setup.rcut_j) 

111 splines[setup] = (rcut, phi_j, phit_j, nc, nct) 

112 rcut, phi_j, phit_j, nc, nct = splines[setup] 

113 

114 # Expected integral of PAW correction: 

115 electrons_s = np.zeros(ncomponents) 

116 if skip_core: 

117 electrons_s[:nspins] = -setup.Nct / nspins 

118 else: 

119 electrons_s[:nspins] = (setup.Nc - setup.Nct) / nspins 

120 electrons_s += (4 * pi)**0.5 * np.einsum('sij, ij -> s', 

121 D_sii, 

122 setup.Delta_iiL[:, :, 0]) 

123 

124 # Add PAW correction: 

125 R_v = relpos_c @ grid.cell_cv 

126 electrons_s -= add(R_v, n_sR, phi_j, phit_j, nc, nct, rcut, D_sii) 

127 electrons_as[a] = electrons_s 

128 

129 if not skip_core: 

130 # Add missing charge to grid-points closest to atoms: 

131 grid.comm.sum(electrons_as) 

132 R_ac = np.around(grid.size * self.relpos_ac).astype(int) 

133 R_ac %= grid.size 

134 for R_c, electrons_s in zip(R_ac, electrons_as): 

135 R_c -= grid.start_c 

136 if (R_c >= 0).all() and (R_c < grid.mysize_c).all(): 

137 for n_R, e in zip(n_sR.data, electrons_s): 

138 n_R[tuple(R_c)] += e / grid.dv 

139 

140 return n_sR.scaled(Bohr, Bohr**-3) 

141 

142 def spin_contamination(self, majority_spin=None): 

143 """Calculate the spin contamination. 

144 

145 Spin contamination is defined as the integral over the 

146 spin density difference, where it is negative (i.e. the 

147 minority spin density is larger than the majority spin density. 

148 """ 

149 n_sR = self.all_electron_densities() 

150 m0, m1 = n_sR.integrate() 

151 if majority_spin is None: 

152 majority_spin = int(m1 > m0) 

153 d_R = n_sR[0].data - n_sR[1].data 

154 if majority_spin == 0: 

155 d_R *= -1.0 

156 d_R = np.where(d_R > 0, d_R, 0.0) 

157 return n_sR.desc.from_data(d_R).integrate() 

158 

159 

160def add(R_v: Vector, 

161 a_sR: UGArray, 

162 phi_j: list[Spline], 

163 phit_j: list[Spline], 

164 nc: Spline, 

165 nct: Spline, 

166 rcut: float, 

167 D_sii: Array3D) -> Array1D: 

168 """Add PAW corrections to real-space grid. 

169 

170 Returns number of elctrons added. 

171 """ 

172 ug = a_sR.desc 

173 R_Rv = ug.xyz() 

174 lmax = max(phi.l for phi in phi_j) 

175 ncomponents = a_sR.dims[0] 

176 nspins = ncomponents % 3 

177 electrons_s = np.zeros(ncomponents) 

178 start_c = 0 - ug.pbc 

179 stop_c = 1 + ug.pbc 

180 for u0 in range(start_c[0], stop_c[0]): 

181 for u1 in range(start_c[1], stop_c[1]): 

182 for u2 in range(start_c[2], stop_c[2]): 

183 d_Rv = R_Rv - (R_v + (u0, u1, u2) @ ug.cell_cv) 

184 d_R = (d_Rv**2).sum(3)**0.5 

185 mask_R = d_R < rcut 

186 npoints = mask_R.sum() 

187 if npoints == 0: 

188 continue 

189 

190 a_sr = np.zeros((ncomponents, npoints)) 

191 d_rv = d_Rv[mask_R] 

192 d_r = d_R[mask_R] 

193 Y_Lr = [Y(L, *d_rv.T) for L in range((lmax + 1)**2)] 

194 phi_jr = [phi.map(d_r) for phi in phi_j] 

195 phit_jr = [phit.map(d_r) for phit in phit_j] 

196 l_j = [phi.l for phi in phi_j] 

197 

198 i1 = 0 

199 for l1, phi1_r, phit1_r in zip(l_j, phi_jr, phit_jr): 

200 i2 = 0 

201 i1b = i1 + 2 * l1 + 1 

202 D_smi = D_sii[:, i1:i1b] 

203 for l2, phi2_r, phit2_r in zip(l_j, phi_jr, phit_jr): 

204 i2b = i2 + 2 * l2 + 1 

205 D_smm = D_smi[:, :, i2:i2b] 

206 b_sr = np.einsum( 

207 'smn, mr, nr -> sr', 

208 D_smm, 

209 Y_Lr[l1**2:(l1 + 1)**2], 

210 Y_Lr[l2**2:(l2 + 1)**2]) * ( 

211 phi1_r * phi2_r - phit1_r * phit2_r) 

212 a_sr += b_sr 

213 i2 = i2b 

214 i1 = i1b 

215 

216 dn_r = nc.map(d_r) - nct.map(d_r) 

217 a_sr[:nspins] += dn_r * ((4 * pi)**-0.5 / nspins) 

218 electrons_s += a_sr.sum(1) * a_sR.desc.dv 

219 a_sR.data[:, mask_R] += a_sr 

220 return electrons_s