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