TB2J 0.9.12.18__py3-none-any.whl → 0.9.12.22__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 (40) hide show
  1. TB2J/MAE.py +8 -1
  2. TB2J/MAEGreen.py +0 -2
  3. TB2J/exchange.py +11 -4
  4. TB2J/exchangeCL2.py +2 -0
  5. TB2J/exchange_params.py +24 -0
  6. TB2J/green.py +15 -3
  7. TB2J/interfaces/abacus/gen_exchange_abacus.py +2 -1
  8. TB2J/io_exchange/__init__.py +19 -1
  9. TB2J/io_exchange/edit.py +594 -0
  10. TB2J/io_exchange/io_exchange.py +238 -74
  11. TB2J/io_exchange/io_tomsasd.py +4 -3
  12. TB2J/io_exchange/io_txt.py +72 -7
  13. TB2J/io_exchange/io_vampire.py +1 -1
  14. TB2J/io_merge.py +60 -40
  15. TB2J/magnon/magnon3.py +27 -2
  16. TB2J/mathutils/rotate_spin.py +7 -7
  17. TB2J/mycfr.py +11 -11
  18. TB2J/plot.py +26 -0
  19. TB2J/scripts/TB2J_edit.py +403 -0
  20. TB2J/scripts/TB2J_plot_exchange.py +48 -0
  21. TB2J/spinham/hamiltonian.py +156 -13
  22. TB2J/spinham/hamiltonian_terms.py +40 -1
  23. TB2J/spinham/spin_xml.py +40 -8
  24. TB2J/symmetrize_J.py +138 -7
  25. TB2J/tests/test_cli_remove_sublattice.py +33 -0
  26. TB2J/tests/test_cli_toggle_exchange.py +50 -0
  27. {tb2j-0.9.12.18.dist-info → tb2j-0.9.12.22.dist-info}/METADATA +7 -3
  28. {tb2j-0.9.12.18.dist-info → tb2j-0.9.12.22.dist-info}/RECORD +32 -35
  29. {tb2j-0.9.12.18.dist-info → tb2j-0.9.12.22.dist-info}/WHEEL +1 -1
  30. {tb2j-0.9.12.18.dist-info → tb2j-0.9.12.22.dist-info}/entry_points.txt +2 -0
  31. TB2J/.gitignore +0 -5
  32. TB2J/agent_files/debug_spinphon_fd/debug_main.py +0 -156
  33. TB2J/agent_files/debug_spinphon_fd/test_compute_dJdx.py +0 -272
  34. TB2J/agent_files/debug_spinphon_fd/test_ispin0_only.py +0 -120
  35. TB2J/agent_files/debug_spinphon_fd/test_no_d2j.py +0 -31
  36. TB2J/agent_files/debug_spinphon_fd/test_with_d2j.py +0 -28
  37. TB2J/interfaces/abacus/test_read_HRSR.py +0 -43
  38. TB2J/interfaces/abacus/test_read_stru.py +0 -32
  39. {tb2j-0.9.12.18.dist-info → tb2j-0.9.12.22.dist-info}/licenses/LICENSE +0 -0
  40. {tb2j-0.9.12.18.dist-info → tb2j-0.9.12.22.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,594 @@
1
+ """
2
+ TB2J_edit: A library for modifying TB2J results.
3
+
4
+ This library provides a simple interface for editing TB2J exchange parameters,
5
+ including single ion anisotropy, DMI, anisotropic exchange, and symmetry operations.
6
+
7
+ Example:
8
+ >>> from TB2J.io_exchange.edit import load, set_anisotropy, toggle_DMI, save
9
+ >>> spinio = load('TB2J_results/TB2J.pickle')
10
+ >>> set_anisotropy(spinio, species='Sm', k1=5.0, k1dir=[0, 0, 1])
11
+ >>> toggle_DMI(spinio, enabled=False)
12
+ >>> save(spinio, 'modified_results')
13
+ """
14
+
15
+ import os
16
+
17
+ import numpy as np
18
+
19
+ from TB2J.io_exchange.io_exchange import SpinIO
20
+
21
+ __all__ = [
22
+ "load",
23
+ "save",
24
+ "set_anisotropy",
25
+ "set_sia_tensor",
26
+ "remove_sia_tensor",
27
+ "toggle_DMI",
28
+ "toggle_Jani",
29
+ "toggle_exchange",
30
+ "remove_sublattice",
31
+ "symmetrize_exchange",
32
+ ]
33
+
34
+
35
+ def load(path="TB2J_results/TB2J.pickle"):
36
+ """
37
+ Load TB2J results from a pickle file.
38
+
39
+ Parameters
40
+ ----------
41
+ path : str, optional
42
+ Path to the pickle file. Default is 'TB2J_results/TB2J.pickle'.
43
+
44
+ Returns
45
+ -------
46
+ spinio : SpinIO
47
+ The loaded SpinIO object containing all TB2J results.
48
+
49
+ Raises
50
+ ------
51
+ FileNotFoundError
52
+ If the specified pickle file does not exist.
53
+
54
+ Examples
55
+ --------
56
+ >>> spinio = load('my_results/TB2J.pickle')
57
+ >>> print(spinio.exchange_Jdict)
58
+ """
59
+ path = os.path.abspath(path)
60
+ if os.path.isdir(path):
61
+ path = os.path.join(path, "TB2J.pickle")
62
+ if not os.path.exists(path):
63
+ raise FileNotFoundError(f"TB2J pickle file not found: {path}")
64
+
65
+ dirname, fname = os.path.split(path)
66
+ return SpinIO.load_pickle(path=dirname, fname=fname)
67
+
68
+
69
+ def save(spinio, path="modified_results"):
70
+ """
71
+ Save modified TB2J results to disk in all supported formats.
72
+
73
+ This writes the pickle file, TXT format, Multibinit XML, and other formats.
74
+
75
+ Parameters
76
+ ----------
77
+ spinio : SpinIO
78
+ The SpinIO object to save.
79
+ path : str, optional
80
+ Output directory path. Default is 'modified_results'.
81
+ Will be created if it doesn't exist.
82
+
83
+ Examples
84
+ --------
85
+ >>> save(spinio, 'my_modified_results')
86
+ """
87
+ spinio.write_all(path=path)
88
+
89
+ # Fix mb.in to use SIA from exchange.xml when SIA is present
90
+ if spinio.has_uniaxial_anistropy or (
91
+ hasattr(spinio, "has_sia_tensor") and spinio.has_sia_tensor
92
+ ):
93
+ mb_in_path = os.path.join(path, "Multibinit", "mb.in")
94
+ if os.path.exists(mb_in_path):
95
+ with open(mb_in_path, "r") as f:
96
+ content = f.read()
97
+ # Replace spin_sia_add = 1 with spin_sia_add = 0
98
+ content = content.replace("spin_sia_add = 1", "spin_sia_add = 0")
99
+ with open(mb_in_path, "w") as f:
100
+ f.write(content)
101
+
102
+
103
+ def set_anisotropy(spinio, species, k1=None, k1dir=None):
104
+ """
105
+ Set single ion anisotropy for all atoms of a specified species.
106
+
107
+ Modifies the k1 (amplitude) and/or k1dir (direction) for all magnetic
108
+ atoms of the given chemical species.
109
+
110
+ Parameters
111
+ ----------
112
+ spinio : SpinIO
113
+ The SpinIO object to modify.
114
+ species : str
115
+ Chemical species symbol (e.g., 'Sm', 'Fe').
116
+ k1 : float, optional
117
+ Anisotropy amplitude in eV. If None, only k1dir is modified.
118
+ k1dir : array-like, optional
119
+ Anisotropy direction as a 3D vector [x, y, z].
120
+ Will be normalized automatically. If None, only k1 is modified.
121
+
122
+ Notes
123
+ -----
124
+ - Only magnetic atoms (those with index_spin >= 0) are modified.
125
+ - If k1/k1dir are None in the SpinIO object, they will be initialized.
126
+ - k1dir is always normalized to a unit vector.
127
+
128
+ Examples
129
+ --------
130
+ >>> # Set Sm anisotropy to 5 meV along z-axis
131
+ >>> set_anisotropy(spinio, species='Sm', k1=0.005, k1dir=[0, 0, 1])
132
+
133
+ >>> # Set only the direction for Fe
134
+ >>> set_anisotropy(spinio, species='Fe', k1dir=[1, 0, 0])
135
+ """
136
+ # Get symbols and find target atoms
137
+ symbols = spinio.atoms.get_chemical_symbols()
138
+
139
+ target_indices = [
140
+ i
141
+ for i, (sym, idx) in enumerate(zip(symbols, spinio.index_spin))
142
+ if sym == species and idx >= 0
143
+ ]
144
+
145
+ if not target_indices:
146
+ import warnings
147
+
148
+ warnings.warn(
149
+ f"No magnetic atoms found for species '{species}'. "
150
+ f"Either the species doesn't exist or has no magnetic atoms.",
151
+ UserWarning,
152
+ )
153
+ return
154
+
155
+ # Initialize k1/k1dir if not present
156
+ n_spins = (
157
+ max(spinio.index_spin) + 1
158
+ if max(spinio.index_spin) >= 0
159
+ else len(spinio.index_spin)
160
+ )
161
+ if spinio.k1 is None:
162
+ spinio.k1 = [0.0] * n_spins
163
+ if spinio.k1dir is None:
164
+ spinio.k1dir = [[0.0, 0.0, 1.0]] * n_spins
165
+
166
+ # Set values for matching atoms
167
+ for iatom in target_indices:
168
+ ispin = spinio.index_spin[iatom]
169
+ if k1 is not None:
170
+ spinio.k1[ispin] = k1 # eV
171
+ if k1dir is not None:
172
+ k1dir_array = np.array(k1dir, dtype=float)
173
+ norm = np.linalg.norm(k1dir_array)
174
+ if norm == 0:
175
+ raise ValueError(
176
+ f"k1dir cannot be a zero vector for species '{species}'"
177
+ )
178
+ spinio.k1dir[ispin] = k1dir_array / norm
179
+
180
+ # Set the flag to enable SIA in output
181
+ spinio.has_uniaxial_anistropy = True
182
+
183
+
184
+ def set_sia_tensor(spinio, species, tensor):
185
+ """
186
+ Set full single ion anisotropy tensor for all atoms of a specified species.
187
+
188
+ Parameters
189
+ ----------
190
+ spinio : SpinIO
191
+ The SpinIO object to modify.
192
+ species : str
193
+ Chemical species symbol (e.g., 'Sm', 'Fe').
194
+ tensor : array-like
195
+ 3x3 anisotropy tensor in eV.
196
+
197
+ Examples
198
+ --------
199
+ >>> # Set Sm anisotropy tensor
200
+ >>> tensor = np.diag([0.001, 0.002, 0.003])
201
+ >>> set_sia_tensor(spinio, species='Sm', tensor=tensor)
202
+ """
203
+ # Get symbols and find target atoms
204
+ symbols = spinio.atoms.get_chemical_symbols()
205
+
206
+ target_indices = [
207
+ i
208
+ for i, (sym, idx) in enumerate(zip(symbols, spinio.index_spin))
209
+ if sym == species and idx >= 0
210
+ ]
211
+
212
+ if not target_indices:
213
+ import warnings
214
+
215
+ warnings.warn(
216
+ f"No magnetic atoms found for species '{species}'. "
217
+ f"Either the species doesn't exist or has no magnetic atoms.",
218
+ UserWarning,
219
+ )
220
+ return
221
+
222
+ # Initialize sia_tensor dict if not present
223
+ if not hasattr(spinio, "sia_tensor") or spinio.sia_tensor is None:
224
+ spinio.sia_tensor = {}
225
+
226
+ tensor_array = np.array(tensor, dtype=float)
227
+ if tensor_array.shape != (3, 3):
228
+ raise ValueError("SIA tensor must be a 3x3 matrix")
229
+
230
+ # Set values for matching atoms
231
+ for iatom in target_indices:
232
+ ispin = spinio.index_spin[iatom]
233
+ spinio.sia_tensor[ispin] = tensor_array.copy()
234
+
235
+ # Set the flag to enable SIA tensor in output
236
+ spinio.has_sia_tensor = True
237
+
238
+
239
+ def remove_sia_tensor(spinio, species=None):
240
+ """
241
+ Remove single ion anisotropy tensor.
242
+
243
+ Parameters
244
+ ----------
245
+ spinio : SpinIO
246
+ The SpinIO object to modify.
247
+ species : str, optional
248
+ Chemical species symbol (e.g., 'Sm', 'Fe').
249
+ If None, remove for all atoms.
250
+ """
251
+ if not hasattr(spinio, "sia_tensor") or spinio.sia_tensor is None:
252
+ return
253
+
254
+ if species is None:
255
+ spinio.sia_tensor = None
256
+ spinio.has_sia_tensor = False
257
+ else:
258
+ symbols = spinio.atoms.get_chemical_symbols()
259
+ target_indices = [
260
+ i
261
+ for i, (sym, idx) in enumerate(zip(symbols, spinio.index_spin))
262
+ if sym == species and idx >= 0
263
+ ]
264
+ for iatom in target_indices:
265
+ ispin = spinio.index_spin[iatom]
266
+ if ispin in spinio.sia_tensor:
267
+ del spinio.sia_tensor[ispin]
268
+ if not spinio.sia_tensor:
269
+ spinio.has_sia_tensor = False
270
+
271
+
272
+ def toggle_DMI(spinio, enabled=None):
273
+ """
274
+ Enable or disable Dzyaloshinskii-Moriya interactions (DMI).
275
+
276
+ When disabling, the DMI values are backed up and can be restored
277
+ by re-enabling.
278
+
279
+ Parameters
280
+ ----------
281
+ spinio : SpinIO
282
+ The SpinIO object to modify.
283
+ enabled : bool, optional
284
+ If True, enable DMI. If False, disable DMI.
285
+ If None, toggle the current state.
286
+
287
+ Examples
288
+ --------
289
+ >>> # Disable DMI
290
+ >>> toggle_DMI(spinio, enabled=False)
291
+
292
+ >>> # Toggle DMI (disable if enabled, enable if disabled)
293
+ >>> toggle_DMI(spinio)
294
+
295
+ >>> # Re-enable DMI
296
+ >>> toggle_DMI(spinio, enabled=True)
297
+ """
298
+ if enabled is None:
299
+ # Toggle current state
300
+ enabled = not spinio.has_dmi
301
+
302
+ if enabled and not spinio.has_dmi:
303
+ # Re-enable: restore from backup or initialize empty
304
+ if hasattr(spinio, "_dmi_backup"):
305
+ spinio.dmi_ddict = spinio._dmi_backup
306
+ else:
307
+ spinio.dmi_ddict = {}
308
+ elif not enabled and spinio.has_dmi:
309
+ # Disable: backup and clear
310
+ spinio._dmi_backup = spinio.dmi_ddict
311
+ spinio.dmi_ddict = None
312
+
313
+
314
+ def toggle_Jani(spinio, enabled=None):
315
+ """
316
+ Enable or disable symmetric anisotropic exchange (Jani).
317
+
318
+ When disabling, the Jani values are backed up and can be restored
319
+ by re-enabling.
320
+
321
+ Parameters
322
+ ----------
323
+ spinio : SpinIO
324
+ The SpinIO object to modify.
325
+ enabled : bool, optional
326
+ If True, enable Jani. If False, disable Jani.
327
+ If None, toggle the current state.
328
+
329
+ Examples
330
+ --------
331
+ >>> # Disable anisotropic exchange
332
+ >>> toggle_Jani(spinio, enabled=False)
333
+
334
+ >>> # Toggle anisotropic exchange
335
+ >>> toggle_Jani(spinio)
336
+
337
+ >>> # Re-enable anisotropic exchange
338
+ >>> toggle_Jani(spinio, enabled=True)
339
+ """
340
+ if enabled is None:
341
+ # Toggle current state
342
+ enabled = not spinio.has_bilinear
343
+
344
+ if enabled and not spinio.has_bilinear:
345
+ # Re-enable: restore from backup or initialize empty
346
+ if hasattr(spinio, "_jani_backup"):
347
+ spinio.Jani_dict = spinio._jani_backup
348
+ else:
349
+ spinio.Jani_dict = {}
350
+ elif not enabled and spinio.has_bilinear:
351
+ # Disable: backup and clear
352
+ spinio._jani_backup = spinio.Jani_dict
353
+ spinio.Jani_dict = None
354
+
355
+
356
+ def toggle_exchange(spinio, enabled=None):
357
+ """
358
+ Enable or disable isotropic exchange parameters.
359
+
360
+ When disabling, the exchange values are backed up and can be restored
361
+ by re-enabling.
362
+
363
+ Parameters
364
+ ----------
365
+ spinio : SpinIO
366
+ The SpinIO object to modify.
367
+ enabled : bool, optional
368
+ If True, enable exchange. If False, disable exchange.
369
+ If None, toggle the current state.
370
+
371
+ Examples
372
+ --------
373
+ >>> # Disable isotropic exchange
374
+ >>> toggle_exchange(spinio, enabled=False)
375
+
376
+ >>> # Toggle isotropic exchange
377
+ >>> toggle_exchange(spinio)
378
+
379
+ >>> # Re-enable isotropic exchange
380
+ >>> toggle_exchange(spinio, enabled=True)
381
+ """
382
+ if enabled is None:
383
+ # Toggle current state
384
+ enabled = not spinio.has_exchange
385
+
386
+ if enabled and not spinio.has_exchange:
387
+ # Re-enable: restore from backup or initialize empty
388
+ if hasattr(spinio, "_exchange_backup"):
389
+ spinio.exchange_Jdict = spinio._exchange_backup
390
+ else:
391
+ spinio.exchange_Jdict = {}
392
+ elif not enabled and spinio.has_exchange:
393
+ # Disable: backup and clear
394
+ spinio._exchange_backup = spinio.exchange_Jdict
395
+ spinio.exchange_Jdict = None
396
+
397
+
398
+ def remove_sublattice(spinio, sublattice_name):
399
+ """
400
+ Remove all magnetic interactions associated with a specific sublattice.
401
+
402
+ This includes:
403
+ - Single-ion anisotropy (SIA) for atoms in the sublattice.
404
+ - Exchange interactions (J) where i or j belongs to the sublattice.
405
+ - Dzyaloshinskii-Moriya interactions (DMI) where i or j belongs to the sublattice.
406
+ - Anisotropic exchange where i or j belongs to the sublattice.
407
+
408
+ Parameters
409
+ ----------
410
+ spinio : SpinIO
411
+ The SpinIO object to modify.
412
+ sublattice_name : str
413
+ The name of the sublattice (species symbol) to remove.
414
+
415
+ Examples
416
+ --------
417
+ >>> # Remove all interactions involving Sm atoms
418
+ >>> remove_sublattice(spinio, 'Sm')
419
+ """
420
+ symbols = spinio.atoms.get_chemical_symbols()
421
+ sublattice_indices = [
422
+ i
423
+ for i, (sym, idx) in enumerate(zip(symbols, spinio.index_spin))
424
+ if sym == sublattice_name and idx >= 0
425
+ ]
426
+
427
+ if not sublattice_indices:
428
+ import warnings
429
+
430
+ warnings.warn(f"No magnetic atoms found for sublattice '{sublattice_name}'.")
431
+ return
432
+
433
+ sublattice_spin_indices = set(spinio.index_spin[i] for i in sublattice_indices)
434
+
435
+ for i in sublattice_indices:
436
+ spinio.index_spin[i] = -1
437
+
438
+ # Re-index spins
439
+ # Map old spin indices to new spin indices
440
+ # Remaining spins will be compacted to 0, 1, 2...
441
+
442
+ old_to_new_spin_index = {}
443
+ current_spin_index = 0
444
+ # max_old_spin = max(spinio.index_spin)
445
+
446
+ for iatom, ispin in enumerate(spinio.index_spin):
447
+ if ispin >= 0:
448
+ if ispin not in old_to_new_spin_index:
449
+ old_to_new_spin_index[ispin] = current_spin_index
450
+ current_spin_index += 1
451
+ spinio.index_spin[iatom] = old_to_new_spin_index[ispin]
452
+
453
+ def reindex_interaction(interaction_dict):
454
+ if interaction_dict is None:
455
+ return None
456
+ new_dict = {}
457
+ for key, val in interaction_dict.items():
458
+ R, i, j = key
459
+ if i in old_to_new_spin_index and j in old_to_new_spin_index:
460
+ new_i = old_to_new_spin_index[i]
461
+ new_j = old_to_new_spin_index[j]
462
+ new_dict[(R, new_i, new_j)] = val
463
+ return new_dict
464
+
465
+ def filter_interaction(interaction_dict):
466
+ if interaction_dict is None:
467
+ return None
468
+ new_dict = {}
469
+ for key, val in interaction_dict.items():
470
+ R, i, j = key
471
+ if i not in sublattice_spin_indices and j not in sublattice_spin_indices:
472
+ new_dict[key] = val
473
+ return new_dict
474
+
475
+ def filter_distance_dict(distance_dict):
476
+ if distance_dict is None:
477
+ return None
478
+ new_dict = {}
479
+ for key, val in distance_dict.items():
480
+ R, i, j = key
481
+ if i not in sublattice_spin_indices and j not in sublattice_spin_indices:
482
+ new_dict[key] = val
483
+ return new_dict
484
+
485
+ if spinio.has_exchange:
486
+ spinio.exchange_Jdict = filter_interaction(spinio.exchange_Jdict)
487
+ spinio.exchange_Jdict = reindex_interaction(spinio.exchange_Jdict)
488
+ spinio.distance_dict = filter_distance_dict(spinio.distance_dict)
489
+ spinio.distance_dict = reindex_interaction(spinio.distance_dict)
490
+ if hasattr(spinio, "Jiso_orb") and spinio.Jiso_orb:
491
+ spinio.Jiso_orb = filter_interaction(spinio.Jiso_orb)
492
+ spinio.Jiso_orb = reindex_interaction(spinio.Jiso_orb)
493
+
494
+ if spinio.has_dmi:
495
+ spinio.dmi_ddict = filter_interaction(spinio.dmi_ddict)
496
+ spinio.dmi_ddict = reindex_interaction(spinio.dmi_ddict)
497
+ if hasattr(spinio, "DMI_orb") and spinio.DMI_orb:
498
+ spinio.DMI_orb = filter_interaction(spinio.DMI_orb)
499
+ spinio.DMI_orb = reindex_interaction(spinio.DMI_orb)
500
+
501
+ if spinio.has_bilinear:
502
+ spinio.Jani_dict = filter_interaction(spinio.Jani_dict)
503
+ spinio.Jani_dict = reindex_interaction(spinio.Jani_dict)
504
+ if hasattr(spinio, "Jani_orb") and spinio.Jani_orb:
505
+ spinio.Jani_orb = filter_interaction(spinio.Jani_orb)
506
+ spinio.Jani_orb = reindex_interaction(spinio.Jani_orb)
507
+
508
+ if hasattr(spinio, "dJdx") and spinio.dJdx:
509
+ spinio.dJdx = filter_distance_dict(spinio.dJdx)
510
+ spinio.dJdx = reindex_interaction(spinio.dJdx)
511
+ if hasattr(spinio, "dJdx2") and spinio.dJdx2:
512
+ spinio.dJdx2 = filter_distance_dict(spinio.dJdx2)
513
+ spinio.dJdx2 = reindex_interaction(spinio.dJdx2)
514
+ if hasattr(spinio, "biquadratic_Jdict") and spinio.biquadratic_Jdict:
515
+ spinio.biquadratic_Jdict = filter_interaction(spinio.biquadratic_Jdict)
516
+ spinio.biquadratic_Jdict = reindex_interaction(spinio.biquadratic_Jdict)
517
+ if hasattr(spinio, "NJT_Jdict") and spinio.NJT_Jdict:
518
+ spinio.NJT_Jdict = filter_interaction(spinio.NJT_Jdict)
519
+ spinio.NJT_Jdict = reindex_interaction(spinio.NJT_Jdict)
520
+ if hasattr(spinio, "NJT_ddict") and spinio.NJT_ddict:
521
+ spinio.NJT_ddict = filter_interaction(spinio.NJT_ddict)
522
+ spinio.NJT_ddict = reindex_interaction(spinio.NJT_ddict)
523
+
524
+ if spinio.has_uniaxial_anistropy:
525
+ if spinio.k1 is not None:
526
+ new_k1 = [0.0] * current_spin_index
527
+ new_k1dir = [[0.0, 0.0, 1.0]] * current_spin_index
528
+
529
+ for old_idx, new_idx in old_to_new_spin_index.items():
530
+ if old_idx < len(spinio.k1):
531
+ new_k1[new_idx] = spinio.k1[old_idx]
532
+ new_k1dir[new_idx] = spinio.k1dir[old_idx]
533
+
534
+ spinio.k1 = new_k1
535
+ spinio.k1dir = new_k1dir
536
+
537
+ if (
538
+ hasattr(spinio, "has_sia_tensor")
539
+ and spinio.has_sia_tensor
540
+ and spinio.sia_tensor is not None
541
+ ):
542
+ new_sia_tensor = {}
543
+ for old_idx, tensor in spinio.sia_tensor.items():
544
+ if old_idx in old_to_new_spin_index:
545
+ new_idx = old_to_new_spin_index[old_idx]
546
+ new_sia_tensor[new_idx] = tensor
547
+ spinio.sia_tensor = new_sia_tensor
548
+ # If no tensors remain, set has_sia_tensor to False
549
+ if not spinio.sia_tensor:
550
+ spinio.has_sia_tensor = False
551
+
552
+ spinio._ind_atoms = {}
553
+ for iatom, ispin in enumerate(spinio.index_spin):
554
+ if ispin >= 0:
555
+ spinio._ind_atoms[ispin] = iatom
556
+
557
+
558
+ def symmetrize_exchange(spinio, atoms, symprec=1e-3):
559
+ """
560
+ Symmetrize isotropic exchange based on a provided atomic structure.
561
+
562
+ The symmetry is detected from the provided atomic structure using spglib.
563
+ Exchange parameters for symmetry-equivalent atom pairs are averaged.
564
+
565
+ Parameters
566
+ ----------
567
+ spinio : SpinIO
568
+ The SpinIO object to modify.
569
+ atoms : ase.Atoms
570
+ Atomic structure that defines the target symmetry.
571
+ For example, provide a cubic structure to symmetrize to cubic symmetry.
572
+ symprec : float, optional
573
+ Symmetry precision in Angstrom. Default is 1e-3.
574
+
575
+ Notes
576
+ -----
577
+ - Only isotropic exchange (exchange_Jdict) is modified.
578
+ - DMI and anisotropic exchange are unchanged.
579
+ - The spinio.atoms structure is NOT modified; only exchange values change.
580
+ - Atoms are mapped between input and SpinIO structures by species and position.
581
+
582
+ Examples
583
+ --------
584
+ >>> from ase.io import read
585
+ >>> # Symmetrize to cubic symmetry
586
+ >>> cubic_structure = read('cubic_smfeo3.cif')
587
+ >>> symmetrize_exchange(spinio, atoms=cubic_structure)
588
+
589
+ >>> # Symmetrize to original Pnma symmetry (averaging within groups)
590
+ >>> symmetrize_exchange(spinio, atoms=spinio.atoms)
591
+ """
592
+ from TB2J.symmetrize_J import symmetrize_exchange as sym_exchange
593
+
594
+ sym_exchange(spinio, atoms, symprec)