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/exchangeCL2.py
CHANGED
|
@@ -4,6 +4,7 @@ Exchange from Green's function
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from collections import defaultdict
|
|
7
|
+
from itertools import product
|
|
7
8
|
|
|
8
9
|
import numpy as np
|
|
9
10
|
from tqdm import tqdm
|
|
@@ -28,6 +29,7 @@ class ExchangeCL2(ExchangeCL):
|
|
|
28
29
|
efermi=self.efermi,
|
|
29
30
|
use_cache=self._use_cache,
|
|
30
31
|
nproc=self.nproc,
|
|
32
|
+
smearing_width=self.smearing,
|
|
31
33
|
)
|
|
32
34
|
self.Gdn = TBGreen(
|
|
33
35
|
tbmodel=self.tbmodel_dn,
|
|
@@ -35,6 +37,7 @@ class ExchangeCL2(ExchangeCL):
|
|
|
35
37
|
efermi=self.efermi,
|
|
36
38
|
use_cache=self._use_cache,
|
|
37
39
|
nproc=self.nproc,
|
|
40
|
+
smearing_width=self.smearing,
|
|
38
41
|
)
|
|
39
42
|
if self.write_density_matrix:
|
|
40
43
|
self.Gup.write_rho_R(
|
|
@@ -76,8 +79,8 @@ class ExchangeCL2(ExchangeCL):
|
|
|
76
79
|
del self.Gdn.tbmodel
|
|
77
80
|
|
|
78
81
|
def _adjust_emin(self):
|
|
79
|
-
emin_up = self.Gup.
|
|
80
|
-
emin_dn = self.Gdn.
|
|
82
|
+
emin_up = self.Gup.adjusted_emin
|
|
83
|
+
emin_dn = self.Gdn.adjusted_emin
|
|
81
84
|
self.emin = min(emin_up, emin_dn)
|
|
82
85
|
print(f"A gap is found at {self.emin}, set emin to it.")
|
|
83
86
|
|
|
@@ -108,11 +111,15 @@ class ExchangeCL2(ExchangeCL):
|
|
|
108
111
|
Rij_done = set()
|
|
109
112
|
Jorb_list = dict()
|
|
110
113
|
JJ_list = dict()
|
|
111
|
-
for
|
|
112
|
-
if (iatom, jatom) in ijpairs and (
|
|
113
|
-
Gij_up = self.GR_atom(Gup[
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
for iR, ijpairs in self.R_ijatom_dict.items():
|
|
115
|
+
if (iatom, jatom) in ijpairs and (iR, iatom, jatom) not in Rij_done:
|
|
116
|
+
Gij_up = self.GR_atom(Gup[iR], iatom, jatom)
|
|
117
|
+
iRm = self.R_negative_index[iR]
|
|
118
|
+
if iRm is None:
|
|
119
|
+
R_vec = self.R_vectors[iR]
|
|
120
|
+
Rm_vec = tuple(-x for x in R_vec)
|
|
121
|
+
raise KeyError(f"Negative R vector {Rm_vec} not found in R_vectors")
|
|
122
|
+
Gji_dn = self.GR_atom(Gdn[iRm], jatom, iatom)
|
|
116
123
|
tmp = 0.0j
|
|
117
124
|
Deltai = self.get_Delta(iatom)
|
|
118
125
|
Deltaj = self.get_Delta(jatom)
|
|
@@ -132,13 +139,13 @@ class ExchangeCL2(ExchangeCL):
|
|
|
132
139
|
# np.matmul(Deltaj, Gji_down),
|
|
133
140
|
# )
|
|
134
141
|
tmp = np.sum(t)
|
|
135
|
-
self.Jorb_list[(
|
|
136
|
-
self.JJ_list[(
|
|
137
|
-
Rij_done.add((
|
|
138
|
-
if (
|
|
139
|
-
Jorb_list[(
|
|
140
|
-
JJ_list[(
|
|
141
|
-
Rij_done.add((
|
|
142
|
+
self.Jorb_list[(iR, iatom, jatom)].append(t / (4.0 * np.pi))
|
|
143
|
+
self.JJ_list[(iR, iatom, jatom)].append(tmp / (4.0 * np.pi))
|
|
144
|
+
Rij_done.add((iR, iatom, jatom))
|
|
145
|
+
if (iRm, jatom, iatom) not in Rij_done:
|
|
146
|
+
Jorb_list[(iRm, jatom, iatom)] = t.T / (4.0 * np.pi)
|
|
147
|
+
JJ_list[(iRm, jatom, iatom)] = tmp / (4.0 * np.pi)
|
|
148
|
+
Rij_done.add((iRm, jatom, iatom))
|
|
142
149
|
return Jorb_list, JJ_list
|
|
143
150
|
|
|
144
151
|
def get_all_A(self, Gup, Gdn):
|
|
@@ -151,16 +158,23 @@ class ExchangeCL2(ExchangeCL):
|
|
|
151
158
|
Rij_done = set()
|
|
152
159
|
Jorb_list = dict()
|
|
153
160
|
JJ_list = dict()
|
|
154
|
-
for
|
|
155
|
-
for iatom, jatom in
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
if
|
|
161
|
+
for iR in self.R_ijatom_dict:
|
|
162
|
+
for iatom, jatom in self.R_ijatom_dict[iR]:
|
|
163
|
+
if (iR, iatom, jatom) not in Rij_done:
|
|
164
|
+
iRm = self.R_negative_index[iR]
|
|
165
|
+
if iRm is None:
|
|
166
|
+
R_vec = self.short_Rlist[iR]
|
|
167
|
+
Rm_vec = tuple(-x for x in R_vec)
|
|
159
168
|
raise KeyError(
|
|
160
|
-
f"
|
|
169
|
+
f"Negative R vector {Rm_vec} not found in short_Rlist"
|
|
161
170
|
)
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
|
|
172
|
+
if (iRm, jatom, iatom) in Rij_done:
|
|
173
|
+
raise KeyError(
|
|
174
|
+
f"Strange (iRm, jatom, iatom) has already been calculated! {(iRm, jatom, iatom)}"
|
|
175
|
+
)
|
|
176
|
+
Gij_up = self.GR_atom(Gup[iR], iatom, jatom)
|
|
177
|
+
Gji_dn = self.GR_atom(Gdn[iRm], jatom, iatom)
|
|
164
178
|
tmp = 0.0j
|
|
165
179
|
# t = self.get_Delta(iatom) @ Gij_up @ self.get_Delta(jatom) @ Gji_dn
|
|
166
180
|
t = np.einsum(
|
|
@@ -169,15 +183,78 @@ class ExchangeCL2(ExchangeCL):
|
|
|
169
183
|
np.matmul(self.get_Delta(jatom), Gji_dn),
|
|
170
184
|
)
|
|
171
185
|
tmp = np.sum(t)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
# Use R vectors for keys for I/O compatibility
|
|
187
|
+
R_vec = self.short_Rlist[iR]
|
|
188
|
+
Rm_vec = self.short_Rlist[iRm]
|
|
189
|
+
Jorb_list[(R_vec, iatom, jatom)] = t / (4.0 * np.pi)
|
|
190
|
+
JJ_list[(R_vec, iatom, jatom)] = tmp / (4.0 * np.pi)
|
|
191
|
+
Rij_done.add((iR, iatom, jatom))
|
|
192
|
+
if (iRm, jatom, iatom) not in Rij_done:
|
|
193
|
+
Jorb_list[(Rm_vec, jatom, iatom)] = t.T / (4.0 * np.pi)
|
|
194
|
+
JJ_list[(Rm_vec, jatom, iatom)] = tmp / (4.0 * np.pi)
|
|
195
|
+
Rij_done.add((iRm, jatom, iatom))
|
|
179
196
|
return Jorb_list, JJ_list
|
|
180
197
|
|
|
198
|
+
def get_all_A_vectorized(self, Gup, Gdn):
|
|
199
|
+
"""
|
|
200
|
+
Vectorized calculation of all A matrix elements for collinear exchange.
|
|
201
|
+
Follows the pattern from TB2J/exchange.py get_all_A_vectorized method.
|
|
202
|
+
|
|
203
|
+
:param Gup: Green's function array for spin up, shape (nR, nbasis, nbasis)
|
|
204
|
+
:param Gdn: Green's function array for spin down, shape (nR, nbasis, nbasis)
|
|
205
|
+
:returns: tuple of (Jorb_list, JJ_list) with R vector keys
|
|
206
|
+
"""
|
|
207
|
+
# Get magnetic sites and their orbital indices
|
|
208
|
+
magnetic_sites = self.ind_mag_atoms
|
|
209
|
+
iorbs = [self.iorb(site) for site in magnetic_sites]
|
|
210
|
+
|
|
211
|
+
# Build the Delta matrices for all magnetic sites
|
|
212
|
+
Delta = [self.get_Delta(site) for site in magnetic_sites]
|
|
213
|
+
|
|
214
|
+
# Initialize results dictionaries
|
|
215
|
+
Jorb_list = {}
|
|
216
|
+
JJ_list = {}
|
|
217
|
+
|
|
218
|
+
# Batch compute all exchange tensors following the vectorized approach
|
|
219
|
+
for i, j in product(range(len(magnetic_sites)), repeat=2):
|
|
220
|
+
idx, jdx = iorbs[i], iorbs[j]
|
|
221
|
+
|
|
222
|
+
# Extract Gij and Gji for all R vectors at once
|
|
223
|
+
Gij = Gup[:, idx][:, :, jdx] # Shape: (nR, ni, nj)
|
|
224
|
+
# Since short_Rlist is properly ordered, we can flip Gji along R axis
|
|
225
|
+
# to get Gji(-R) for Gij(R)
|
|
226
|
+
Gji = np.flip(Gdn[:, jdx][:, :, idx], axis=0) # Shape: (nR, nj, ni)
|
|
227
|
+
|
|
228
|
+
# Compute exchange tensors for all R vectors at once
|
|
229
|
+
# Following collinear formula: Delta_i @ Gij @ Delta_j @ Gji
|
|
230
|
+
t_tensor = np.einsum(
|
|
231
|
+
"ab,rbc,cd,rda->rac", Delta[i], Gij, Delta[j], Gji, optimize="optimal"
|
|
232
|
+
) / (4.0 * np.pi)
|
|
233
|
+
tmp_tensor = np.sum(t_tensor, axis=(1, 2)) # Shape: (nR,)
|
|
234
|
+
|
|
235
|
+
mi, mj = (magnetic_sites[i], magnetic_sites[j])
|
|
236
|
+
|
|
237
|
+
# Store results for each R vector
|
|
238
|
+
for iR, R_vec in enumerate(self.short_Rlist):
|
|
239
|
+
# Store with R vector key for compatibility
|
|
240
|
+
Jorb_list[(R_vec, mi, mj)] = t_tensor[iR] # Shape: (ni, nj)
|
|
241
|
+
JJ_list[(R_vec, mi, mj)] = tmp_tensor[iR] # Scalar
|
|
242
|
+
|
|
243
|
+
# Filter results to only include atom pairs within cutoff distance
|
|
244
|
+
# This matches the behavior of the original get_all_A method
|
|
245
|
+
filtered_Jorb_list = {}
|
|
246
|
+
filtered_JJ_list = {}
|
|
247
|
+
|
|
248
|
+
for iR, atom_pairs in self.R_ijatom_dict.items():
|
|
249
|
+
R_vec = self.short_Rlist[iR]
|
|
250
|
+
for iatom, jatom in atom_pairs:
|
|
251
|
+
key = (R_vec, iatom, jatom)
|
|
252
|
+
if key in Jorb_list:
|
|
253
|
+
filtered_Jorb_list[key] = Jorb_list[key]
|
|
254
|
+
filtered_JJ_list[key] = JJ_list[key]
|
|
255
|
+
|
|
256
|
+
return filtered_Jorb_list, filtered_JJ_list
|
|
257
|
+
|
|
181
258
|
def A_to_Jtensor(self):
|
|
182
259
|
for key, val in self.JJ.items():
|
|
183
260
|
# key:(R, iatom, jatom)
|
|
@@ -230,27 +307,182 @@ class ExchangeCL2(ExchangeCL):
|
|
|
230
307
|
# integrate = trapezoidal_nonuniform
|
|
231
308
|
# elif method == "simpson":
|
|
232
309
|
# integrate = simpson_nonuniform
|
|
233
|
-
for
|
|
234
|
-
for iatom, jatom in
|
|
235
|
-
#
|
|
236
|
-
|
|
310
|
+
for iR in self.R_ijatom_dict:
|
|
311
|
+
for iatom, jatom in self.R_ijatom_dict[iR]:
|
|
312
|
+
# Use R vector key consistently
|
|
313
|
+
R_vec = self.short_Rlist[iR]
|
|
314
|
+
key = (R_vec, iatom, jatom)
|
|
315
|
+
# self.Jorb[key] = integrate(
|
|
316
|
+
# self.contour.path, self.Jorb_list[key]
|
|
237
317
|
# )
|
|
238
|
-
# self.JJ[
|
|
239
|
-
# self.contour.path, self.JJ_list[
|
|
318
|
+
# self.JJ[key] = integrate(
|
|
319
|
+
# self.contour.path, self.JJ_list[key]
|
|
240
320
|
# )
|
|
241
|
-
self.Jorb[
|
|
242
|
-
|
|
243
|
-
)
|
|
244
|
-
self.JJ[(R, iatom, jatom)] = self.contour.integrate_values(
|
|
245
|
-
self.JJ_list[(R, iatom, jatom)]
|
|
246
|
-
)
|
|
321
|
+
self.Jorb[key] = self.contour.integrate_values(self.Jorb_list[key])
|
|
322
|
+
self.JJ[key] = self.contour.integrate_values(self.JJ_list[key])
|
|
247
323
|
|
|
248
324
|
def get_quantities_per_e(self, e):
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
325
|
+
# short_Rlist now contains actual R vectors
|
|
326
|
+
GR_up = self.Gup.get_GR(self.short_Rlist, energy=e)
|
|
327
|
+
GR_dn = self.Gdn.get_GR(self.short_Rlist, energy=e)
|
|
328
|
+
|
|
329
|
+
# Save diagonal elements of Green's functions for charge and magnetic moment calculation
|
|
330
|
+
# Only if debug option is enabled
|
|
331
|
+
if self.debug_options.get("compute_charge_moments", False):
|
|
332
|
+
self.save_greens_function_diagonals_collinear(GR_up, GR_dn, e)
|
|
333
|
+
|
|
334
|
+
# Use vectorized method with fallback to original method
|
|
335
|
+
try:
|
|
336
|
+
Jorb_list, JJ_list = self.get_all_A_vectorized(GR_up, GR_dn)
|
|
337
|
+
# Jorb_list, JJ_list = self.get_all_A(GR_up, GR_dn)
|
|
338
|
+
except Exception as ex:
|
|
339
|
+
print(f"Vectorized method failed: {ex}, falling back to original method")
|
|
340
|
+
Jorb_list, JJ_list = self.get_all_A(GR_up, GR_dn)
|
|
252
341
|
return dict(Jorb_list=Jorb_list, JJ_list=JJ_list)
|
|
253
342
|
|
|
343
|
+
def save_greens_function_diagonals_collinear(self, GR_up, GR_dn, energy):
|
|
344
|
+
"""
|
|
345
|
+
Save diagonal elements of spin-resolved Green's functions for each atom.
|
|
346
|
+
These will be used to compute charge and magnetic moments in collinear case.
|
|
347
|
+
|
|
348
|
+
:param GR_up: Spin-up Green's function array of shape (nR, nbasis, nbasis)
|
|
349
|
+
:param GR_dn: Spin-down Green's function array of shape (nR, nbasis, nbasis)
|
|
350
|
+
:param energy: Current energy value
|
|
351
|
+
"""
|
|
352
|
+
# For proper charge and magnetic moment calculation, we need to sum over k-points
|
|
353
|
+
# with weights: Σ_k S(k)·G(k)·w(k)
|
|
354
|
+
|
|
355
|
+
# Initialize the k-summed SG matrices for this energy
|
|
356
|
+
nbasis = GR_up.shape[1]
|
|
357
|
+
SG_up_ksum = np.zeros((nbasis, nbasis), dtype=complex)
|
|
358
|
+
SG_dn_ksum = np.zeros((nbasis, nbasis), dtype=complex)
|
|
359
|
+
|
|
360
|
+
# Get k-points and weights from Green's function object
|
|
361
|
+
kpts = self.Gup.kpts
|
|
362
|
+
kweights = self.Gup.kweights
|
|
363
|
+
|
|
364
|
+
# Use the passed energy parameter
|
|
365
|
+
current_energy = energy
|
|
366
|
+
|
|
367
|
+
# Sum over all k-points
|
|
368
|
+
for ik, kpt in enumerate(kpts):
|
|
369
|
+
# Get G(k) for current energy
|
|
370
|
+
Gk_up = self.Gup.get_Gk(ik, energy=current_energy)
|
|
371
|
+
Gk_dn = self.Gdn.get_Gk(ik, energy=current_energy)
|
|
372
|
+
|
|
373
|
+
if not self.Gup.is_orthogonal:
|
|
374
|
+
Sk = self.Gup.get_Sk(ik) # Same overlap for both spins
|
|
375
|
+
SG_up_ksum += Sk @ Gk_up * kweights[ik]
|
|
376
|
+
SG_dn_ksum += Sk @ Gk_dn * kweights[ik]
|
|
377
|
+
else:
|
|
378
|
+
# For orthogonal case, S is identity
|
|
379
|
+
SG_up_ksum += Gk_up * kweights[ik]
|
|
380
|
+
SG_dn_ksum += Gk_dn * kweights[ik]
|
|
381
|
+
|
|
382
|
+
# Store both spin channels
|
|
383
|
+
if not hasattr(self, "G_diagonal_up"):
|
|
384
|
+
self.G_diagonal_up = {iatom: [] for iatom in range(len(self.atoms))}
|
|
385
|
+
self.G_diagonal_dn = {iatom: [] for iatom in range(len(self.atoms))}
|
|
386
|
+
|
|
387
|
+
for iatom in self.orb_dict:
|
|
388
|
+
# Get orbital indices for this atom
|
|
389
|
+
orbi = self.iorb(iatom)
|
|
390
|
+
# Extract diagonal elements for this atom
|
|
391
|
+
G_up_diag = np.diag(SG_up_ksum[np.ix_(orbi, orbi)])
|
|
392
|
+
G_dn_diag = np.diag(SG_dn_ksum[np.ix_(orbi, orbi)])
|
|
393
|
+
|
|
394
|
+
self.G_diagonal_up[iatom].append(G_up_diag)
|
|
395
|
+
self.G_diagonal_dn[iatom].append(G_dn_diag)
|
|
396
|
+
|
|
397
|
+
def compute_charge_and_magnetic_moments(self):
|
|
398
|
+
"""
|
|
399
|
+
Compute charge and magnetic moments from stored spin-resolved Green's function diagonals.
|
|
400
|
+
Uses the relation for collinear case:
|
|
401
|
+
- Charge: n_i = -1/π ∫ (Im[Tr(S·G_ii^↑(E))] + Im[Tr(S·G_ii^↓(E))]) dE
|
|
402
|
+
- Magnetic moment: m_i = -1/π ∫ (Im[Tr(S·G_ii^↑(E))] - Im[Tr(S·G_ii^↓(E))]) dE
|
|
403
|
+
where S is the overlap matrix.
|
|
404
|
+
"""
|
|
405
|
+
# Only run if debug option is enabled
|
|
406
|
+
if not self.debug_options.get("compute_charge_moments", False):
|
|
407
|
+
# Just use density matrix method directly
|
|
408
|
+
self.get_rho_atom()
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
if not hasattr(self, "G_diagonal_up") or not self.G_diagonal_up:
|
|
412
|
+
print(
|
|
413
|
+
"Warning: No Green's function diagonals stored. Cannot compute charge and magnetic moments."
|
|
414
|
+
)
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
self.charges = np.zeros(len(self.atoms))
|
|
418
|
+
self.spinat = np.zeros((len(self.atoms), 3))
|
|
419
|
+
|
|
420
|
+
# Arrays to store Green's function results for comparison
|
|
421
|
+
gf_charges = np.zeros(len(self.atoms))
|
|
422
|
+
gf_spinat = np.zeros((len(self.atoms), 3))
|
|
423
|
+
|
|
424
|
+
for iatom in range(len(self.atoms)):
|
|
425
|
+
if not self.G_diagonal_up[iatom] or not self.G_diagonal_dn[iatom]:
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
# Stack all diagonal elements for this atom
|
|
429
|
+
G_up_diags = np.array(
|
|
430
|
+
self.G_diagonal_up[iatom]
|
|
431
|
+
) # shape: (n_energies, n_orbitals)
|
|
432
|
+
G_dn_diags = np.array(
|
|
433
|
+
self.G_diagonal_dn[iatom]
|
|
434
|
+
) # shape: (n_energies, n_orbitals)
|
|
435
|
+
|
|
436
|
+
# Integrate over energy using the same contour as exchange calculation
|
|
437
|
+
# Charge: -1/π ∫ Im[G_up + G_dn] dE
|
|
438
|
+
integrated_up = -np.imag(self.contour.integrate_values(G_up_diags)) / np.pi
|
|
439
|
+
integrated_dn = -np.imag(self.contour.integrate_values(G_dn_diags)) / np.pi
|
|
440
|
+
|
|
441
|
+
# Sum over orbitals and spin channels to get total charge
|
|
442
|
+
gf_charge = np.sum(integrated_up) + np.sum(integrated_dn)
|
|
443
|
+
|
|
444
|
+
# Magnetic moment (assuming z-direction for collinear case)
|
|
445
|
+
# m_z = -1/π ∫ Im[G_up - G_dn] dE
|
|
446
|
+
gf_spin_z = np.sum(integrated_up) - np.sum(integrated_dn)
|
|
447
|
+
|
|
448
|
+
# Store Green's function results
|
|
449
|
+
gf_charges[iatom] = gf_charge
|
|
450
|
+
gf_spinat[iatom, 2] = gf_spin_z
|
|
451
|
+
|
|
452
|
+
# Compute using density matrix method for comparison
|
|
453
|
+
self.get_rho_atom() # This computes charges and spinat using density matrix
|
|
454
|
+
dm_charge = self.charges[iatom]
|
|
455
|
+
dm_spin_z = self.spinat[iatom, 2]
|
|
456
|
+
|
|
457
|
+
# Compare methods if difference is above threshold
|
|
458
|
+
charge_diff = abs(gf_charge - dm_charge)
|
|
459
|
+
spin_diff = abs(gf_spin_z - dm_spin_z)
|
|
460
|
+
threshold = self.debug_options.get("charge_moment_threshold", 1e-4)
|
|
461
|
+
|
|
462
|
+
if charge_diff > threshold or spin_diff > threshold:
|
|
463
|
+
print(f"Atom {iatom}:")
|
|
464
|
+
print(f" Green's function charge: {gf_charge:.6f}")
|
|
465
|
+
print(f" Density matrix charge: {dm_charge:.6f}")
|
|
466
|
+
if charge_diff > threshold:
|
|
467
|
+
print(
|
|
468
|
+
f" Charge difference: {charge_diff:.6f} (threshold: {threshold})"
|
|
469
|
+
)
|
|
470
|
+
print(f" Green's function magnetic moment (z): {gf_spin_z:.6f}")
|
|
471
|
+
print(f" Density matrix magnetic moment (z): {dm_spin_z:.6f}")
|
|
472
|
+
if spin_diff > threshold:
|
|
473
|
+
print(
|
|
474
|
+
f" Spin difference: {spin_diff:.6f} (threshold: {threshold})"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# By default, use density matrix output unless debug option says otherwise
|
|
478
|
+
if not self.debug_options.get("use_density_matrix_output", True):
|
|
479
|
+
# Override with Green's function values (not recommended)
|
|
480
|
+
self.charges[iatom] = gf_charge
|
|
481
|
+
self.spinat[iatom, 2] = gf_spin_z
|
|
482
|
+
|
|
483
|
+
# Restore the final values from density matrix method
|
|
484
|
+
self.get_rho_atom()
|
|
485
|
+
|
|
254
486
|
def calculate_all(self):
|
|
255
487
|
"""
|
|
256
488
|
The top level.
|
|
@@ -271,13 +503,19 @@ class ExchangeCL2(ExchangeCL):
|
|
|
271
503
|
for i, result in enumerate(results):
|
|
272
504
|
Jorb_list = result["Jorb_list"]
|
|
273
505
|
JJ_list = result["JJ_list"]
|
|
274
|
-
for iR
|
|
275
|
-
for iatom, jatom in self.R_ijatom_dict[
|
|
276
|
-
|
|
506
|
+
for iR in self.R_ijatom_dict:
|
|
507
|
+
for iatom, jatom in self.R_ijatom_dict[iR]:
|
|
508
|
+
# Use R vector key consistently across all dictionaries
|
|
509
|
+
R_vec = self.short_Rlist[iR]
|
|
510
|
+
key = (R_vec, iatom, jatom)
|
|
277
511
|
self.Jorb_list[key].append(Jorb_list[key])
|
|
278
512
|
self.JJ_list[key].append(JJ_list[key])
|
|
279
513
|
self.integrate()
|
|
280
514
|
self.get_rho_atom()
|
|
515
|
+
|
|
516
|
+
# Compute charge and magnetic moments from Green's function diagonals
|
|
517
|
+
self.compute_charge_and_magnetic_moments()
|
|
518
|
+
|
|
281
519
|
self.A_to_Jtensor()
|
|
282
520
|
|
|
283
521
|
def write_output(self, path="TB2J_results"):
|
TB2J/exchange_params.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
|
|
4
|
+
import ase.units
|
|
4
5
|
import yaml
|
|
5
6
|
|
|
6
7
|
__all__ = ["ExchangeParams", "add_exchange_args_to_parser", "parser_argument_to_dict"]
|
|
@@ -13,6 +14,7 @@ class ExchangeParams:
|
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
efermi: float
|
|
17
|
+
smearing: float = 600.0 * ase.units.kB
|
|
16
18
|
basis = []
|
|
17
19
|
magnetic_elements = []
|
|
18
20
|
index_magnetic_atoms = None
|
|
@@ -33,10 +35,17 @@ class ExchangeParams:
|
|
|
33
35
|
mae_angles = None
|
|
34
36
|
orth = False
|
|
35
37
|
ibz = False
|
|
38
|
+
# Debug options
|
|
39
|
+
debug_options = {
|
|
40
|
+
"compute_charge_moments": False, # Whether to compute charge and magnetic moments with Green's function method
|
|
41
|
+
"use_density_matrix_output": True, # Whether to use density matrix results as default output
|
|
42
|
+
"charge_moment_threshold": 1e-4, # Threshold for printing comparison when difference > threshold
|
|
43
|
+
}
|
|
36
44
|
|
|
37
45
|
def __init__(
|
|
38
46
|
self,
|
|
39
47
|
efermi=-10.0,
|
|
48
|
+
smearing=600.0 * ase.units.kB,
|
|
40
49
|
basis=None,
|
|
41
50
|
magnetic_elements=None,
|
|
42
51
|
include_orbs=None,
|
|
@@ -57,8 +66,10 @@ class ExchangeParams:
|
|
|
57
66
|
orth=False,
|
|
58
67
|
ibz=False,
|
|
59
68
|
index_magnetic_atoms=None,
|
|
69
|
+
debug_options=None,
|
|
60
70
|
):
|
|
61
71
|
self.efermi = efermi
|
|
72
|
+
self.smearing = smearing
|
|
62
73
|
self.basis = basis
|
|
63
74
|
# self.magnetic_elements = magnetic_elements
|
|
64
75
|
# self.include_orbs = include_orbs
|
|
@@ -83,6 +94,23 @@ class ExchangeParams:
|
|
|
83
94
|
self.ibz = ibz
|
|
84
95
|
self.index_magnetic_atoms = index_magnetic_atoms
|
|
85
96
|
|
|
97
|
+
# Initialize debug options
|
|
98
|
+
if debug_options is None:
|
|
99
|
+
self.debug_options = {
|
|
100
|
+
"compute_charge_moments": False,
|
|
101
|
+
"use_density_matrix_output": True,
|
|
102
|
+
"charge_moment_threshold": 1e-4,
|
|
103
|
+
}
|
|
104
|
+
else:
|
|
105
|
+
# Update default debug options with user-provided ones
|
|
106
|
+
default_debug = {
|
|
107
|
+
"compute_charge_moments": False,
|
|
108
|
+
"use_density_matrix_output": True,
|
|
109
|
+
"charge_moment_threshold": 1e-4,
|
|
110
|
+
}
|
|
111
|
+
default_debug.update(debug_options)
|
|
112
|
+
self.debug_options = default_debug
|
|
113
|
+
|
|
86
114
|
def set_params(self, **kwargs):
|
|
87
115
|
for key, val in kwargs.items():
|
|
88
116
|
setattr(self, key, val)
|
|
@@ -133,6 +161,12 @@ def add_exchange_args_to_parser(parser: argparse.ArgumentParser):
|
|
|
133
161
|
type=float,
|
|
134
162
|
)
|
|
135
163
|
parser.add_argument("--efermi", help="Fermi energy in eV", default=None, type=float)
|
|
164
|
+
parser.add_argument(
|
|
165
|
+
"--smearing",
|
|
166
|
+
help="Fermi smearing width for density computation and CFR. Units: K (default) or eV. Example: '600K', '0.2eV'. Default: 600K.",
|
|
167
|
+
default="600K",
|
|
168
|
+
type=str,
|
|
169
|
+
)
|
|
136
170
|
parser.add_argument(
|
|
137
171
|
"--ne",
|
|
138
172
|
help="number of electrons in the unit cell. If not given, TB2J will use the fermi energy to compute it.",
|
|
@@ -255,8 +289,22 @@ def parser_argument_to_dict(args) -> dict:
|
|
|
255
289
|
ind_mag_atoms = [int(i) - 1 for i in ind_mag_atoms]
|
|
256
290
|
else:
|
|
257
291
|
ind_mag_atoms = None
|
|
292
|
+
|
|
293
|
+
smearing_str = args.smearing
|
|
294
|
+
if smearing_str is None:
|
|
295
|
+
smearing = 600.0 * ase.units.kB
|
|
296
|
+
else:
|
|
297
|
+
s = smearing_str.strip().lower()
|
|
298
|
+
if s.endswith("ev"):
|
|
299
|
+
smearing = float(s[:-2])
|
|
300
|
+
elif s.endswith("k"):
|
|
301
|
+
smearing = float(s[:-1]) * ase.units.kB
|
|
302
|
+
else:
|
|
303
|
+
smearing = float(s) * ase.units.kB
|
|
304
|
+
|
|
258
305
|
return {
|
|
259
306
|
"efermi": args.efermi,
|
|
307
|
+
"smearing": smearing,
|
|
260
308
|
"magnetic_elements": args.elements,
|
|
261
309
|
"kmesh": args.kmesh,
|
|
262
310
|
"emin": args.emin,
|