Coverage for gpaw/spinorbit.py: 77%

483 statements  

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

1from __future__ import annotations 

2from math import nan 

3from operator import attrgetter 

4from pathlib import Path 

5from typing import (TYPE_CHECKING, Callable, Dict, Iterable, Iterator, List, 

6 Optional, Tuple) 

7 

8import numpy as np 

9from ase.units import Bohr, Ha, alpha 

10 

11from gpaw.band_descriptor import BandDescriptor 

12from gpaw.grid_descriptor import GridDescriptor 

13from gpaw.ibz2bz import IBZ2BZMaps 

14from gpaw.kpoint import KPoint 

15from gpaw.kpt_descriptor import KPointDescriptor 

16from gpaw.mpi import broadcast_array, serial_comm 

17from gpaw.occupations import OccupationNumberCalculator, ParallelLayout 

18from gpaw.projections import Projections 

19from gpaw.setup import Setup 

20from gpaw.typing import Array1D, Array2D, Array3D, Array4D, ArrayND 

21from gpaw.utilities.partition import AtomPartition 

22 

23if TYPE_CHECKING: 

24 from gpaw.calculator import GPAW # noqa 

25 from gpaw.new.ase_interface import ASECalculator 

26 

27_L_vlmm: List[List[np.ndarray]] = [] # see get_L_vlmm() below 

28 

29 

30class WaveFunction: 

31 def __init__(self, 

32 eigenvalues: Array1D, 

33 projections: Projections, 

34 bz_index: int = None): 

35 self.eig_m = eigenvalues 

36 self.projections = projections 

37 self.spin_projection_mv: Optional[Array2D] = None 

38 self.v_mn: Optional[Array2D] = None 

39 self.f_m = np.empty_like(self.eig_m) 

40 self.f_m[:] = nan 

41 self.bz_index = bz_index 

42 

43 def transform(self, IBZ2BZMap, K) -> WaveFunction: 

44 """Transforms PAW projections from IBZ to BZ k-point.""" 

45 projections = IBZ2BZMap.map_projections(self.projections) 

46 return WaveFunction(self.eig_m.copy(), projections, K) 

47 

48 def redistribute_atoms(self, 

49 atom_partition: AtomPartition 

50 ) -> WaveFunction: 

51 projections = self.projections.redist(atom_partition) 

52 return WaveFunction(self.eig_m.copy(), projections, self.bz_index) 

53 

54 def add_soc(self, 

55 dVL_avii: Dict[int, Array3D], 

56 s_vss: List[Array2D], 

57 C_ss: Array2D) -> None: 

58 """Evaluate H in a basis of S_z eigenstates.""" 

59 if self.projections.bcomm.rank > 0: 

60 return 

61 

62 M = self.projections.nbands 

63 H_mm = np.zeros((M, M), complex) 

64 for a, dVL_vii in dVL_avii.items(): 

65 ni = dVL_vii.shape[1] 

66 H_ssii = np.zeros((2, 2, ni, ni), complex) 

67 H_ssii[0, 0] = dVL_vii[2] 

68 H_ssii[0, 1] = dVL_vii[0] - 1.0j * dVL_vii[1] 

69 H_ssii[1, 0] = dVL_vii[0] + 1.0j * dVL_vii[1] 

70 H_ssii[1, 1] = -dVL_vii[2] 

71 

72 # Tranform to theta, phi basis 

73 H_ssii = np.tensordot(C_ss, H_ssii, (0, 1)) 

74 H_ssii = np.tensordot(C_ss.T.conj(), H_ssii, (1, 1)) 

75 H_ssii *= Ha 

76 

77 P_msi = self.projections[a] 

78 for s1 in range(2): 

79 for s2 in range(2): 

80 H_ii = H_ssii[s1, s2] 

81 P1_mi = P_msi[:, s1] 

82 P2_mi = P_msi[:, s2] 

83 H_mm += np.dot(np.dot(P1_mi.conj(), H_ii), P2_mi.T) 

84 

85 domain_comm = self.projections.atom_partition.comm 

86 domain_comm.sum(H_mm, 0) 

87 if domain_comm.rank == 0: 

88 H_mm += np.diag(self.eig_m) 

89 self.eig_m, v_nm = np.linalg.eigh(H_mm) 

90 else: 

91 v_nm = np.empty((M, M), complex) 

92 

93 domain_comm.broadcast(v_nm, 0) 

94 

95 P_mI = self.projections.matrix.array 

96 P_mI[:] = v_nm.T.copy().dot(P_mI) 

97 

98 sx_m = [] 

99 sy_m = [] 

100 sz_m = [] 

101 

102 v_msn = v_nm.copy().reshape((M // 2, 2, M)).T.copy() 

103 for v_sn in v_msn: 

104 sx_m.append(np.trace(v_sn.T.conj().dot(s_vss[0]).dot(v_sn))) 

105 sy_m.append(np.trace(v_sn.T.conj().dot(s_vss[1]).dot(v_sn))) 

106 sz_m.append(np.trace(v_sn.T.conj().dot(s_vss[2]).dot(v_sn))) 

107 

108 self.spin_projection_mv = np.array([sx_m, sy_m, sz_m]).real.T.copy() 

109 self.v_mn = v_nm.T 

110 

111 def wavefunctions(self, calc, periodic=True): 

112 kd = calc.wfs.kd 

113 assert kd.nibzkpts == kd.nbzkpts 

114 

115 # For spinors the number of bands is doubled and a 

116 # spin dimension is added 

117 Ns = calc.wfs.nspins 

118 Nm, Nn = self.v_mn.shape 

119 

120 if calc.wfs.collinear: 

121 u_snR = [[calc.wfs.get_wave_function_array(n, self.bz_index, s, 

122 periodic=periodic) 

123 for n in range(Nn // 2)] 

124 for s in range(Ns)] 

125 u_msR = np.empty((Nm, 2) + u_snR[0][0].shape, complex) 

126 np.einsum('mn, nabc -> mabc', self.v_mn[:, ::2], u_snR[0], 

127 out=u_msR[:, 0]) 

128 np.einsum('mn, nabc -> mabc', self.v_mn[:, 1::2], u_snR[-1], 

129 out=u_msR[:, 1]) 

130 else: 

131 u_nsR = np.array( 

132 [calc.wfs.get_wave_function_array(n, self.bz_index, 0, 

133 periodic=periodic) 

134 for n in range(Nn)]) 

135 u_msR = np.einsum('mn, nsxyz -> msxyz', 

136 self.v_mn, u_nsR) 

137 

138 return u_msR 

139 

140 @property 

141 def P_amj(self): 

142 M = self.projections.nbands 

143 return { 

144 a: P_msi.transpose((0, 2, 1)).copy().reshape((M, -1)) 

145 for a, P_msi in self.projections.items()} 

146 

147 def pdos_weights(self, 

148 a: int, 

149 indices: List[int] 

150 ) -> Array3D: 

151 """PDOS weights.""" 

152 dos_ms = np.zeros((self.projections.nbands, 2)) 

153 

154 P_amsi = self.projections 

155 

156 if a in P_amsi: 

157 dos_ms[:, :] = (abs(P_amsi[a][:, :, indices])**2).sum(2) 

158 

159 return dos_ms 

160 

161 

162class BZWaveFunctions: 

163 """Container for eigenvalues and PAW projections (all of BZ).""" 

164 def __init__(self, 

165 kd: KPointDescriptor, 

166 wfs: Dict[int, WaveFunction], 

167 occ: Optional[OccupationNumberCalculator], 

168 nelectrons: float, 

169 n_aj: List[List[int]], 

170 l_aj: List[List[int]]): 

171 self.wfs = wfs 

172 self.occ = occ 

173 self.nelectrons = nelectrons 

174 self.n_aj = n_aj 

175 self.l_aj = l_aj 

176 

177 self.nbzkpts = kd.nbzkpts 

178 

179 # Initialize ranks: 

180 self.ranks = np.zeros(kd.nbzkpts, int) 

181 for k in wfs: 

182 self.ranks[k] = kd.comm.rank 

183 kd.comm.sum(self.ranks) 

184 

185 wf = next(iter(wfs.values())) # get the first WaveFunction object 

186 

187 self.shape = (kd.nbzkpts, wf.projections.nbands) 

188 self.domain_comm = wf.projections.atom_partition.comm 

189 self.bcomm = wf.projections.bcomm 

190 self.kpt_comm = kd.comm 

191 

192 self.fermi_level = self._calculate_occ_numbers_and_fermi_level() 

193 

194 self.size = kd.N_c 

195 self.bz2ibz_map = np.arange(self.nbzkpts) 

196 

197 def weights(self): 

198 return np.zeros(len(self)) + 1 / self.nbzkpts 

199 

200 def _calculate_occ_numbers_and_fermi_level(self) -> float: 

201 if self.occ is not None: 

202 eig_im = [wf.eig_m for wf in self] 

203 weight = 1.0 / self.nbzkpts 

204 weight_i = [weight] * len(eig_im) 

205 

206 f_im, (fermi_level,), _ = self.occ.calculate( 

207 self.nelectrons, 

208 eig_im, 

209 weight_i) 

210 for wf, f_m in zip(self, f_im): 

211 wf.f_m[:] = f_m 

212 else: 

213 fermi_level = 0.0 

214 

215 if self.domain_comm.rank == 0: 

216 fermi_level = self.bcomm.sum_scalar(fermi_level) 

217 fermi_level = self.domain_comm.sum_scalar(fermi_level) 

218 

219 return fermi_level 

220 

221 def calculate_band_energy(self) -> float: 

222 """Calculate sum over occupied eigenvalues.""" 

223 if self.domain_comm.rank == 0 and self.bcomm.rank == 0: 

224 weight = 1.0 / self.nbzkpts 

225 e_band = sum(wf.eig_m.dot(wf.f_m) for wf in self) * weight 

226 e_band = self.kpt_comm.sum_scalar(e_band) 

227 else: 

228 e_band = 0.0 

229 

230 if self.domain_comm.rank == 0: 

231 e_band = self.bcomm.sum_scalar(e_band) 

232 e_band = self.domain_comm.sum_scalar(e_band) 

233 return e_band 

234 

235 def __iter__(self): 

236 for bz_index in sorted(self.wfs): 

237 yield self[bz_index] 

238 

239 def __getitem__(self, bz_index): 

240 return self.wfs[bz_index] 

241 

242 def __len__(self): 

243 return len(self.wfs) 

244 

245 def eigenvalues(self, 

246 broadcast: bool = True 

247 ) -> Array2D: 

248 """Eigenvalues in eV for the whole BZ.""" 

249 return self._collect(attrgetter('eig_m'), broadcast=broadcast) 

250 

251 def occupation_numbers(self, 

252 broadcast: bool = True 

253 ) -> Array2D: 

254 """Occupation numbers for the whole BZ.""" 

255 return self._collect(attrgetter('f_m'), broadcast=broadcast) 

256 

257 def eigenvectors(self, 

258 broadcast: bool = True 

259 ) -> Array4D: 

260 """Eigenvectors for the whole BZ.""" 

261 nbands = self.shape[1] 

262 assert nbands % 2 == 0 

263 return self._collect(attrgetter('v_mn'), (nbands,), complex, 

264 broadcast=broadcast) 

265 

266 def spin_projections(self, 

267 broadcast: bool = True 

268 ) -> Array3D: 

269 """Spin projections for the whole BZ.""" 

270 return self._collect(attrgetter('spin_projection_mv'), (3,), 

271 broadcast=broadcast) 

272 

273 def get_atomic_density_matrices(self): 

274 """Returns atomic density matrices for each atom.""" 

275 from gpaw.new.wave_functions import add_to_4component_density_matrix 

276 

277 assert self.domain_comm.size == 1 

278 assert self.bcomm.size == 1 

279 

280 D_asii = {} # Could also be an AtomArrays object? 

281 for a, l_j in enumerate(self.l_aj): 

282 ni = (2 * np.array(l_j) + 1).sum() 

283 D_sii = np.zeros([4, ni, ni], dtype=complex) 

284 for wfs, weight in zip(self.wfs.values(), self.weights()): 

285 add_to_4component_density_matrix(D_sii, wfs.projections[a], 

286 wfs.f_m * weight, np) 

287 self.kpt_comm.sum(D_sii) 

288 D_asii[a] = D_sii 

289 

290 return D_asii 

291 

292 def get_orbital_magnetic_moments(self): 

293 """Returns the orbital magnetic moment vector for each atom a.""" 

294 D_asii = self.get_atomic_density_matrices() 

295 

296 from gpaw.new.orbmag import calculate_orbmag_from_density 

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

298 

299 def pdos_weights(self, 

300 a: int, 

301 indices: List[int], 

302 broadcast: bool = True 

303 ) -> Array4D: 

304 """Projections for PDOS. 

305 

306 Returns (nbzkpts, nbands, 2)-shaped ndarray 

307 of the square of absolute value of the projections. 

308 """ 

309 def func(wf): 

310 return wf.pdos_weights(a, indices) 

311 

312 return self._collect(func, 

313 (2,), 

314 broadcast=broadcast, 

315 sum_over_domain=True) 

316 

317 def _collect(self, 

318 func: Callable[[WaveFunction], ArrayND], 

319 shape: Tuple[int, ...] = None, 

320 dtype=float, 

321 broadcast: bool = True, 

322 sum_over_domain: bool = False) -> ArrayND: 

323 """Helper method for collecting (and broadcasting) ndarrays.""" 

324 

325 total_shape = self.shape + (shape or ()) 

326 

327 if broadcast: 

328 array_kmx = self._collect(func, 

329 shape, 

330 dtype, 

331 False, 

332 sum_over_domain) 

333 if array_kmx.ndim == 0: 

334 array_kmx = np.empty(total_shape, dtype) 

335 return broadcast_array(array_kmx, 

336 self.kpt_comm, self.bcomm, self.domain_comm) 

337 

338 if self.bcomm.rank != 0: 

339 return np.empty(shape=()) 

340 

341 if not sum_over_domain and self.domain_comm.rank != 0: 

342 return np.empty(shape=()) 

343 

344 comm = self.kpt_comm 

345 if comm.rank == 0: 

346 array_kmx = np.empty(total_shape, dtype) 

347 for k, rank in enumerate(self.ranks): 

348 if rank == 0: 

349 array_kmx[k] = func(self[k]) 

350 else: 

351 comm.receive(array_kmx[k], rank) 

352 if sum_over_domain: 

353 self.domain_comm.sum(array_kmx) 

354 if self.domain_comm.rank == 0: 

355 return array_kmx 

356 else: 

357 return np.empty(shape=()) 

358 

359 for k, rank in enumerate(self.ranks): 

360 if rank == comm.rank: 

361 comm.send(func(self[k]), 0) 

362 

363 return np.empty(shape=()) 

364 

365 

366def soc_eigenstates_raw(ibzwfs: Iterable[Tuple[int, WaveFunction]], 

367 dVL_avii: Dict[int, Array3D], 

368 ibz2bzmaps: IBZ2BZMaps, 

369 atom_partition, 

370 theta: float = 0.0, 

371 phi: float = 0.0) -> Dict[int, WaveFunction]: 

372 

373 theta *= np.pi / 180 

374 phi *= np.pi / 180 

375 

376 # Hamiltonian with SO in KS basis 

377 # The even indices in H_mm are spin up along \hat n defined by \theta, phi 

378 # Basis change matrix for constructing Pauli matrices in \theta,\phi basis: 

379 # \sigma_i^n = C^\dag\sigma_i C 

380 C_ss = np.array([[np.cos(theta / 2) * np.exp(-1.0j * phi / 2), 

381 -np.sin(theta / 2) * np.exp(-1.0j * phi / 2)], 

382 [np.sin(theta / 2) * np.exp(1.0j * phi / 2), 

383 np.cos(theta / 2) * np.exp(1.0j * phi / 2)]]) 

384 

385 sx_ss = np.array([[0, 1], [1, 0]], complex) 

386 sy_ss = np.array([[0, -1.0j], [1.0j, 0]], complex) 

387 sz_ss = np.array([[1, 0], [0, -1]], complex) 

388 s_vss = [C_ss.T.conj() @ sx_ss @ C_ss, 

389 C_ss.T.conj() @ sy_ss @ C_ss, 

390 C_ss.T.conj() @ sz_ss @ C_ss] 

391 

392 bzwfs = {} 

393 for ibz_index, ibzwf in ibzwfs: 

394 for K in np.nonzero(ibz2bzmaps.kd.bz2ibz_k == ibz_index)[0]: 

395 bzwf = ibzwf.transform(ibz2bzmaps[K], K) 

396 

397 # Redistribute to match dVL_avii: 

398 bzwf = bzwf.redistribute_atoms(atom_partition) 

399 

400 bzwf.add_soc(dVL_avii, s_vss, C_ss) 

401 bzwfs[int(K)] = bzwf # MYPY: signedinteger[_32Bit | _64Bit] -> int 

402 

403 return bzwfs 

404 

405 

406def extract_ibz_wave_functions(kpt_qs: List[List[KPoint]], 

407 bd: BandDescriptor, 

408 gd: GridDescriptor, 

409 n1: int, 

410 n2: int, 

411 eigenvalues: Array3D = None 

412 ) -> Iterator[Tuple[int, WaveFunction]]: 

413 """Yield tuples of IBZ-index and wave functions. 

414 

415 All atoms and bands will be on rank == 0 of gd.comm and bd.comm 

416 respectively. This makes slicing the bands (from n1 to n2-1) 

417 and symmetry operations on the projections easier. 

418 """ 

419 

420 nproj_a = kpt_qs[0][0].projections.nproj_a 

421 

422 collinear = kpt_qs[0][0].s is not None 

423 

424 nbands = n2 - n1 

425 if collinear: 

426 nbands *= 2 

427 

428 # All atoms on rank-0: 

429 atom_partition = AtomPartition(gd.comm, np.zeros_like(nproj_a)) 

430 

431 # All bands on rank-0 (nrow * ncol = 1 * 1): 

432 bdist = (bd.comm, 1, 1) 

433 

434 for kpt_s in kpt_qs: 

435 # Collect bands and atoms to bd.comm.rank == 0 and gd.comm.rank == 0: 

436 if eigenvalues is None: 

437 eig_sn = [bd.collect(kpt.eps_n) for kpt in kpt_s] 

438 P_snI = [kpt.projections.collect() for kpt in kpt_s] 

439 

440 projections = Projections( 

441 nbands=nbands, 

442 nproj_a=nproj_a, 

443 atom_partition=atom_partition, 

444 bdist=bdist, 

445 collinear=False) 

446 

447 if bd.comm.rank == 0 and gd.comm.rank == 0: 

448 P1_nI = P_snI[0] 

449 P2_nI = P_snI[-1] 

450 assert P1_nI is not None 

451 assert P2_nI is not None 

452 if collinear: 

453 eig_m = np.empty((n2 - n1) * 2) 

454 if eigenvalues is None: 

455 eig_m[::2] = eig_sn[0][n1:n2] * Ha 

456 eig_m[1::2] = eig_sn[-1][n1:n2] * Ha 

457 else: 

458 for s in range(2): 

459 eig_m[s::2] = eigenvalues[-s, kpt_s[-s].k] 

460 projections.array[:] = 0.0 

461 projections.array[::2, 0] = P1_nI[n1:n2] 

462 projections.array[1::2, 1] = P2_nI[n1:n2] 

463 else: 

464 eig_m = eig_sn[0][n1:n2] * Ha 

465 projections.matrix.array[:] = P1_nI[n1:n2] 

466 else: 

467 eig_m = np.empty(0) 

468 

469 ibz_index = kpt_s[0].k 

470 

471 yield ibz_index, WaveFunction(eig_m, projections) 

472 

473 

474def soc_eigenstates(calc: ASECalculator | GPAW | str | Path, 

475 n1: int = None, 

476 n2: int = None, 

477 scale: float = 1.0, 

478 theta: float = 0.0, # degrees 

479 phi: float = 0.0, # degrees 

480 eigenvalues: Array3D = None, # eV 

481 occcalc: OccupationNumberCalculator = None, 

482 projected: bool = False 

483 ) -> BZWaveFunctions: 

484 """Calculate SOC eigenstates. 

485 

486 Parameters: 

487 calc: Calculator 

488 GPAW calculator or path to gpw-file. 

489 n1, n2: int 

490 Range of bands to include (n1 <= n < n2). Default is all 

491 bands available. 

492 scale: float 

493 Scale the spinorbit coupling by this amount. 

494 theta: float 

495 Angle in degrees. 

496 phi: float 

497 Angle in degrees. 

498 eigenvalues: ndarray 

499 Optionally use these eigenvalues instead for those from *calc*. 

500 The shape must be: (nspins, nibzkpts, n2 - n1). Units: eV. 

501 occcalc: 

502 Occupation-number calculator. By default, the one from *calc* 

503 will be used. 

504 

505 Returns a BZWaveFunctions object covering the whole BZ. 

506 """ 

507 

508 from gpaw.calculator import GPAW # noqa 

509 

510 if isinstance(calc, (str, Path)): 

511 calc = GPAW(calc) 

512 

513 n1 = n1 or 0 

514 n2 = n2 or 0 

515 if n2 <= 0: 

516 if eigenvalues is None: 

517 nbands = calc.get_number_of_bands() 

518 else: 

519 nbands = eigenvalues.shape[2] 

520 n2 += nbands 

521 

522 # <phi_i|dV_adr / r * L_v|phi_j> 

523 dVL_avii = {a: soc(calc.wfs.setups[a], 

524 calc.hamiltonian.xc, D_sp) * scale 

525 for a, D_sp in calc.density.D_asp.items()} 

526 

527 if projected: 

528 dVL_avii = {a: projected_soc(dVL_vii, theta=theta, phi=phi) 

529 for a, dVL_vii in dVL_avii.items()} 

530 

531 kd = calc.wfs.kd 

532 bd = calc.wfs.bd 

533 gd = calc.wfs.gd 

534 atom_partition = calc.density.atom_partition 

535 

536 if eigenvalues is not None: 

537 assert eigenvalues.shape == (kd.nspins, kd.nibzkpts, n2 - n1) 

538 

539 ibzwfs = extract_ibz_wave_functions(calc.wfs.kpt_qs, 

540 bd, gd, n1, n2, eigenvalues) 

541 ibz2bzmaps = IBZ2BZMaps.from_calculator(calc) 

542 

543 bzwfs = soc_eigenstates_raw(ibzwfs, 

544 dVL_avii, 

545 ibz2bzmaps, 

546 atom_partition, 

547 theta, phi) 

548 

549 if bd.comm.rank == 0 and gd.comm.rank == 0: 

550 parallel_layout = ParallelLayout(BandDescriptor(1), 

551 kd.comm, 

552 serial_comm) 

553 occcalc = occcalc or calc.wfs.occupations 

554 occcalc = occcalc.copy(bz2ibzmap=list(range(kd.nbzkpts)), 

555 parallel_layout=parallel_layout) 

556 else: 

557 occcalc = None 

558 

559 n_aj = [setup.n_j for setup in calc.wfs.setups] 

560 l_aj = [setup.l_j for setup in calc.wfs.setups] 

561 

562 return BZWaveFunctions(kd, bzwfs, occcalc, calc.wfs.nvalence, n_aj, l_aj) 

563 

564 

565def soc(a: Setup, xc, D_sp: Array2D) -> Array3D: 

566 """<phi_i|dU^a/dr / r * L_v|phi_j>""" 

567 v_g = get_radial_potential_derivative(a, xc, D_sp) 

568 Ng = len(v_g) 

569 phi_jg = a.data.phi_jg 

570 

571 Lx_lmm, Ly_lmm, Lz_lmm = get_L_vlmm() 

572 

573 dVL_vii = np.zeros((3, a.ni, a.ni), complex) 

574 N1 = 0 

575 for j1, l1 in enumerate(a.l_j): 

576 Nm = 2 * l1 + 1 

577 N2 = 0 

578 for j2, l2 in enumerate(a.l_j): 

579 if l1 == l2: 

580 f_g = phi_jg[j1][:Ng] * v_g * phi_jg[j2][:Ng] 

581 c = a.xc_correction.rgd.integrate(f_g, n=-1) / (4 * np.pi) 

582 dVL_vii[0, N1:N1 + Nm, N2:N2 + Nm] = c * Lx_lmm[l1] 

583 dVL_vii[1, N1:N1 + Nm, N2:N2 + Nm] = c * Ly_lmm[l1] 

584 dVL_vii[2, N1:N1 + Nm, N2:N2 + Nm] = c * Lz_lmm[l1] 

585 else: 

586 pass 

587 N2 += 2 * l2 + 1 

588 N1 += Nm 

589 return dVL_vii * alpha**2 / 4.0 

590 

591 

592def projected_soc(dVL_vii: Array3D, 

593 theta: float = 0, 

594 phi: float = 0) -> Array3D: 

595 """ 

596 Optional Args: 

597 theta (float): The angle from z-axis in degrees 

598 phi (float): The angle from x-axis in degrees 

599 """ 

600 theta *= np.pi / 180 

601 phi *= np.pi / 180 

602 n_v = np.array([np.sin(theta) * np.cos(phi), 

603 np.sin(theta) * np.sin(phi), 

604 np.cos(theta)]) 

605 dVL_vii = (np.dot(dVL_vii.T, n_v)[:, :, np.newaxis] * n_v).T 

606 return dVL_vii 

607 

608 

609def get_radial_potential_derivative(a: Setup, xc, D_sp: Array2D) -> Array1D: 

610 """Calculates (dU/dr) for the effective potential. 

611 Below, f_g denotes dU/dr which is also the negative of the radial force""" 

612 

613 rgd = a.xc_correction.rgd 

614 r_g = rgd.r_g.copy() 

615 r_g[0] = 1.0e-12 

616 dr_g = rgd.dr_g 

617 

618 B_pq = a.xc_correction.B_pqL[:, :, 0] 

619 n_qg = a.xc_correction.n_qg 

620 D_sq = np.dot(D_sp, B_pq) 

621 n_sg = np.dot(D_sq, n_qg) / (4 * np.pi)**0.5 

622 Ns = len(D_sp) 

623 if Ns == 4: 

624 Ns = 1 

625 n_sg[:Ns] += a.xc_correction.nc_g / Ns 

626 

627 # Coulomb force from nucleus 

628 fc_g = a.Z / r_g**2 

629 

630 # Hartree force 

631 rho_g = 4 * np.pi * r_g**2 * dr_g * np.sum(n_sg[:Ns], axis=0) 

632 fh_g = -np.array([np.sum(rho_g[:ig]) for ig in range(len(r_g))]) / r_g**2 

633 

634 f_g = fc_g + fh_g 

635 

636 # xc force 

637 if xc.type != 'GLLB': 

638 v_sg = np.zeros_like(n_sg) 

639 xc.calculate_spherical(rgd, n_sg, v_sg) 

640 fxc_g = np.mean([rgd.derivative(v_g) for v_g in v_sg[:Ns]], 

641 axis=0) 

642 f_g += fxc_g 

643 

644 return f_g 

645 

646 

647def get_anisotropy(calc, theta=0.0, phi=0.0, nbands=0, width=None): 

648 """Calculates the sum of occupied spinorbit eigenvalues. 

649 

650 Returns the result relative to the sum of eigenvalues without 

651 spinorbit coupling. 

652 """ 

653 raise RuntimeError('Please use BZWaveFunctions.calculate_band_energy() ' 

654 'instead.') 

655 

656 

657def get_magnetic_moments(calc, theta=0.0, phi=0.0, nbands=None, width=None): 

658 """Calculates the magnetic moments inside all PAW spheres""" 

659 

660 raise RuntimeError( 

661 'This function has no tests. It is very likely that it no longer ' 

662 'works correctly after merging !677.') 

663 

664 from gpaw.utilities import unpack_hermitian 

665 

666 if nbands is None: 

667 nbands = calc.get_number_of_bands() 

668 Nk = len(calc.get_ibz_k_points()) 

669 

670 C_ss = np.array([[np.cos(theta / 2) * np.exp(-1.0j * phi / 2), 

671 -np.sin(theta / 2) * np.exp(-1.0j * phi / 2)], 

672 [np.sin(theta / 2) * np.exp(1.0j * phi / 2), 

673 np.cos(theta / 2) * np.exp(1.0j * phi / 2)]]) 

674 sx_ss = np.array([[0, 1], [1, 0]], complex) 

675 sy_ss = np.array([[0, -1.0j], [1.0j, 0]], complex) 

676 sz_ss = np.array([[1, 0], [0, -1]], complex) 

677 sx_ss = C_ss.T.conj().dot(sx_ss).dot(C_ss) 

678 sy_ss = C_ss.T.conj().dot(sy_ss).dot(C_ss) 

679 sz_ss = C_ss.T.conj().dot(sz_ss).dot(C_ss) 

680 

681 states = soc_eigenstates(calc, 

682 theta=theta, 

683 phi=phi, 

684 return_wfs=True, 

685 bands=range(nbands)) 

686 e_km = states['eigenvalues'] 

687 v_knm = states['eigenstates'] 

688 

689 from gpaw.occupations import occupation_numbers 

690 if width is None: 

691 assert calc.wfs.occupations.name == 'fermi-dirac' 

692 width = calc.wfs.occupations._width 

693 if width == 0.0: 

694 width = 1.e-6 

695 weight_k = calc.get_k_point_weights() / 2 

696 ne = calc.wfs.setups.nvalence - calc.density.charge 

697 f_km = occupation_numbers({'name': 'fermi-dirac', 'width': width}, 

698 e_km[np.newaxis], 

699 weight_k=weight_k, 

700 nelectrons=ne)[0][0] 

701 

702 m_v = np.zeros(3, complex) 

703 for ik in range(Nk): 

704 ut0_nG = np.array([calc.wfs.get_wave_function_array(n, ik, 0) 

705 for n in range(nbands)]) 

706 ut1_nG = np.array([calc.wfs.get_wave_function_array(n, ik, 1) 

707 for n in range(nbands)]) 

708 mocc = np.where(f_km[ik] * Nk - 1.0e-6 < 0.0)[0][0] 

709 for m in range(mocc + 1): 

710 f = f_km[ik, m] 

711 ut0_G = np.dot(v_knm[ik][::2, m], np.swapaxes(ut0_nG, 0, 2)) 

712 ut1_G = np.dot(v_knm[ik][1::2, m], np.swapaxes(ut1_nG, 0, 2)) 

713 ut_sG = np.array([ut0_G, ut1_G]) 

714 

715 mx_G = np.zeros(np.shape(ut0_G), complex) 

716 my_G = np.zeros(np.shape(ut0_G), complex) 

717 mz_G = np.zeros(np.shape(ut0_G), complex) 

718 for s1 in range(2): 

719 for s2 in range(2): 

720 mx_G += ut_sG[s1].conj() * sx_ss[s1, s2] * ut_sG[s2] 

721 my_G += ut_sG[s1].conj() * sy_ss[s1, s2] * ut_sG[s2] 

722 mz_G += ut_sG[s1].conj() * sz_ss[s1, s2] * ut_sG[s2] 

723 m_v += calc.wfs.gd.integrate(np.array([mx_G, my_G, mz_G])) * f 

724 

725 m_av = [] 

726 for a in range(len(calc.atoms)): 

727 N0_p = calc.density.setups[a].N0_p.copy() 

728 N0_ij = unpack_hermitian(N0_p) 

729 Dx_ij = np.zeros_like(N0_ij, complex) 

730 Dy_ij = np.zeros_like(N0_ij, complex) 

731 Dz_ij = np.zeros_like(N0_ij, complex) 

732 Delta_p = calc.density.setups[a].Delta_pL[:, 0].copy() 

733 Delta_ij = unpack_hermitian(Delta_p) 

734 for ik in range(Nk): 

735 P_ami = ... # get_spinorbit_projections(calc, ik, v_knm[ik]) 

736 P_smi = np.array([P_ami[a][:, ::2], P_ami[a][:, 1::2]]) 

737 P_smi = np.dot(C_ss, np.swapaxes(P_smi, 0, 1)) 

738 

739 P0_mi = P_smi[0] 

740 P1_mi = P_smi[1] 

741 f_mm = np.diag(f_km[ik]) 

742 

743 Dx_ij += P0_mi.conj().T.dot(f_mm).dot(P1_mi) 

744 Dx_ij += P1_mi.conj().T.dot(f_mm).dot(P0_mi) 

745 Dy_ij -= 1.0j * P0_mi.conj().T.dot(f_mm).dot(P1_mi) 

746 Dy_ij += 1.0j * P1_mi.conj().T.dot(f_mm).dot(P0_mi) 

747 Dz_ij += P0_mi.conj().T.dot(f_mm).dot(P0_mi) 

748 Dz_ij -= P1_mi.conj().T.dot(f_mm).dot(P1_mi) 

749 

750 mx = np.sum(N0_ij * Dx_ij).real 

751 my = np.sum(N0_ij * Dy_ij).real 

752 mz = np.sum(N0_ij * Dz_ij).real 

753 

754 m_av.append([mx, my, mz]) 

755 m_v[0] += np.sum(Delta_ij * Dx_ij) * (4 * np.pi)**0.5 

756 m_v[1] += np.sum(Delta_ij * Dy_ij) * (4 * np.pi)**0.5 

757 m_v[2] += np.sum(Delta_ij * Dz_ij) * (4 * np.pi)**0.5 

758 

759 return m_v.real, m_av 

760 

761 

762def get_parity_eigenvalues(calc, ik=0, spin_orbit=False, bands=None, Nv=None, 

763 inversion_center=[0, 0, 0], deg_tol=1.0e-6, 

764 tol=1.0e-6): 

765 """Calculates parity eigenvalues at time-reversal invariant k-points. 

766 Only works in plane wave mode. 

767 """ 

768 

769 assert len(calc.get_bz_k_points()) == len(calc.get_ibz_k_points()) 

770 

771 kpt_c = calc.get_ibz_k_points()[ik] 

772 if Nv is None: 

773 Nv = int(calc.get_number_of_electrons() / 2) 

774 

775 if bands is None: 

776 bands = range(calc.get_number_of_bands()) 

777 

778 # Find degenerate subspaces 

779 eig_n = calc.get_eigenvalues(kpt=ik)[bands] 

780 e_in = [] 

781 used_n = [] 

782 for n1, e1 in enumerate(eig_n): 

783 if n1 not in used_n: 

784 n_n = [] 

785 for n2, e2 in enumerate(eig_n): 

786 if np.abs(e1 - e2) < deg_tol: 

787 n_n.append(n2) 

788 used_n.append(n2) 

789 e_in.append(n_n) 

790 

791 print() 

792 print(f' Inversion center at: {inversion_center}') 

793 print(f' Calculating inversion eigenvalues at k = {kpt_c}') 

794 print() 

795 

796 center_v = np.array(inversion_center) / Bohr 

797 G_Gv = calc.wfs.pd.get_reciprocal_vectors(q=ik, add_q=True) 

798 

799 psit_nG = np.array([calc.wfs.kpt_u[ik].psit_nG[n] 

800 for n in bands]) 

801 if spin_orbit: 

802 n1 = bands[0] 

803 n2 = bands[-1] + 1 

804 assert (bands == np.arange(n1, n2)).all() 

805 soc = soc_eigenstates(calc, n1=n1, n2=n2) 

806 v_kmn = soc.eigenvectors() 

807 psit0_mG = np.dot(v_kmn[ik, :, ::2], psit_nG) 

808 psit1_mG = np.dot(v_kmn[ik, :, 1::2], psit_nG) 

809 for n in range(len(bands)): 

810 psit_nG[n] /= (np.sum(np.abs(psit_nG[n])**2))**0.5 

811 if spin_orbit: 

812 for n in range(2 * len(bands)): 

813 A = np.sum(np.abs(psit0_mG[n])**2) + np.sum(np.abs(psit1_mG[n])**2) 

814 psit0_mG[n] /= A**0.5 

815 psit1_mG[n] /= A**0.5 

816 

817 P_GG = np.ones((len(G_Gv), len(G_Gv)), float) 

818 for iG, G_v in enumerate(G_Gv): 

819 P_GG[iG] -= ((G_Gv[:] + G_v).round(6)).any(axis=1) 

820 assert (P_GG == P_GG.T).all() 

821 

822 phase_G = np.exp(-2.0j * np.dot(G_Gv, center_v)) 

823 

824 p_n = [] 

825 print('n P_n') 

826 for n_n in e_in: 

827 if spin_orbit: 

828 # The dimension of parity matrix is doubled with spinorbit 

829 m_m = [2 * n_n[0] + i for i in range(2 * len(n_n))] 

830 Ppsit0_mG = np.dot(P_GG, psit0_mG[m_m].T).T 

831 Ppsit0_mG[:] *= phase_G 

832 Ppsit1_mG = np.dot(P_GG, psit1_mG[m_m].T).T 

833 Ppsit1_mG[:] *= phase_G 

834 P_nn = np.dot(psit0_mG[m_m].conj(), np.array(Ppsit0_mG).T) 

835 P_nn += np.dot(psit1_mG[m_m].conj(), np.array(Ppsit1_mG).T) 

836 else: 

837 Ppsit_nG = np.dot(P_GG, psit_nG[n_n].T).T 

838 Ppsit_nG[:] *= phase_G 

839 P_nn = np.dot(psit_nG[n_n].conj(), np.array(Ppsit_nG).T) 

840 P_eig = np.linalg.eigh(P_nn)[0] 

841 if np.allclose(np.abs(P_eig), 1, tol): 

842 P_n = np.sign(P_eig).astype(int).tolist() 

843 if spin_orbit: 

844 # Only include one of the degenerate pair of eigenvalues 

845 Pm = np.sign(P_eig).tolist().count(-1) 

846 Pp = np.sign(P_eig).tolist().count(1) 

847 P_n = Pm // 2 * [-1] + Pp // 2 * [1] 

848 print(f'{str(n_n)[1:-1]}: {str(P_n)[1:-1]}') 

849 p_n += P_n 

850 else: 

851 print(f' {n_n} are not parity eigenstates') 

852 print(f' P_n: {P_eig}') 

853 print(f' e_n: {eig_n[n_n]}') 

854 p_n += [0 for n in n_n] 

855 

856 return np.ravel(p_n) 

857 

858 

859def get_L_vlmm(): 

860 if len(_L_vlmm) == 3: 

861 return _L_vlmm 

862 

863 s = np.array([[0.0]]) 

864 p = np.zeros((3, 3), complex) # y, z, x 

865 p[0, 1] = -1.0j 

866 p[1, 0] = 1.0j 

867 d = np.zeros((5, 5), complex) # xy, yz, z^2, xz, x^2-y^2 

868 d[0, 3] = -1.0j 

869 d[3, 0] = 1.0j 

870 d[1, 2] = -3**0.5 * 1.0j 

871 d[2, 1] = 3**0.5 * 1.0j 

872 d[1, 4] = -1.0j 

873 d[4, 1] = 1.0j 

874 f = np.zeros((7, 7), complex) 

875 f[0, 5] = -0.5 * 6**0.5 * 1.0j 

876 f[5, 0] = 0.5 * 6**0.5 * 1.0j 

877 f[1, 4] = -0.5 * 10**0.5 * 1.0j 

878 f[4, 1] = 0.5 * 10**0.5 * 1.0j 

879 f[1, 6] = -0.5 * 6**0.5 * 1.0j 

880 f[6, 1] = 0.5 * 6**0.5 * 1.0j 

881 f[2, 3] = -6**0.5 * 1.0j 

882 f[3, 2] = 6**0.5 * 1.0j 

883 f[2, 5] = -0.5 * 10**0.5 * 1.0j 

884 f[5, 2] = 0.5 * 10**0.5 * 1.0j 

885 _L_vlmm.append([s, p, d, f]) 

886 

887 p = np.zeros((3, 3), complex) # y, z, x 

888 p[1, 2] = -1.0j 

889 p[2, 1] = 1.0j 

890 d = np.zeros((5, 5), complex) # xy, yz, z^2, xz, x^2-y^2 

891 d[0, 1] = 1.0j 

892 d[1, 0] = -1.0j 

893 d[2, 3] = -3**0.5 * 1.0j 

894 d[3, 2] = 3**0.5 * 1.0j 

895 d[3, 4] = -1.0j 

896 d[4, 3] = 1.0j 

897 f = np.zeros((7, 7), complex) 

898 f[0, 1] = 0.5 * 6**0.5 * 1.0j 

899 f[1, 0] = -0.5 * 6**0.5 * 1.0j 

900 f[1, 2] = 0.5 * 10**0.5 * 1.0j 

901 f[2, 1] = -0.5 * 10**0.5 * 1.0j 

902 f[3, 4] = -6**0.5 * 1.0j 

903 f[4, 3] = 6**0.5 * 1.0j 

904 f[4, 5] = -0.5 * 10**0.5 * 1.0j 

905 f[5, 4] = 0.5 * 10**0.5 * 1.0j 

906 f[5, 6] = -0.5 * 6**0.5 * 1.0j 

907 f[6, 5] = 0.5 * 6**0.5 * 1.0j 

908 _L_vlmm.append([s, p, d, f]) 

909 

910 p = np.zeros((3, 3), complex) # y, z, x 

911 p[0, 2] = 1.0j 

912 p[2, 0] = -1.0j 

913 d = np.zeros((5, 5), complex) # xy, yz, z^2, xz, x^2-y^2 

914 d[0, 4] = 2.0j 

915 d[4, 0] = -2.0j 

916 d[1, 3] = 1.0j 

917 d[3, 1] = -1.0j 

918 f = np.zeros((7, 7), complex) 

919 f[0, 6] = 3.0j 

920 f[6, 0] = -3.0j 

921 f[1, 5] = 2.0j 

922 f[5, 1] = -2.0j 

923 f[2, 4] = 1.0j 

924 f[4, 2] = -1.0j 

925 _L_vlmm.append([s, p, d, f]) 

926 return _L_vlmm