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