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