Coverage for gpaw/new/density.py: 94%

231 statements  

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

1from __future__ import annotations 

2 

3from math import pi, sqrt 

4 

5import numpy as np 

6from ase.units import Bohr, Ha 

7from gpaw.core.atom_arrays import AtomArrays, AtomDistribution 

8from gpaw.core.atom_centered_functions import (AtomArraysLayout, 

9 AtomCenteredFunctions) 

10from gpaw.core.plane_waves import PWDesc 

11from gpaw.core.uniform_grid import UGArray, UGDesc 

12from gpaw.gpu import as_np 

13from gpaw.mpi import MPIComm 

14from gpaw.new import trace, zips 

15from gpaw.typing import Array3D, Vector 

16from gpaw.utilities import unpack_hermitian, unpack_density 

17from gpaw.new.symmetry import SymmetrizationPlan, GPUSymmetrizationPlan 

18from gpaw.new.ibzwfs import IBZWaveFunctions 

19from gpaw.setup import Setups 

20 

21 

22class Density: 

23 @classmethod 

24 def from_data_and_setups(cls, 

25 nt_sR: UGArray, 

26 taut_sR: UGArray, 

27 D_asii: AtomArrays, 

28 charge: float, 

29 setups: Setups, 

30 nct_aX: AtomCenteredFunctions, 

31 tauct_aX: AtomCenteredFunctions) -> Density: 

32 xp = nt_sR.xp 

33 return cls(nt_sR, 

34 taut_sR, 

35 D_asii, 

36 charge, 

37 setups.nvalence + setups.core_charge, 

38 [xp.asarray(setup.Delta_iiL) for setup in setups], 

39 [setup.Delta0 for setup in setups], 

40 [unpack_hermitian(setup.N0_p) for setup in setups], 

41 [setup.n_j for setup in setups], 

42 [setup.l_j for setup in setups], 

43 nct_aX, 

44 tauct_aX) 

45 

46 @classmethod 

47 def from_superposition(cls, 

48 *, 

49 grid, 

50 nct_aX, 

51 tauct_aX, 

52 atomdist, 

53 setups, 

54 basis_set, 

55 magmom_av, 

56 ncomponents, 

57 charge=0.0, 

58 hund=False, 

59 mgga=False): 

60 nt_sR = grid.zeros(ncomponents) 

61 atom_array_layout = AtomArraysLayout( 

62 [(setup.ni, setup.ni) for setup in setups], 

63 atomdist=atomdist, dtype=float if ncomponents < 4 else complex) 

64 D_asii = atom_array_layout.empty(ncomponents) 

65 f_asi = {a: atomic_occupation_numbers(setup, 

66 magmom_v, 

67 ncomponents, 

68 hund, 

69 charge / len(setups)) 

70 for a, (setup, magmom_v) 

71 in enumerate(zips(setups, magmom_av))} 

72 basis_set.add_to_density(nt_sR.data, f_asi) 

73 for a, D_sii in D_asii.items(): 

74 D_sii[:] = unpack_density( 

75 setups[a].initialize_density_matrix(f_asi[a])) 

76 

77 xp = nct_aX.xp 

78 nt_sR = nt_sR.to_xp(xp) 

79 density = cls.from_data_and_setups(nt_sR, 

80 None, 

81 D_asii.to_xp(xp), 

82 charge, 

83 setups, 

84 nct_aX, 

85 tauct_aX) 

86 ndensities = ncomponents % 3 

87 density.nt_sR.data[:ndensities] += density.nct_R.data 

88 if mgga: 

89 density.taut_sR = nt_sR.new() 

90 density.taut_sR.data[:] = density.tauct_R.data 

91 return density 

92 

93 def __init__(self, 

94 nt_sR: UGArray, 

95 taut_sR: UGArray | None, 

96 D_asii: AtomArrays, 

97 charge: float, 

98 nvalence: float, 

99 delta_aiiL: list[Array3D], 

100 delta0_a: list[float], 

101 N0_aii, 

102 n_aj: list[list[int]], 

103 l_aj: list[list[int]], 

104 nct_aX: AtomCenteredFunctions, 

105 tauct_aX: AtomCenteredFunctions): 

106 self.nt_sR = nt_sR 

107 self.taut_sR = taut_sR 

108 self.D_asii = D_asii 

109 self.delta_aiiL = delta_aiiL 

110 self.delta0_a = delta0_a 

111 self.N0_aii = N0_aii 

112 self.n_aj = n_aj 

113 self.l_aj = l_aj 

114 self.charge = charge 

115 self.nvalence = nvalence 

116 self.nct_aX = nct_aX 

117 self.tauct_aX = tauct_aX 

118 

119 self.grid = nt_sR.desc 

120 self.ncomponents = nt_sR.dims[0] 

121 self.ndensities = self.ncomponents % 3 

122 self.collinear = self.ncomponents != 4 

123 self.natoms = len(delta0_a) 

124 

125 self._nct_R = None 

126 self._tauct_R = None 

127 

128 self.symplan = None 

129 

130 def __repr__(self): 

131 return f'Density({self.nt_sR}, {self.D_asii}, charge={self.charge})' 

132 

133 def __str__(self) -> str: 

134 return (f'density:\n' 

135 f' valence electrons: {self.nvalence}\n' 

136 f' components: {self.ncomponents}\n' 

137 f' grid points: {self.nt_sR.desc.size}\n' 

138 f' charge: {self.charge} # |e|\n') 

139 

140 @property 

141 def nct_R(self): 

142 if self._nct_R is None: 

143 self._nct_R = self.grid.empty(xp=self.nt_sR.xp) 

144 self.nct_aX.to_uniform_grid(out=self._nct_R, 

145 scale=1.0 / (self.ncomponents % 3)) 

146 return self._nct_R 

147 

148 @property 

149 def tauct_R(self): 

150 if self._tauct_R is None: 

151 self._tauct_R = self.grid.empty(xp=self.nt_sR.xp) 

152 self.tauct_aX.to_uniform_grid(out=self._tauct_R, 

153 scale=1.0 / (self.ncomponents % 3)) 

154 return self._tauct_R 

155 

156 def new(self, new_grid, pw, relpos_ac, atomdist): 

157 self.move(relpos_ac, atomdist) 

158 new_pw = PWDesc(ecut=0.99 * new_grid.ekin_max(), 

159 cell=new_grid.cell, 

160 comm=new_grid.comm) 

161 old_grid = self.nt_sR.desc 

162 old_pw = PWDesc(ecut=0.99 * old_grid.ekin_max(), 

163 cell=old_grid.cell, 

164 comm=new_grid.comm) 

165 new_nt_sR = new_grid.empty(self.ncomponents, xp=self.nt_sR.xp) 

166 for new_nt_R, old_nt_R in zips(new_nt_sR, self.nt_sR): 

167 old_nt_R.fft(pw=old_pw).morph(new_pw).ifft(out=new_nt_R) 

168 

169 self.nct_aX.change_cell(pw) 

170 self.tauct_aX.change_cell(pw) 

171 

172 return Density( 

173 new_nt_sR, 

174 None if self.taut_sR is None else new_nt_sR.new(zeroed=True), 

175 self.D_asii, 

176 self.charge, 

177 self.nvalence, 

178 self.delta_aiiL, 

179 self.delta0_a, 

180 self.N0_aii, 

181 self.n_aj, 

182 self.l_aj, 

183 self.nct_aX, 

184 self.tauct_aX) 

185 

186 @trace 

187 def calculate_compensation_charge_coefficients(self) -> AtomArrays: 

188 xp = self.D_asii.layout.xp 

189 ccc_aL = AtomArraysLayout( 

190 [delta_iiL.shape[2] for delta_iiL in self.delta_aiiL], 

191 atomdist=self.D_asii.layout.atomdist, 

192 xp=xp).empty() 

193 

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

195 Q_L = xp.einsum('sij, ijL -> L', 

196 D_sii[:self.ndensities].real, self.delta_aiiL[a]) 

197 Q_L[0] += self.delta0_a[a] 

198 ccc_aL[a] = Q_L 

199 

200 return ccc_aL 

201 

202 def normalize(self, background_charge: float) -> None: 

203 comp_charge = 0.0 

204 xp = self.D_asii.layout.xp 

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

206 comp_charge += xp.einsum('sij, ij ->', 

207 D_sii[:self.ndensities].real, 

208 self.delta_aiiL[a][:, :, 0]) 

209 comp_charge += self.delta0_a[a] 

210 # comp_charge could be cupy.ndarray: 

211 comp_charge = float(comp_charge) * sqrt(4 * pi) 

212 comp_charge = self.nt_sR.desc.comm.sum_scalar(comp_charge) 

213 charge = comp_charge + self.charge - background_charge 

214 pseudo_charge = self.nt_sR[:self.ndensities].integrate().sum() 

215 if pseudo_charge != 0.0: 

216 x = -charge / pseudo_charge 

217 self.nt_sR.data *= x 

218 

219 @trace 

220 def update(self, ibzwfs: IBZWaveFunctions, ked=False): 

221 self.nt_sR.data[:] = 0.0 

222 self.D_asii.data[:] = 0.0 

223 ibzwfs.add_to_density(self.nt_sR, self.D_asii) 

224 self.nt_sR.data[:self.ndensities] += self.nct_R.data 

225 

226 # LCAO ...: 

227 ibzwfs.normalize_density(self) 

228 

229 if ked: 

230 self.update_ked(ibzwfs, symmetrize=False) 

231 

232 self.symmetrize(ibzwfs.ibz.symmetries) 

233 

234 def update_ked(self, ibzwfs, symmetrize=True): 

235 if self.taut_sR is None: 

236 self.taut_sR = self.nt_sR.new(zeroed=True) 

237 else: 

238 self.taut_sR.data[:] = 0.0 

239 ibzwfs.add_to_ked(self.taut_sR) 

240 self.taut_sR.data[:self.ndensities] += self.tauct_R.data 

241 if symmetrize: 

242 symmetries = ibzwfs.ibz.symmetries 

243 self.taut_sR.symmetrize(symmetries.rotation_scc, 

244 symmetries.translation_sc) 

245 

246 @trace 

247 def symmetrize(self, symmetries): 

248 self.nt_sR.symmetrize(symmetries.rotation_scc, 

249 symmetries.translation_sc) 

250 if self.taut_sR is not None: 

251 self.taut_sR.symmetrize(symmetries.rotation_scc, 

252 symmetries.translation_sc) 

253 

254 xp = self.nt_sR.xp 

255 if xp is np: 

256 D_asii = self.D_asii.gather(broadcast=True, copy=True) 

257 if self.symplan is None: 

258 self.symplan = SymmetrizationPlan(symmetries, self.l_aj) 

259 self.symplan.apply_distributed(D_asii, self.D_asii) 

260 else: 

261 # GPU version does all the work in rank 0 for now 

262 D_asii = self.D_asii.gather(copy=True) 

263 if self.D_asii.layout.atomdist.comm.rank == 0: 

264 if self.symplan is None: 

265 self.symplan = GPUSymmetrizationPlan( 

266 symmetries, self.l_aj, D_asii.layout) 

267 self.symplan.apply(D_asii.data, D_asii.data) 

268 self.D_asii.scatter_from(D_asii) 

269 

270 @trace 

271 def move(self, relpos_ac, atomdist): 

272 self.nt_sR.data[:self.ndensities] -= self.nct_R.data 

273 self.nct_aX.move(relpos_ac, atomdist) 

274 self.tauct_aX.move(relpos_ac, atomdist) 

275 self._nct_R = None 

276 self._tauct_R = None 

277 self.nt_sR.data[:self.ndensities] += self.nct_R.data 

278 self.D_asii = self.D_asii.moved(atomdist) 

279 

280 @trace 

281 def redist(self, 

282 grid: UGDesc, 

283 xdesc, 

284 atomdist: AtomDistribution, 

285 comm1: MPIComm, 

286 comm2: MPIComm) -> Density: 

287 return Density( 

288 self.nt_sR.redist(grid, comm1, comm2), 

289 None 

290 if self.taut_sR is None else 

291 self.taut_sR.redist(grid, comm1, comm2), 

292 self.D_asii.redist(atomdist, comm1, comm2), 

293 self.charge, 

294 self.nvalence, 

295 self.delta_aiiL, 

296 self.delta0_a, 

297 self.N0_aii, 

298 self.n_aj, 

299 self.l_aj, 

300 nct_aX=self.nct_aX.new(xdesc, atomdist), 

301 tauct_aX=self.tauct_aX.new(xdesc, atomdist)) 

302 

303 def calculate_dipole_moment(self, relpos_ac): 

304 dip_v = np.zeros(3) 

305 ccc_aL = self.calculate_compensation_charge_coefficients() 

306 ccc_aL = ccc_aL.to_cpu() 

307 pos_av = relpos_ac @ self.nt_sR.desc.cell_cv 

308 for a, ccc_L in ccc_aL.items(): 

309 c = ccc_L[0] 

310 dip_v -= c * (4 * pi)**0.5 * pos_av[a] 

311 if len(ccc_L) > 1: 

312 y, z, x = ccc_L[1:4] 

313 dip_v -= np.array([x, y, z]) * (4 * pi / 3)**0.5 

314 self.nt_sR.desc.comm.sum(dip_v) 

315 for nt_R in self.nt_sR[:self.ndensities]: 

316 dip_v -= as_np(nt_R.moment()) 

317 return dip_v 

318 

319 def calculate_orbital_magnetic_moments(self): 

320 if self.collinear: 

321 from gpaw.new.calculation import CalculationModeError 

322 raise CalculationModeError( 

323 'Calculator is in collinear mode. ' 

324 'Collinear calculations require spin–orbit ' 

325 'coupling for nonzero orbital magnetic moments.') 

326 

327 D_asii = self.D_asii 

328 if D_asii.layout.size != D_asii.layout.mysize: 

329 raise ValueError( 

330 'Atomic density matrices should be collected on all ' 

331 'ranks when calculating orbital magnetic moments.') 

332 

333 from gpaw.new.orbmag import calculate_orbmag_from_density 

334 return calculate_orbmag_from_density(D_asii, self.n_aj, self.l_aj) 

335 

336 def calculate_magnetic_moments(self): 

337 magmom_av = np.zeros((self.natoms, 3)) 

338 magmom_v = np.zeros(3) 

339 domain_comm = self.nt_sR.desc.comm 

340 

341 if self.ncomponents == 2: 

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

343 M_ii = as_np(D_sii[0] - D_sii[1]) 

344 magmom_av[a, 2] = np.einsum('ij, ij ->', M_ii, self.N0_aii[a]) 

345 delta_ii = as_np(self.delta_aiiL[a][:, :, 0]) 

346 magmom_v[2] += (np.einsum('ij, ij ->', M_ii, delta_ii) * 

347 sqrt(4 * pi)) 

348 domain_comm.sum(magmom_av) 

349 domain_comm.sum(magmom_v) 

350 

351 M_s = self.nt_sR.integrate() 

352 magmom_v[2] += M_s[0] - M_s[1] 

353 

354 elif self.ncomponents == 4: 

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

356 M_vii = D_sii[1:4].real 

357 magmom_av[a] = np.einsum('vij, ij -> v', 

358 M_vii, self.N0_aii[a]) 

359 magmom_v += (np.einsum('vij, ij -> v', M_vii, 

360 self.delta_aiiL[a][:, :, 0]) * 

361 sqrt(4 * pi)) 

362 domain_comm.sum(magmom_av) 

363 domain_comm.sum(magmom_v) 

364 magmom_v += self.nt_sR.integrate()[1:] 

365 

366 return magmom_v, magmom_av 

367 

368 @trace 

369 def write_to_gpw(self, writer, flags): 

370 D_asp = self.D_asii.to_cpu().to_lower_triangle().gather() 

371 nt_sR = self.nt_sR.to_xp(np).gather() 

372 if self.taut_sR is not None: 

373 taut_sR = self.taut_sR.to_xp(np).gather() 

374 if D_asp is None: 

375 return # let master do the writing 

376 writer.write( 

377 density=flags.to_storage_dtype(nt_sR.data * Bohr**-3), 

378 atomic_density_matrices=D_asp.data) 

379 if self.taut_sR is not None: 

380 writer.write(ked=flags.to_storage_dtype( 

381 taut_sR.data * (Ha * Bohr**-3))) 

382 

383 

384def atomic_occupation_numbers(setup, 

385 magmom_v: Vector, 

386 ncomponents: int, 

387 hund: bool = False, 

388 charge: float = 0.0): 

389 M = np.linalg.norm(magmom_v) 

390 nspins = min(ncomponents, 2) 

391 f_si = setup.calculate_initial_occupation_numbers( 

392 M, hund, charge=charge, nspins=nspins) 

393 

394 if ncomponents == 1: 

395 pass 

396 elif ncomponents == 2: 

397 if magmom_v[2] < 0: 

398 f_si = f_si[::-1].copy() 

399 else: 

400 f_i = f_si.sum(0) 

401 fm_i = f_si[0] - f_si[1] 

402 f_si = np.zeros((4, len(f_i))) 

403 f_si[0] = f_i 

404 if M > 0: 

405 f_si[1:] = np.asarray(magmom_v)[:, np.newaxis] / M * fm_i 

406 

407 return f_si