TB2J 0.9.0.1__py3-none-any.whl → 0.9.2rc0__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 (38) hide show
  1. TB2J/Jdownfolder.py +110 -24
  2. TB2J/Jtensor.py +1 -1
  3. TB2J/MAE.py +185 -0
  4. TB2J/abacus/MAE.py +320 -0
  5. TB2J/abacus/abacus_wrapper.py +103 -4
  6. TB2J/abacus/occupations.py +278 -0
  7. TB2J/abacus/test_density_matrix.py +38 -0
  8. TB2J/cut_cell.py +82 -0
  9. TB2J/exchange.py +99 -73
  10. TB2J/exchangeCL2.py +10 -5
  11. TB2J/green.py +14 -15
  12. TB2J/io_exchange/io_pickle.py +0 -0
  13. TB2J/manager.py +10 -4
  14. TB2J/mathutils/__init__.py +1 -0
  15. TB2J/mathutils/fermi.py +22 -0
  16. TB2J/mathutils/kR_convert.py +90 -0
  17. TB2J/mathutils/lowdin.py +12 -0
  18. TB2J/mathutils/rotate_spin.py +56 -0
  19. TB2J/patch.py +50 -0
  20. TB2J/pauli.py +17 -0
  21. TB2J/spinham/h_matrix.py +68 -0
  22. TB2J/spinham/obtain_J.py +79 -0
  23. TB2J/supercell.py +532 -0
  24. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/TB2J_downfold.py +8 -0
  25. {TB2J-0.9.0.1.dist-info → TB2J-0.9.2rc0.dist-info}/METADATA +1 -1
  26. {TB2J-0.9.0.1.dist-info → TB2J-0.9.2rc0.dist-info}/RECORD +38 -23
  27. {TB2J-0.9.0.1.dist-info → TB2J-0.9.2rc0.dist-info}/WHEEL +1 -1
  28. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/TB2J_eigen.py +0 -0
  29. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/TB2J_magnon.py +0 -0
  30. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/TB2J_magnon_dos.py +0 -0
  31. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/TB2J_merge.py +0 -0
  32. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/TB2J_rotate.py +0 -0
  33. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/TB2J_rotateDM.py +0 -0
  34. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/abacus2J.py +0 -0
  35. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/siesta2J.py +0 -0
  36. {TB2J-0.9.0.1.data → TB2J-0.9.2rc0.data}/scripts/wann2J.py +0 -0
  37. {TB2J-0.9.0.1.dist-info → TB2J-0.9.2rc0.dist-info}/LICENSE +0 -0
  38. {TB2J-0.9.0.1.dist-info → TB2J-0.9.2rc0.dist-info}/top_level.txt +0 -0
TB2J/exchangeCL2.py CHANGED
@@ -234,11 +234,11 @@ class ExchangeCL2(ExchangeCL):
234
234
  self.contour.path, self.JJ_list[(R, iatom, jatom)]
235
235
  )
236
236
 
237
- def get_AijR(self, e):
237
+ def get_quantities_per_e(self, e):
238
238
  GR_up = self.Gup.get_GR(self.short_Rlist, energy=e, get_rho=False)
239
239
  GR_dn = self.Gdn.get_GR(self.short_Rlist, energy=e, get_rho=False)
240
240
  Jorb_list, JJ_list = self.get_all_A(GR_up, GR_dn)
241
- return Jorb_list, JJ_list
241
+ return dict(Jorb_list=Jorb_list, JJ_list=JJ_list)
242
242
 
243
243
  def calculate_all(self):
244
244
  """
@@ -248,13 +248,18 @@ class ExchangeCL2(ExchangeCL):
248
248
 
249
249
  npole = len(self.contour.path)
250
250
  if self.np == 1:
251
- results = map(self.get_AijR, tqdm(self.contour.path, total=npole))
251
+ results = map(
252
+ self.get_quantities_per_e, tqdm(self.contour.path, total=npole)
253
+ )
252
254
  else:
253
255
  # pool = ProcessPool(nodes=self.np)
254
256
  # results = pool.map(self.get_AijR_rhoR ,self.contour.path)
255
- results = p_map(self.get_AijR, self.contour.path, num_cpus=self.np)
257
+ results = p_map(
258
+ self.get_quantities_per_e, self.contour.path, num_cpus=self.np
259
+ )
256
260
  for i, result in enumerate(results):
257
- Jorb_list, JJ_list = result
261
+ Jorb_list = result["Jorb_list"]
262
+ JJ_list = result["JJ_list"]
258
263
  for iR, R in enumerate(self.R_ijatom_dict):
259
264
  for iatom, jatom in self.R_ijatom_dict[R]:
260
265
  key = (R, iatom, jatom)
TB2J/green.py CHANGED
@@ -7,6 +7,8 @@ import tempfile
7
7
  from pathos.multiprocessing import ProcessPool
8
8
  import sys
9
9
  import pickle
10
+ import warnings
11
+ from TB2J.mathutils.fermi import fermi
10
12
 
11
13
  MAX_EXP_ARGUMENT = np.log(sys.float_info.max)
12
14
 
@@ -26,19 +28,6 @@ def eigen_to_G(evals, evecs, efermi, energy):
26
28
  )
27
29
 
28
30
 
29
- def fermi(e, mu, width=0.01):
30
- """
31
- the fermi function.
32
- .. math::
33
- f=\\frac{1}{\exp((e-\mu)/width)+1}
34
-
35
- :param e,mu,width: e,\mu,width
36
- """
37
-
38
- x = (e - mu) / width
39
- return np.where(x < MAX_EXP_ARGUMENT, 1 / (1.0 + np.exp(x)), 0.0)
40
-
41
-
42
31
  def find_energy_ingap(evals, rbound, gap=4.0):
43
32
  """
44
33
  find a energy inside a gap below rbound (right bound),
@@ -281,7 +270,14 @@ class TBGreen:
281
270
  # Gk = np.linalg.inv((energy+self.efermi)*self.S[ik,:,:] - self.H[ik,:,:])
282
271
  return Gk
283
272
 
284
- def get_GR(self, Rpts, energy, get_rho=False):
273
+ def get_Gk_all(self, energy):
274
+ """Green's function G(k) for one energy for all kpoints"""
275
+ Gk_all = np.zeros((self.nkpts, self.nbasis, self.nbasis), dtype=complex)
276
+ for ik, _ in enumerate(self.kpts):
277
+ Gk_all[ik] = self.get_Gk(ik, energy)
278
+ return Gk_all
279
+
280
+ def get_GR(self, Rpts, energy, get_rho=False, Gk_all=None):
285
281
  """calculate real space Green's function for one energy, all R points.
286
282
  G(R, epsilon) = G(k, epsilon) exp(-2\pi i R.dot. k)
287
283
  :param Rpts: R points
@@ -293,7 +289,10 @@ class TBGreen:
293
289
  GR = defaultdict(lambda: 0.0j)
294
290
  rhoR = defaultdict(lambda: 0.0j)
295
291
  for ik, kpt in enumerate(self.kpts):
296
- Gk = self.get_Gk(ik, energy)
292
+ if Gk_all is None:
293
+ Gk = self.get_Gk(ik, energy)
294
+ else:
295
+ Gk = Gk_all[ik]
297
296
  if get_rho:
298
297
  if self.is_orthogonal:
299
298
  rhok = Gk
File without changes
TB2J/manager.py CHANGED
@@ -6,7 +6,13 @@ from TB2J.exchangeCL2 import ExchangeCL2
6
6
  from TB2J.exchange_qspace import ExchangeCLQspace
7
7
  from TB2J.utils import read_basis, auto_assign_basis_name
8
8
  from ase.io import read
9
- from TB2J.sisl_wrapper import SislWrapper
9
+
10
+ #from TB2J.sisl_wrapper import SislWrapper
11
+ try:
12
+ from HamiltonIO.siesta import SislWrapper
13
+ except ImportError:
14
+ print("Cannot import SislWrapper from HamiltonIO.siesta. Please install HamiltonIO first.")
15
+ from TB2J.sisl_wrapper import SislWrapper
10
16
  from TB2J.gpaw_wrapper import GPAWWrapper
11
17
  from TB2J.wannier import parse_atoms
12
18
 
@@ -283,8 +289,8 @@ def gen_exchange_siesta(
283
289
  geom = H.geometry
284
290
  if H.spin.is_colinear:
285
291
  print("Reading Siesta hamiltonian: colinear spin.")
286
- tbmodel_up = SislWrapper(H, spin=0, geom=geom)
287
- tbmodel_dn = SislWrapper(H, spin=1, geom=geom)
292
+ tbmodel_up = SislWrapper(fdf_fname=None,sisl_hamiltonian=H, spin=0, geom=geom)
293
+ tbmodel_dn = SislWrapper(fdf_fname=None, sisl_hamiltonian=H, spin=1, geom=geom)
288
294
  basis = dict(zip(tbmodel_up.orbs, list(range(tbmodel_up.norb))))
289
295
  print("Starting to calculate exchange.")
290
296
  description = f""" Input from collinear Siesta data.
@@ -351,7 +357,7 @@ def gen_exchange_siesta(
351
357
 
352
358
  elif H.spin.is_spinorbit or H.spin.is_noncolinear:
353
359
  print("Reading Siesta hamiltonian: non-colinear spin.")
354
- tbmodel = SislWrapper(H, spin=None, geom=geom)
360
+ tbmodel = SislWrapper(fdf_fname=None, sisl_hamiltonian=H, spin=None, geom=geom)
355
361
  basis = dict(zip(tbmodel.orbs, list(range(tbmodel.nbasis))))
356
362
  print("Starting to calculate exchange.")
357
363
  description = f""" Input from non-collinear Siesta data.
@@ -0,0 +1 @@
1
+ from .lowdin import Lowdin
@@ -0,0 +1,22 @@
1
+ import numpy as np
2
+ import warnings
3
+ import sys
4
+
5
+ MAX_EXP_ARGUMENT = np.log(sys.float_info.max)
6
+
7
+
8
+ def fermi(e, mu, width=0.01):
9
+ """
10
+ the fermi function.
11
+ .. math::
12
+ f=\\frac{1}{\exp((e-\mu)/width)+1}
13
+
14
+ :param e,mu,width: e,\mu,width
15
+ """
16
+ x = (e - mu) / width
17
+ # disable overflow warning
18
+ with warnings.catch_warnings():
19
+ warnings.simplefilter("ignore")
20
+ ret = np.where(x < MAX_EXP_ARGUMENT, 1 / (1.0 + np.exp(x)), 0.0)
21
+
22
+ return ret
@@ -0,0 +1,90 @@
1
+ import numpy as np
2
+
3
+
4
+ def HR_to_k(HR, Rlist, kpts):
5
+ # Hk[k,:,:] = sum_R (H[R] exp(i2pi k.R))
6
+ phase = np.exp(2.0j * np.pi * np.tensordot(kpts, Rlist, axes=([1], [1])))
7
+ Hk = np.einsum("rlm, kr -> klm", HR, phase)
8
+ return Hk
9
+
10
+
11
+ def Hk_to_R(Hk, Rlist, kpts, kweights):
12
+ phase = np.exp(-2.0j * np.pi * np.tensordot(kpts, Rlist, axes=([1], [1])))
13
+ HR = np.einsum("klm, kr, k->rlm", Hk, phase, kweights)
14
+ return HR
15
+
16
+
17
+ def k_to_R(kpts, Rlist, Mk, kweights=None):
18
+ """
19
+ Transform k-space wavefunctions to real space.
20
+ params:
21
+ kpts: k-points
22
+ Rlist: list of R vectors
23
+ Mk: matrix of shape [nkpt, n1, n2] in k-space.
24
+
25
+ return:
26
+ MR: matrix of shape [nR, n1, n2], the matrix in R-space.
27
+
28
+ """
29
+ nkpt, n1, n2 = Mk.shape
30
+ if kweights is None:
31
+ kweights = np.ones(nkpt, dtype=float) / nkpt
32
+ phase = np.exp(-2.0j * np.pi * np.tensordot(kpts, Rlist, axes=([1], [1])))
33
+ MR = np.einsum("klm, kr, k -> rlm", Mk, phase, kweights)
34
+ return MR
35
+
36
+ # nkpt, n1, n2 = Mk.shape
37
+ # nR = Rlist.shape[0]
38
+ # MR = np.zeros((nR, n1, n2), dtype=complex)
39
+ # if kweights is None:
40
+ # kweights = np.ones(nkpt, dtype=float)/nkpt
41
+ # for iR, R in enumerate(Rlist):
42
+ # for ik in range(nkpt):
43
+ # MR[iR] += Mk[ik] * np.exp(-2.0j*np.pi * np.dot(kpts[ik], R)) * kweights[ik]
44
+ # return MR
45
+
46
+
47
+ def R_to_k(kpts, Rlist, MR):
48
+ """
49
+ Transform real-space wavefunctions to k-space.
50
+ params:
51
+ kpts: k-points
52
+ Rlist: list of R vectors
53
+ MR: matrix of shape [nR, n1, n2] in R-space.
54
+
55
+ return:
56
+ Mk: matrix of shape [nkpt, n1, n2], the matrix in k-space.
57
+
58
+ """
59
+ phase = np.exp(2.0 * np.pi * 1j * np.tensordot(kpts, Rlist, axes=([1], [1])))
60
+ Mk = np.einsum("rlm, kr -> klm", MR, phase)
61
+
62
+ # nkpt, n1, n2 = Mk.shape
63
+ # nR = Rlist.shape[0]
64
+ # Mk = np.zeros((nkpt, n1, n2), dtype=complex)
65
+ # for iR, R in enumerate(Rlist):
66
+ # for ik in range(nkpt):
67
+ # Mk[ik] += MR[iR] * np.exp(2.0 * np.pi * 1j * np.dot(kpts[ik], R))
68
+ return Mk
69
+
70
+
71
+ def R_to_onek(kpt, Rlist, MR):
72
+ """
73
+ Transform real-space wavefunctions to k-space.
74
+ params:
75
+ kpt: k-point
76
+ Rlist: list of R vectors
77
+ MR: matrix of shape [nR, n1, n2] in R-space.
78
+
79
+ return:
80
+ Mk: matrix of shape [n1, n2], the matrix in k-space.
81
+
82
+ """
83
+ phase = np.exp(2.0j * np.pi * np.dot(Rlist, kpt))
84
+ Mk = np.einsum("rlm, r -> lm", MR, phase)
85
+ return Mk
86
+ # n1, n2 = MR.shape[1:]
87
+ # Mk = np.zeros((n1, n2), dtype=complex)
88
+ # for iR, R in enumerate(Rlist):
89
+ # Mk += MR[iR] * np.exp(2.0j*np.pi * np.dot(kpt, R))
90
+ # return Mk
@@ -0,0 +1,12 @@
1
+ import numpy as np
2
+ from scipy.linalg import inv, eigh
3
+
4
+
5
+ def Lowdin(S):
6
+ """
7
+ Calculate S^(-1/2).
8
+ Which is used in lowind's symmetric orthonormalization.
9
+ psi_prime = S^(-1/2) psi
10
+ """
11
+ eigval, eigvec = eigh(S)
12
+ return eigvec @ np.diag(np.sqrt(1.0 / eigval)) @ (eigvec.T.conj())
@@ -0,0 +1,56 @@
1
+ import numpy as np
2
+ from TB2J.pauli import pauli_block_all, s0, s1, s2, s3, gather_pauli_blocks
3
+
4
+
5
+ def rotate_Matrix_from_z_to_axis(M, axis, normalize=True):
6
+ """
7
+ Given a spinor matrix M, rotate it from z-axis to axis.
8
+ The spinor matrix M is a 2x2 matrix, which can be decomposed as I, x, y, z components using Pauli matrices.
9
+ """
10
+ MI, Mx, My, Mz = pauli_block_all(M)
11
+ axis = axis / np.linalg.norm(axis)
12
+ # M_new = s0* MI + Mz * (axis[0] * s1 + axis[1] * s2 + axis[2] * s3) *2
13
+ M_new = gather_pauli_blocks(MI, Mz * axis[0], Mz * axis[1], Mz * axis[2])
14
+ return M_new
15
+
16
+
17
+ def spherical_to_cartesian(theta, phi, normalize=True):
18
+ """
19
+ Convert spherical coordinates to cartesian
20
+ """
21
+ x = np.sin(theta) * np.cos(phi)
22
+ y = np.sin(theta) * np.sin(phi)
23
+ z = np.cos(theta)
24
+ vec = np.array([x, y, z])
25
+ if normalize:
26
+ vec = vec / np.linalg.norm(vec)
27
+ return vec
28
+
29
+
30
+ def rotate_Matrix_from_z_to_spherical(M, theta, phi, normalize=True):
31
+ """
32
+ Given a spinor matrix M, rotate it from z-axis to spherical coordinates
33
+ """
34
+ axis = spherical_to_cartesian(theta, phi, normalize)
35
+ return rotate_Matrix_from_z_to_axis(M, axis, normalize)
36
+
37
+
38
+ def test_rotate_Matrix_from_z_to_axis():
39
+ M = np.array([[1.1, 0], [0, 0.9]])
40
+ print(pauli_block_all(M))
41
+ Mnew = rotate_Matrix_from_z_to_axis(M, [1, 1, 1])
42
+ print(pauli_block_all(Mnew))
43
+ print(Mnew)
44
+
45
+ M = np.array(
46
+ [
47
+ [-9.90532976e-06 + 0.0j, 0.00000000e00 + 0.0j],
48
+ [0.00000000e00 + 0.0j, -9.88431291e-06 + 0.0j],
49
+ ]
50
+ )
51
+ print(M)
52
+ print(rotate_Matrix_from_z_to_axis(M, [0, 0, 1]))
53
+
54
+
55
+ if __name__ == "__main__":
56
+ test_rotate_Matrix_from_z_to_axis()
TB2J/patch.py ADDED
@@ -0,0 +1,50 @@
1
+ import types
2
+ from unittest import mock
3
+
4
+ class A(object):#but seems to work for old style objects too
5
+ def funcx(self,x):
6
+ print("x1=",x)
7
+ print("called from", self)
8
+
9
+ def method(self,x):
10
+ print("xmethod=",x)
11
+ print("called from", self)
12
+
13
+ def patch_me(target):
14
+ def method(target,x):
15
+ print("x=",x)
16
+ print("called from", target)
17
+ target.method = types.MethodType(method,target)
18
+
19
+ def method(self,x):
20
+ print("x=",x)
21
+ print("called from", self)
22
+
23
+ @mock.patch("__main__.A")
24
+ def funcx(self,x):
25
+ print("new x=",x)
26
+ print("called from", self)
27
+
28
+ A.method=method
29
+ #add more if needed
30
+ a = A()
31
+ print(A.__dict__)
32
+ print(a)
33
+ #out: <__main__.A object at 0x2b73ac88bfd0>
34
+
35
+ @mock.patch("__main__.a")
36
+ def funcx(self,x):
37
+ print("x=",x)
38
+ print("called from", self)
39
+
40
+ a.funcx(3)
41
+ patch_me(a) #patch instance
42
+ a.method=method
43
+ #a.method(5)
44
+ #out: x= 5
45
+ #out: called from <__main__.A object at 0x2b73ac88bfd0>
46
+ patch_me(A)
47
+
48
+ a.method(6) #can patch class too
49
+ #out: x= 6
50
+ #out: called from <class '__main__.A'>
TB2J/pauli.py CHANGED
@@ -143,7 +143,24 @@ def pauli_block_all(M):
143
143
  return MI, Mx, My, Mz
144
144
 
145
145
 
146
+ def gather_pauli_blocks(MI, Mx, My, Mz):
147
+ """
148
+ Gather the I, x, y, z component of a matrix.
149
+ """
150
+ return np.kron(MI, s0) + np.kron(Mx, s1) + np.kron(My, s2) + np.kron(Mz, s3)
151
+
152
+
153
+ def test_gather_pauli_blocks():
154
+ M = np.random.rand(4, 4)
155
+ MI, Mx, My, Mz = pauli_block_all(M)
156
+ M2 = gather_pauli_blocks(MI, Mx, My, Mz)
157
+ assert np.allclose(M, M2)
158
+
159
+
146
160
  def op_norm(M):
161
+ """
162
+ Return the operator norm of a matrix.
163
+ """
147
164
  return max(svd(M)[1])
148
165
 
149
166
 
@@ -0,0 +1,68 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import aiida
4
+ from aiida_tb2j.data import ExchangeData
5
+
6
+ def plot_dispersion(bands, kpoint_labels, color='blue', title=None):
7
+
8
+ fig, axs = plt.subplots(1, 1, constrained_layout=True)
9
+ fig.set_size_inches(6, 6/1.618)
10
+
11
+ '''
12
+ Plot the bands
13
+ '''
14
+ kpoints = np.arange(len(bands))
15
+ axs.plot(kpoints, bands, color=color, linewidth=1.5)
16
+
17
+ '''
18
+ Plot the symmetry points
19
+ '''
20
+ bmin = bands.min(); bmax = bands.max()
21
+ ymin = bmin - 0.05*np.abs(bmin-bmax); ymax = bmax + 0.05*np.abs(bmax-bmin);
22
+ axs.set_xticks(kpoint_labels[0], kpoint_labels[1], fontsize=10)
23
+ axs.vlines(x=kpoint_labels[0], ymin=ymin, ymax=ymax, color='black', linewidth=0.5)
24
+ axs.set_xlim([0, len(kpoints)])
25
+ axs.set_ylim([ymin, ymax])
26
+
27
+ if title is not None:
28
+ plt.title(title, fontsize=10)
29
+
30
+ plt.show()
31
+
32
+
33
+ if __name__ == "__main__":
34
+
35
+ from argparse import ArgumentParser
36
+
37
+ parser = ArgumentParser()
38
+ parser.add_argument('-f', '--pickle_filename', type=str, help="Path of the 'TB2J.pickle' file.", required=True)
39
+ args = parser.parse_args()
40
+
41
+ '''
42
+ Right now the implementation depends on AiiDA and so we must create and load an AiiDA profile,
43
+ even if we do not store any information on a data base.
44
+ '''
45
+ aiida.load_profile()
46
+ '''
47
+ Create an ExchangeData object with the informations from the TB2J.pickle file
48
+ '''
49
+ exchange = ExchangeData.load_tb2j(pickle_file=args.pickle_filename, isotropic=False, pbc=(True, True, True))
50
+ '''
51
+ Compute the magnon band structure along a high symmetry path generated with
52
+ the ASE package. The informations is stored in an AiiDA BandsData object.
53
+ Here tol is the symmetry tolerance to determine the space group of the system.
54
+ They are in units of eV
55
+ '''
56
+ magnon_data = exchange.get_magnon_bands(npoints=300, tol=1e-1, with_DMI=True, with_Jani=True)
57
+ magnon_bands = 1000*magnon_data.get_bands() # Convert to meV
58
+ raw_labels = [(k, '$\Gamma$') if s == 'GAMMA' else (k, s) for k, s in magnon_data.labels]
59
+ kpoint_labels = list( zip( *raw_labels ) )
60
+ plot_dispersion(magnon_bands, kpoint_labels, color='blue', title='Magnon Bands')
61
+ '''
62
+ We can also obtain the dynamical matrix h instead of the actual magnon bands. The result
63
+ is stored in a numpy array with shape (number of kpoints, 2*natoms, 2*natoms)
64
+ '''
65
+ kpoints = magnon_data.get_kpoints() # The shape of the kpoints must be (nkpoints, 3)
66
+ h_matrix = 1000*exchange._H_matrix(kpoints, with_DMI=True, with_Jani=True) # Convert to meV
67
+ h_dispersion = np.linalg.eigvalsh(h_matrix) # We can also get the eigenvectors with np.linalg.eigh
68
+ plot_dispersion(h_dispersion, kpoint_labels, color='red', title='h matrix dispersion')
@@ -0,0 +1,79 @@
1
+ import numpy as np
2
+ from aiida_tb2j.data import ExchangeData
3
+ from aiida_tb2j.data.exchange import get_rotation_arrays
4
+ from itertools import combinations_with_replacement
5
+
6
+ ux, uy, uz = np.eye(3).reshape((3, 1, 3))
7
+
8
+ def combine_arrays(u, v):
9
+
10
+ return np.concatenate([u*v, np.roll(u, -1, axis=-1)*v, np.roll(v, -1, axis=-1)*u], axis=-1)
11
+
12
+ def get_coefficients(magmoms, indices):
13
+
14
+ i, j = indices
15
+
16
+ U, V = zip(*[get_rotation_arrays(magmoms, u=u) for u in [ux, uy, uz]])
17
+ U = np.stack(U).swapaxes(0, 1)
18
+ V = np.stack(V).swapaxes(0, 1)
19
+
20
+ uc = combine_arrays(U[i], U[j].conj())
21
+ ur = combine_arrays(U[i], U[j])
22
+ uc2 = combine_arrays(U[i].conj(), U[j])
23
+ u = np.concatenate([uc, ur, uc2], axis=1)
24
+
25
+ return u, V
26
+
27
+ def get_C(H0, u, V):
28
+
29
+ n = int(H0.shape[-1]/2)
30
+ upi = np.triu_indices(n)
31
+ dig = np.diag_indices(n)
32
+
33
+ i, j = upi
34
+ AB0 = H0[:, [i, i, i+n], [j, j+n, j+n]]
35
+ AB0 = np.swapaxes(AB0, 0, 2).reshape(len(i), 9)
36
+ J0_flat = np.linalg.solve(u, AB0)
37
+
38
+ J0 = np.empty((n, n, 3, 3), dtype=complex)
39
+ J0[*upi] = J0_flat[:, [0, 6, 5, 3, 1, 7, 8, 4, 2]].reshape(-1, 3, 3)
40
+ J0 += J0.swapaxes(0, 1)
41
+ J0[*dig] = 0.0
42
+
43
+ C = np.array([np.diag(a) for a in np.einsum('imx,ijxy,jmy->mi', V, 2*J0, V)])
44
+
45
+ return C
46
+
47
+ def get_J(H, kpoints, exchange):
48
+
49
+ n = int(H.shape[-1]/2)
50
+ upi = np.triu_indices(n)
51
+ dig = np.diag_indices(n)
52
+ i, j = upi
53
+
54
+ magmoms = exchange.magmoms()[np.unique(exchange.pairs)]
55
+ magmoms /= np.linalg.norm(magmoms, axis=-1).reshape(-1, 1)
56
+ u, V = get_coefficients(magmoms, indices=upi)
57
+
58
+ H0 = np.stack([1000*exchange._H_matrix(kpoints=np.zeros((1, 3)), with_DMI=True, with_Jani=True, u=u) for u in [ux, uy, uz]])[:, 0, :, :]
59
+ C = get_C(H0, u, V)
60
+ H[:, :, :n, :n] += C.reshape(3, 1, n, n)
61
+ H[:, :, n:, n:] += C.reshape(3, 1, n, n)
62
+
63
+ AB = H[:, :, [i, i, i+n], [j, j+n, j+n]]
64
+ AB[:, :, 2, :] = AB[:, ::-1, 2, :]
65
+ AB = np.moveaxis(AB, [2, 3], [1, 0]).reshape(len(i), 9, -1)
66
+
67
+ vectors = exchange.get_vectors()
68
+ exp_summand = np.exp( -2j*np.pi*vectors @ kpoints.T )
69
+ nAB = np.einsum('nik,ndk->nid', AB, exp_summand) / len(kpoints)
70
+
71
+ ii = np.where(i == j)
72
+ i0 = np.where(np.linalg.norm(vectors, axis=-1) == 0.0)
73
+ J = np.linalg.solve(u, nAB).swapaxes(1, 2)
74
+ J = J[:, :, [0, 6, 5, 3, 1, 7, 8, 4, 2]].reshape(len(i), -1, 3, 3)
75
+ J *= -1
76
+ J[ii] *= 2
77
+ J[i0] *= 0
78
+
79
+ return J