TB2J 0.9.9.9__py3-none-any.whl → 0.9.9.12__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 (33) hide show
  1. TB2J/Jtensor.py +5 -6
  2. TB2J/MAEGreen.py +8 -0
  3. TB2J/__init__.py +1 -1
  4. TB2J/exchange.py +1 -1
  5. TB2J/interfaces/abacus/stru_api.py +3 -1
  6. TB2J/interfaces/siesta_interface.py +2 -1
  7. TB2J/io_exchange/io_exchange.py +65 -23
  8. TB2J/io_exchange/io_vampire.py +6 -3
  9. TB2J/io_merge.py +15 -1
  10. TB2J/magnon/__init__.py +2 -2
  11. TB2J/magnon/magnon3.py +566 -56
  12. TB2J/magnon/magnon_band.py +185 -0
  13. TB2J/magnon/magnon_math.py +1 -0
  14. TB2J/magnon/plot.py +60 -21
  15. TB2J/mathutils/auto_kpath.py +151 -0
  16. tb2j-0.9.9.12.data/scripts/TB2J_plot_magnon_bands.py +7 -0
  17. {tb2j-0.9.9.9.dist-info → tb2j-0.9.9.12.dist-info}/METADATA +4 -2
  18. {tb2j-0.9.9.9.dist-info → tb2j-0.9.9.12.dist-info}/RECORD +33 -30
  19. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/TB2J_downfold.py +0 -0
  20. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/TB2J_eigen.py +0 -0
  21. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/TB2J_magnon.py +0 -0
  22. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/TB2J_magnon2.py +0 -0
  23. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/TB2J_magnon_dos.py +0 -0
  24. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/TB2J_merge.py +0 -0
  25. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/TB2J_rotate.py +0 -0
  26. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/TB2J_rotateDM.py +0 -0
  27. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/abacus2J.py +0 -0
  28. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/siesta2J.py +0 -0
  29. {tb2j-0.9.9.9.data → tb2j-0.9.9.12.data}/scripts/wann2J.py +0 -0
  30. {tb2j-0.9.9.9.dist-info → tb2j-0.9.9.12.dist-info}/WHEEL +0 -0
  31. {tb2j-0.9.9.9.dist-info → tb2j-0.9.9.12.dist-info}/entry_points.txt +0 -0
  32. {tb2j-0.9.9.9.dist-info → tb2j-0.9.9.12.dist-info}/licenses/LICENSE +0 -0
  33. {tb2j-0.9.9.9.dist-info → tb2j-0.9.9.12.dist-info}/top_level.txt +0 -0
TB2J/magnon/magnon3.py CHANGED
@@ -1,11 +1,58 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import asdict, dataclass
2
+ from pathlib import Path
3
+ from typing import List, Optional, Tuple, Union
2
4
 
3
5
  import numpy as np
6
+ import tomli
7
+ import tomli_w
4
8
  from scipy.spatial.transform import Rotation
5
9
 
6
- from ..io_exchange import SpinIO
7
- from ..mathutils import Hermitize, get_rotation_arrays
8
- from .plot import BandsPlot
10
+ from TB2J.io_exchange import SpinIO
11
+ from TB2J.magnon.magnon_band import MagnonBand
12
+ from TB2J.magnon.magnon_math import get_rotation_arrays
13
+ from TB2J.mathutils.auto_kpath import auto_kpath
14
+
15
+
16
+ @dataclass
17
+ class MagnonParameters:
18
+ """Parameters for magnon band structure calculations"""
19
+
20
+ path: str = "TB2J_results"
21
+ kpath: str = None
22
+ npoints: int = 300
23
+ filename: str = "magnon_bands.png"
24
+ Jiso: bool = True
25
+ Jani: bool = False
26
+ DMI: bool = False
27
+ Q: Optional[List[float]] = None
28
+ uz_file: Optional[str] = None
29
+ n: Optional[List[float]] = None
30
+ show: bool = False
31
+
32
+ @classmethod
33
+ def from_toml(cls, filename: str) -> "MagnonParameters":
34
+ """Load parameters from a TOML file"""
35
+ with open(filename, "rb") as f:
36
+ data = tomli.load(f)
37
+ return cls(**data)
38
+
39
+ def to_toml(self, filename: str):
40
+ """Save parameters to a TOML file"""
41
+ # Convert to dict and remove None values
42
+ data = {k: v for k, v in asdict(self).items() if v is not None}
43
+ with open(filename, "wb") as f:
44
+ tomli_w.dump(data, f)
45
+
46
+ def __post_init__(self):
47
+ """Validate parameters after initialization"""
48
+ if self.Q is not None and len(self.Q) != 3:
49
+ raise ValueError("Q must be a list of 3 numbers")
50
+ if self.n is not None and len(self.n) != 3:
51
+ raise ValueError("n must be a list of 3 numbers")
52
+
53
+ # Convert path to absolute path if uz_file is relative to it
54
+ if self.uz_file and not Path(self.uz_file).is_absolute():
55
+ self.uz_file = str(Path(self.path) / self.uz_file)
9
56
 
10
57
 
11
58
  @dataclass
@@ -19,9 +66,11 @@ class Magnon:
19
66
  magmom: np.ndarray
20
67
  Rlist: np.ndarray
21
68
  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)
69
+ cell: np.ndarray
70
+ _Q: np.ndarray
71
+ _uz: np.ndarray
72
+ _n: np.ndarray
73
+ pbc: tuple = (True, True, True)
25
74
 
26
75
  def set_reference(self, Q, uz, n):
27
76
  """
@@ -102,7 +151,7 @@ class Magnon:
102
151
  # J'_mn(R) = R_m(ϕ)^T J(R) R_n(ϕ) using Einstein summation.
103
152
  # Here m is always in the R=0, thus the rotation is only applied on the
104
153
  # n , so only on the right.
105
- JRprime[iR] = np.einsum(" rijxy, yb -> rijab", JR[iR], Rmat)
154
+ JRprime[iR] = np.einsum(" ijxy, yb -> ijxb", JR[iR], Rmat)
106
155
 
107
156
  nkpt = kpoints.shape[0]
108
157
  Jq = np.zeros((nkpt, self.nspin, self.nspin, 3, 3), dtype=complex)
@@ -113,15 +162,13 @@ class Magnon:
113
162
  phase = 2 * np.pi * R @ qpt
114
163
  Jq[iqpt] += np.exp(1j * phase) * JRprime[iR]
115
164
 
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
165
+ #Jq_copy = Jq.copy()
166
+ #Jq.swapaxes(-1, -2) # swap xyz
167
+ #Jq.swapaxes(-3, -4) # swap ij
168
+ #Jq = (Jq.conj() + Jq_copy) / 2.0
122
169
  return Jq
123
170
 
124
- def Hq(self, kpoints, anisotropic=True):
171
+ def Hq(self, kpoints):
125
172
  """
126
173
  Compute the magnon Hamiltonian in reciprocal space.
127
174
 
@@ -140,22 +187,28 @@ class Magnon:
140
187
  magmoms = self.magmom.copy()
141
188
  magmoms /= np.linalg.norm(magmoms, axis=-1)[:, None]
142
189
 
190
+
143
191
  U, V = get_rotation_arrays(magmoms, u=self._uz)
144
192
 
145
- J0 = self.Jq(np.zeros((1, 3)), anisotropic=anisotropic)
146
- J0 = -Hermitize(J0)[:, :, 0]
147
- Jq = -Hermitize(self.Jq(kpoints, anisotropic=anisotropic))
193
+ J0 = -self.Jq(np.zeros((1, 3)))[0]
194
+ # J0 = -Hermitize(J0)[:, :, 0]
195
+ # Jq = -Hermitize(self.Jq(kpoints, anisotropic=anisotropic))
196
+
197
+ Jq = -self.Jq(kpoints)
198
+ print(f"J0 shape: {J0.shape}")
148
199
 
149
200
  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)
201
+ B = np.einsum("ix,kijxy,jy->kij", U, Jq, U)
202
+ A1 = np.einsum("ix,kijxy,jy->kij", U, Jq, U.conj())
203
+ A2 = np.einsum("ix,kijxy,jy->kij", U.conj(), Jq, U)
153
204
 
154
- return np.block([[A1 - C, B], [B.swapaxes(-1, -2).conj(), A2 - C]])
205
+ H = np.block([[A1 - C, B], [B.swapaxes(-1, -2).conj(), A2 - C]])
206
+ print(f"H shape: {H.shape}")
207
+ return H
155
208
 
156
- def _magnon_energies(self, kpoints, anisotropic=True, u=None):
209
+ def _magnon_energies(self, kpoints, u=None):
157
210
  """Calculate magnon energies"""
158
- H = self.Hq(kpoints, anisotropic=anisotropic)
211
+ H = self.Hq(kpoints)
159
212
  n = H.shape[-1] // 2
160
213
  I = np.eye(n)
161
214
 
@@ -176,8 +229,9 @@ class Magnon:
176
229
 
177
230
  g = np.block([[1 * I, 0 * I], [0 * I, -1 * I]])
178
231
  KH = K.swapaxes(-1, -2).conj()
179
-
232
+ # Why only n:?
180
233
  return np.linalg.eigvalsh(KH @ g @ K)[:, n:] + min_eig
234
+ # return np.linalg.eigvalsh(KH @ g @ K)[:, :] + min_eig
181
235
 
182
236
  def get_magnon_bands(
183
237
  self,
@@ -192,33 +246,84 @@ class Magnon:
192
246
  anisotropic: bool = True,
193
247
  u: np.array = None,
194
248
  ):
195
- """Get magnon band structure"""
196
- pbc = self._pbc if pbc is None else pbc
249
+ """Get magnon band structure.
250
+
251
+ Parameters
252
+ ----------
253
+ kpoints : np.array, optional
254
+ Explicit k-points to calculate bands at. If empty, generates k-points from path.
255
+ path : str, optional
256
+ String specifying the k-path. If None, generates automatically using auto_kpath.
257
+ npoints : int, optional
258
+ Number of k-points along the path. Default is 300.
259
+ special_points : dict, optional
260
+ Dictionary of special points coordinates.
261
+ tol : float, optional
262
+ Tolerance for k-point comparisons. Default is 2e-4.
263
+ pbc : tuple, optional
264
+ Periodic boundary conditions. Default is None.
265
+ cartesian : bool, optional
266
+ Whether k-points are in cartesian coordinates. Default is False.
267
+ labels : list, optional
268
+ List of k-point labels. Default is None.
269
+ anisotropic : bool, optional
270
+ Whether to include anisotropic interactions. Default is True.
271
+ u : np.array, optional
272
+ Quantization axis. Default is None.
273
+
274
+ Returns
275
+ -------
276
+ tuple
277
+ - labels : list of (index, name) tuples for special k-points
278
+ - bands : array of band energies
279
+ - xlist : list of arrays with x-coordinates for plotting (if using auto_kpath)
280
+ """
281
+ pbc = self.pbc if pbc is None else pbc
282
+ pbc = [True, True, True]
197
283
  u = self._uz if u is None else u
198
284
  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
- ]
285
+ if path is None:
286
+ # Use auto_kpath to generate path automatically
287
+ xlist, kptlist, Xs, knames, spk = auto_kpath(
288
+ self.cell, None, npoints=npoints
289
+ )
290
+ kpoints = np.concatenate(kptlist)
291
+ # Create labels from special points
292
+ labels = []
293
+ current_pos = 0
294
+ for i, (x, k) in enumerate(zip(xlist, kptlist)):
295
+ for name in knames:
296
+ matches = np.where((k == spk[name]).all(axis=1))[0]
297
+ if matches.size > 0:
298
+ labels.append((matches[0] + current_pos, name))
299
+ current_pos += len(k)
300
+ else:
301
+ bandpath = self.cell.bandpath(
302
+ path=path,
303
+ npoints=npoints,
304
+ special_points=special_points,
305
+ eps=tol,
306
+ pbc=pbc,
307
+ )
308
+ kpoints = bandpath.kpts
309
+ spk = bandpath.special_points
310
+ spk[r"$\Gamma$"] = spk.pop("G", np.zeros(3))
311
+ labels = [
312
+ (i, symbol)
313
+ for symbol in spk
314
+ for i in np.where((kpoints == spk[symbol]).all(axis=1))[0]
315
+ ]
216
316
  elif cartesian:
217
- kpoints = np.linalg.solve(self._cell.T, kpoints.T).T
317
+ kpoints = np.linalg.solve(self.cell.T, kpoints.T).T
218
318
 
219
- bands = self._magnon_energies(kpoints, anisotropic=anisotropic)
319
+ bands = self._magnon_energies(kpoints)
320
+ print(f"bands shape: {bands.shape}")
220
321
 
221
- return labels, bands
322
+ if path is None and kpoints.size == 0: # Fixed condition
323
+ # When using auto_kpath, return xlist for segmented plotting
324
+ return labels, bands, xlist
325
+ else:
326
+ return labels, bands, None
222
327
 
223
328
  def plot_magnon_bands(self, **kwargs):
224
329
  """
@@ -227,12 +332,41 @@ class Magnon:
227
332
  Parameters
228
333
  ----------
229
334
  **kwargs
230
- Additional keyword arguments passed to get_magnon_bands and plotting functions
335
+ Additional keyword arguments passed to get_magnon_bands and plotting functions.
336
+ Supported plotting options:
337
+ - filename : str, optional
338
+ Output filename for saving the plot
339
+ - ax : matplotlib.axes.Axes, optional
340
+ Axes for plotting. If None, creates new figure
341
+ - show : bool, optional
342
+ Whether to show the plot on screen
231
343
  """
232
344
  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)
345
+ kpath_labels, bands, xlist = self.get_magnon_bands(**kwargs)
346
+
347
+ # Get k-points and special points
348
+ if "path" in kwargs and kwargs["path"] is None:
349
+ _, kptlist, _, _, spk = auto_kpath(
350
+ self.cell, None, npoints=kwargs.get("npoints", 300)
351
+ )
352
+ kpoints = np.concatenate(kptlist)
353
+ else:
354
+ bandpath = self.cell.bandpath(
355
+ path=kwargs.get("path", "GXMG"), npoints=kwargs.get("npoints", 300)
356
+ )
357
+ kpoints = bandpath.kpts
358
+ spk = bandpath.special_points.copy()
359
+ spk[r"$\Gamma$"] = spk.pop("G", np.zeros(3))
360
+
361
+ bands_plot = MagnonBand(
362
+ energies=bands * 1000, # Convert to meV
363
+ kpoints=kpoints,
364
+ kpath_labels=kpath_labels,
365
+ special_points=spk,
366
+ xcoords=xlist,
367
+ )
368
+
369
+ return bands_plot.plot(filename=filename, **kwargs)
236
370
 
237
371
  @classmethod
238
372
  def load_from_io(cls, exc: SpinIO, **kwargs):
@@ -251,11 +385,23 @@ class Magnon:
251
385
  Magnon
252
386
  Initialized Magnon instance
253
387
  """
388
+ # Get magnetic moments for magnetic atoms
389
+ magmoms = exc.get_magnetic_moments()
390
+ # nspin = len(magmoms) # Number of magnetic atoms
391
+
392
+ cell = exc.atoms.get_cell()
393
+ pbc = exc.atoms.get_pbc()
394
+
254
395
  return cls(
255
396
  nspin=exc.nspin,
256
- magmom=exc.magmoms,
397
+ magmom=magmoms,
257
398
  Rlist=exc.Rlist,
258
- JR=exc.get_full_Jtensor_for_Rlist(order="ij33", **kwargs),
399
+ JR=exc.get_full_Jtensor_for_Rlist(order="ij33", asr=False, **kwargs),
400
+ cell=cell,
401
+ _Q=np.zeros(3), # Default propagation vector
402
+ _uz=np.array([[0.0, 0.0, 1.0]]), # Default quantization axis
403
+ _n=np.array([0.0, 0.0, 1.0]), # Default rotation axis
404
+ pbc=pbc,
259
405
  )
260
406
 
261
407
  @classmethod
@@ -294,7 +440,7 @@ def test_magnon(path="TB2J_results"):
294
440
 
295
441
  # Load magnon calculator from TB2J results
296
442
  print(f"Loading exchange parameters from {path}...")
297
- magnon = Magnon.from_TB2J_results(path=path, iso_only=True)
443
+ magnon = Magnon.from_TB2J_results(path=path, Jiso=True, Jani=False, DMI=False)
298
444
 
299
445
  # Define high-symmetry points for a cube
300
446
  kpoints = np.array(
@@ -325,10 +471,374 @@ def test_magnon(path="TB2J_results"):
325
471
  print("-" * 50)
326
472
  for i, (k, label) in enumerate(zip(kpoints, klabels)):
327
473
  print(f"\n{label}-point k={k}:")
328
- print(f"Energies: {energies[i] * 1000:.3f} meV") # Convert to meV
474
+ # print(f"Energies: {energies[i] * 1000:.3f} meV") # Convert to meV
475
+ print(f"Energies: {energies[i] * 1000} meV") # Convert to meV
476
+
477
+ print("\nPlotting magnon bands...")
478
+ magnon.plot_magnon_bands(
479
+ # kpoints=kpoints,
480
+ # labels=klabels,
481
+ path="GHPGPH,PN",
482
+ filename="magnon_bands.png",
483
+ )
329
484
 
330
485
  return magnon, Jq, energies
331
486
 
332
487
 
488
+ def create_plot_script(filename: str):
489
+ """Create a Python script for plotting the saved band structure data.
490
+
491
+ Parameters
492
+ ----------
493
+ filename : str
494
+ Base filename (without extension) to use for the plot script
495
+ """
496
+ script_name = f"plot_{filename}.py"
497
+ script = '''#!/usr/bin/env python3
498
+ """Simple script to plot magnon band structure from saved data."""
499
+
500
+ from TB2J.magnon.magnon_band import MagnonBand
501
+ import matplotlib.pyplot as plt
502
+
503
+ def plot_magnon_bands(input_file, output_file=None, ax=None, color='blue', show=True):
504
+ """Load and plot magnon band structure.
505
+
506
+ Parameters
507
+ ----------
508
+ input_file : str
509
+ JSON file containing band structure data
510
+ output_file : str, optional
511
+ Output file for saving the plot
512
+ ax : matplotlib.axes.Axes, optional
513
+ Axes for plotting. If None, creates new figure
514
+ color : str, optional
515
+ Color of the band lines (default: blue)
516
+ show : bool, optional
517
+ Whether to show the plot on screen (default: True)
518
+
519
+ Returns
520
+ -------
521
+ matplotlib.axes.Axes
522
+ The plotting axes
523
+ """
524
+ # Load band structure data
525
+ bands = MagnonBand.load(input_file)
526
+
527
+ # Create plot
528
+ ax = bands.plot(
529
+ ax=ax,
530
+ filename=output_file,
531
+ color=color,
532
+ show=show
533
+ )
534
+ return ax
535
+
536
+ if __name__ == "__main__":
537
+ # Usage example
538
+ # Example usage
539
+ import matplotlib.pyplot as plt
540
+
541
+ # Create a figure and axis (optional)
542
+ fig, ax = plt.subplots(figsize=(6, 4))
543
+
544
+ # Plot bands with custom color on given axis
545
+ plot_magnon_bands(
546
+ input_file="magnon_bands.json",
547
+ output_file="magnon_bands.png",
548
+ ax=ax,
549
+ color='red',
550
+ show=True
551
+ )
552
+ '''
553
+
554
+ with open(script_name, "w") as f:
555
+ f.write(script)
556
+
557
+ import os
558
+
559
+ os.chmod(script_name, 0o755) # Make executable
560
+
561
+
562
+ def save_bands_data(
563
+ kpoints: np.ndarray,
564
+ energies: np.ndarray,
565
+ kpath_labels: List[Tuple[int, str]],
566
+ special_points: dict,
567
+ xcoords: Optional[Union[np.ndarray, List[np.ndarray]]],
568
+ filename: str,
569
+ ):
570
+ """Save magnon band structure data to a JSON file using MagnonBand class.
571
+
572
+ Parameters
573
+ ----------
574
+ kpoints : array_like
575
+ Array of k-points coordinates
576
+ energies : array_like
577
+ Array of band energies (in meV)
578
+ kpath_labels : list of (int, str)
579
+ List of tuples containing k-point indices and their labels
580
+ special_points : dict
581
+ Dictionary of special points and their coordinates
582
+ xcoords : array_like or list of arrays
583
+ x-coordinates for plotting (can be segmented)
584
+ filename : str
585
+ Output filename
586
+ """
587
+ from TB2J.magnon.magnon_band import MagnonBand # Using same import as above
588
+
589
+ bands = MagnonBand(
590
+ energies=energies,
591
+ kpoints=kpoints,
592
+ kpath_labels=kpath_labels,
593
+ special_points=special_points,
594
+ xcoords=xcoords,
595
+ )
596
+ bands.save(filename)
597
+
598
+ # Create plotting script
599
+ base_name = filename.rsplit(".", 1)[0]
600
+ create_plot_script(base_name)
601
+
602
+ print(f"Band structure data saved to {filename}")
603
+ print(f"Created plotting script: plot_{base_name}.py")
604
+ print("Usage: ")
605
+ print(f"See plot_{base_name}.py for example usage")
606
+
607
+ return bands
608
+
609
+
610
+ def plot_magnon_bands_from_TB2J(
611
+ params: MagnonParameters,
612
+ ):
613
+ """
614
+ Load TB2J results and plot magnon band structure along a specified k-path.
615
+
616
+ Parameters
617
+ ----------
618
+ path : str, optional
619
+ Path to TB2J results directory, default is "TB2J_results"
620
+ kpath : str, optional
621
+ String specifying the k-path, e.g. "GXMR" for Gamma-X-M-R path
622
+ Default is "GXMR"
623
+ npoints : int, optional
624
+ Number of k-points along the path, default is 300
625
+ filename : str, optional
626
+ Output file name for the plot, default is "magnon_bands.png"
627
+ Jiso : bool, optional
628
+ Include isotropic exchange interactions, default is True
629
+ Jani : bool, optional
630
+ Include anisotropic exchange interactions, default is False
631
+ DMI : bool, optional
632
+ Include Dzyaloshinskii-Moriya interactions, default is False
633
+ Q : array-like, optional
634
+ Propagation vector [Qx, Qy, Qz], default is [0, 0, 0]
635
+ uz_file : str, optional
636
+ Path to file containing quantization axes for each spin (natom×3 array)
637
+ If not provided, default [0, 0, 1] will be used for all spins
638
+ n : array-like, optional
639
+ Normal vector for rotation [nx, ny, nz], default is [0, 0, 1]
640
+ show: bool, optional
641
+ whether to show figure.
642
+
643
+ Returns
644
+ -------
645
+ magnon : Magnon
646
+ The Magnon instance used for calculations
647
+ """
648
+ # Load magnon calculator from TB2J results
649
+ print(f"Loading exchange parameters from {params.path}...")
650
+ magnon = Magnon.from_TB2J_results(
651
+ path=params.path, Jiso=params.Jiso, Jani=params.Jani, DMI=params.DMI
652
+ )
653
+
654
+ # Set reference vectors if provided
655
+ if any(x is not None for x in [params.Q, params.uz_file, params.n]):
656
+ Q = [0, 0, 0] if params.Q is None else params.Q
657
+ n = [0, 0, 1] if params.n is None else params.n
658
+
659
+ # Handle quantization axes
660
+ if params.uz_file is not None:
661
+ uz = np.loadtxt(params.uz_file)
662
+ if uz.shape[1] != 3:
663
+ raise ValueError(
664
+ f"Quantization axes file should contain a natom×3 array. Got shape {uz.shape}"
665
+ )
666
+ if uz.shape[0] != magnon.nspin:
667
+ raise ValueError(
668
+ f"Number of spins in uz file ({uz.shape[0]}) does not match the system ({magnon.nspin})"
669
+ )
670
+ else:
671
+ # Default: [0, 0, 1] for all spins
672
+ uz = np.array([[0.0, 0.0, 1.0] for _ in range(magnon.nspin)])
673
+
674
+ magnon.set_reference(Q, uz, n)
675
+
676
+ # Get band structure data
677
+ print(f"\nCalculating bands along path {params.kpath}...")
678
+ kpath_labels, bands, xlist = magnon.get_magnon_bands(
679
+ path=params.kpath,
680
+ npoints=params.npoints,
681
+ )
682
+
683
+ # Convert energies to meV
684
+ bands_meV = bands * 1000
685
+
686
+ # Save band structure data and create plot
687
+ data_file = params.filename.rsplit(".", 1)[0] + ".json"
688
+ print(f"\nSaving band structure data to {data_file}")
689
+
690
+ # Get k-points and special points
691
+ if params.kpath is None:
692
+ _, kptlist, _, _, spk = auto_kpath(magnon.cell, None, npoints=params.npoints)
693
+ kpoints = np.concatenate(kptlist)
694
+ else:
695
+ bandpath = magnon.cell.bandpath(path=params.kpath, npoints=params.npoints)
696
+ kpoints = bandpath.kpts
697
+ spk = bandpath.special_points
698
+ spk[r"$\Gamma$"] = spk.pop("G", np.zeros(3))
699
+
700
+ magnon_bands = save_bands_data(
701
+ kpoints=kpoints,
702
+ energies=bands_meV,
703
+ kpath_labels=kpath_labels,
704
+ special_points=spk,
705
+ xcoords=xlist,
706
+ filename=data_file,
707
+ )
708
+
709
+ # Plot band structure
710
+ print(f"Plotting bands to {params.filename}")
711
+ magnon_bands.plot(filename=params.filename)
712
+
713
+ return magnon
714
+
715
+
716
+ def main():
717
+ """Command line interface for plotting magnon bands from TB2J results."""
718
+ import argparse
719
+
720
+ parser = argparse.ArgumentParser(
721
+ description="Plot magnon band structure from TB2J results"
722
+ )
723
+
724
+ # Add a mutually exclusive group for config file vs. command line arguments
725
+ group = parser.add_mutually_exclusive_group()
726
+ group.add_argument(
727
+ "--config",
728
+ type=str,
729
+ help="Path to TOML configuration file",
730
+ )
731
+ group.add_argument(
732
+ "--save-config",
733
+ type=str,
734
+ help="Save default configuration to specified TOML file",
735
+ )
736
+
737
+ # Command line arguments (used if no config file is provided)
738
+ parser.add_argument(
739
+ "--path",
740
+ default="TB2J_results",
741
+ help="Path to TB2J results directory (default: TB2J_results)",
742
+ )
743
+ parser.add_argument(
744
+ "--kpath",
745
+ default=None,
746
+ help="k-path specification (default: auto-detected from type of cell)",
747
+ )
748
+ parser.add_argument(
749
+ "--npoints",
750
+ type=int,
751
+ default=300,
752
+ help="Number of k-points along the path (default: 300)",
753
+ )
754
+ parser.add_argument(
755
+ "--output",
756
+ default="magnon_bands.png",
757
+ help="Output file name (default: magnon_bands.png)",
758
+ )
759
+ parser.add_argument(
760
+ "--Jiso",
761
+ action="store_true",
762
+ default=True,
763
+ help="Include isotropic exchange interactions (default: True)",
764
+ )
765
+ parser.add_argument(
766
+ "--no-Jiso",
767
+ action="store_false",
768
+ dest="Jiso",
769
+ help="Exclude isotropic exchange interactions",
770
+ )
771
+ parser.add_argument(
772
+ "--Jani",
773
+ action="store_true",
774
+ default=False,
775
+ help="Include anisotropic exchange interactions (default: False)",
776
+ )
777
+ parser.add_argument(
778
+ "--DMI",
779
+ action="store_true",
780
+ default=False,
781
+ help="Include Dzyaloshinskii-Moriya interactions (default: False)",
782
+ )
783
+ parser.add_argument(
784
+ "--Q",
785
+ nargs=3,
786
+ type=float,
787
+ metavar=("Qx", "Qy", "Qz"),
788
+ help="Propagation vector [Qx, Qy, Qz] (default: [0, 0, 0])",
789
+ )
790
+ parser.add_argument(
791
+ "--uz-file",
792
+ type=str,
793
+ help="Path to file containing quantization axes for each spin (natom×3 array)",
794
+ )
795
+ parser.add_argument(
796
+ "--n",
797
+ nargs=3,
798
+ type=float,
799
+ metavar=("nx", "ny", "nz"),
800
+ help="Normal vector for rotation [nx, ny, nz] (default: [0, 0, 1])",
801
+ )
802
+
803
+ parser.add_argument(
804
+ "--show",
805
+ action="store_true",
806
+ default=False,
807
+ help="show figure on screen.",
808
+ )
809
+
810
+
811
+ args = parser.parse_args()
812
+
813
+ # Handle configuration file options
814
+ if args.save_config:
815
+ # Create default parameters and save to file
816
+ params = MagnonParameters()
817
+ params.to_toml(args.save_config)
818
+ print(f"Saved default configuration to {args.save_config}")
819
+ return
820
+
821
+ if args.config:
822
+ # Load parameters from config file
823
+ params = MagnonParameters.from_toml(args.config)
824
+ else:
825
+ # Create parameters from command line arguments
826
+ params = MagnonParameters(
827
+ path=args.path,
828
+ kpath=args.kpath,
829
+ npoints=args.npoints,
830
+ filename=args.output,
831
+ Jiso=args.Jiso,
832
+ Jani=args.Jani,
833
+ DMI=args.DMI,
834
+ Q=args.Q if args.Q is not None else None,
835
+ uz_file=args.uz_file,
836
+ n=args.n if args.n is not None else None,
837
+ show=args.show
838
+ )
839
+
840
+ plot_magnon_bands_from_TB2J(params)
841
+
842
+
333
843
  if __name__ == "__main__":
334
- test_magnon()
844
+ main()