TB2J 0.9.12.7__py3-none-any.whl → 0.9.12.18__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.
TB2J/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.o
2
+ *.x
3
+ *.mod
4
+ exchanges.sublime-project
5
+ exchanges.sublime-workspace
TB2J/Jdownfolder.py CHANGED
@@ -47,8 +47,8 @@ class JDownfolder:
47
47
  self.qmesh = qmesh
48
48
  self.qpts = monkhorst_pack(qmesh)
49
49
  self.nqpt = len(self.qpts)
50
- self.nMn = self.nM * 3
51
- self.nLn = self.nL * 3
50
+ self.nMn = self.nM * self.nxyz
51
+ self.nLn = self.nL * self.nxyz
52
52
  self.iso_only = iso_only
53
53
 
54
54
  def get_JR(self):
@@ -62,6 +62,7 @@ class JDownfolder:
62
62
  for iR, R in enumerate(self.Rlist):
63
63
  phase = np.exp(-2.0j * np.pi * np.dot(q, R))
64
64
  JR_downfolded[iR] += np.real(Jq_downfolded[iq] * phase / self.nqpt)
65
+ return JR_downfolded, self.Rlist
65
66
 
66
67
  def downfold_oneq(self, J):
67
68
  JMM = J[np.ix_(self.iMn, self.iMn)]
@@ -121,7 +122,7 @@ class PWFDownfolder:
121
122
  marker="o",
122
123
  ax=None,
123
124
  savefig="downfold_band.png",
124
- show=True,
125
+ show=False,
125
126
  )
126
127
  self.JR_downfolded = ewf.HwannR
127
128
  self.Rlist = ewf.Rlist
TB2J/MAEGreen.py CHANGED
@@ -51,7 +51,7 @@ class MAEGreen(ExchangeNCL):
51
51
 
52
52
  nangles = len(self.thetas)
53
53
  self.es = np.zeros(nangles, dtype=complex)
54
- self.es_atom = np.zeros((nangles, self.natoms), dtype=complex)
54
+ self.es_matrix = np.zeros((nangles, self.natoms, self.natoms), dtype=complex)
55
55
  self.es_atom_orb = DefaultDict(lambda: 0)
56
56
 
57
57
  def set_angles_xyz(self):
@@ -153,9 +153,8 @@ class MAEGreen(ExchangeNCL):
153
153
  Hsoc_k = self.tbmodel.get_Hk_soc(self.G.kpts)
154
154
  na = len(thetas)
155
155
  dE_angle = np.zeros(na, dtype=complex)
156
- dE_angle_atom = np.zeros((na, self.natoms), dtype=complex)
156
+ dE_angle_matrix = np.zeros((na, self.natoms, self.natoms), dtype=complex)
157
157
  # dE_angle_orbitals = np.zeros((na, self.natoms, self.norb, self.norb), dtype=complex)
158
- # dE_angle_orbitals = DefaultDict(lambda: 0)
159
158
  dE_angle_atom_orb = DefaultDict(lambda: 0)
160
159
  for iangle, (theta, phi) in enumerate(zip(thetas, phis)):
161
160
  for ik, dHk in enumerate(Hsoc_k):
@@ -179,23 +178,30 @@ class MAEGreen(ExchangeNCL):
179
178
  # dE_angle[iangle] += np.trace(GdH@GdH) * self.G.kweights[ik]
180
179
  # dE_angle[iangle] += np.trace(GdH@G0K[ik].T.conj()@dHi ) * self.G.kweights[ik]
181
180
  dE_angle[iangle] += dG2sum * self.G.kweights[ik]
181
+
182
+ # Calculate atom-atom matrix interactions
182
183
  for iatom in range(self.natoms):
183
184
  iorb = self.iorb(iatom)
184
- # dG2= dG2[::2, ::2] + dG2[1::2, 1::2] + dG2[1::2, ::2] + dG2[::2, 1::2]
185
- dE_atom_orb = dG2[np.ix_(iorb, iorb)] * self.G.kweights[ik]
186
- dE_atom_orb = (
187
- dE_atom_orb[::2, ::2]
188
- + dE_atom_orb[1::2, 1::2]
189
- + dE_atom_orb[1::2, ::2]
190
- + dE_atom_orb[::2, 1::2]
191
- )
192
- dE_atom = np.sum(dE_atom_orb)
193
- mmat = self.mmats[iatom]
194
- dE_atom_orb = mmat.T @ dE_atom_orb @ mmat
195
- dE_angle_atom_orb[(iangle, iatom)] += dE_atom_orb
196
- # dE_atom = np.sum(dG2diag[iorb]) * self.G.kweights[ik]
197
- dE_angle_atom[iangle, iatom] += dE_atom
198
- return dE_angle, dE_angle_atom, dE_angle_atom_orb
185
+ for jatom in range(self.natoms):
186
+ jorb = self.iorb(jatom)
187
+ # Calculate cross terms between atoms i and j
188
+ dE_ij_orb = dG2[np.ix_(iorb, jorb)] * self.G.kweights[ik]
189
+ dE_ij_orb = (
190
+ dE_ij_orb[::2, ::2]
191
+ + dE_ij_orb[1::2, 1::2]
192
+ + dE_ij_orb[1::2, ::2]
193
+ + dE_ij_orb[::2, 1::2]
194
+ )
195
+ dE_ij = np.sum(dE_ij_orb)
196
+ # Transform to local orbital basis
197
+ mmat_i = self.mmats[iatom]
198
+ mmat_j = self.mmats[jatom]
199
+ dE_ij_orb = mmat_i.T @ dE_ij_orb @ mmat_j
200
+ dE_angle_matrix[iangle, iatom, jatom] += dE_ij
201
+ # Store orbital-resolved data for diagonal terms
202
+ if iatom == jatom:
203
+ dE_angle_atom_orb[(iangle, iatom)] += dE_ij_orb
204
+ return dE_angle, dE_angle_matrix, dE_angle_atom_orb
199
205
 
200
206
  def get_perturbed_R(self, e, thetas, phis):
201
207
  self.tbmodel.set_so_strength(0.0)
@@ -233,16 +239,16 @@ class MAEGreen(ExchangeNCL):
233
239
  npole = len(self.contour.path)
234
240
  results = map(func, tqdm.tqdm(self.contour.path, total=npole))
235
241
  for i, result in enumerate(results):
236
- dE_angle, dE_angle_atom, dE_angle_atom_orb = result
242
+ dE_angle, dE_angle_matrix, dE_angle_atom_orb = result
237
243
  self.es += dE_angle * self.contour.weights[i]
238
- self.es_atom += dE_angle_atom * self.contour.weights[i]
244
+ self.es_matrix += dE_angle_matrix * self.contour.weights[i]
239
245
  for key, value in dE_angle_atom_orb.items():
240
246
  self.es_atom_orb[key] += (
241
247
  dE_angle_atom_orb[key] * self.contour.weights[i]
242
248
  )
243
249
 
244
250
  self.es = -np.imag(self.es) / (2 * np.pi)
245
- self.es_atom = -np.imag(self.es_atom) / (2 * np.pi)
251
+ self.es_matrix = -np.imag(self.es_matrix) / (2 * np.pi)
246
252
  for key, value in self.es_atom_orb.items():
247
253
  self.es_atom_orb[key] = -np.imag(value) / (2 * np.pi)
248
254
 
@@ -260,6 +266,7 @@ class MAEGreen(ExchangeNCL):
260
266
  Path(output_path).mkdir(exist_ok=True)
261
267
  fname = f"{output_path}/MAE.dat"
262
268
  fname_orb = f"{output_path}/MAE_orb.dat"
269
+ fname_matrix = f"{output_path}/MAE_matrix.dat"
263
270
  # fname_tensor = f"{output_path}/MAE_tensor.dat"
264
271
  # if figure3d is not None:
265
272
  # fname_fig3d = f"{output_path}/{figure3d}"
@@ -270,24 +277,32 @@ class MAEGreen(ExchangeNCL):
270
277
  if with_eigen:
271
278
  fname_eigen = f"{output_path}/MAE_eigen.dat"
272
279
  with open(fname_eigen, "w") as f:
273
- f.write("# theta, phi, MAE(total), MAE(atom-wise) Unit: meV\n")
274
- for i, (theta, phi, e, es) in enumerate(
275
- zip(self.thetas, self.phis, self.es2, self.es_atom)
280
+ f.write("# theta, phi, MAE(total) Unit: meV\n")
281
+ for i, (theta, phi, e) in enumerate(
282
+ zip(self.thetas, self.phis, self.es2)
276
283
  ):
277
- f.write(f"{theta:.5f} {phi:.5f} {e*1e3:.8f} ")
278
- for ea in es:
279
- f.write(f"{ea*1e3:.8f} ")
280
- f.write("\n")
284
+ f.write(f"{theta:.5f} {phi:.5f} {e*1e3:.8f}\n")
281
285
 
282
286
  with open(fname, "w") as f:
283
- f.write("# theta (rad), phi(rad), MAE(total), MAE(atom-wise) Unit: meV\n")
284
- for i, (theta, phi, e, es) in enumerate(
285
- zip(self.thetas, self.phis, self.es, self.es_atom)
286
- ):
287
- f.write(f"{theta%np.pi:.5f} {phi%(2*np.pi):.5f} {e*1e3:.8f} ")
288
- for ea in es:
289
- f.write(f"{ea*1e3:.8f} ")
290
- f.write("\n")
287
+ f.write("# theta (rad), phi(rad), MAE(total) Unit: meV\n")
288
+ for i, (theta, phi, e) in enumerate(zip(self.thetas, self.phis, self.es)):
289
+ f.write(f"{theta%np.pi:.5f} {phi%(2*np.pi):.5f} {e*1e3:.8f}\n")
290
+
291
+ # Write matrix data to MAE_matrix.dat
292
+ with open(fname_matrix, "w") as fmat:
293
+ fmat.write("# MAE atom-atom interaction matrices\n")
294
+ fmat.write("# Format: angle_index theta phi atom_i atom_j MAE_ij(meV)\n")
295
+ fmat.write("# Units: theta and phi in radians, MAE in meV\n")
296
+ for iangle, (theta, phi) in enumerate(zip(self.thetas, self.phis)):
297
+ for iatom in range(self.natoms):
298
+ for jatom in range(self.natoms):
299
+ mae_ij = (
300
+ self.es_matrix[iangle, iatom, jatom] * 1e3
301
+ ) # Convert to meV
302
+ fmat.write(
303
+ f"{iangle:4d} {theta:.5f} {phi:.5f} {iatom:4d} {jatom:4d} {mae_ij:.8f}\n"
304
+ )
305
+ fmat.write("\n") # Empty line between angles for readability
291
306
 
292
307
  # self.ani = self.fit_anisotropy_tensor()
293
308
  # with open(fname_tensor, "w") as f:
@@ -313,34 +328,37 @@ class MAEGreen(ExchangeNCL):
313
328
  for orb in self.orbital_names[iatom]:
314
329
  f.write(f"{orb} ")
315
330
  f.write("\n")
316
- for i, (theta, phi, e, eatom) in enumerate(
317
- zip(self.thetas, self.phis, self.es, self.es_atom)
318
- ):
331
+ for i, (theta, phi, e) in enumerate(zip(self.thetas, self.phis, self.es)):
319
332
  f.write("-" * 60 + "\n")
320
333
  f.write(f"Angle {i:03d}: theta={theta:.5f} phi={phi:.5f} \n ")
321
334
  f.write(f"E: {e*1e3:.8f} \n")
322
- for iatom, ea in enumerate(eatom):
323
- f.write(f"Atom {iatom:03d}: {ea*1e3:.8f} \n")
324
- f.write("Orbital: ")
325
- eorb = self.es_atom_orb[(i, iatom)]
326
-
327
- # write numpy matrix to file
328
- f.write(
329
- np.array2string(
330
- eorb * 1e3, precision=4, separator=",", suppress_small=True
331
- )
332
- )
333
-
334
- eorb_diff = eorb - self.es_atom_orb[(0, iatom)]
335
- f.write("Diference to the first angle: ")
336
- f.write(
337
- np.array2string(
338
- eorb_diff * 1e3,
339
- precision=4,
340
- separator=",",
341
- suppress_small=True,
335
+ for iatom in range(self.natoms):
336
+ f.write(f"Atom {iatom:03d} orbital matrix:\n")
337
+ if (i, iatom) in self.es_atom_orb:
338
+ eorb = self.es_atom_orb[(i, iatom)]
339
+ # write numpy matrix to file
340
+ f.write(
341
+ np.array2string(
342
+ eorb * 1e3,
343
+ precision=4,
344
+ separator=",",
345
+ suppress_small=True,
346
+ )
342
347
  )
343
- )
348
+ f.write("\n")
349
+
350
+ if (0, iatom) in self.es_atom_orb:
351
+ eorb_diff = eorb - self.es_atom_orb[(0, iatom)]
352
+ f.write("Difference to the first angle: ")
353
+ f.write(
354
+ np.array2string(
355
+ eorb_diff * 1e3,
356
+ precision=4,
357
+ separator=",",
358
+ suppress_small=True,
359
+ )
360
+ )
361
+ f.write("\n")
344
362
  f.write("\n")
345
363
 
346
364
  def run(self, output_path="TB2J_anisotropy", with_eigen=False):
TB2J/__init__.py CHANGED
@@ -1,5 +1,3 @@
1
1
  import importlib.metadata
2
- import TB2J
3
2
 
4
3
  __version__ = importlib.metadata.version("TB2J")
5
-
@@ -0,0 +1,156 @@
1
+ """
2
+ Debug script for spin-phonon coupling using double-sided finite difference method.
3
+
4
+ PURPOSE:
5
+ This script uses the double-sided finite difference method from Oiju_FD2.py to
6
+ calculate spin-phonon coupling parameters and their derivatives dJ/dx. It computes
7
+ exchange parameters at both positive and negative displacements, then calculates
8
+ dJ/dx = (J(+dx) - J(-dx)) / (2*dx).
9
+
10
+ USAGE:
11
+ uv run python agent_files/debug_spinphon_fd/debug_main.py
12
+
13
+ EXPECTED OUTPUT:
14
+ The script will create output directories with subdirectories:
15
+ - original/: Exchange parameters at zero displacement (only if compute_d2J=True)
16
+ - negative/: Exchange parameters at -amplitude displacement
17
+ - positive/: Exchange parameters at +amplitude displacement
18
+ - dJdx/: Computed derivatives dJ/dx in both text and pickle formats
19
+
20
+ PERFORMANCE OPTIONS:
21
+ - compute_d2J: Set False to skip J(0) calculation and d2J/dx2 (~33% faster)
22
+ - ispin0_only: Set True to only compute pairs with ispin=0 or jspin=0 (~93% faster)
23
+ - Combined: ~95% reduction in computation time!
24
+
25
+ FILES USED:
26
+ - Oiju_FD2.py: Source of double-sided finite difference implementation
27
+ - Oiju_epw2.py: Reference for data path structure and interface
28
+
29
+ DEBUG NOTES:
30
+ - Uses double-sided finite difference for better numerical accuracy
31
+ - Computes full exchange tensor derivatives including DMI components
32
+ - Results include isotropic, anisotropic, and DMI contributions to dJ/dx
33
+ - Default settings use both optimizations for maximum speed
34
+ """
35
+
36
+
37
+ # Add the TB2J directory to the Python path
38
+ # sys.path.insert(0, '/home_phythema/hexu/projects/TB2J/TB2J')
39
+
40
+ import numpy as np
41
+
42
+ from TB2J.Oiju_FD2 import gen_exchange_Oiju_FD_double_sided
43
+
44
+
45
+ def main():
46
+ """Main function to run spin-phonon coupling calculation using double-sided finite difference method."""
47
+
48
+ # Use the same data path structure as Oiju_epw2.py
49
+ path = "/home_phythema/hexu/spinphon/2025-10-02_newdata/k555q555"
50
+ nsc=2
51
+ nkpt = 5
52
+
53
+
54
+ # Configuration parameters matching Oiju_epw2.py example
55
+ config = {
56
+ "path": path,
57
+ "colinear": True,
58
+ "posfile": "scf.pwi",
59
+ "prefix_up": "up/SrMnO3",
60
+ "prefix_dn": "down/SrMnO3.down",
61
+ "prefix_SOC": "wannier90",
62
+ "epw_up_path": f"{path}/up",
63
+ "epw_down_path": f"{path}/down",
64
+ "epw_prefix_up": "epmat",
65
+ "epw_prefix_dn": "epmat",
66
+ "Ru": (0, 0, 0),
67
+ "Rcut": 5,
68
+ "efermi": 11.26,
69
+ "magnetic_elements": ["Mn"],
70
+ "kmesh": [3, 3, 3],
71
+ "emin": -7.3363330034071295,
72
+ "emax": 0.0,
73
+ "nz": 70,
74
+ "np": 1,
75
+ "exclude_orbs": [],
76
+ "description": "Double-sided finite difference calculation for dJ/dx",
77
+ "list_iatom": None,
78
+ "output_path": f"FD_spinphon_results_sc{nsc}_k{nkpt}",
79
+ # Additional parameters for finite difference method
80
+ "supercell_matrix": np.eye(3, dtype=int)*nsc,
81
+ "amplitude": 0.003,
82
+ "max_distance": None,
83
+ # Performance optimization options
84
+ "compute_d2J": False, # Set True to compute second derivative d2J/dx2
85
+ "ispin0_only": True, # Set True to only compute pairs with ispin=0 or jspin=0
86
+ }
87
+
88
+ # Run calculation for a single displacement pattern (idisp=0)
89
+ # You can modify this to loop over multiple displacement patterns
90
+ displacement_patterns = [5, 6, 8, 0] # Change to range(15) for all patterns
91
+
92
+ print("=" * 80)
93
+ print("Starting double-sided finite difference spin-phonon coupling calculation")
94
+ print("=" * 80)
95
+ print(f"Data path: {path}")
96
+ print(f"Output path: {config['output_path']}")
97
+ print(f"Displacement patterns: {displacement_patterns}")
98
+ print(f"Amplitude: {config['amplitude']}")
99
+ print(
100
+ f"Method: dJ/dx = (J(+{config['amplitude']}) - J(-{config['amplitude']})) / (2*{config['amplitude']})"
101
+ )
102
+ print("\nPerformance optimizations:")
103
+ print(f" - compute_d2J: {config['compute_d2J']} {'(computes d2J/dx2)' if config['compute_d2J'] else '(skips J(0) calculation, ~33% faster)'}")
104
+ print(f" - ispin0_only: {config['ispin0_only']} {'(only pairs with ispin=0 or jspin=0, ~93% faster)' if config['ispin0_only'] else '(all pairs)'}")
105
+ if not config['compute_d2J'] and config['ispin0_only']:
106
+ print(f" - Combined speedup: ~95% reduction in computation time!")
107
+ print("=" * 80)
108
+
109
+ for idisp in displacement_patterns:
110
+ print(f"\n{'='*80}")
111
+ print(f"Processing displacement pattern {idisp}")
112
+ print(f"{'='*80}")
113
+
114
+ try:
115
+ gen_exchange_Oiju_FD_double_sided(idisp=idisp, **config)
116
+ print(f"\n{'='*80}")
117
+ print(f"Successfully completed displacement pattern {idisp}")
118
+ print("Results saved in:")
119
+ if config['compute_d2J']:
120
+ print(f" - {config['output_path']}/idisp{idisp}_Ru{config['Ru'][0]}_{config['Ru'][1]}_{config['Ru'][2]}/original/")
121
+ print(f" - {config['output_path']}/idisp{idisp}_Ru{config['Ru'][0]}_{config['Ru'][1]}_{config['Ru'][2]}/negative/")
122
+ print(f" - {config['output_path']}/idisp{idisp}_Ru{config['Ru'][0]}_{config['Ru'][1]}_{config['Ru'][2]}/positive/")
123
+ print(f" - {config['output_path']}/idisp{idisp}_Ru{config['Ru'][0]}_{config['Ru'][1]}_{config['Ru'][2]}/dJdx/")
124
+ print(f"{'='*80}")
125
+
126
+ except Exception as e:
127
+ print(f"\nError processing displacement pattern {idisp}: {e}")
128
+ import traceback
129
+
130
+ traceback.print_exc()
131
+
132
+ print("\n" + "=" * 80)
133
+ print("Calculation completed!")
134
+ print("=" * 80)
135
+ print("Results structure:")
136
+ print(f" {config['output_path']}/")
137
+ print(f" └── idisp{{N}}_Ru{{X}}_{{Y}}_{{Z}}/")
138
+ if config['compute_d2J']:
139
+ print(" ├── original/ # Exchange at zero displacement")
140
+ print(" ├── negative/ # Exchange at -amplitude")
141
+ print(" ├── positive/ # Exchange at +amplitude")
142
+ print(" └── dJdx/ # Computed dJ/dx derivatives")
143
+ print(" ├── exchange.out # Human-readable format")
144
+ print(" └── dJdx.pickle # Python dictionary format")
145
+ print("\nOutput format:")
146
+ if config['compute_d2J']:
147
+ print(" - 14 columns: includes J_0, J_neg, J_pos, dJ/dx, d2J/dx2")
148
+ else:
149
+ print(" - 12 columns: includes J_neg, J_pos, dJ/dx (no J_0, no d2J/dx2)")
150
+ if config['ispin0_only']:
151
+ print(" - Only pairs with ispin=0 or jspin=0 included")
152
+ print("=" * 80)
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()
@@ -0,0 +1,272 @@
1
+ """
2
+ Test script for compute_dJdx_from_exchanges function with distance-based sorting.
3
+
4
+ PURPOSE:
5
+ Tests the compute_dJdx_from_exchanges function using existing exchange data
6
+ from FD_spinphon_results to verify:
7
+ 1. Distance-based sorting works correctly
8
+ 2. Output format matches TB2J exchange output order
9
+ 3. dJ/dx and d²J/dx² values are reasonable
10
+
11
+ USAGE:
12
+ uv run python agent_files/debug_spinphon_fd/test_compute_dJdx.py
13
+
14
+ EXPECTED OUTPUT:
15
+ Creates dJdx/exchange.out with sorted exchange interactions by distance
16
+ and prints verification information.
17
+
18
+ FILES USED:
19
+ - FD_spinphon_results/original/TB2J.pickle
20
+ - FD_spinphon_results/negative/TB2J.pickle
21
+ - FD_spinphon_results/positive/TB2J.pickle
22
+
23
+ DEBUG NOTES:
24
+ - Uses pickle.load to read existing ExchangeNCL objects
25
+ - Does not require supercellmap module
26
+ - Tests only the derivative computation and output formatting
27
+ """
28
+
29
+ import os
30
+ import pickle
31
+
32
+
33
+ def load_exchange(pickle_path):
34
+ """Load exchange object from pickle file."""
35
+ with open(pickle_path, "rb") as f:
36
+ return pickle.load(f)
37
+
38
+
39
+ def compute_dJdx_from_exchanges(
40
+ exchange_orig, exchange_neg, exchange_pos, amplitude, output_path, compute_d2J=True
41
+ ):
42
+ """
43
+ Compute dJ/dx and optionally d²J/dx² from exchanges at three positions using finite difference.
44
+
45
+ Args:
46
+ exchange_orig: Exchange dict at zero displacement (can be None if compute_d2J=False)
47
+ exchange_neg: Exchange dict at -amplitude
48
+ exchange_pos: Exchange dict at +amplitude
49
+ amplitude: Displacement amplitude in Angstrom
50
+ output_path: Directory to save dJdx results
51
+ compute_d2J: Whether to compute second derivative (default=True)
52
+ """
53
+ os.makedirs(output_path, exist_ok=True)
54
+
55
+ # Extract isotropic exchange values (dict already contains the data)
56
+ Jiso_orig = exchange_orig["exchange_Jdict"] if exchange_orig is not None else None
57
+ Jiso_neg = exchange_neg["exchange_Jdict"]
58
+ Jiso_pos = exchange_pos["exchange_Jdict"]
59
+
60
+ # Get distance information (use neg or pos if orig not available)
61
+ if exchange_orig is not None:
62
+ distance_dict = exchange_orig["distance_dict"]
63
+ else:
64
+ distance_dict = exchange_neg["distance_dict"]
65
+
66
+ # Compute first derivative: dJ/dx = (J_pos - J_neg) / (2*dx)
67
+ # Note: multiply by 1e3 to convert from eV/A to meV/A
68
+ dJiso_dx = {}
69
+ keys_to_use = Jiso_orig.keys() if Jiso_orig is not None else Jiso_neg.keys()
70
+ for key in keys_to_use:
71
+ if key in Jiso_pos and key in Jiso_neg:
72
+ dJiso_dx[key] = (Jiso_pos[key] - Jiso_neg[key]) / (2.0 * amplitude) * 1e3
73
+
74
+ # Compute second derivative: d²J/dx² = (J_pos - 2*J_orig + J_neg) / (dx²)
75
+ # Note: multiply by 1e3 to convert from eV/A² to meV/A²
76
+ d2Jiso_dx2 = {}
77
+ if compute_d2J and Jiso_orig is not None:
78
+ for key in Jiso_orig.keys():
79
+ if key in Jiso_pos and key in Jiso_neg:
80
+ d2Jiso_dx2[key] = (
81
+ (Jiso_pos[key] - 2.0 * Jiso_orig[key] + Jiso_neg[key])
82
+ / (amplitude**2)
83
+ * 1e3
84
+ )
85
+
86
+ # Sort keys by first atom index (ispin), then by distance
87
+ sorted_keys = sorted(dJiso_dx.keys(), key=lambda x: (x[1], distance_dict[x][1]))
88
+
89
+ # Save results in text format (sorted by ispin then distance)
90
+ output_file = os.path.join(output_path, "exchange.out")
91
+ with open(output_file, "w") as f:
92
+ if compute_d2J:
93
+ f.write(
94
+ "# Derivatives of isotropic exchange computed from finite difference\n"
95
+ )
96
+ else:
97
+ f.write(
98
+ "# First derivative of isotropic exchange computed from finite difference\n"
99
+ )
100
+ f.write(f"# dJ/dx = (J(+{amplitude}) - J(-{amplitude})) / (2*{amplitude})\n")
101
+ if compute_d2J:
102
+ f.write(
103
+ f"# d²J/dx² = (J(+{amplitude}) - 2*J(0) + J(-{amplitude})) / ({amplitude}²)\n"
104
+ )
105
+ if compute_d2J:
106
+ f.write(
107
+ "# Units: J in meV, distances in Angstrom, dJ/dx in meV/Angstrom, d²J/dx² in meV/Angstrom²\n"
108
+ )
109
+ else:
110
+ f.write("# Units: J in meV, distances in Angstrom, dJ/dx in meV/Angstrom\n")
111
+ f.write("# Sorted by: first atom index (ispin), then by distance\n")
112
+ if compute_d2J and Jiso_orig is not None:
113
+ f.write(
114
+ "# ispin jspin Rx Ry Rz distance(A) dx(A) dy(A) dz(A) J_0(meV) J_neg(meV) J_pos(meV) dJ/dx d2J/dx2\n"
115
+ )
116
+ elif Jiso_orig is None:
117
+ if compute_d2J:
118
+ f.write(
119
+ "# ispin jspin Rx Ry Rz distance(A) dx(A) dy(A) dz(A) J_neg(meV) J_pos(meV) dJ/dx d2J/dx2\n"
120
+ )
121
+ else:
122
+ f.write(
123
+ "# ispin jspin Rx Ry Rz distance(A) dx(A) dy(A) dz(A) J_neg(meV) J_pos(meV) dJ/dx\n"
124
+ )
125
+ else:
126
+ f.write(
127
+ "# ispin jspin Rx Ry Rz distance(A) dx(A) dy(A) dz(A) J_0(meV) J_neg(meV) J_pos(meV) dJ/dx\n"
128
+ )
129
+
130
+ for key in sorted_keys:
131
+ R, ispin, jspin = key
132
+ Rx, Ry, Rz = R
133
+
134
+ # Get distance vector and norm
135
+ vec, distance = distance_dict[key]
136
+
137
+ # Get J values (convert from eV to meV)
138
+ Jneg = Jiso_neg[key] * 1e3
139
+ Jpos = Jiso_pos[key] * 1e3
140
+
141
+ # Write output line
142
+ line = (
143
+ f"{ispin:3d} {jspin:3d} {Rx:3d} {Ry:3d} {Rz:3d} "
144
+ f"{distance:12.8f} {vec[0]:10.6f} {vec[1]:10.6f} {vec[2]:10.6f} "
145
+ )
146
+ if Jiso_orig is not None:
147
+ J0 = Jiso_orig[key] * 1e3
148
+ line += f"{J0:16.8f} "
149
+ line += f"{Jneg:16.8f} {Jpos:16.8f} {dJiso_dx[key]:16.8f}"
150
+ if compute_d2J and key in d2Jiso_dx2:
151
+ line += f" {d2Jiso_dx2[key]:16.8f}"
152
+ line += "\n"
153
+ f.write(line)
154
+
155
+ # Save in pickle format
156
+ pickle_file = os.path.join(output_path, "dJdx.pickle")
157
+ results = {
158
+ "dJiso_dx": dJiso_dx,
159
+ "Jiso_neg": Jiso_neg,
160
+ "Jiso_pos": Jiso_pos,
161
+ "amplitude": amplitude,
162
+ }
163
+ if Jiso_orig is not None:
164
+ results["Jiso_orig"] = Jiso_orig
165
+ if compute_d2J:
166
+ results["d2Jiso_dx2"] = d2Jiso_dx2
167
+
168
+ with open(pickle_file, "wb") as f:
169
+ pickle.dump(results, f)
170
+
171
+ print("Results saved to:")
172
+ print(f" {output_file}")
173
+ print(f" {pickle_file}")
174
+ print(f"\nNumber of exchange interactions: {len(dJiso_dx)}")
175
+
176
+
177
+ def main():
178
+ """Test compute_dJdx_from_exchanges with existing data."""
179
+
180
+ # Define paths
181
+ results_dir = "FD_spinphon_results"
182
+ orig_pickle = os.path.join(results_dir, "original", "TB2J.pickle")
183
+ neg_pickle = os.path.join(results_dir, "negative", "TB2J.pickle")
184
+ pos_pickle = os.path.join(results_dir, "positive", "TB2J.pickle")
185
+ dJdx_dir = os.path.join(results_dir, "dJdx")
186
+
187
+ print("=" * 80)
188
+ print("Testing compute_dJdx_from_exchanges with distance-based sorting")
189
+ print("=" * 80)
190
+
191
+ # Load exchange objects
192
+ print("\nLoading exchange data...")
193
+ print(f" Original: {orig_pickle}")
194
+ print(f" Negative: {neg_pickle}")
195
+ print(f" Positive: {pos_pickle}")
196
+
197
+ exchange_orig = load_exchange(orig_pickle)
198
+ exchange_neg = load_exchange(neg_pickle)
199
+ exchange_pos = load_exchange(pos_pickle)
200
+
201
+ print("\nExchange objects loaded successfully!")
202
+ print(f" Original has {len(exchange_orig['exchange_Jdict'])} interactions")
203
+ print(f" Negative has {len(exchange_neg['exchange_Jdict'])} interactions")
204
+ print(f" Positive has {len(exchange_pos['exchange_Jdict'])} interactions")
205
+
206
+ # Compute derivatives
207
+ amplitude = 0.01 # Angstrom
208
+ print(f"\nComputing dJ/dx with amplitude = {amplitude} Angstrom...")
209
+
210
+ compute_dJdx_from_exchanges(
211
+ exchange_orig, exchange_neg, exchange_pos, amplitude, dJdx_dir
212
+ )
213
+
214
+ # Read and display first few lines of output
215
+ output_file = os.path.join(dJdx_dir, "exchange.out")
216
+ print("\n" + "=" * 80)
217
+ print("First 10 lines of output (sorted by distance):")
218
+ print("=" * 80)
219
+
220
+ with open(output_file, "r") as f:
221
+ lines = f.readlines()
222
+ for i, line in enumerate(lines[:15]): # Show header + first 10 data lines
223
+ print(line.rstrip())
224
+
225
+ # Verify manual calculation for first non-header line
226
+ print("\n" + "=" * 80)
227
+ print("Manual verification of first interaction:")
228
+ print("=" * 80)
229
+
230
+ # Find first data line
231
+ with open(output_file, "r") as f:
232
+ for line in f:
233
+ if not line.startswith("#"):
234
+ parts = line.split()
235
+ if len(parts) == 14: # Updated: now has distance vector components
236
+ ispin, jspin, Rx, Ry, Rz = map(int, parts[:5])
237
+ distance, dx, dy, dz, J0, Jneg, Jpos, dJdx, d2Jdx2 = map(
238
+ float, parts[5:]
239
+ )
240
+
241
+ print(
242
+ f"Interaction: ispin={ispin}, jspin={jspin}, R=({Rx},{Ry},{Rz})"
243
+ )
244
+ print(f"Distance: {distance:.8f} Å")
245
+ print(f"Distance vector: ({dx:.8f}, {dy:.8f}, {dz:.8f}) Å")
246
+ print(f"J(0) = {J0:.8f} meV")
247
+ print(f"J(-dx) = {Jneg:.8f} meV")
248
+ print(f"J(+dx) = {Jpos:.8f} meV")
249
+ print(f"\nComputed dJ/dx = {dJdx:.8f} meV/Å")
250
+
251
+ # Manual calculation
252
+ manual_dJdx = (Jpos - Jneg) / (2.0 * amplitude)
253
+ print(f"Manual dJ/dx = (J(+dx) - J(-dx)) / (2*{amplitude})")
254
+ print(f" = ({Jpos:.8f} - {Jneg:.8f}) / {2*amplitude}")
255
+ print(f" = {manual_dJdx:.8f} meV/Å")
256
+
257
+ if abs(dJdx - manual_dJdx) < 1e-6:
258
+ print("\n✓ Manual calculation matches!")
259
+ else:
260
+ print(
261
+ f"\n✗ Manual calculation differs by {abs(dJdx - manual_dJdx):.2e}"
262
+ )
263
+
264
+ break
265
+
266
+ print("\n" + "=" * 80)
267
+ print("Test completed successfully!")
268
+ print("=" * 80)
269
+
270
+
271
+ if __name__ == "__main__":
272
+ main()