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.
Files changed (40) hide show
  1. TB2J/MAE.py +8 -1
  2. TB2J/MAEGreen.py +78 -61
  3. TB2J/contour.py +3 -2
  4. TB2J/exchange.py +346 -51
  5. TB2J/exchangeCL2.py +285 -47
  6. TB2J/exchange_params.py +48 -0
  7. TB2J/green.py +73 -36
  8. TB2J/interfaces/abacus/gen_exchange_abacus.py +2 -1
  9. TB2J/interfaces/wannier90_interface.py +4 -4
  10. TB2J/io_exchange/__init__.py +19 -1
  11. TB2J/io_exchange/edit.py +594 -0
  12. TB2J/io_exchange/io_espins.py +276 -0
  13. TB2J/io_exchange/io_exchange.py +248 -76
  14. TB2J/io_exchange/io_tomsasd.py +4 -3
  15. TB2J/io_exchange/io_txt.py +72 -7
  16. TB2J/io_exchange/io_vampire.py +4 -2
  17. TB2J/io_merge.py +60 -40
  18. TB2J/magnon/magnon3.py +27 -2
  19. TB2J/mathutils/rotate_spin.py +7 -7
  20. TB2J/myTB.py +11 -11
  21. TB2J/mycfr.py +11 -11
  22. TB2J/pauli.py +32 -2
  23. TB2J/plot.py +26 -0
  24. TB2J/rotate_atoms.py +9 -6
  25. TB2J/scripts/TB2J_edit.py +403 -0
  26. TB2J/scripts/TB2J_plot_exchange.py +48 -0
  27. TB2J/spinham/hamiltonian.py +156 -13
  28. TB2J/spinham/hamiltonian_terms.py +40 -1
  29. TB2J/spinham/spin_xml.py +40 -8
  30. TB2J/symmetrize_J.py +140 -9
  31. TB2J/tests/test_cli_remove_sublattice.py +33 -0
  32. TB2J/tests/test_cli_toggle_exchange.py +50 -0
  33. {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/METADATA +10 -7
  34. {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/RECORD +38 -34
  35. {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/WHEEL +1 -1
  36. {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/entry_points.txt +2 -0
  37. TB2J/interfaces/abacus/test_read_HRSR.py +0 -43
  38. TB2J/interfaces/abacus/test_read_stru.py +0 -32
  39. {tb2j-0.9.12.9.dist-info → tb2j-0.9.12.22.dist-info}/licenses/LICENSE +0 -0
  40. {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.find_energy_ingap(rbound=self.efermi - 5.0) - self.efermi
80
- emin_dn = self.Gdn.find_energy_ingap(rbound=self.efermi - 5.0) - self.efermi
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 R, ijpairs in self.R_ijatom_dict.items():
112
- if (iatom, jatom) in ijpairs and (R, iatom, jatom) not in Rij_done:
113
- Gij_up = self.GR_atom(Gup[R], iatom, jatom)
114
- Rm = tuple(-x for x in R)
115
- Gji_dn = self.GR_atom(Gdn[Rm], jatom, iatom)
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[(R, iatom, jatom)].append(t / (4.0 * np.pi))
136
- self.JJ_list[(R, iatom, jatom)].append(tmp / (4.0 * np.pi))
137
- Rij_done.add((R, iatom, jatom))
138
- if (Rm, jatom, iatom) not in Rij_done:
139
- Jorb_list[(Rm, jatom, iatom)] = t.T / (4.0 * np.pi)
140
- JJ_list[(Rm, jatom, iatom)] = tmp / (4.0 * np.pi)
141
- Rij_done.add((Rm, jatom, iatom))
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 R, ijpairs in self.R_ijatom_dict.items():
155
- for iatom, jatom in ijpairs:
156
- if (R, iatom, jatom) not in Rij_done:
157
- Rm = tuple(-x for x in R)
158
- if (Rm, jatom, iatom) in Rij_done:
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"Strange (Rm, jatom, iatom) has already been calculated! {(Rm, jatom, iatom)}"
169
+ f"Negative R vector {Rm_vec} not found in short_Rlist"
161
170
  )
162
- Gij_up = self.GR_atom(Gup[R], iatom, jatom)
163
- Gji_dn = self.GR_atom(Gdn[Rm], jatom, iatom)
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
- Jorb_list[(R, iatom, jatom)] = t / (4.0 * np.pi)
173
- JJ_list[(R, iatom, jatom)] = tmp / (4.0 * np.pi)
174
- Rij_done.add((R, iatom, jatom))
175
- if (Rm, jatom, iatom) not in Rij_done:
176
- Jorb_list[(Rm, jatom, iatom)] = t.T / (4.0 * np.pi)
177
- JJ_list[(Rm, jatom, iatom)] = tmp / (4.0 * np.pi)
178
- Rij_done.add((Rm, jatom, iatom))
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 R, ijpairs in self.R_ijatom_dict.items():
234
- for iatom, jatom in ijpairs:
235
- # self.Jorb[(R, iatom, jatom)] = integrate(
236
- # self.contour.path, self.Jorb_list[(R, iatom, jatom)]
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[(R, iatom, jatom)] = integrate(
239
- # self.contour.path, self.JJ_list[(R, iatom, jatom)]
318
+ # self.JJ[key] = integrate(
319
+ # self.contour.path, self.JJ_list[key]
240
320
  # )
241
- self.Jorb[(R, iatom, jatom)] = self.contour.integrate_values(
242
- self.Jorb_list[(R, iatom, jatom)]
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
- GR_up = self.Gup.get_GR(self.short_Rlist, energy=e, get_rho=False)
250
- GR_dn = self.Gdn.get_GR(self.short_Rlist, energy=e, get_rho=False)
251
- Jorb_list, JJ_list = self.get_all_A(GR_up, GR_dn)
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, R in enumerate(self.R_ijatom_dict):
275
- for iatom, jatom in self.R_ijatom_dict[R]:
276
- key = (R, iatom, jatom)
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,