TB2J 0.9.9.4__py3-none-any.whl → 0.9.9.7__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 +3 -2
  2. TB2J/MAEGreen.py +22 -17
  3. TB2J/exchange.py +1 -1
  4. TB2J/exchange_params.py +1 -2
  5. TB2J/interfaces/abacus/abacus_wrapper.py +0 -3
  6. TB2J/interfaces/abacus/gen_exchange_abacus.py +3 -0
  7. TB2J/interfaces/abacus/orbital_api.py +1 -0
  8. TB2J/interfaces/siesta_interface.py +15 -7
  9. TB2J/io_exchange/__init__.py +2 -0
  10. TB2J/io_exchange/io_exchange.py +40 -12
  11. TB2J/magnon/__init__.py +3 -0
  12. TB2J/magnon/io_exchange2.py +695 -0
  13. TB2J/magnon/magnon3.py +334 -0
  14. TB2J/magnon/magnon_io.py +48 -0
  15. TB2J/magnon/magnon_math.py +53 -0
  16. TB2J/magnon/plot.py +58 -0
  17. TB2J/magnon/structure.py +348 -0
  18. TB2J/mathutils/__init__.py +2 -0
  19. TB2J/mathutils/fibonacci_sphere.py +1 -1
  20. TB2J/mathutils/rotate_spin.py +0 -3
  21. TB2J/symmetrize_J.py +1 -1
  22. tb2j-0.9.9.7.data/scripts/TB2J_magnon2.py +78 -0
  23. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/abacus2J.py +0 -1
  24. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.7.dist-info}/METADATA +2 -3
  25. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.7.dist-info}/RECORD +38 -30
  26. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/TB2J_downfold.py +0 -0
  27. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/TB2J_eigen.py +0 -0
  28. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/TB2J_magnon.py +0 -0
  29. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/TB2J_magnon_dos.py +0 -0
  30. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/TB2J_merge.py +0 -0
  31. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/TB2J_rotate.py +0 -0
  32. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/TB2J_rotateDM.py +0 -0
  33. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/siesta2J.py +0 -0
  34. {tb2j-0.9.9.4.data → tb2j-0.9.9.7.data}/scripts/wann2J.py +0 -0
  35. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.7.dist-info}/WHEEL +0 -0
  36. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.7.dist-info}/entry_points.txt +0 -0
  37. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.7.dist-info}/licenses/LICENSE +0 -0
  38. {tb2j-0.9.9.4.dist-info → tb2j-0.9.9.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,695 @@
1
+ """
2
+ This module provides functionality for handling magnetic exchange interactions and computing magnon band structures.
3
+
4
+ It includes classes and functions for:
5
+ - Reading and writing exchange interaction data
6
+ - Computing exchange tensors and magnon energies
7
+ - Plotting magnon band structures
8
+ - Converting between different magnetic structure representations
9
+ """
10
+
11
+ import numpy as np
12
+ from scipy.spatial.transform import Rotation
13
+
14
+ from .magnon_math import generate_grid, get_rotation_arrays, round_to_precision, uz
15
+ from .plot import BandsPlot
16
+ from .structure import BaseMagneticStructure, get_attribute_array
17
+
18
+ __all__ = [
19
+ "ExchangeIO",
20
+ "plot_tb2j_magnon_bands",
21
+ ]
22
+
23
+
24
+ def branched_keys(tb2j_keys, npairs):
25
+ """
26
+ Organize TB2J keys into branches based on magnetic site pairs.
27
+
28
+ Parameters
29
+ ----------
30
+ tb2j_keys : list
31
+ List of TB2J dictionary keys containing interaction information
32
+ npairs : int
33
+ Number of magnetic site pairs
34
+
35
+ Returns
36
+ -------
37
+ list
38
+ List of branched keys organized by magnetic site pairs
39
+ """
40
+ msites = int((2 * npairs) ** 0.5)
41
+ branch_size = len(tb2j_keys) // msites**2
42
+ new_keys = sorted(tb2j_keys, key=lambda x: -x[1] + x[2])[
43
+ (npairs - msites) * branch_size :
44
+ ]
45
+ new_keys.sort(key=lambda x: x[1:])
46
+ bkeys = [
47
+ new_keys[i : i + branch_size] for i in range(0, len(new_keys), branch_size)
48
+ ]
49
+
50
+ return [sorted(branch, key=lambda x: np.linalg.norm(x[0])) for branch in bkeys]
51
+
52
+
53
+ def correct_content(content, quadratic=False):
54
+ """
55
+ Ensure content dictionary has all required entries with proper initialization.
56
+
57
+ Parameters
58
+ ----------
59
+ content : dict
60
+ Dictionary containing exchange interaction data
61
+ quadratic : bool, optional
62
+ Whether to include biquadratic interactions, by default False
63
+ """
64
+ n = max(content["index_spin"]) + 1
65
+ data_shape = {"exchange_Jdict": ()}
66
+
67
+ if not content["colinear"]:
68
+ data_shape |= {"Jani_dict": (3, 3), "dmi_ddict": (3,)}
69
+ if quadratic:
70
+ data_shape["biquadratic_Jdict"] = (2,)
71
+
72
+ for label, shape in data_shape.items():
73
+ content[label] |= {((0, 0, 0), i, i): np.zeros(shape) for i in range(n)}
74
+
75
+
76
+ def Hermitize(array):
77
+ """
78
+ Convert an array into its Hermitian form by constructing a Hermitian matrix.
79
+
80
+ A Hermitian matrix H has the property that H = H†, where H† is the conjugate transpose.
81
+ This means H[i,j] = conj(H[j,i]) for all indices i,j. The function takes an input array
82
+ representing the upper triangular part of the matrix and constructs the full Hermitian
83
+ matrix by:
84
+ 1. Placing the input values in the upper triangular part
85
+ 2. Computing the conjugate transpose of these values for the lower triangular part
86
+
87
+ This is commonly used in quantum mechanics and magnetic systems where Hamiltonians
88
+ must be Hermitian to ensure real eigenvalues.
89
+
90
+ Parameters
91
+ ----------
92
+ array : numpy.ndarray
93
+ Input array containing the upper triangular elements of the matrix.
94
+ Shape should be (n*(n+1)/2, ...) where n is the dimension of
95
+ the resulting square matrix.
96
+
97
+ Returns
98
+ -------
99
+ numpy.ndarray
100
+ Full Hermitian matrix with shape (n, n, ...), where n is computed
101
+ from the input array size. The result satisfies result[i,j] = conj(result[j,i])
102
+ for all indices i,j.
103
+
104
+ Example
105
+ -------
106
+ >>> arr = np.array([1+0j, 2+1j, 3+0j]) # Upper triangular elements for 2x2 matrix
107
+ >>> Hermitize(arr)
108
+ array([[1.+0.j, 2.+1.j],
109
+ [2.-1.j, 3.+0.j]])
110
+ """
111
+ n = int((2 * array.shape[0]) ** 0.5)
112
+ result = np.zeros((n, n) + array.shape[1:], dtype=complex)
113
+ u_indices = np.triu_indices(n)
114
+
115
+ # for python>=3.11
116
+ # result[*u_indices] = array
117
+ # result.swapaxes(0, 1)[*u_indices] = array.swapaxes(-1, -2).conj()
118
+ # for python<3.11
119
+ result[u_indices[0], u_indices[1]] = array
120
+ result.swapaxes(0, 1)[u_indices[0], u_indices[1]] = array.swapaxes(-1, -2).conj()
121
+
122
+ return result
123
+
124
+
125
+ class ExchangeIO(BaseMagneticStructure):
126
+ """
127
+ Class for handling magnetic exchange interactions and computing magnon properties.
128
+
129
+ This class provides functionality for:
130
+ - Managing magnetic structure information
131
+ - Computing exchange tensors
132
+ - Calculating magnon band structures
133
+ - Reading TB2J format files
134
+ - Visualizing magnon bands
135
+
136
+ Parameters
137
+ ----------
138
+ atoms : ase.Atoms, optional
139
+ ASE atoms object containing the structure
140
+ cell : array_like, optional
141
+ 3x3 matrix defining the unit cell
142
+ elements : list, optional
143
+ List of chemical symbols for atoms
144
+ positions : array_like, optional
145
+ Atomic positions
146
+ magmoms : array_like, optional
147
+ Magnetic moments for each atom
148
+ pbc : tuple, optional
149
+ Periodic boundary conditions, default (True, True, True)
150
+ magnetic_elements : list, optional
151
+ List of magnetic elements in the structure
152
+ kmesh : list, optional
153
+ k-point mesh dimensions, default [1, 1, 1]
154
+ collinear : bool, optional
155
+ Whether the magnetic structure is collinear, default True
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ atoms=None,
161
+ cell=None,
162
+ elements=None,
163
+ positions=None,
164
+ magmoms=None,
165
+ pbc=(True, True, True),
166
+ magnetic_elements=[],
167
+ kmesh=[1, 1, 1],
168
+ collinear=True,
169
+ ):
170
+ super().__init__(
171
+ atoms=atoms,
172
+ cell=cell,
173
+ elements=elements,
174
+ positions=positions,
175
+ pbc=pbc,
176
+ magmoms=magmoms,
177
+ collinear=collinear,
178
+ )
179
+
180
+ self.magnetic_elements = magnetic_elements
181
+ self.kmesh = kmesh
182
+
183
+ num_terms = 4 if collinear else 18
184
+ self._exchange_values = np.empty((0, 0, num_terms), dtype=float)
185
+
186
+ @property
187
+ def magnetic_elements(self):
188
+ """List of magnetic elements in the structure."""
189
+ return self._magnetic_elements
190
+
191
+ @magnetic_elements.setter
192
+ def magnetic_elements(self, value):
193
+ from .structure import validate_symbols
194
+
195
+ symbols = validate_symbols(value)
196
+ for symbol in symbols:
197
+ if symbol not in self.elements:
198
+ raise ValueError(f"Symbol '{symbol}' not in 'elements'.")
199
+
200
+ self._magnetic_elements = symbols
201
+ self._set_index_pairs()
202
+
203
+ @property
204
+ def interacting_pairs(self):
205
+ """List of pairs of interacting magnetic sites."""
206
+ return self._pairs
207
+
208
+ def _set_index_pairs(self):
209
+ from itertools import combinations_with_replacement
210
+
211
+ magnetic_elements = self.magnetic_elements
212
+ elements = self.elements
213
+ indices = [
214
+ i for i, symbol in enumerate(elements) if symbol in magnetic_elements
215
+ ]
216
+ index_pairs = list(combinations_with_replacement(indices, 2))
217
+ index_spin = np.sort(np.unique(index_pairs))
218
+
219
+ self._pairs = index_pairs
220
+ self._index_spin = index_spin
221
+
222
+ @property
223
+ def kmesh(self):
224
+ """K-point mesh dimensions for sampling the Brillouin zone."""
225
+ return self._kmesh
226
+
227
+ @kmesh.setter
228
+ def kmesh(self, values):
229
+ try:
230
+ the_kmesh = [int(k) for k in values]
231
+ except (ValueError, TypeError):
232
+ raise ValueError("Argument must be an iterable with 'int' elements.")
233
+ if len(the_kmesh) != 3:
234
+ raise ValueError("Argument must be of length 3.")
235
+ if any(k < 1 for k in the_kmesh):
236
+ raise ValueError("Argument must contain only positive numbers.")
237
+
238
+ self._kmesh = the_kmesh
239
+
240
+ @property
241
+ def vectors(self):
242
+ """Array of interaction vectors between magnetic sites."""
243
+ return self._exchange_values[:, :, :3]
244
+
245
+ def set_vectors(self, values=None, cartesian=False):
246
+ """
247
+ Set the interaction vectors between magnetic sites.
248
+
249
+ Parameters
250
+ ----------
251
+ values : array_like, optional
252
+ Array of interaction vectors
253
+ cartesian : bool, optional
254
+ Whether the vectors are in Cartesian coordinates, default False
255
+ """
256
+ try:
257
+ pairs = self._pairs
258
+ except AttributeError:
259
+ raise AttributeError("'magnetic_elements' attribute has not been set yet.")
260
+ else:
261
+ n_pairs = len(pairs)
262
+
263
+ if values is None:
264
+ i, j = zip(*pairs)
265
+ positions = self.positions
266
+ base_vectors = positions[i, :] - positions[j, :]
267
+ grid = generate_grid(self.kmesh)
268
+ vectors = base_vectors[:, None, :] + grid[None, :, :]
269
+ m_interactions = np.prod(self.kmesh)
270
+ else:
271
+ vectors = get_attribute_array(values, "vectors", dtype=float)
272
+ if vectors.ndim != 3 or vectors.shape[::2] != (n_pairs, 3):
273
+ raise ValueError(
274
+ f"'vectors' must have the shape (n, m, ), where n={n_pairs} is the number of\n"
275
+ "pairs of interacting species."
276
+ )
277
+ if cartesian:
278
+ vectors = np.linalg.solve(self.cell.T, vectors.swapaxes(1, -1))
279
+ vectors = vectors.swapaxes(-1, 1)
280
+ m_interactions = vectors.shape[1]
281
+
282
+ shape = (
283
+ (n_pairs, m_interactions, 4)
284
+ if self.collinear
285
+ else (n_pairs, m_interactions, 18)
286
+ )
287
+ exchange_values = np.zeros(shape, dtype=float)
288
+ exchange_values[:, :, :3] = vectors
289
+ self._exchange_values = exchange_values
290
+
291
+ def _get_neighbor_indices(self, neighbors, tol=1e-4):
292
+ """
293
+ Get indices of neighbor pairs based on distance.
294
+
295
+ Parameters
296
+ ----------
297
+ neighbors : list
298
+ List of neighbor shells to consider
299
+ tol : float, optional
300
+ Distance tolerance for neighbor shell assignment, default 1e-4
301
+
302
+ Returns
303
+ -------
304
+ tuple
305
+ Indices corresponding to the specified neighbor shells
306
+ """
307
+ distance = np.linalg.norm(self.vectors @ self.cell, axis=-1)
308
+ distance = round_to_precision(distance, tol)
309
+ neighbors_distance = np.unique(np.sort(distance))
310
+ indices = np.where(
311
+ distance[:, :, None] == neighbors_distance[neighbors][None, None, :]
312
+ )
313
+
314
+ return indices
315
+
316
+ def set_exchange_array(self, name, values, neighbors=None, tol=1e-4):
317
+ """
318
+ Set exchange interaction values for specified neighbors.
319
+
320
+ Parameters
321
+ ----------
322
+ name : str
323
+ Type of exchange interaction ('Jiso', 'Biquad', 'DMI', or 'Jani')
324
+ values : array_like
325
+ Exchange interaction values
326
+ neighbors : list, optional
327
+ List of neighbor shells to assign values to
328
+ tol : float, optional
329
+ Distance tolerance for neighbor shell assignment, default 1e-4
330
+ """
331
+ if self.vectors.size == 0:
332
+ raise AttributeError("The intraction vectors must be set first.")
333
+
334
+ array = get_attribute_array(values, name, dtype=float)
335
+
336
+ if neighbors is not None:
337
+ if len(array) != len(neighbors):
338
+ raise ValueError(
339
+ "The number of neighbors and exchange values does not coincide."
340
+ )
341
+ *array_indices, value_indices = self._get_neighbor_indices(
342
+ list(neighbors), tol=tol
343
+ )
344
+ else:
345
+ if self._exchange_values.shape[:2] != array.shape[:2]:
346
+ raise ValueError(
347
+ f"The shape of the array is incompatible with '{self.exchange_values.shape}'"
348
+ )
349
+ array_indices, value_indices = (
350
+ [slice(None), slice(None)],
351
+ (slice(None), slice(None)),
352
+ )
353
+
354
+ if name == "Jiso":
355
+ self._exchange_values[*array_indices, 3] = array[value_indices]
356
+ elif name == "Biquad":
357
+ self._exchange_values[*array_indices, 4:6] = array[value_indices]
358
+ elif name == "DMI":
359
+ self._exchange_values[*array_indices, 6:9] = array[value_indices]
360
+ elif name == "Jani":
361
+ self._exchange_values[*array_indices, 9:] = array[value_indices].reshape(
362
+ array.shape[:2] + (9,)
363
+ )
364
+ else:
365
+ raise ValueError(f"Unrecognized exchange array name: '{name}'.")
366
+
367
+ @property
368
+ def Jiso(self):
369
+ return self._exchange_values[:, :, 3]
370
+
371
+ @property
372
+ def Biquad(self):
373
+ return self._exchange_values[:, :, 4:6]
374
+
375
+ @property
376
+ def DMI(self):
377
+ return self._exchange_values[:, :, 6:9]
378
+
379
+ @property
380
+ def Jani(self):
381
+ matrix_shape = self._exchange_values.shape[:2] + (3, 3)
382
+ return self._exchange_values[:, :, 9:].reshape(matrix_shape)
383
+
384
+ def exchange_tensor(self, anisotropic=True):
385
+ """
386
+ Compute the exchange interaction tensor.
387
+
388
+ Parameters
389
+ ----------
390
+ anisotropic : bool, optional
391
+ Whether to include anisotropic interactions, default True
392
+
393
+ Returns
394
+ -------
395
+ numpy.ndarray
396
+ Exchange interaction tensor
397
+ """
398
+ shape = self._exchange_values.shape[:2] + (3, 3)
399
+ tensor = np.zeros(shape, dtype=float)
400
+
401
+ if anisotropic and not self.collinear:
402
+ # anisotropic exchange tensor
403
+ tensor += self._exchange_values[:, :, 9:].reshape(shape)
404
+ # DMI
405
+ pos_indices = ([1, 2, 0], [2, 0, 1])
406
+ neg_indices = ([2, 0, 1], [1, 2, 0])
407
+ tensor[:, :, *pos_indices] += self._exchange_values[:, :, 6:9]
408
+ tensor[:, :, *neg_indices] -= self._exchange_values[:, :, 6:9]
409
+ # isotropic exchange
410
+ diag_indices = ([0, 1, 2], [0, 1, 2])
411
+ tensor[:, :, *diag_indices] += self._exchange_values[:, :, 3, None]
412
+
413
+ return tensor
414
+
415
+ def Jq(self, kpoints, anisotropic=True):
416
+ """
417
+ Compute the exchange interactions in reciprocal space.
418
+
419
+ Parameters
420
+ ----------
421
+ kpoints : array_like
422
+ k-points at which to evaluate the exchange interactions
423
+ anisotropic : bool, optional
424
+ Whether to include anisotropic interactions, default True
425
+
426
+ Returns
427
+ -------
428
+ numpy.ndarray
429
+ Exchange interactions in reciprocal space
430
+ """
431
+ vectors = self._exchange_values[:, :, :3].copy()
432
+ tensor = self.exchange_tensor(anisotropic=anisotropic)
433
+
434
+ if self._Q is not None:
435
+ phi = 2 * np.pi * vectors.round(3).astype(int) @ self._Q
436
+ rv = np.einsum("ij,k->ijk", phi, self._n)
437
+ R = (
438
+ Rotation.from_rotvec(rv.reshape(-1, 3))
439
+ .as_matrix()
440
+ .reshape(vectors.shape[:2] + (3, 3))
441
+ )
442
+ np.einsum("nmij,nmjk->nmik", tensor, R, out=tensor)
443
+
444
+ exp_summand = np.exp(2j * np.pi * vectors @ kpoints.T)
445
+ Jexp = exp_summand[:, :, :, None, None] * tensor[:, :, None]
446
+ Jq = np.sum(Jexp, axis=1)
447
+
448
+ pairs = np.array(self._pairs)
449
+ idx = np.where(pairs[:, 0] == pairs[:, 1])
450
+ Jq[idx] /= 2
451
+
452
+ return Jq
453
+
454
+ def Hq(self, kpoints, anisotropic=True, u=uz):
455
+ """
456
+ Compute the magnon Hamiltonian in reciprocal space.
457
+
458
+ Parameters
459
+ ----------
460
+ kpoints : array_like
461
+ k-points at which to evaluate the Hamiltonian
462
+ anisotropic : bool, optional
463
+ Whether to include anisotropic interactions, default True
464
+ u : array_like, optional
465
+ Reference direction for spin quantization axis
466
+
467
+ Returns
468
+ -------
469
+ numpy.ndarray
470
+ Magnon Hamiltonian matrix at each k-point
471
+ """
472
+ if self.collinear:
473
+ magmoms = np.zeros((self._index_spin.size, 3))
474
+ magmoms[:, 2] = self.magmoms[self._index_spin]
475
+ else:
476
+ magmoms = self.magmoms[self._index_spin]
477
+ magmoms /= np.linalg.norm(magmoms, axis=-1)[:, None]
478
+
479
+ U, V = get_rotation_arrays(magmoms, u=u)
480
+
481
+ J0 = self.Jq(np.zeros((1, 3)), anisotropic=anisotropic)
482
+ J0 = -Hermitize(J0)[:, :, 0]
483
+ Jq = -Hermitize(self.Jq(kpoints, anisotropic=anisotropic))
484
+
485
+ C = np.diag(np.einsum("ix,ijxy,jy->i", V, 2 * J0, V))
486
+ B = np.einsum("ix,ijkxy,jy->kij", U, Jq, U)
487
+ A1 = np.einsum("ix,ijkxy,jy->kij", U, Jq, U.conj())
488
+ A2 = np.einsum("ix,ijkxy,jy->kij", U.conj(), Jq, U)
489
+
490
+ return np.block([[A1 - C, B], [B.swapaxes(-1, -2).conj(), A2 - C]])
491
+
492
+ def _magnon_energies(self, kpoints, anisotropic=True, u=uz):
493
+ H = self.Hq(kpoints, anisotropic=anisotropic, u=u)
494
+ n = H.shape[-1] // 2
495
+ I = np.eye(n)
496
+
497
+ min_eig = 0.0
498
+ try:
499
+ K = np.linalg.cholesky(H)
500
+ except np.linalg.LinAlgError:
501
+ try:
502
+ K = np.linalg.cholesky(H + 1e-6 * np.eye(2 * n))
503
+ except np.linalg.LinAlgError:
504
+ from warnings import warn
505
+
506
+ min_eig = np.min(np.linalg.eigvalsh(H))
507
+ K = np.linalg.cholesky(H - (min_eig - 1e-6) * np.eye(2 * n))
508
+ warn(
509
+ f"WARNING: The system may be far from the magnetic ground-state. Minimum eigenvalue: {min_eig}. The magnon energies might be unphysical."
510
+ )
511
+
512
+ g = np.block([[1 * I, 0 * I], [0 * I, -1 * I]])
513
+ KH = K.swapaxes(-1, -2).conj()
514
+
515
+ return np.linalg.eigvalsh(KH @ g @ K)[:, n:] + min_eig
516
+
517
+ def get_magnon_bands(
518
+ self,
519
+ kpoints: np.array = np.array([]),
520
+ path: str = None,
521
+ npoints: int = 300,
522
+ special_points: dict = None,
523
+ tol: float = 2e-4,
524
+ pbc: tuple = None,
525
+ cartesian: bool = False,
526
+ labels: list = None,
527
+ anisotropic: bool = True,
528
+ u: np.array = uz,
529
+ ):
530
+ pbc = self._pbc if pbc is None else pbc
531
+
532
+ if kpoints.size == 0:
533
+ from ase.cell import Cell
534
+
535
+ bandpath = Cell(self._cell).bandpath(
536
+ path=path,
537
+ npoints=npoints,
538
+ special_points=special_points,
539
+ eps=tol,
540
+ pbc=pbc,
541
+ )
542
+ kpoints = bandpath.kpts
543
+ spk = bandpath.special_points
544
+ spk[r"$\Gamma$"] = spk.pop("G", np.zeros(3))
545
+ labels = [
546
+ (i, symbol)
547
+ for symbol in spk
548
+ for i in np.where((kpoints == spk[symbol]).all(axis=1))[0]
549
+ ]
550
+ elif cartesian:
551
+ kpoints = np.linalg.solve(self._cell.T, kpoints.T).T
552
+
553
+ bands = self._magnon_energies(kpoints, anisotropic=anisotropic, u=u)
554
+
555
+ return labels, bands
556
+
557
+ def plot_magnon_bands(self, **kwargs):
558
+ """
559
+ Plot magnon band structure.
560
+
561
+ Parameters
562
+ ----------
563
+ **kwargs
564
+ Additional keyword arguments passed to get_magnon_bands and plotting functions
565
+ """
566
+ filename = kwargs.pop("filename", None)
567
+ kpath, bands = self.get_magnon_bands(**kwargs)
568
+ bands_plot = BandsPlot(bands, kpath)
569
+ bands_plot.plot(filename=filename)
570
+
571
+ @classmethod
572
+ def load_tb2j(
573
+ cls,
574
+ pickle_file: str = "TB2J.pickle",
575
+ pbc: tuple = (True, True, True),
576
+ anisotropic: bool = False,
577
+ quadratic: bool = False,
578
+ ):
579
+ from pickle import load
580
+
581
+ try:
582
+ with open(pickle_file, "rb") as File:
583
+ content = load(File)
584
+ except FileNotFoundError:
585
+ raise FileNotFoundError(
586
+ f"No such file or directory: '{pickle_file}'. Please provide a valid .pickle file."
587
+ )
588
+ else:
589
+ correct_content(content)
590
+
591
+ magmoms = content["magmoms"] if content["colinear"] else content["spinat"]
592
+ magnetic_elements = {
593
+ content["atoms"].numbers[i]
594
+ for i, j in enumerate(content["index_spin"])
595
+ if j > -1
596
+ }
597
+
598
+ exchange = cls(
599
+ atoms=content["atoms"],
600
+ magmoms=magmoms,
601
+ pbc=pbc,
602
+ collinear=content["colinear"],
603
+ magnetic_elements=magnetic_elements,
604
+ )
605
+
606
+ num_pairs = len(exchange.interacting_pairs)
607
+ bkeys = branched_keys(content["distance_dict"].keys(), num_pairs)
608
+
609
+ vectors = [
610
+ [content["distance_dict"][key][0] for key in branch] for branch in bkeys
611
+ ]
612
+ exchange.set_vectors(vectors, cartesian=True)
613
+ Jiso = [[content["exchange_Jdict"][key] for key in branch] for branch in bkeys]
614
+ exchange.set_exchange_array("Jiso", Jiso)
615
+
616
+ if not content["colinear"] and anisotropic:
617
+ Jani = [[content["Jani_dict"][key] for key in branch] for branch in bkeys]
618
+ exchange.set_exchange_array("Jani", Jani)
619
+ DMI = [[content["dmi_ddict"][key] for key in branch] for branch in bkeys]
620
+ exchange.set_exchange_array("DMI", DMI)
621
+ if quadratic:
622
+ Biquad = [
623
+ [content["biquadratic_Jdict"][key] for key in branch]
624
+ for branch in bkeys
625
+ ]
626
+ exchange.set_exchange_array("Biquad", Biquad)
627
+
628
+ return exchange
629
+
630
+
631
+ def plot_tb2j_magnon_bands(
632
+ pickle_file: str = "TB2J.pickle",
633
+ path: str = None,
634
+ npoints: int = 300,
635
+ special_points: dict = None,
636
+ anisotropic: bool = False,
637
+ quadratic: bool = False,
638
+ pbc: tuple = (True, True, True),
639
+ filename: str = None,
640
+ ):
641
+ """
642
+ Load TB2J data and plot magnon band structure in one step.
643
+
644
+ This is a convenience function that combines loading TB2J data and plotting
645
+ magnon bands. It first loads the magnetic structure and exchange interactions
646
+ from a TB2J pickle file, then calculates and plots the magnon band structure.
647
+
648
+ Parameters
649
+ ----------
650
+ pickle_file : str, optional
651
+ Path to the TB2J pickle file, default "TB2J.pickle"
652
+ path : str, optional
653
+ High-symmetry k-point path for band structure plot
654
+ (e.g., "GXMG" for a square lattice)
655
+ npoints : int, optional
656
+ Number of k-points for band structure calculation, default 300
657
+ special_points : dict, optional
658
+ Dictionary of special k-points for custom paths
659
+ anisotropic : bool, optional
660
+ Whether to include anisotropic interactions, default False
661
+ quadratic : bool, optional
662
+ Whether to include biquadratic interactions, default False
663
+ pbc : tuple, optional
664
+ Periodic boundary conditions, default (True, True, True)
665
+ filename : str, optional
666
+ If provided, save the plot to this file
667
+
668
+ Returns
669
+ -------
670
+ exchange : ExchangeIO
671
+ The ExchangeIO instance containing the loaded data and plot
672
+
673
+ Example
674
+ -------
675
+ >>> # Basic usage with default parameters
676
+ >>> plot_tb2j_magnon_bands()
677
+
678
+ >>> # Custom path and saving to file
679
+ >>> plot_tb2j_magnon_bands(
680
+ ... path="GXMG",
681
+ ... anisotropic=True,
682
+ ... filename="magnon_bands.png"
683
+ ... )
684
+ """
685
+ # Load the TB2J data
686
+ exchange = ExchangeIO.load_tb2j(
687
+ pickle_file=pickle_file, pbc=pbc, anisotropic=anisotropic, quadratic=quadratic
688
+ )
689
+
690
+ # Plot the magnon bands
691
+ exchange.plot_magnon_bands(
692
+ path=path, npoints=npoints, special_points=special_points, filename=filename
693
+ )
694
+
695
+ return exchange