TB2J 0.9.9.4__py3-none-any.whl → 0.9.9.6__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 (36) hide show
  1. TB2J/Jdownfolder.py +3 -2
  2. TB2J/MAEGreen.py +22 -17
  3. TB2J/__init__.py +1 -1
  4. TB2J/exchange_params.py +1 -2
  5. TB2J/interfaces/abacus/gen_exchange_abacus.py +3 -0
  6. TB2J/interfaces/siesta_interface.py +15 -7
  7. TB2J/io_exchange/__init__.py +2 -0
  8. TB2J/io_exchange/io_exchange.py +40 -12
  9. TB2J/magnon/__init__.py +3 -0
  10. TB2J/magnon/io_exchange2.py +695 -0
  11. TB2J/magnon/magnon3.py +334 -0
  12. TB2J/magnon/magnon_io.py +48 -0
  13. TB2J/magnon/magnon_math.py +53 -0
  14. TB2J/magnon/plot.py +58 -0
  15. TB2J/magnon/structure.py +348 -0
  16. TB2J/mathutils/__init__.py +2 -0
  17. TB2J/mathutils/fibonacci_sphere.py +1 -1
  18. TB2J/mathutils/rotate_spin.py +0 -3
  19. TB2J/symmetrize_J.py +1 -1
  20. tb2j-0.9.9.6.data/scripts/TB2J_magnon2.py +78 -0
  21. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.6.dist-info}/METADATA +1 -2
  22. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.6.dist-info}/RECORD +36 -28
  23. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/TB2J_downfold.py +0 -0
  24. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/TB2J_eigen.py +0 -0
  25. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/TB2J_magnon.py +0 -0
  26. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/TB2J_magnon_dos.py +0 -0
  27. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/TB2J_merge.py +0 -0
  28. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/TB2J_rotate.py +0 -0
  29. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/TB2J_rotateDM.py +0 -0
  30. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/abacus2J.py +0 -0
  31. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/siesta2J.py +0 -0
  32. {tb2j-0.9.9.4.data → tb2j-0.9.9.6.data}/scripts/wann2J.py +0 -0
  33. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.6.dist-info}/WHEEL +0 -0
  34. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.6.dist-info}/entry_points.txt +0 -0
  35. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.6.dist-info}/licenses/LICENSE +0 -0
  36. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.6.dist-info}/top_level.txt +0 -0
TB2J/magnon/magnon3.py ADDED
@@ -0,0 +1,334 @@
1
+ from dataclasses import dataclass
2
+
3
+ import numpy as np
4
+ from scipy.spatial.transform import Rotation
5
+
6
+ from ..io_exchange import SpinIO
7
+ from ..mathutils import Hermitize, get_rotation_arrays
8
+ from .plot import BandsPlot
9
+
10
+
11
+ @dataclass
12
+ class Magnon:
13
+ """
14
+ Magnon calculator implementation using dataclass
15
+ """
16
+
17
+ nspin: int
18
+ # ind_atoms: list
19
+ magmom: np.ndarray
20
+ Rlist: np.ndarray
21
+ JR: np.ndarray
22
+ _Q: np.ndarray = np.array([0.0, 0.0, 0.0], dtype=float)
23
+ _uz: np.ndarray = np.array([[0.0, 0.0, 1.0]], dtype=float)
24
+ _n: np.ndarray = np.array([0, 0, 1], dtype=float)
25
+
26
+ def set_reference(self, Q, uz, n):
27
+ """
28
+ Set reference propagation vector and quantization axis
29
+
30
+ Parameters
31
+ ----------
32
+ Q : array_like
33
+ Propagation vector
34
+ uz : array_like
35
+ Quantization axis
36
+ n : array_like
37
+ Normal vector for rotation
38
+ """
39
+ self.set_propagation_vector(Q)
40
+ self._uz = np.array(uz, dtype=float)
41
+ self._n = np.array(n, dtype=float)
42
+
43
+ def set_propagation_vector(self, Q):
44
+ """Set propagation vector"""
45
+ self._Q = np.array(Q)
46
+
47
+ @property
48
+ def Q(self):
49
+ """Get propagation vector"""
50
+ if self._Q is None:
51
+ raise ValueError("Propagation vector Q is not set.")
52
+ return self._Q
53
+
54
+ @Q.setter
55
+ def Q(self, value):
56
+ if not isinstance(value, (list, np.ndarray)):
57
+ raise TypeError("Propagation vector Q must be a list or numpy array.")
58
+ if len(value) != 3:
59
+ raise ValueError("Propagation vector Q must have three components.")
60
+ self._Q = np.array(value)
61
+
62
+ def Jq(self, kpoints):
63
+ """
64
+ Compute the exchange interactions in reciprocal space.
65
+
66
+ The exchange interactions J(q) are computed using the Fourier transform:
67
+ J(q) = ∑_R J(R) exp(iq·R)
68
+
69
+ Array shapes and indices:
70
+ - kpoints: (nkpt, 3) array of k-points
71
+ - Rlist: (nR, 3) array of real-space lattice vectors
72
+ - JR: (nR, nspin, nspin, 3, 3) array of exchange tensors in real space
73
+ where nspin is number of magnetic atoms
74
+ - Output Jq: (nkpt, nspin, nspin, 3, 3) array of exchange tensors in q-space
75
+
76
+ If propagation vector Q is set, each J(R) is rotated before the Fourier transform:
77
+ J'_mn(R) = R_m(ϕ)^T J(R) R_n(ϕ)
78
+ where ϕ = 2π R·Q and R(ϕ) is rotation matrix around axis n by angle ϕ
79
+
80
+ Parameters
81
+ ----------
82
+ kpoints : array_like (nkpt, 3)
83
+ k-points at which to evaluate the exchange interactions
84
+
85
+ Returns
86
+ -------
87
+ numpy.ndarray (nkpt, nspin, nspin, 3, 3)
88
+ Exchange interaction tensors J(q) at each k-point
89
+ First two indices are for magnetic atom pairs
90
+ Last two indices are for 3x3 tensor components
91
+ """
92
+ Rlist = np.array(self.Rlist)
93
+ JR = self.JR
94
+ JRprime = JR.copy()
95
+
96
+ for iR, R in enumerate(Rlist):
97
+ if self._Q is not None:
98
+ # Rotate exchange tensors based on propagation vector
99
+ phi = 2 * np.pi * R @ self._Q # angle ϕ = 2π R·Q
100
+ rv = phi * self._n # rotation vector
101
+ Rmat = Rotation.from_rotvec(rv).as_matrix()
102
+ # J'_mn(R) = R_m(ϕ)^T J(R) R_n(ϕ) using Einstein summation.
103
+ # Here m is always in the R=0, thus the rotation is only applied on the
104
+ # n , so only on the right.
105
+ JRprime[iR] = np.einsum(" rijxy, yb -> rijab", JR[iR], Rmat)
106
+
107
+ nkpt = kpoints.shape[0]
108
+ Jq = np.zeros((nkpt, self.nspin, self.nspin, 3, 3), dtype=complex)
109
+
110
+ for iR, R in enumerate(Rlist):
111
+ for iqpt, qpt in enumerate(kpoints):
112
+ # Fourier transform of exchange tensors
113
+ phase = 2 * np.pi * R @ qpt
114
+ Jq[iqpt] += np.exp(1j * phase) * JRprime[iR]
115
+
116
+ # Ensure Hermiticity: J(q) = J(-q)†
117
+ for iqpt in range(nkpt):
118
+ Jq[iqpt, :, :, :, :] += np.conj(
119
+ np.moveaxis(Jq[iqpt, :, :, :, :], [1, 3], [2, 4])
120
+ )
121
+ Jq[iqpt, :, :, :, :] /= 2.0
122
+ return Jq
123
+
124
+ def Hq(self, kpoints, anisotropic=True):
125
+ """
126
+ Compute the magnon Hamiltonian in reciprocal space.
127
+
128
+ Parameters
129
+ ----------
130
+ kpoints : array_like
131
+ k-points at which to evaluate the Hamiltonian
132
+ anisotropic : bool, optional
133
+ Whether to include anisotropic interactions, default True
134
+
135
+ Returns
136
+ -------
137
+ numpy.ndarray
138
+ Magnon Hamiltonian matrix at each k-point
139
+ """
140
+ magmoms = self.magmom.copy()
141
+ magmoms /= np.linalg.norm(magmoms, axis=-1)[:, None]
142
+
143
+ U, V = get_rotation_arrays(magmoms, u=self._uz)
144
+
145
+ J0 = self.Jq(np.zeros((1, 3)), anisotropic=anisotropic)
146
+ J0 = -Hermitize(J0)[:, :, 0]
147
+ Jq = -Hermitize(self.Jq(kpoints, anisotropic=anisotropic))
148
+
149
+ C = np.diag(np.einsum("ix,ijxy,jy->i", V, 2 * J0, V))
150
+ B = np.einsum("ix,ijkxy,jy->kij", U, Jq, U)
151
+ A1 = np.einsum("ix,ijkxy,jy->kij", U, Jq, U.conj())
152
+ A2 = np.einsum("ix,ijkxy,jy->kij", U.conj(), Jq, U)
153
+
154
+ return np.block([[A1 - C, B], [B.swapaxes(-1, -2).conj(), A2 - C]])
155
+
156
+ def _magnon_energies(self, kpoints, anisotropic=True, u=None):
157
+ """Calculate magnon energies"""
158
+ H = self.Hq(kpoints, anisotropic=anisotropic)
159
+ n = H.shape[-1] // 2
160
+ I = np.eye(n)
161
+
162
+ min_eig = 0.0
163
+ try:
164
+ K = np.linalg.cholesky(H)
165
+ except np.linalg.LinAlgError:
166
+ try:
167
+ K = np.linalg.cholesky(H + 1e-6 * np.eye(2 * n))
168
+ except np.linalg.LinAlgError:
169
+ from warnings import warn
170
+
171
+ min_eig = np.min(np.linalg.eigvalsh(H))
172
+ K = np.linalg.cholesky(H - (min_eig - 1e-6) * np.eye(2 * n))
173
+ warn(
174
+ f"WARNING: The system may be far from the magnetic ground-state. Minimum eigenvalue: {min_eig}. The magnon energies might be unphysical."
175
+ )
176
+
177
+ g = np.block([[1 * I, 0 * I], [0 * I, -1 * I]])
178
+ KH = K.swapaxes(-1, -2).conj()
179
+
180
+ return np.linalg.eigvalsh(KH @ g @ K)[:, n:] + min_eig
181
+
182
+ def get_magnon_bands(
183
+ self,
184
+ kpoints: np.array = np.array([]),
185
+ path: str = None,
186
+ npoints: int = 300,
187
+ special_points: dict = None,
188
+ tol: float = 2e-4,
189
+ pbc: tuple = None,
190
+ cartesian: bool = False,
191
+ labels: list = None,
192
+ anisotropic: bool = True,
193
+ u: np.array = None,
194
+ ):
195
+ """Get magnon band structure"""
196
+ pbc = self._pbc if pbc is None else pbc
197
+ u = self._uz if u is None else u
198
+ if kpoints.size == 0:
199
+ from ase.cell import Cell
200
+
201
+ bandpath = Cell(self._cell).bandpath(
202
+ path=path,
203
+ npoints=npoints,
204
+ special_points=special_points,
205
+ eps=tol,
206
+ pbc=pbc,
207
+ )
208
+ kpoints = bandpath.kpts
209
+ spk = bandpath.special_points
210
+ spk[r"$\Gamma$"] = spk.pop("G", np.zeros(3))
211
+ labels = [
212
+ (i, symbol)
213
+ for symbol in spk
214
+ for i in np.where((kpoints == spk[symbol]).all(axis=1))[0]
215
+ ]
216
+ elif cartesian:
217
+ kpoints = np.linalg.solve(self._cell.T, kpoints.T).T
218
+
219
+ bands = self._magnon_energies(kpoints, anisotropic=anisotropic)
220
+
221
+ return labels, bands
222
+
223
+ def plot_magnon_bands(self, **kwargs):
224
+ """
225
+ Plot magnon band structure.
226
+
227
+ Parameters
228
+ ----------
229
+ **kwargs
230
+ Additional keyword arguments passed to get_magnon_bands and plotting functions
231
+ """
232
+ filename = kwargs.pop("filename", None)
233
+ kpath, bands = self.get_magnon_bands(**kwargs)
234
+ bands_plot = BandsPlot(bands, kpath)
235
+ bands_plot.plot(filename=filename)
236
+
237
+ @classmethod
238
+ def load_from_io(cls, exc: SpinIO, **kwargs):
239
+ """
240
+ Create Magnon instance from SpinIO
241
+
242
+ Parameters
243
+ ----------
244
+ exc : SpinIO
245
+ SpinIO instance with exchange parameters
246
+ **kwargs : dict
247
+ Additional arguments passed to get_full_Jtensor_for_Rlist
248
+
249
+ Returns
250
+ -------
251
+ Magnon
252
+ Initialized Magnon instance
253
+ """
254
+ return cls(
255
+ nspin=exc.nspin,
256
+ magmom=exc.magmoms,
257
+ Rlist=exc.Rlist,
258
+ JR=exc.get_full_Jtensor_for_Rlist(order="ij33", **kwargs),
259
+ )
260
+
261
+ @classmethod
262
+ def from_TB2J_results(cls, path=None, fname="TB2J.pickle", **kwargs):
263
+ """
264
+ Create Magnon instance from TB2J results.
265
+
266
+ Parameters
267
+ ----------
268
+ path : str, optional
269
+ Path to the TB2J results file
270
+ fname : str, optional
271
+ Filename of the TB2J results file, default "TB2J.pickle"
272
+ **kwargs : dict
273
+ Additional arguments passed to load_from_io
274
+
275
+ Returns
276
+ -------
277
+ Magnon
278
+ Initialized Magnon instance
279
+ """
280
+ exc = SpinIO.load_pickle(path=path, fname=fname)
281
+ return cls.load_from_io(exc, **kwargs)
282
+
283
+
284
+ def test_magnon(path="TB2J_results"):
285
+ """Test the magnon calculator by loading from TB2J_results and computing at high-symmetry points."""
286
+ from pathlib import Path
287
+
288
+ import numpy as np
289
+
290
+ # Check if TB2J_results exists
291
+ results_path = Path(path)
292
+ if not results_path.exists():
293
+ raise FileNotFoundError(f"TB2J_results directory not found at {path}")
294
+
295
+ # Load magnon calculator from TB2J results
296
+ print(f"Loading exchange parameters from {path}...")
297
+ magnon = Magnon.from_TB2J_results(path=path, iso_only=True)
298
+
299
+ # Define high-symmetry points for a cube
300
+ kpoints = np.array(
301
+ [
302
+ [0.0, 0.0, 0.0], # Γ (Gamma)
303
+ [0.5, 0.0, 0.0], # X
304
+ [0.5, 0.5, 0.0], # M
305
+ [0.5, 0.5, 0.5], # R
306
+ ]
307
+ )
308
+ klabels = ["Gamma", "X", "M", "R"]
309
+
310
+ print("\nComputing exchange interactions at high-symmetry points...")
311
+ Jq = magnon.Jq(kpoints)
312
+
313
+ print(f"\nResults for {len(kpoints)} k-points:")
314
+ print("-" * 50)
315
+ print("Exchange interactions J(q):")
316
+ print(f"Shape of Jq tensor: {Jq.shape}")
317
+ print(
318
+ f"Dimensions: (n_kpoints={Jq.shape[0]}, n_spin={Jq.shape[1]}, n_spin={Jq.shape[2]}, xyz={Jq.shape[3]}, xyz={Jq.shape[4]})"
319
+ )
320
+
321
+ print("\nComputing magnon energies...")
322
+ energies = magnon._magnon_energies(kpoints)
323
+
324
+ print("\nMagnon energies at high-symmetry points (in meV):")
325
+ print("-" * 50)
326
+ for i, (k, label) in enumerate(zip(kpoints, klabels)):
327
+ print(f"\n{label}-point k={k}:")
328
+ print(f"Energies: {energies[i] * 1000:.3f} meV") # Convert to meV
329
+
330
+ return magnon, Jq, energies
331
+
332
+
333
+ if __name__ == "__main__":
334
+ test_magnon()
@@ -0,0 +1,48 @@
1
+ import numpy as np
2
+
3
+ from TB2J.io_exchange import SpinIO
4
+
5
+
6
+ class MagnonIO:
7
+ """Handle IO operations for magnon calculations"""
8
+
9
+ def __init__(self, exc: SpinIO):
10
+ """
11
+ Initialize MagnonIO with a SpinIO instance
12
+
13
+ Parameters
14
+ ----------
15
+ exc : SpinIO
16
+ SpinIO instance containing exchange information
17
+ """
18
+ self.exc = exc
19
+
20
+ def get_nspin(self):
21
+ """Get number of spins"""
22
+ return self.exc.get_nspin()
23
+
24
+ def get_ind_atoms(self):
25
+ """Get atom indices"""
26
+ return self.exc.ind_atoms
27
+
28
+ def get_magmom(self):
29
+ """Get magnetic moments"""
30
+ nspin = self.get_nspin()
31
+ return np.array([self.exc.spinat[self.exc.iatom(i)] for i in range(nspin)])
32
+
33
+ def get_rlist(self):
34
+ """Get R-vectors list"""
35
+ return self.exc.Rlist
36
+
37
+ def get_jtensor(self, asr=False, iso_only=False):
38
+ """
39
+ Get full J tensor for R-list
40
+
41
+ Parameters
42
+ ----------
43
+ asr : bool, optional
44
+ Acoustic sum rule, default False
45
+ iso_only : bool, optional
46
+ Only isotropic interactions, default False
47
+ """
48
+ return self.exc.get_full_Jtensor_for_Rlist(asr=asr, iso_only=iso_only)
@@ -0,0 +1,53 @@
1
+ import numpy as np
2
+
3
+ __all__ = ["generate_grid", "get_rotation_arrays", "round_to_precision", "uz", "I"]
4
+
5
+ I = np.eye(3)
6
+ uz = np.array([[0.0, 0.0, 1.0]])
7
+
8
+
9
+ def generate_grid(kmesh, sort=True):
10
+ half_grid = [int(n / 2) for n in kmesh]
11
+ grid = np.stack(
12
+ np.meshgrid(*[np.arange(-n, n + 1) for n in half_grid]), axis=-1
13
+ ).reshape(-1, 3)
14
+
15
+ if sort:
16
+ idx = np.linalg.norm(grid, axis=-1).argsort()
17
+ grid = grid[idx]
18
+
19
+ return grid
20
+
21
+
22
+ # def JR_to_Jq(JR, Rlist, qpt, vecn):
23
+ # for iR, R in enumerate(Rlist):
24
+ # phase = 2 * np.pi * R @ qpt
25
+ # rv = phase * vecn
26
+ # Rot = Rotation.from_rotvec(rv.reshape(-1, 3)).as_matrix().reshape(R.shape[0], 3, 3)
27
+ #
28
+
29
+
30
+ def get_rotation_arrays(magmoms, u=uz):
31
+ dim = magmoms.shape[0]
32
+ v = magmoms
33
+ n = np.cross(u, v)
34
+ n /= np.linalg.norm(n, axis=-1).reshape(dim, 1)
35
+ z = np.repeat(u, dim, axis=0)
36
+ A = np.stack([z, np.cross(n, z), n], axis=1)
37
+ B = np.stack([v, np.cross(n, v), n], axis=1)
38
+ R = np.einsum("nki,nkj->nij", A, B)
39
+
40
+ Rnan = np.isnan(R)
41
+ if Rnan.any():
42
+ nanidx = np.where(Rnan)[0]
43
+ R[nanidx] = I
44
+ R[nanidx, 2] = v[nanidx]
45
+
46
+ U = R[:, 0] + 1j * R[:, 1]
47
+ V = R[:, 2]
48
+
49
+ return U, V
50
+
51
+
52
+ def round_to_precision(array, precision):
53
+ return precision * np.round(array / precision)
TB2J/magnon/plot.py ADDED
@@ -0,0 +1,58 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+
4
+
5
+ class BandsPlot:
6
+ _UNITS = "meV"
7
+ _NSYSTEMS = 1
8
+
9
+ def __init__(self, bands, kpath, **kwargs):
10
+ self.bands = bands
11
+ self.kpath = kpath
12
+ self.bands *= 1000
13
+
14
+ plot_options = kwargs
15
+ self.linewidth = plot_options.pop("linewidth", 1.5)
16
+ self.color = plot_options.pop("color", "blue")
17
+ self.fontsize = plot_options.pop("fontsize", 12)
18
+ self.ticksize = plot_options.pop("ticksize", 10)
19
+ self.plot_options = plot_options
20
+
21
+ def plot(self, filename=None):
22
+ fig, axs = plt.subplots(1, self._NSYSTEMS, constrained_layout=True)
23
+
24
+ kdata = np.arange(self.bands.shape[0])
25
+ for band in self.bands.T:
26
+ axs.plot(
27
+ kdata,
28
+ band,
29
+ linewidth=self.linewidth,
30
+ color=self.color,
31
+ **self.plot_options,
32
+ )
33
+
34
+ bmin, bmax = self.bands.min(), self.bands.max()
35
+ ymin, ymax = (
36
+ bmin - 0.05 * np.abs(bmin - bmax),
37
+ bmax + 0.05 * np.abs(bmax - bmin),
38
+ )
39
+
40
+ axs.set_ylim([ymin, ymax])
41
+ axs.set_xlim([0, kdata[-1]])
42
+
43
+ kpoint_labels = list(zip(*self.kpath))
44
+ axs.set_xticks(*kpoint_labels, fontsize=self.ticksize)
45
+ axs.vlines(
46
+ x=kpoint_labels[0],
47
+ ymin=ymin,
48
+ ymax=ymax,
49
+ color="black",
50
+ linewidth=self.linewidth / 5,
51
+ )
52
+
53
+ axs.set_ylabel(f"Energy ({self._UNITS})", fontsize=self.fontsize)
54
+
55
+ if filename is None:
56
+ plt.show()
57
+ else:
58
+ fig.save(filename, dpi=300, bbox_inches="tight")