TB2J 0.9.10.1__py3-none-any.whl → 0.9.12.17__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.

Potentially problematic release.


This version of TB2J might be problematic. Click here for more details.

Files changed (49) hide show
  1. TB2J/.gitignore +5 -0
  2. TB2J/Jdownfolder.py +43 -19
  3. TB2J/MAEGreen.py +78 -60
  4. TB2J/__init__.py +3 -1
  5. TB2J/anisotropy.py +2 -2
  6. TB2J/basis.py +0 -3
  7. TB2J/contour.py +3 -2
  8. TB2J/exchange.py +335 -48
  9. TB2J/exchangeCL2.py +289 -51
  10. TB2J/exchange_params.py +25 -1
  11. TB2J/gpaw_wrapper.py +0 -3
  12. TB2J/green.py +58 -33
  13. TB2J/interfaces/wannier90_interface.py +4 -4
  14. TB2J/io_exchange/io_espins.py +276 -0
  15. TB2J/io_exchange/io_exchange.py +53 -12
  16. TB2J/io_exchange/io_txt.py +9 -8
  17. TB2J/io_exchange/io_uppasd.py +0 -1
  18. TB2J/io_exchange/io_vampire.py +3 -1
  19. TB2J/magnon/magnon3.py +76 -28
  20. TB2J/magnon/plot_magnon_dos_cli.py +115 -3
  21. TB2J/myTB.py +11 -11
  22. TB2J/pauli.py +32 -2
  23. TB2J/plot.py +8 -7
  24. TB2J/rotate_atoms.py +10 -7
  25. TB2J/scripts/TB2J_downfold.py +97 -0
  26. TB2J/scripts/TB2J_eigen.py +49 -0
  27. TB2J/scripts/TB2J_magnon.py +117 -0
  28. TB2J/scripts/TB2J_magnon2.py +78 -0
  29. TB2J/scripts/TB2J_magnon_dos.py +5 -0
  30. TB2J/scripts/TB2J_merge.py +49 -0
  31. TB2J/scripts/TB2J_plot_magnon_bands.py +22 -0
  32. TB2J/scripts/TB2J_rotate.py +29 -0
  33. TB2J/scripts/TB2J_rotateDM.py +21 -0
  34. TB2J/scripts/__init__.py +0 -0
  35. TB2J/scripts/abacus2J.py +61 -0
  36. TB2J/scripts/siesta2J.py +78 -0
  37. TB2J/scripts/wann2J.py +101 -0
  38. TB2J/spinham/hamiltonian.py +0 -1
  39. TB2J/symmetrize_J.py +2 -2
  40. TB2J/tensor_rotate.py +1 -2
  41. TB2J/versioninfo.py +3 -0
  42. TB2J/wannier/w90_tb_parser.py +0 -2
  43. {tb2j-0.9.10.1.dist-info → tb2j-0.9.12.17.dist-info}/METADATA +7 -7
  44. {tb2j-0.9.10.1.dist-info → tb2j-0.9.12.17.dist-info}/RECORD +48 -33
  45. tb2j-0.9.12.17.dist-info/entry_points.txt +15 -0
  46. tb2j-0.9.10.1.dist-info/entry_points.txt +0 -16
  47. {tb2j-0.9.10.1.dist-info → tb2j-0.9.12.17.dist-info}/WHEEL +0 -0
  48. {tb2j-0.9.10.1.dist-info → tb2j-0.9.12.17.dist-info}/licenses/LICENSE +0 -0
  49. {tb2j-0.9.10.1.dist-info → tb2j-0.9.12.17.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
@@ -76,8 +77,8 @@ class ExchangeCL2(ExchangeCL):
76
77
  del self.Gdn.tbmodel
77
78
 
78
79
  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
80
+ emin_up = self.Gup.adjusted_emin
81
+ emin_dn = self.Gdn.adjusted_emin
81
82
  self.emin = min(emin_up, emin_dn)
82
83
  print(f"A gap is found at {self.emin}, set emin to it.")
83
84
 
@@ -108,11 +109,15 @@ class ExchangeCL2(ExchangeCL):
108
109
  Rij_done = set()
109
110
  Jorb_list = dict()
110
111
  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)
112
+ for iR, ijpairs in self.R_ijatom_dict.items():
113
+ if (iatom, jatom) in ijpairs and (iR, iatom, jatom) not in Rij_done:
114
+ Gij_up = self.GR_atom(Gup[iR], iatom, jatom)
115
+ iRm = self.R_negative_index[iR]
116
+ if iRm is None:
117
+ R_vec = self.R_vectors[iR]
118
+ Rm_vec = tuple(-x for x in R_vec)
119
+ raise KeyError(f"Negative R vector {Rm_vec} not found in R_vectors")
120
+ Gji_dn = self.GR_atom(Gdn[iRm], jatom, iatom)
116
121
  tmp = 0.0j
117
122
  Deltai = self.get_Delta(iatom)
118
123
  Deltaj = self.get_Delta(jatom)
@@ -132,13 +137,13 @@ class ExchangeCL2(ExchangeCL):
132
137
  # np.matmul(Deltaj, Gji_down),
133
138
  # )
134
139
  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))
140
+ self.Jorb_list[(iR, iatom, jatom)].append(t / (4.0 * np.pi))
141
+ self.JJ_list[(iR, iatom, jatom)].append(tmp / (4.0 * np.pi))
142
+ Rij_done.add((iR, iatom, jatom))
143
+ if (iRm, jatom, iatom) not in Rij_done:
144
+ Jorb_list[(iRm, jatom, iatom)] = t.T / (4.0 * np.pi)
145
+ JJ_list[(iRm, jatom, iatom)] = tmp / (4.0 * np.pi)
146
+ Rij_done.add((iRm, jatom, iatom))
142
147
  return Jorb_list, JJ_list
143
148
 
144
149
  def get_all_A(self, Gup, Gdn):
@@ -151,16 +156,23 @@ class ExchangeCL2(ExchangeCL):
151
156
  Rij_done = set()
152
157
  Jorb_list = dict()
153
158
  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:
159
+ for iR in self.R_ijatom_dict:
160
+ for iatom, jatom in self.R_ijatom_dict[iR]:
161
+ if (iR, iatom, jatom) not in Rij_done:
162
+ iRm = self.R_negative_index[iR]
163
+ if iRm is None:
164
+ R_vec = self.short_Rlist[iR]
165
+ Rm_vec = tuple(-x for x in R_vec)
159
166
  raise KeyError(
160
- f"Strange (Rm, jatom, iatom) has already been calculated! {(Rm, jatom, iatom)}"
167
+ f"Negative R vector {Rm_vec} not found in short_Rlist"
161
168
  )
162
- Gij_up = self.GR_atom(Gup[R], iatom, jatom)
163
- Gji_dn = self.GR_atom(Gdn[Rm], jatom, iatom)
169
+
170
+ if (iRm, jatom, iatom) in Rij_done:
171
+ raise KeyError(
172
+ f"Strange (iRm, jatom, iatom) has already been calculated! {(iRm, jatom, iatom)}"
173
+ )
174
+ Gij_up = self.GR_atom(Gup[iR], iatom, jatom)
175
+ Gji_dn = self.GR_atom(Gdn[iRm], jatom, iatom)
164
176
  tmp = 0.0j
165
177
  # t = self.get_Delta(iatom) @ Gij_up @ self.get_Delta(jatom) @ Gji_dn
166
178
  t = np.einsum(
@@ -169,15 +181,78 @@ class ExchangeCL2(ExchangeCL):
169
181
  np.matmul(self.get_Delta(jatom), Gji_dn),
170
182
  )
171
183
  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))
184
+ # Use R vectors for keys for I/O compatibility
185
+ R_vec = self.short_Rlist[iR]
186
+ Rm_vec = self.short_Rlist[iRm]
187
+ Jorb_list[(R_vec, iatom, jatom)] = t / (4.0 * np.pi)
188
+ JJ_list[(R_vec, iatom, jatom)] = tmp / (4.0 * np.pi)
189
+ Rij_done.add((iR, iatom, jatom))
190
+ if (iRm, jatom, iatom) not in Rij_done:
191
+ Jorb_list[(Rm_vec, jatom, iatom)] = t.T / (4.0 * np.pi)
192
+ JJ_list[(Rm_vec, jatom, iatom)] = tmp / (4.0 * np.pi)
193
+ Rij_done.add((iRm, jatom, iatom))
179
194
  return Jorb_list, JJ_list
180
195
 
196
+ def get_all_A_vectorized(self, Gup, Gdn):
197
+ """
198
+ Vectorized calculation of all A matrix elements for collinear exchange.
199
+ Follows the pattern from TB2J/exchange.py get_all_A_vectorized method.
200
+
201
+ :param Gup: Green's function array for spin up, shape (nR, nbasis, nbasis)
202
+ :param Gdn: Green's function array for spin down, shape (nR, nbasis, nbasis)
203
+ :returns: tuple of (Jorb_list, JJ_list) with R vector keys
204
+ """
205
+ # Get magnetic sites and their orbital indices
206
+ magnetic_sites = self.ind_mag_atoms
207
+ iorbs = [self.iorb(site) for site in magnetic_sites]
208
+
209
+ # Build the Delta matrices for all magnetic sites
210
+ Delta = [self.get_Delta(site) for site in magnetic_sites]
211
+
212
+ # Initialize results dictionaries
213
+ Jorb_list = {}
214
+ JJ_list = {}
215
+
216
+ # Batch compute all exchange tensors following the vectorized approach
217
+ for i, j in product(range(len(magnetic_sites)), repeat=2):
218
+ idx, jdx = iorbs[i], iorbs[j]
219
+
220
+ # Extract Gij and Gji for all R vectors at once
221
+ Gij = Gup[:, idx][:, :, jdx] # Shape: (nR, ni, nj)
222
+ # Since short_Rlist is properly ordered, we can flip Gji along R axis
223
+ # to get Gji(-R) for Gij(R)
224
+ Gji = np.flip(Gdn[:, jdx][:, :, idx], axis=0) # Shape: (nR, nj, ni)
225
+
226
+ # Compute exchange tensors for all R vectors at once
227
+ # Following collinear formula: Delta_i @ Gij @ Delta_j @ Gji
228
+ t_tensor = np.einsum(
229
+ "ab,rbc,cd,rda->rac", Delta[i], Gij, Delta[j], Gji, optimize="optimal"
230
+ ) / (4.0 * np.pi)
231
+ tmp_tensor = np.sum(t_tensor, axis=(1, 2)) # Shape: (nR,)
232
+
233
+ mi, mj = (magnetic_sites[i], magnetic_sites[j])
234
+
235
+ # Store results for each R vector
236
+ for iR, R_vec in enumerate(self.short_Rlist):
237
+ # Store with R vector key for compatibility
238
+ Jorb_list[(R_vec, mi, mj)] = t_tensor[iR] # Shape: (ni, nj)
239
+ JJ_list[(R_vec, mi, mj)] = tmp_tensor[iR] # Scalar
240
+
241
+ # Filter results to only include atom pairs within cutoff distance
242
+ # This matches the behavior of the original get_all_A method
243
+ filtered_Jorb_list = {}
244
+ filtered_JJ_list = {}
245
+
246
+ for iR, atom_pairs in self.R_ijatom_dict.items():
247
+ R_vec = self.short_Rlist[iR]
248
+ for iatom, jatom in atom_pairs:
249
+ key = (R_vec, iatom, jatom)
250
+ if key in Jorb_list:
251
+ filtered_Jorb_list[key] = Jorb_list[key]
252
+ filtered_JJ_list[key] = JJ_list[key]
253
+
254
+ return filtered_Jorb_list, filtered_JJ_list
255
+
181
256
  def A_to_Jtensor(self):
182
257
  for key, val in self.JJ.items():
183
258
  # key:(R, iatom, jatom)
@@ -186,15 +261,17 @@ class ExchangeCL2(ExchangeCL):
186
261
  jspin = self.ispin(jatom)
187
262
  keyspin = (R, ispin, jspin)
188
263
  is_nonself = not (R == (0, 0, 0) and iatom == jatom)
189
- Jij = np.imag(val) / np.sign(np.dot(self.spinat[iatom], self.spinat[jatom]))
190
264
  Jorbij = np.imag(self.Jorb[key]) / np.sign(
191
265
  np.dot(self.spinat[iatom], self.spinat[jatom])
192
266
  )
267
+
268
+ Jij = np.imag(val) / np.sign(np.dot(self.spinat[iatom], self.spinat[jatom]))
269
+
193
270
  if is_nonself:
194
271
  self.exchange_Jdict[keyspin] = Jij
195
- self.Jiso_orb[keyspin] = self.simplify_orbital_contributions(
196
- Jorbij, iatom, jatom
197
- )
272
+ Jsimp = self.simplify_orbital_contributions(Jorbij, iatom, jatom)
273
+ self.Jiso_orb[keyspin] = Jsimp
274
+ self.exchange_Jdict[keyspin] = np.sum(Jsimp)
198
275
 
199
276
  def get_rho_e(self, rho_up, rho_dn):
200
277
  # self.rho_up_list.append(-1.0 / np.pi * np.imag(rho_up[(0,0,0)]))
@@ -228,27 +305,182 @@ class ExchangeCL2(ExchangeCL):
228
305
  # integrate = trapezoidal_nonuniform
229
306
  # elif method == "simpson":
230
307
  # integrate = simpson_nonuniform
231
- for R, ijpairs in self.R_ijatom_dict.items():
232
- for iatom, jatom in ijpairs:
233
- # self.Jorb[(R, iatom, jatom)] = integrate(
234
- # self.contour.path, self.Jorb_list[(R, iatom, jatom)]
308
+ for iR in self.R_ijatom_dict:
309
+ for iatom, jatom in self.R_ijatom_dict[iR]:
310
+ # Use R vector key consistently
311
+ R_vec = self.short_Rlist[iR]
312
+ key = (R_vec, iatom, jatom)
313
+ # self.Jorb[key] = integrate(
314
+ # self.contour.path, self.Jorb_list[key]
235
315
  # )
236
- # self.JJ[(R, iatom, jatom)] = integrate(
237
- # self.contour.path, self.JJ_list[(R, iatom, jatom)]
316
+ # self.JJ[key] = integrate(
317
+ # self.contour.path, self.JJ_list[key]
238
318
  # )
239
- self.Jorb[(R, iatom, jatom)] = self.contour.integrate_values(
240
- self.Jorb_list[(R, iatom, jatom)]
241
- )
242
- self.JJ[(R, iatom, jatom)] = self.contour.integrate_values(
243
- self.JJ_list[(R, iatom, jatom)]
244
- )
319
+ self.Jorb[key] = self.contour.integrate_values(self.Jorb_list[key])
320
+ self.JJ[key] = self.contour.integrate_values(self.JJ_list[key])
245
321
 
246
322
  def get_quantities_per_e(self, e):
247
- GR_up = self.Gup.get_GR(self.short_Rlist, energy=e, get_rho=False)
248
- GR_dn = self.Gdn.get_GR(self.short_Rlist, energy=e, get_rho=False)
249
- Jorb_list, JJ_list = self.get_all_A(GR_up, GR_dn)
323
+ # short_Rlist now contains actual R vectors
324
+ GR_up = self.Gup.get_GR(self.short_Rlist, energy=e)
325
+ GR_dn = self.Gdn.get_GR(self.short_Rlist, energy=e)
326
+
327
+ # Save diagonal elements of Green's functions for charge and magnetic moment calculation
328
+ # Only if debug option is enabled
329
+ if self.debug_options.get("compute_charge_moments", False):
330
+ self.save_greens_function_diagonals_collinear(GR_up, GR_dn, e)
331
+
332
+ # Use vectorized method with fallback to original method
333
+ try:
334
+ Jorb_list, JJ_list = self.get_all_A_vectorized(GR_up, GR_dn)
335
+ # Jorb_list, JJ_list = self.get_all_A(GR_up, GR_dn)
336
+ except Exception as ex:
337
+ print(f"Vectorized method failed: {ex}, falling back to original method")
338
+ Jorb_list, JJ_list = self.get_all_A(GR_up, GR_dn)
250
339
  return dict(Jorb_list=Jorb_list, JJ_list=JJ_list)
251
340
 
341
+ def save_greens_function_diagonals_collinear(self, GR_up, GR_dn, energy):
342
+ """
343
+ Save diagonal elements of spin-resolved Green's functions for each atom.
344
+ These will be used to compute charge and magnetic moments in collinear case.
345
+
346
+ :param GR_up: Spin-up Green's function array of shape (nR, nbasis, nbasis)
347
+ :param GR_dn: Spin-down Green's function array of shape (nR, nbasis, nbasis)
348
+ :param energy: Current energy value
349
+ """
350
+ # For proper charge and magnetic moment calculation, we need to sum over k-points
351
+ # with weights: Σ_k S(k)·G(k)·w(k)
352
+
353
+ # Initialize the k-summed SG matrices for this energy
354
+ nbasis = GR_up.shape[1]
355
+ SG_up_ksum = np.zeros((nbasis, nbasis), dtype=complex)
356
+ SG_dn_ksum = np.zeros((nbasis, nbasis), dtype=complex)
357
+
358
+ # Get k-points and weights from Green's function object
359
+ kpts = self.Gup.kpts
360
+ kweights = self.Gup.kweights
361
+
362
+ # Use the passed energy parameter
363
+ current_energy = energy
364
+
365
+ # Sum over all k-points
366
+ for ik, kpt in enumerate(kpts):
367
+ # Get G(k) for current energy
368
+ Gk_up = self.Gup.get_Gk(ik, energy=current_energy)
369
+ Gk_dn = self.Gdn.get_Gk(ik, energy=current_energy)
370
+
371
+ if not self.Gup.is_orthogonal:
372
+ Sk = self.Gup.get_Sk(ik) # Same overlap for both spins
373
+ SG_up_ksum += Sk @ Gk_up * kweights[ik]
374
+ SG_dn_ksum += Sk @ Gk_dn * kweights[ik]
375
+ else:
376
+ # For orthogonal case, S is identity
377
+ SG_up_ksum += Gk_up * kweights[ik]
378
+ SG_dn_ksum += Gk_dn * kweights[ik]
379
+
380
+ # Store both spin channels
381
+ if not hasattr(self, "G_diagonal_up"):
382
+ self.G_diagonal_up = {iatom: [] for iatom in range(len(self.atoms))}
383
+ self.G_diagonal_dn = {iatom: [] for iatom in range(len(self.atoms))}
384
+
385
+ for iatom in self.orb_dict:
386
+ # Get orbital indices for this atom
387
+ orbi = self.iorb(iatom)
388
+ # Extract diagonal elements for this atom
389
+ G_up_diag = np.diag(SG_up_ksum[np.ix_(orbi, orbi)])
390
+ G_dn_diag = np.diag(SG_dn_ksum[np.ix_(orbi, orbi)])
391
+
392
+ self.G_diagonal_up[iatom].append(G_up_diag)
393
+ self.G_diagonal_dn[iatom].append(G_dn_diag)
394
+
395
+ def compute_charge_and_magnetic_moments(self):
396
+ """
397
+ Compute charge and magnetic moments from stored spin-resolved Green's function diagonals.
398
+ Uses the relation for collinear case:
399
+ - Charge: n_i = -1/π ∫ (Im[Tr(S·G_ii^↑(E))] + Im[Tr(S·G_ii^↓(E))]) dE
400
+ - Magnetic moment: m_i = -1/π ∫ (Im[Tr(S·G_ii^↑(E))] - Im[Tr(S·G_ii^↓(E))]) dE
401
+ where S is the overlap matrix.
402
+ """
403
+ # Only run if debug option is enabled
404
+ if not self.debug_options.get("compute_charge_moments", False):
405
+ # Just use density matrix method directly
406
+ self.get_rho_atom()
407
+ return
408
+
409
+ if not hasattr(self, "G_diagonal_up") or not self.G_diagonal_up:
410
+ print(
411
+ "Warning: No Green's function diagonals stored. Cannot compute charge and magnetic moments."
412
+ )
413
+ return
414
+
415
+ self.charges = np.zeros(len(self.atoms))
416
+ self.spinat = np.zeros((len(self.atoms), 3))
417
+
418
+ # Arrays to store Green's function results for comparison
419
+ gf_charges = np.zeros(len(self.atoms))
420
+ gf_spinat = np.zeros((len(self.atoms), 3))
421
+
422
+ for iatom in range(len(self.atoms)):
423
+ if not self.G_diagonal_up[iatom] or not self.G_diagonal_dn[iatom]:
424
+ continue
425
+
426
+ # Stack all diagonal elements for this atom
427
+ G_up_diags = np.array(
428
+ self.G_diagonal_up[iatom]
429
+ ) # shape: (n_energies, n_orbitals)
430
+ G_dn_diags = np.array(
431
+ self.G_diagonal_dn[iatom]
432
+ ) # shape: (n_energies, n_orbitals)
433
+
434
+ # Integrate over energy using the same contour as exchange calculation
435
+ # Charge: -1/π ∫ Im[G_up + G_dn] dE
436
+ integrated_up = -np.imag(self.contour.integrate_values(G_up_diags)) / np.pi
437
+ integrated_dn = -np.imag(self.contour.integrate_values(G_dn_diags)) / np.pi
438
+
439
+ # Sum over orbitals and spin channels to get total charge
440
+ gf_charge = np.sum(integrated_up) + np.sum(integrated_dn)
441
+
442
+ # Magnetic moment (assuming z-direction for collinear case)
443
+ # m_z = -1/π ∫ Im[G_up - G_dn] dE
444
+ gf_spin_z = np.sum(integrated_up) - np.sum(integrated_dn)
445
+
446
+ # Store Green's function results
447
+ gf_charges[iatom] = gf_charge
448
+ gf_spinat[iatom, 2] = gf_spin_z
449
+
450
+ # Compute using density matrix method for comparison
451
+ self.get_rho_atom() # This computes charges and spinat using density matrix
452
+ dm_charge = self.charges[iatom]
453
+ dm_spin_z = self.spinat[iatom, 2]
454
+
455
+ # Compare methods if difference is above threshold
456
+ charge_diff = abs(gf_charge - dm_charge)
457
+ spin_diff = abs(gf_spin_z - dm_spin_z)
458
+ threshold = self.debug_options.get("charge_moment_threshold", 1e-4)
459
+
460
+ if charge_diff > threshold or spin_diff > threshold:
461
+ print(f"Atom {iatom}:")
462
+ print(f" Green's function charge: {gf_charge:.6f}")
463
+ print(f" Density matrix charge: {dm_charge:.6f}")
464
+ if charge_diff > threshold:
465
+ print(
466
+ f" Charge difference: {charge_diff:.6f} (threshold: {threshold})"
467
+ )
468
+ print(f" Green's function magnetic moment (z): {gf_spin_z:.6f}")
469
+ print(f" Density matrix magnetic moment (z): {dm_spin_z:.6f}")
470
+ if spin_diff > threshold:
471
+ print(
472
+ f" Spin difference: {spin_diff:.6f} (threshold: {threshold})"
473
+ )
474
+
475
+ # By default, use density matrix output unless debug option says otherwise
476
+ if not self.debug_options.get("use_density_matrix_output", True):
477
+ # Override with Green's function values (not recommended)
478
+ self.charges[iatom] = gf_charge
479
+ self.spinat[iatom, 2] = gf_spin_z
480
+
481
+ # Restore the final values from density matrix method
482
+ self.get_rho_atom()
483
+
252
484
  def calculate_all(self):
253
485
  """
254
486
  The top level.
@@ -269,13 +501,19 @@ class ExchangeCL2(ExchangeCL):
269
501
  for i, result in enumerate(results):
270
502
  Jorb_list = result["Jorb_list"]
271
503
  JJ_list = result["JJ_list"]
272
- for iR, R in enumerate(self.R_ijatom_dict):
273
- for iatom, jatom in self.R_ijatom_dict[R]:
274
- key = (R, iatom, jatom)
504
+ for iR in self.R_ijatom_dict:
505
+ for iatom, jatom in self.R_ijatom_dict[iR]:
506
+ # Use R vector key consistently across all dictionaries
507
+ R_vec = self.short_Rlist[iR]
508
+ key = (R_vec, iatom, jatom)
275
509
  self.Jorb_list[key].append(Jorb_list[key])
276
510
  self.JJ_list[key].append(JJ_list[key])
277
511
  self.integrate()
278
512
  self.get_rho_atom()
513
+
514
+ # Compute charge and magnetic moments from Green's function diagonals
515
+ self.compute_charge_and_magnetic_moments()
516
+
279
517
  self.A_to_Jtensor()
280
518
 
281
519
  def write_output(self, path="TB2J_results"):
TB2J/exchange_params.py CHANGED
@@ -33,6 +33,12 @@ class ExchangeParams:
33
33
  mae_angles = None
34
34
  orth = False
35
35
  ibz = False
36
+ # Debug options
37
+ debug_options = {
38
+ "compute_charge_moments": False, # Whether to compute charge and magnetic moments with Green's function method
39
+ "use_density_matrix_output": True, # Whether to use density matrix results as default output
40
+ "charge_moment_threshold": 1e-4, # Threshold for printing comparison when difference > threshold
41
+ }
36
42
 
37
43
  def __init__(
38
44
  self,
@@ -57,6 +63,7 @@ class ExchangeParams:
57
63
  orth=False,
58
64
  ibz=False,
59
65
  index_magnetic_atoms=None,
66
+ debug_options=None,
60
67
  ):
61
68
  self.efermi = efermi
62
69
  self.basis = basis
@@ -83,6 +90,23 @@ class ExchangeParams:
83
90
  self.ibz = ibz
84
91
  self.index_magnetic_atoms = index_magnetic_atoms
85
92
 
93
+ # Initialize debug options
94
+ if debug_options is None:
95
+ self.debug_options = {
96
+ "compute_charge_moments": False,
97
+ "use_density_matrix_output": True,
98
+ "charge_moment_threshold": 1e-4,
99
+ }
100
+ else:
101
+ # Update default debug options with user-provided ones
102
+ default_debug = {
103
+ "compute_charge_moments": False,
104
+ "use_density_matrix_output": True,
105
+ "charge_moment_threshold": 1e-4,
106
+ }
107
+ default_debug.update(debug_options)
108
+ self.debug_options = default_debug
109
+
86
110
  def set_params(self, **kwargs):
87
111
  for key, val in kwargs.items():
88
112
  setattr(self, key, val)
@@ -177,7 +201,7 @@ def add_exchange_args_to_parser(parser: argparse.ArgumentParser):
177
201
  )
178
202
 
179
203
  parser.add_argument(
180
- "-np",
204
+ "--np",
181
205
  help="number of cpu cores to use in parallel, default: 1",
182
206
  default=1,
183
207
  type=int,
TB2J/gpaw_wrapper.py CHANGED
@@ -2,10 +2,8 @@ try:
2
2
  from gpaw.lcao.tightbinding import TightBinding
3
3
  from gpaw import restart, GPAW
4
4
  from gpaw.lcao.tools import (
5
- get_lead_lcao_hamiltonian,
6
5
  get_lcao_hamiltonian,
7
6
  get_bf_centers,
8
- get_bfi,
9
7
  )
10
8
 
11
9
  _has_gpaw = True
@@ -16,7 +14,6 @@ except:
16
14
  import numpy as np
17
15
  from scipy.linalg import eigh
18
16
  from ase.dft.kpoints import monkhorst_pack
19
- from ase.units import Ha
20
17
  import pickle
21
18
 
22
19
 
TB2J/green.py CHANGED
@@ -56,13 +56,26 @@ def find_energy_ingap(evals, rbound, gap=2.0):
56
56
  find a energy inside a gap below rbound (right bound),
57
57
  return the energy gap top - 0.5.
58
58
  """
59
+ # print("Finding energy in gap...")
60
+ # print(f"Right bound: {rbound}, min gap size: {gap}")
59
61
  m0 = np.sort(evals.flatten())
60
- m = m0[m0 < rbound]
62
+ # print(f"Min eigenvalue: {m0[0]}, Max eigenvalue: {m0[-1]}")
63
+ m = m0[m0 <= rbound]
64
+ # append the next state above rbound to m
65
+ if len(m0) > len(m):
66
+ m = np.append(m, m0[len(m)])
67
+ # print(f"Number of states below right bound: {len(m)}")
68
+ # print(f"Max eigenvalue below right bound: {m[-1]}")
61
69
  ind = np.where(np.diff(m) > gap)[0]
70
+ # print(f"Number of gaps found: {len(ind)}")
71
+ # print("ind[-1]: ", ind[-1] if len(ind) > 0 else "N/A")
72
+ emin = 0.0
62
73
  if len(ind) == 0:
63
- return m0[0] - 0.5
74
+ emin = m0[0] - 0.5
64
75
  else:
65
- return m[ind[-1] + 1] - 0.5
76
+ emin = m[ind[-1] + 1] - 0.5
77
+ # print("emin:", emin)
78
+ return emin
66
79
 
67
80
 
68
81
  class TBGreen:
@@ -79,12 +92,22 @@ class TBGreen:
79
92
  use_cache=False,
80
93
  cache_path=None,
81
94
  nproc=1,
95
+ initial_emin=-25,
82
96
  ):
83
97
  """
84
98
  :param tbmodel: A tight binding model
85
99
  :param kmesh: size of monkhorst pack. e.g [6,6,6]
86
100
  :param efermi: fermi energy.
101
+ :param gamma: whether to include gamma point in monkhorst pack
102
+ :param kpts: user defined kpoints
103
+ :param kweights: weights for user defined kpoints
104
+ :param k_sym: whether the kpoints are symmetrized
105
+ :param use_cache: whether to use cache to store wavefunctions
106
+ :param cache_path: path to store cache
107
+ :param nproc: number of processes to use
108
+ :param emin: minimum energy relative to fermi level to consider
87
109
  """
110
+ self.initial_emin = initial_emin
88
111
  self.tbmodel = tbmodel
89
112
  self.is_orthogonal = tbmodel.is_orthogonal
90
113
  self.R2kfactor = tbmodel.R2kfactor
@@ -235,9 +258,21 @@ class TBGreen:
235
258
  self.efermi = occ.efermi(copy.deepcopy(self.evals))
236
259
  print(f"Fermi energy found: {self.efermi}")
237
260
 
238
- # self.evals, self.evecs = self._reduce_eigens(
239
- # self.evals, self.evecs, emin=self.efermi - 15.0, emax=self.efermi + 10.1
240
- # )
261
+ self.adjusted_emin = (
262
+ find_energy_ingap(
263
+ self.evals, rbound=self.efermi + self.initial_emin, gap=2.0
264
+ )
265
+ - self.efermi
266
+ )
267
+ # print(f"Adjusted emin relative to Fermi level: {self.adjusted_emin}")
268
+ self.evals, self.evecs = self._reduce_eigens(
269
+ self.evals,
270
+ self.evecs,
271
+ emin=self.efermi + self.adjusted_emin,
272
+ emax=self.efermi + 5.1,
273
+ # emin=self.efermi -10,
274
+ # emax=self.efermi + 10,
275
+ )
241
276
  if self._use_cache:
242
277
  evecs = self.evecs
243
278
  self.evecs_shape = self.evecs.shape
@@ -376,38 +411,28 @@ class TBGreen:
376
411
  Gk_all[ik] = self.get_Gk(ik, energy)
377
412
  return Gk_all
378
413
 
379
- def get_GR(self, Rpts, energy, get_rho=False, Gk_all=None):
414
+ def compute_GR(self, Rpts, kpts, Gks):
415
+ Rvecs = np.array(Rpts)
416
+ phase = np.exp(self.k2Rfactor * np.einsum("ni,mi->nm", Rvecs, kpts))
417
+ phase *= self.kweights[None]
418
+ GR = np.einsum("kij,rk->rij", Gks, phase, optimize="optimal")
419
+ return GR
420
+
421
+ def get_GR(self, Rpts, energy, Gk_all=None):
380
422
  """calculate real space Green's function for one energy, all R points.
381
423
  G(R, epsilon) = G(k, epsilon) exp(-2\pi i R.dot. k)
382
424
  :param Rpts: R points
383
- :param energy:
384
- :returns: real space green's function for one energy for a list of R.
385
- :rtype: dictionary, the keys are tuple of R, values are matrices of nbasis*nbasis
425
+ :param energy: energy value
426
+ :param Gk_all: optional pre-computed Gk for all k-points
427
+ :returns: real space green's function for one energy for a list of R.
428
+ :rtype: numpy array of shape (len(Rpts), nbasis, nbasis)
386
429
  """
387
- Rpts = [tuple(R) for R in Rpts]
388
- GR = defaultdict(lambda: 0.0j)
389
- rhoR = defaultdict(lambda: 0.0j)
390
- for ik, kpt in enumerate(self.kpts):
391
- if Gk_all is None:
392
- Gk = self.get_Gk(ik, energy)
393
- else:
394
- Gk = Gk_all[ik]
395
- if get_rho:
396
- if self.is_orthogonal:
397
- rhok = Gk
398
- else:
399
- rhok = self.get_Sk(ik) @ Gk
400
- for iR, R in enumerate(Rpts):
401
- phase = np.exp(self.k2Rfactor * np.dot(R, kpt))
402
- tmp = Gk * (phase * self.kweights[ik])
403
- GR[R] += tmp
404
- # change this if need full rho
405
- if get_rho and R == (0, 0, 0):
406
- rhoR[R] += rhok * (phase * self.kweights[ik])
407
- if get_rho:
408
- return GR, rhoR
430
+ if Gk_all is None:
431
+ Gks = self.get_Gk_all(energy)
409
432
  else:
410
- return GR
433
+ Gks = Gk_all
434
+
435
+ return self.compute_GR(Rpts, self.kpts, Gks)
411
436
 
412
437
  def get_GR_and_dGRdx1(self, Rpts, energy, dHdx):
413
438
  """
@@ -1,8 +1,8 @@
1
1
  import os
2
2
 
3
3
  from ase.io import read
4
+ from HamiltonIO.wannier import WannierHam
4
5
 
5
- from TB2J.myTB import MyTB
6
6
  from TB2J.utils import auto_assign_basis_name
7
7
  from TB2J.wannier import parse_atoms
8
8
 
@@ -62,11 +62,11 @@ class WannierManager(Manager):
62
62
 
63
63
  def prepare_model_colinear(self, path, prefix_up, prefix_dn, atoms, output_path):
64
64
  print("Reading Wannier90 hamiltonian: spin up.")
65
- tbmodel_up = MyTB.read_from_wannier_dir(
65
+ tbmodel_up = WannierHam.read_from_wannier_dir(
66
66
  path=path, prefix=prefix_up, atoms=atoms, nls=False
67
67
  )
68
68
  print("Reading Wannier90 hamiltonian: spin down.")
69
- tbmodel_dn = MyTB.read_from_wannier_dir(
69
+ tbmodel_dn = WannierHam.read_from_wannier_dir(
70
70
  path=path, prefix=prefix_dn, atoms=atoms, nls=False
71
71
  )
72
72
  basis, _ = auto_assign_basis_name(
@@ -82,7 +82,7 @@ class WannierManager(Manager):
82
82
  groupby = groupby.lower().strip()
83
83
  if groupby not in ["spin", "orbital"]:
84
84
  raise ValueError("groupby can only be spin or orbital.")
85
- tbmodel = MyTB.read_from_wannier_dir(
85
+ tbmodel = WannierHam.read_from_wannier_dir(
86
86
  path=path, prefix=prefix_SOC, atoms=atoms, groupby=groupby, nls=True
87
87
  )
88
88
  basis, _ = auto_assign_basis_name(