TB2J 0.9.12.9__py3-none-any.whl → 0.9.12.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- TB2J/MAE.py +8 -1
- TB2J/MAEGreen.py +78 -61
- TB2J/contour.py +3 -2
- TB2J/exchange.py +346 -51
- TB2J/exchangeCL2.py +285 -47
- TB2J/exchange_params.py +48 -0
- TB2J/green.py +73 -36
- TB2J/interfaces/abacus/gen_exchange_abacus.py +2 -1
- TB2J/interfaces/wannier90_interface.py +4 -4
- TB2J/io_exchange/__init__.py +19 -1
- TB2J/io_exchange/edit.py +594 -0
- TB2J/io_exchange/io_espins.py +276 -0
- TB2J/io_exchange/io_exchange.py +248 -76
- TB2J/io_exchange/io_tomsasd.py +4 -3
- TB2J/io_exchange/io_txt.py +72 -7
- TB2J/io_exchange/io_vampire.py +4 -2
- TB2J/io_merge.py +60 -40
- TB2J/magnon/magnon3.py +27 -2
- TB2J/mathutils/rotate_spin.py +7 -7
- TB2J/myTB.py +11 -11
- TB2J/mycfr.py +11 -11
- TB2J/pauli.py +32 -2
- TB2J/plot.py +26 -0
- TB2J/rotate_atoms.py +9 -6
- TB2J/scripts/TB2J_edit.py +403 -0
- TB2J/scripts/TB2J_plot_exchange.py +48 -0
- TB2J/spinham/hamiltonian.py +156 -13
- TB2J/spinham/hamiltonian_terms.py +40 -1
- TB2J/spinham/spin_xml.py +40 -8
- TB2J/symmetrize_J.py +140 -9
- TB2J/tests/test_cli_remove_sublattice.py +33 -0
- TB2J/tests/test_cli_toggle_exchange.py +50 -0
- {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/METADATA +10 -7
- {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/RECORD +38 -34
- {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/WHEEL +1 -1
- {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/entry_points.txt +2 -0
- TB2J/interfaces/abacus/test_read_HRSR.py +0 -43
- TB2J/interfaces/abacus/test_read_stru.py +0 -32
- {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/licenses/LICENSE +0 -0
- {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/top_level.txt +0 -0
TB2J/exchange.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pickle
|
|
3
3
|
from collections import defaultdict
|
|
4
|
+
from itertools import product
|
|
4
5
|
|
|
6
|
+
import ase.units
|
|
5
7
|
import numpy as np
|
|
6
8
|
from tqdm import tqdm
|
|
7
9
|
|
|
@@ -27,8 +29,8 @@ class Exchange(ExchangeParams):
|
|
|
27
29
|
self._prepare_Rlist()
|
|
28
30
|
self.set_tbmodels(tbmodels)
|
|
29
31
|
self._adjust_emin()
|
|
30
|
-
|
|
31
|
-
self._prepare_elist(method="legendre")
|
|
32
|
+
self._prepare_elist(method="CFR")
|
|
33
|
+
# self._prepare_elist(method="legendre")
|
|
32
34
|
self._prepare_basis()
|
|
33
35
|
self._prepare_orb_dict()
|
|
34
36
|
self._prepare_distance()
|
|
@@ -37,17 +39,28 @@ class Exchange(ExchangeParams):
|
|
|
37
39
|
# self._prepare_NijR()
|
|
38
40
|
self._is_collinear = True
|
|
39
41
|
self.has_elistc = False
|
|
42
|
+
|
|
43
|
+
# Store overlap matrix before cleaning tbmodels
|
|
44
|
+
if hasattr(self, "tbmodel") and hasattr(self.tbmodel, "SR"):
|
|
45
|
+
# Find R=0 index in tbmodel.Rlist
|
|
46
|
+
iR_S0 = np.argmin(np.linalg.norm(self.tbmodel.Rlist, axis=1))
|
|
47
|
+
self.S_R0 = self.tbmodel.SR[iR_S0] # R=0 overlap matrix
|
|
48
|
+
else:
|
|
49
|
+
self.S_R0 = None
|
|
50
|
+
|
|
40
51
|
self._clean_tbmodels()
|
|
41
52
|
|
|
53
|
+
# Initialize storage for Green's function diagonals (for charge and magnetic moment calculation)
|
|
54
|
+
self.G_diagonal = {iatom: [] for iatom in range(len(self.atoms))}
|
|
55
|
+
|
|
42
56
|
def _prepare_Jorb_file(self):
|
|
43
57
|
os.makedirs(self.output_path, exist_ok=True)
|
|
44
58
|
self.orbpath = os.path.join(self.output_path, "OrbResolve")
|
|
45
59
|
os.makedirs(self.orbpath, exist_ok=True)
|
|
46
60
|
|
|
47
61
|
def _adjust_emin(self):
|
|
48
|
-
self.emin = self.G.
|
|
62
|
+
self.emin = self.G.adjusted_emin
|
|
49
63
|
# self.emin = self.G.find_energy_ingap(rbound=self.efermi - 15.0) - self.efermi
|
|
50
|
-
# self.emin = -42.0
|
|
51
64
|
# print(f"A gap is found at {self.emin}, set emin to it.")
|
|
52
65
|
|
|
53
66
|
def set_tbmodels(self, tbmodels):
|
|
@@ -62,12 +75,14 @@ class Exchange(ExchangeParams):
|
|
|
62
75
|
for k in kmesh:
|
|
63
76
|
self.kmesh = list(map(lambda x: x // 2 * 2 + 1, kmesh))
|
|
64
77
|
|
|
65
|
-
def _prepare_elist(self, method=
|
|
78
|
+
def _prepare_elist(self, method=None):
|
|
66
79
|
"""
|
|
67
80
|
prepare list of energy for integration.
|
|
68
81
|
The path has three segments:
|
|
69
82
|
emin --1-> emin + 1j*height --2-> emax+1j*height --3-> emax
|
|
70
83
|
"""
|
|
84
|
+
if method is None:
|
|
85
|
+
method = "CFR"
|
|
71
86
|
# if method.lower() == "rectangle":
|
|
72
87
|
# self.contour.build_path_rectangle(
|
|
73
88
|
# height=self.height, nz1=self.nz1, nz2=self.nz2, nz3=self.nz3
|
|
@@ -79,7 +94,10 @@ class Exchange(ExchangeParams):
|
|
|
79
94
|
self.contour = Contour(self.emin, self.emax)
|
|
80
95
|
self.contour.build_path_legendre(npoints=self.nz, endpoint=True)
|
|
81
96
|
elif method.lower() == "cfr":
|
|
82
|
-
|
|
97
|
+
# Convert smearing from eV to temperature (K) for CFR
|
|
98
|
+
# smearing = kB * T => T = smearing / kB
|
|
99
|
+
T_kelvin = self.smearing / ase.units.kB
|
|
100
|
+
self.contour = CFR(nz=self.nz, T=T_kelvin)
|
|
83
101
|
else:
|
|
84
102
|
raise ValueError(f"The path cannot be of type {method}.")
|
|
85
103
|
|
|
@@ -229,12 +247,16 @@ class Exchange(ExchangeParams):
|
|
|
229
247
|
prepare the distance between atoms.
|
|
230
248
|
"""
|
|
231
249
|
self.distance_dict = {}
|
|
232
|
-
self.short_Rlist = []
|
|
250
|
+
self.short_Rlist = [] # Will contain actual R vectors, not indices
|
|
233
251
|
self.R_ijatom_dict = defaultdict(lambda: [])
|
|
234
252
|
ind_matoms = self.ind_mag_atoms
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
|
|
254
|
+
# First pass: identify which R vectors are within Rcut
|
|
255
|
+
# Add both R and -R when within cutoff
|
|
256
|
+
valid_R_vectors = set()
|
|
257
|
+
for R in self.Rlist:
|
|
258
|
+
for ispin, iatom in enumerate(ind_matoms):
|
|
259
|
+
for jspin, jatom in enumerate(ind_matoms):
|
|
238
260
|
pos_i = self.atoms.get_positions()[iatom]
|
|
239
261
|
pos_jR = self.atoms.get_positions()[jatom] + np.dot(
|
|
240
262
|
R, self.atoms.get_cell()
|
|
@@ -242,9 +264,57 @@ class Exchange(ExchangeParams):
|
|
|
242
264
|
vec = pos_jR - pos_i
|
|
243
265
|
distance = np.sqrt(np.sum(vec**2))
|
|
244
266
|
if self.Rcut is None or distance < self.Rcut:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
267
|
+
R_tuple = tuple(R)
|
|
268
|
+
valid_R_vectors.add(R_tuple)
|
|
269
|
+
valid_R_vectors.add(tuple(-x for x in R_tuple))
|
|
270
|
+
|
|
271
|
+
# Sort the valid_R_vectors
|
|
272
|
+
self.short_Rlist = sorted(valid_R_vectors)
|
|
273
|
+
# print(f"short_Rlist contains {len(self.short_Rlist)} R vectors, which are: {self.short_Rlist}")
|
|
274
|
+
|
|
275
|
+
# Second pass: build dictionaries using the clean indexing
|
|
276
|
+
for iR, R_vec in enumerate(self.short_Rlist):
|
|
277
|
+
for ispin, iatom in enumerate(ind_matoms):
|
|
278
|
+
for jspin, jatom in enumerate(ind_matoms):
|
|
279
|
+
pos_i = self.atoms.get_positions()[iatom]
|
|
280
|
+
pos_jR = self.atoms.get_positions()[jatom] + np.dot(
|
|
281
|
+
R_vec, self.atoms.get_cell()
|
|
282
|
+
)
|
|
283
|
+
vec = pos_jR - pos_i
|
|
284
|
+
distance = np.sqrt(np.sum(vec**2))
|
|
285
|
+
if self.Rcut is None or distance < self.Rcut:
|
|
286
|
+
self.distance_dict[(R_vec, ispin, jspin)] = (vec, distance)
|
|
287
|
+
self.R_ijatom_dict[iR].append((iatom, jatom))
|
|
288
|
+
|
|
289
|
+
# Create lookup dictionary for negative R vectors
|
|
290
|
+
self.Rvec_to_shortlist_idx = {
|
|
291
|
+
R_vec: iR for iR, R_vec in enumerate(self.short_Rlist)
|
|
292
|
+
}
|
|
293
|
+
self.R_negative_index = {}
|
|
294
|
+
for iR, R_vec in enumerate(self.short_Rlist):
|
|
295
|
+
Rm_vec = tuple(-x for x in R_vec)
|
|
296
|
+
if Rm_vec in self.Rvec_to_shortlist_idx:
|
|
297
|
+
self.R_negative_index[iR] = self.Rvec_to_shortlist_idx[Rm_vec]
|
|
298
|
+
else:
|
|
299
|
+
self.R_negative_index[iR] = None # No negative R found
|
|
300
|
+
|
|
301
|
+
# Verify the R vector pairing
|
|
302
|
+
pairing_good = True
|
|
303
|
+
for iR, R_vec in enumerate(self.short_Rlist):
|
|
304
|
+
neg_idx = self.R_negative_index[iR]
|
|
305
|
+
if neg_idx is not None:
|
|
306
|
+
expected_neg = tuple(-x for x in R_vec)
|
|
307
|
+
actual_neg = self.short_Rlist[neg_idx]
|
|
308
|
+
if expected_neg != actual_neg:
|
|
309
|
+
print(
|
|
310
|
+
f" R[{iR}] = {R_vec} -> -R[{neg_idx}] = {actual_neg} ✗ (expected {expected_neg})"
|
|
311
|
+
)
|
|
312
|
+
pairing_good = False
|
|
313
|
+
else:
|
|
314
|
+
print(f" R[{iR}] = {R_vec} -> No negative R found")
|
|
315
|
+
|
|
316
|
+
if not pairing_good:
|
|
317
|
+
raise ValueError("R vector pairing check failed.")
|
|
248
318
|
|
|
249
319
|
def iorb(self, iatom):
|
|
250
320
|
"""
|
|
@@ -294,6 +364,8 @@ class ExchangeNCL(Exchange):
|
|
|
294
364
|
efermi=self.efermi,
|
|
295
365
|
use_cache=self._use_cache,
|
|
296
366
|
nproc=self.nproc,
|
|
367
|
+
initial_emin=self.emin,
|
|
368
|
+
smearing_width=self.smearing,
|
|
297
369
|
)
|
|
298
370
|
if self.efermi is None:
|
|
299
371
|
self.efermi = self.G.efermi
|
|
@@ -305,10 +377,11 @@ class ExchangeNCL(Exchange):
|
|
|
305
377
|
self.A_ijR = defaultdict(lambda: np.zeros((4, 4), dtype=complex))
|
|
306
378
|
self.A_ijR_orb = dict()
|
|
307
379
|
# self.HR0 = self.tbmodel.get_H0()
|
|
308
|
-
if hasattr(self.tbmodel, "get_H0"):
|
|
309
|
-
|
|
310
|
-
else:
|
|
311
|
-
|
|
380
|
+
# if hasattr(self.tbmodel, "get_H0"):
|
|
381
|
+
# self.HR0 = self.tbmodel.get_H0()
|
|
382
|
+
# else:
|
|
383
|
+
# self.HR0 = self.G.H0
|
|
384
|
+
self.HR0 = self.G.H0
|
|
312
385
|
self._is_collinear = False
|
|
313
386
|
self.Pdict = {}
|
|
314
387
|
if self.write_density_matrix:
|
|
@@ -362,7 +435,7 @@ class ExchangeNCL(Exchange):
|
|
|
362
435
|
return GR[np.ix_(orbi, orbj)]
|
|
363
436
|
# return GR[self.orb_slice[iatom], self.orb_slice[jatom]]
|
|
364
437
|
|
|
365
|
-
def get_A_ijR(self, G,
|
|
438
|
+
def get_A_ijR(self, G, iR, iatom, jatom):
|
|
366
439
|
"""calculate A from G for a energy slice (de).
|
|
367
440
|
It take the
|
|
368
441
|
.. math::
|
|
@@ -371,20 +444,25 @@ class ExchangeNCL(Exchange):
|
|
|
371
444
|
where u, v are I, x, y, z (index 0, 1,2,3). p(i) = self.get_P_iatom(iatom)
|
|
372
445
|
T^u(ijR) (u=0,1,2,3) = pauli_block_all(G)
|
|
373
446
|
|
|
374
|
-
:param G: Green's function for all R, i, j.
|
|
447
|
+
:param G: Green's function for all R, i, j (numpy array).
|
|
448
|
+
:param iR: index in short_Rlist (position in G array)
|
|
375
449
|
:param iatom: i
|
|
376
450
|
:param jatom: j
|
|
377
|
-
:param de: energy step. used for integeration
|
|
378
451
|
:returns: a matrix of A_ij(u, v), where u, v =(0)0, x(1), y(2), z(3)
|
|
379
452
|
:rtype: 4*4 matrix
|
|
380
453
|
"""
|
|
381
|
-
GR = G[
|
|
454
|
+
GR = G[iR]
|
|
382
455
|
Gij = self.GR_atom(GR, iatom, jatom)
|
|
383
456
|
Gij_Ixyz = pauli_block_all(Gij)
|
|
384
457
|
|
|
385
|
-
# G(j, i, -R)
|
|
386
|
-
|
|
387
|
-
|
|
458
|
+
# G(j, i, -R) - use optimized lookup
|
|
459
|
+
iRm = self.R_negative_index[iR]
|
|
460
|
+
if iRm is None:
|
|
461
|
+
R_vec = self.short_Rlist[iR]
|
|
462
|
+
Rm_vec = tuple(-x for x in R_vec)
|
|
463
|
+
raise KeyError(f"Negative R vector {Rm_vec} not found in short_Rlist")
|
|
464
|
+
|
|
465
|
+
GRm = G[iRm]
|
|
388
466
|
Gji = self.GR_atom(GRm, jatom, iatom)
|
|
389
467
|
Gji_Ixyz = pauli_block_all(Gji)
|
|
390
468
|
|
|
@@ -418,18 +496,85 @@ class ExchangeNCL(Exchange):
|
|
|
418
496
|
"""
|
|
419
497
|
Calculate all A matrix elements
|
|
420
498
|
Loop over all magnetic atoms.
|
|
421
|
-
:param G: Green's function.
|
|
499
|
+
:param G: Green's function (numpy array).
|
|
422
500
|
:param de: energy step.
|
|
423
501
|
"""
|
|
424
502
|
A_ijR_list = {}
|
|
425
503
|
Aorb_ijR_list = {}
|
|
426
|
-
for iR
|
|
427
|
-
for iatom, jatom in self.R_ijatom_dict[
|
|
428
|
-
A, A_orb = self.get_A_ijR(G,
|
|
429
|
-
|
|
430
|
-
|
|
504
|
+
for iR in self.R_ijatom_dict:
|
|
505
|
+
for iatom, jatom in self.R_ijatom_dict[iR]:
|
|
506
|
+
A, A_orb = self.get_A_ijR(G, iR, iatom, jatom)
|
|
507
|
+
# Store with actual R vector for compatibility with existing code
|
|
508
|
+
R_vec = self.short_Rlist[iR]
|
|
509
|
+
A_ijR_list[(R_vec, iatom, jatom)] = A
|
|
510
|
+
Aorb_ijR_list[(R_vec, iatom, jatom)] = A_orb
|
|
431
511
|
return A_ijR_list, Aorb_ijR_list
|
|
432
512
|
|
|
513
|
+
def get_all_A_vectorized(self, GR):
|
|
514
|
+
"""
|
|
515
|
+
Vectorized calculation of all A matrix elements.
|
|
516
|
+
Fully vectorized version based on TB2J_optimization_prototype.ipynb.
|
|
517
|
+
Now works with properly ordered short_Rlist.
|
|
518
|
+
|
|
519
|
+
:param GR: Green's function array of shape (nR, nbasis, nbasis)
|
|
520
|
+
:returns: tuple of (A_ijR_list, Aorb_ijR_list) with R vector keys
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
# Get magnetic sites and their orbital indices
|
|
524
|
+
magnetic_sites = self.ind_mag_atoms
|
|
525
|
+
iorbs = [self.iorb(site) for site in magnetic_sites]
|
|
526
|
+
|
|
527
|
+
# Build the P matrices for all magnetic sites using the same method as original
|
|
528
|
+
P = [self.get_P_iatom(site) for site in magnetic_sites]
|
|
529
|
+
|
|
530
|
+
# Initialize results dictionary
|
|
531
|
+
A = {}
|
|
532
|
+
A_orb = {}
|
|
533
|
+
|
|
534
|
+
# Batch compute all A tensors following the prototype
|
|
535
|
+
for i, j in product(range(len(magnetic_sites)), repeat=2):
|
|
536
|
+
idx, jdx = iorbs[i], iorbs[j]
|
|
537
|
+
Gij = GR[:, idx][:, :, jdx]
|
|
538
|
+
Gji = GR[:, jdx][:, :, idx]
|
|
539
|
+
Gij = pauli_block_all(Gij)
|
|
540
|
+
Gji = pauli_block_all(Gji)
|
|
541
|
+
# NOTE: becareful: this assumes that short_Rlist is properly ordered so that
|
|
542
|
+
# the ith R vector's negative is at -i index.
|
|
543
|
+
Gji = np.flip(Gji, axis=0)
|
|
544
|
+
Pi = P[i]
|
|
545
|
+
Pj = P[j]
|
|
546
|
+
X = Pi @ Gij
|
|
547
|
+
Y = Pj @ Gji
|
|
548
|
+
mi, mj = (magnetic_sites[i], magnetic_sites[j])
|
|
549
|
+
|
|
550
|
+
if self.orb_decomposition:
|
|
551
|
+
# Vectorized orbital decomposition over all R vectors at once
|
|
552
|
+
# X.shape: (nR, 4, ni, nj), Y.shape: (nR, 4, nj, ni)
|
|
553
|
+
A_orb_tensor = (
|
|
554
|
+
np.einsum("ruij,rvji->ruvij", X, Y) / np.pi
|
|
555
|
+
) # Shape: (nR, 4, 4, ni, nj)
|
|
556
|
+
# Vectorized sum over orbitals for simplified A values
|
|
557
|
+
A_val_tensor = np.sum(A_orb_tensor, axis=(-2, -1)) # Shape: (nR, 4, 4)
|
|
558
|
+
else:
|
|
559
|
+
# Compute A_tensor for all R vectors at once
|
|
560
|
+
A_tensor = (
|
|
561
|
+
np.einsum("...uij,...vji->...uv", X, Y) / np.pi
|
|
562
|
+
) # Shape: (nR, 4, 4)
|
|
563
|
+
A_val_tensor = A_tensor # Use pre-computed A_tensor directly
|
|
564
|
+
A_orb_tensor = None
|
|
565
|
+
|
|
566
|
+
# Store results for each R vector
|
|
567
|
+
for iR, R_vec in enumerate(self.short_Rlist):
|
|
568
|
+
A_val = A_val_tensor[iR] # Shape: (4, 4)
|
|
569
|
+
A_orb_val = A_orb_tensor[iR] if A_orb_tensor is not None else None
|
|
570
|
+
|
|
571
|
+
# Store with R vector key for compatibility
|
|
572
|
+
A[(R_vec, mi, mj)] = A_val
|
|
573
|
+
if A_orb_val is not None:
|
|
574
|
+
A_orb[(R_vec, mi, mj)] = A_orb_val
|
|
575
|
+
|
|
576
|
+
return A, A_orb
|
|
577
|
+
|
|
433
578
|
def A_to_Jtensor_orb(self):
|
|
434
579
|
"""
|
|
435
580
|
convert the orbital composition of A into J, DMI, Jani
|
|
@@ -589,28 +734,169 @@ class ExchangeNCL(Exchange):
|
|
|
589
734
|
#
|
|
590
735
|
|
|
591
736
|
# self.rho = integrate(self.contour.path, rhoRs)
|
|
592
|
-
for iR
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
self.A_ijR[(
|
|
737
|
+
for iR in self.R_ijatom_dict:
|
|
738
|
+
R_vec = self.short_Rlist[iR]
|
|
739
|
+
for iatom, jatom in self.R_ijatom_dict[iR]:
|
|
740
|
+
f = AijRs[(R_vec, iatom, jatom)]
|
|
741
|
+
# self.A_ijR[(R_vec, iatom, jatom)] = integrate(self.contour.path, f)
|
|
742
|
+
self.A_ijR[(R_vec, iatom, jatom)] = self.contour.integrate_values(f)
|
|
597
743
|
|
|
598
744
|
if self.orb_decomposition:
|
|
599
|
-
# self.A_ijR_orb[(
|
|
600
|
-
# self.contour.path, AijRs_orb[(
|
|
745
|
+
# self.A_ijR_orb[(R_vec, iatom, jatom)] = integrate(
|
|
746
|
+
# self.contour.path, AijRs_orb[(R_vec, iatom, jatom)]
|
|
601
747
|
# )
|
|
602
|
-
self.
|
|
748
|
+
self.A_ijR_orb[(R_vec, iatom, jatom)] = (
|
|
749
|
+
self.contour.integrate_values(AijRs_orb[(R_vec, iatom, jatom)])
|
|
750
|
+
)
|
|
603
751
|
|
|
604
752
|
def get_quantities_per_e(self, e):
|
|
605
753
|
Gk_all = self.G.get_Gk_all(e)
|
|
606
754
|
# mae = self.get_mae_kspace(Gk_all)
|
|
607
755
|
mae = None
|
|
608
756
|
# TODO: get the MAE from Gk_all
|
|
609
|
-
|
|
757
|
+
# short_Rlist now contains actual R vectors
|
|
758
|
+
GR = self.G.get_GR(self.short_Rlist, energy=e, Gk_all=Gk_all)
|
|
759
|
+
|
|
760
|
+
# Save diagonal elements of Green's function for charge and magnetic moment calculation
|
|
761
|
+
# Only if debug option is enabled
|
|
762
|
+
if self.debug_options.get("compute_charge_moments", False):
|
|
763
|
+
self.save_greens_function_diagonals(GR, e)
|
|
764
|
+
|
|
610
765
|
# TODO: define the quantities for one energy.
|
|
611
|
-
|
|
766
|
+
# Use vectorized method for better performance
|
|
767
|
+
try:
|
|
768
|
+
#
|
|
769
|
+
AijR, AijR_orb = self.get_all_A_vectorized(GR)
|
|
770
|
+
# AijR, AijR_orb = self.get_all_A(GR)
|
|
771
|
+
except Exception as e:
|
|
772
|
+
print(f"Vectorized method failed: {e}, falling back to original method")
|
|
773
|
+
AijR, AijR_orb = self.get_all_A(GR)
|
|
612
774
|
return dict(AijR=AijR, AijR_orb=AijR_orb, mae=mae)
|
|
613
775
|
|
|
776
|
+
def save_greens_function_diagonals(self, GR, energy):
|
|
777
|
+
"""
|
|
778
|
+
Save diagonal elements of Green's function for each atom.
|
|
779
|
+
These will be used to compute charge and magnetic moments.
|
|
780
|
+
|
|
781
|
+
:param GR: Green's function array of shape (nR, nbasis, nbasis)
|
|
782
|
+
:param energy: Current energy value
|
|
783
|
+
"""
|
|
784
|
+
# For proper charge and magnetic moment calculation, we need to sum over k-points
|
|
785
|
+
# with weights: Σ_k S(k)·G(k)·w(k)
|
|
786
|
+
# Since this function is called for each energy, we'll compute the k-sum here
|
|
787
|
+
|
|
788
|
+
# Initialize the k-summed SG matrix for this energy
|
|
789
|
+
nbasis = GR.shape[1]
|
|
790
|
+
SG_ksum = np.zeros((nbasis, nbasis), dtype=complex)
|
|
791
|
+
|
|
792
|
+
# Get k-points and weights from Green's function object
|
|
793
|
+
kpts = self.G.kpts
|
|
794
|
+
kweights = self.G.kweights
|
|
795
|
+
|
|
796
|
+
# Use the passed energy parameter
|
|
797
|
+
current_energy = energy
|
|
798
|
+
|
|
799
|
+
# Sum over all k-points
|
|
800
|
+
for ik, kpt in enumerate(kpts):
|
|
801
|
+
# Get G(k) for current energy
|
|
802
|
+
Gk = self.G.get_Gk(ik, energy=current_energy)
|
|
803
|
+
|
|
804
|
+
if not self.G.is_orthogonal:
|
|
805
|
+
Sk = self.G.get_Sk(ik)
|
|
806
|
+
SG_ksum += Sk @ Gk * kweights[ik]
|
|
807
|
+
else:
|
|
808
|
+
# For orthogonal case, S is identity
|
|
809
|
+
SG_ksum += Gk * kweights[ik]
|
|
810
|
+
|
|
811
|
+
# Now SG_ksum contains Σ_k S(k)·G(k)·w(k) for this energy
|
|
812
|
+
|
|
813
|
+
for iatom in self.orb_dict:
|
|
814
|
+
# Get orbital indices for this atom
|
|
815
|
+
orbi = self.iorb(iatom)
|
|
816
|
+
# Extract diagonal elements for this atom
|
|
817
|
+
G_diag = np.diag(SG_ksum[np.ix_(orbi, orbi)])
|
|
818
|
+
self.G_diagonal[iatom].append(G_diag)
|
|
819
|
+
|
|
820
|
+
def compute_charge_and_magnetic_moments(self):
|
|
821
|
+
"""
|
|
822
|
+
Compute charge and magnetic moments from stored Green's function diagonals.
|
|
823
|
+
Uses the relation:
|
|
824
|
+
- Charge: n_i = -1/π ∫ Im[Tr(S·G_ii(E))] dE
|
|
825
|
+
- Magnetic moment: m_i = -1/π ∫ Im[Tr(S·σ·G_ii(E))] dE
|
|
826
|
+
where S is the overlap matrix.
|
|
827
|
+
"""
|
|
828
|
+
# Only run if debug option is enabled
|
|
829
|
+
if not self.debug_options.get("compute_charge_moments", False):
|
|
830
|
+
# Just use density matrix method directly
|
|
831
|
+
self.get_rho_atom()
|
|
832
|
+
return
|
|
833
|
+
|
|
834
|
+
if not hasattr(self, "G_diagonal") or not self.G_diagonal:
|
|
835
|
+
print(
|
|
836
|
+
"Warning: No Green's function diagonals stored. Cannot compute charge and magnetic moments."
|
|
837
|
+
)
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
self.charges = np.zeros(len(self.atoms))
|
|
841
|
+
self.spinat = np.zeros((len(self.atoms), 3))
|
|
842
|
+
|
|
843
|
+
for iatom in range(len(self.atoms)):
|
|
844
|
+
if not self.G_diagonal[iatom]:
|
|
845
|
+
continue
|
|
846
|
+
|
|
847
|
+
# Stack all diagonal elements for this atom
|
|
848
|
+
G_diags = np.array(
|
|
849
|
+
self.G_diagonal[iatom]
|
|
850
|
+
) # shape: (n_energies, n_orbitals)
|
|
851
|
+
|
|
852
|
+
# Integrate over energy using the same contour as exchange calculation
|
|
853
|
+
# Charge: -1/π Im[∫ diag(G) dE]
|
|
854
|
+
integrated_diag = -np.imag(self.contour.integrate_values(G_diags)) / np.pi
|
|
855
|
+
|
|
856
|
+
# Sum over orbitals to get total charge
|
|
857
|
+
self.charges[iatom] = np.sum(integrated_diag)
|
|
858
|
+
|
|
859
|
+
# For non-collinear case, compute magnetic moments from Green's function
|
|
860
|
+
# Note: The stored diagonals only contain G_ii elements, not the full spin structure
|
|
861
|
+
# For proper magnetic moment calculation, we need the full Green's function matrix
|
|
862
|
+
# Here we'll compute the charge from diagonals and use density matrix for moments
|
|
863
|
+
|
|
864
|
+
# The Green's function method can only compute charge from stored diagonals
|
|
865
|
+
gf_charge = np.sum(integrated_diag)
|
|
866
|
+
|
|
867
|
+
# For magnetic moments, we would need the full G matrix with spin structure
|
|
868
|
+
# Since only diagonals are stored, we cannot compute magnetic moments from GF method
|
|
869
|
+
# gf_spinat = np.array(
|
|
870
|
+
# [np.nan, np.nan, np.nan]
|
|
871
|
+
# ) # Placeholder - cannot compute from diagonals
|
|
872
|
+
|
|
873
|
+
# Compute using density matrix method
|
|
874
|
+
self.get_rho_atom() # This computes charges and spinat using density matrix
|
|
875
|
+
dm_spinat = self.spinat[iatom].copy()
|
|
876
|
+
dm_charge = self.charges[iatom]
|
|
877
|
+
|
|
878
|
+
# Compare methods if difference is above threshold
|
|
879
|
+
charge_diff = abs(gf_charge - dm_charge)
|
|
880
|
+
threshold = self.debug_options.get("charge_moment_threshold", 1e-4)
|
|
881
|
+
|
|
882
|
+
if charge_diff > threshold:
|
|
883
|
+
print(f"Atom {iatom}:")
|
|
884
|
+
print(f" Green's function charge: {gf_charge:.6f}")
|
|
885
|
+
print(f" Density matrix charge: {dm_charge:.6f}")
|
|
886
|
+
print(f" Difference: {charge_diff:.6f} (threshold: {threshold})")
|
|
887
|
+
print(
|
|
888
|
+
f" Density matrix magnetic moment: [{dm_spinat[0]:.6f}, {dm_spinat[1]:.6f}, {dm_spinat[2]:.6f}]"
|
|
889
|
+
)
|
|
890
|
+
print(
|
|
891
|
+
" Note: Magnetic moments from GF method require full Green's function matrix, not just diagonals"
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
# By default, use density matrix output unless debug option says otherwise
|
|
895
|
+
if not self.debug_options.get("use_density_matrix_output", True):
|
|
896
|
+
# Override with Green's function charge (not recommended)
|
|
897
|
+
self.charges[iatom] = gf_charge
|
|
898
|
+
# Magnetic moments cannot be computed from diagonals in non-collinear case
|
|
899
|
+
|
|
614
900
|
def save_AijR(self, AijRs, fname):
|
|
615
901
|
result = dict(path=self.contour.path, AijRs=AijRs)
|
|
616
902
|
with open(fname, "wb") as myfile:
|
|
@@ -644,27 +930,36 @@ class ExchangeNCL(Exchange):
|
|
|
644
930
|
)
|
|
645
931
|
|
|
646
932
|
for i, result in enumerate(results):
|
|
647
|
-
for iR
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
933
|
+
for iR in self.R_ijatom_dict:
|
|
934
|
+
R_vec = self.short_Rlist[iR]
|
|
935
|
+
for iatom, jatom in self.R_ijatom_dict[iR]:
|
|
936
|
+
if (R_vec, iatom, jatom) in AijRs:
|
|
937
|
+
AijRs[(R_vec, iatom, jatom)].append(
|
|
938
|
+
result["AijR"][(R_vec, iatom, jatom)]
|
|
939
|
+
)
|
|
651
940
|
if self.orb_decomposition:
|
|
652
|
-
AijRs_orb[(
|
|
653
|
-
result["AijR_orb"][
|
|
941
|
+
AijRs_orb[(R_vec, iatom, jatom)].append(
|
|
942
|
+
result["AijR_orb"][(R_vec, iatom, jatom)]
|
|
654
943
|
)
|
|
655
944
|
|
|
656
945
|
else:
|
|
657
|
-
AijRs[(
|
|
658
|
-
AijRs[(
|
|
946
|
+
AijRs[(R_vec, iatom, jatom)] = []
|
|
947
|
+
AijRs[(R_vec, iatom, jatom)].append(
|
|
948
|
+
result["AijR"][(R_vec, iatom, jatom)]
|
|
949
|
+
)
|
|
659
950
|
if self.orb_decomposition:
|
|
660
|
-
AijRs_orb[(
|
|
661
|
-
AijRs_orb[(
|
|
662
|
-
result["AijR_orb"][
|
|
951
|
+
AijRs_orb[(R_vec, iatom, jatom)] = []
|
|
952
|
+
AijRs_orb[(R_vec, iatom, jatom)].append(
|
|
953
|
+
result["AijR_orb"][(R_vec, iatom, jatom)]
|
|
663
954
|
)
|
|
664
955
|
|
|
665
956
|
# self.save_AijRs(AijRs)
|
|
666
957
|
self.integrate(AijRs, AijRs_orb)
|
|
667
958
|
self.get_rho_atom()
|
|
959
|
+
|
|
960
|
+
# Compute charge and magnetic moments from Green's function diagonals
|
|
961
|
+
self.compute_charge_and_magnetic_moments()
|
|
962
|
+
|
|
668
963
|
self.A_to_Jtensor()
|
|
669
964
|
self.A_to_Jtensor_orb()
|
|
670
965
|
|