TB2J 0.9.9rc5__py3-none-any.whl → 0.9.9rc7__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/Jdownfolder.py +11 -9
  2. TB2J/MAEGreen.py +123 -19
  3. TB2J/anisotropy.py +104 -30
  4. TB2J/contour.py +16 -0
  5. TB2J/exchange.py +14 -17
  6. TB2J/exchangeCL2.py +20 -11
  7. TB2J/exchange_params.py +8 -0
  8. TB2J/green.py +15 -1
  9. TB2J/interfaces/siesta_interface.py +2 -1
  10. TB2J/io_exchange/io_exchange.py +44 -5
  11. TB2J/io_exchange/io_matjes.py +225 -0
  12. TB2J/kpoints.py +15 -1
  13. TB2J/mathutils/fibonacci_sphere.py +74 -0
  14. TB2J/mycfr.py +114 -0
  15. TB2J/orbital_magmom.py +36 -0
  16. TB2J/orbmap.py +4 -4
  17. TB2J/symmetrize_J.py +24 -11
  18. {TB2J-0.9.9rc5.dist-info → tb2j-0.9.9rc7.dist-info}/METADATA +16 -7
  19. {TB2J-0.9.9rc5.dist-info → tb2j-0.9.9rc7.dist-info}/RECORD +33 -29
  20. {TB2J-0.9.9rc5.dist-info → tb2j-0.9.9rc7.dist-info}/WHEEL +1 -1
  21. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/TB2J_downfold.py +0 -0
  22. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/TB2J_eigen.py +0 -0
  23. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/TB2J_magnon.py +0 -0
  24. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/TB2J_magnon_dos.py +0 -0
  25. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/TB2J_merge.py +0 -0
  26. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/TB2J_rotate.py +0 -0
  27. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/TB2J_rotateDM.py +0 -0
  28. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/abacus2J.py +0 -0
  29. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/siesta2J.py +0 -0
  30. {TB2J-0.9.9rc5.data → tb2j-0.9.9rc7.data}/scripts/wann2J.py +0 -0
  31. {TB2J-0.9.9rc5.dist-info → tb2j-0.9.9rc7.dist-info}/entry_points.txt +0 -0
  32. {TB2J-0.9.9rc5.dist-info → tb2j-0.9.9rc7.dist-info/licenses}/LICENSE +0 -0
  33. {TB2J-0.9.9rc5.dist-info → tb2j-0.9.9rc7.dist-info}/top_level.txt +0 -0
TB2J/Jdownfolder.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import os
2
2
  from collections import defaultdict
3
+
3
4
  import numpy as np
4
5
  from ase.dft.kpoints import monkhorst_pack
6
+
5
7
  from TB2J.io_exchange import SpinIO
6
8
  from TB2J.Jtensor import decompose_J_tensor
7
9
 
@@ -73,8 +75,8 @@ class JDownfolder:
73
75
  class PWFDownfolder:
74
76
  def __init__(self, JR, Rlist, iM, iL, qmesh, atoms=None, iso_only=False, **kwargs):
75
77
  from lawaf.interfaces.magnon.magnon_downfolder import (
76
- MagnonWrapper,
77
78
  MagnonDownfolder,
79
+ MagnonWrapper,
78
80
  )
79
81
 
80
82
  model = MagnonWrapper(JR, Rlist, atoms)
@@ -92,13 +94,13 @@ class PWFDownfolder:
92
94
  # anchors={(0, 0, 0): (-1, -2, -3, -4)},
93
95
  # anchors={(0, 0, 0): ()},
94
96
  # use_proj=True,
95
- enhance_Amn=2.0,
97
+ enhance_Amn=0.0,
96
98
  )
97
99
  params.update(kwargs)
98
100
  wann.set_parameters(**params)
99
101
  print("begin downfold")
100
102
  ewf = wann.downfold()
101
- ewf.save_hr_pickle("downfolded_JR.pickle")
103
+ # ewf.save_hr_pickle("downfolded_JR.pickle")
102
104
 
103
105
  # Plot the band structure.
104
106
  wann.plot_band_fitting(
@@ -135,7 +137,7 @@ class JDownfolder_pickle:
135
137
  qmesh=[7, 7, 7],
136
138
  iso_only=False,
137
139
  method="pwf",
138
- **kwargs
140
+ **kwargs,
139
141
  ):
140
142
  self.exc = SpinIO.load_pickle(path=inpath, fname="TB2J.pickle")
141
143
 
@@ -193,7 +195,7 @@ class JDownfolder_pickle:
193
195
  qmesh=self.qmesh,
194
196
  atoms=self.atoms,
195
197
  iso_only=self.iso_only,
196
- **kwargs
198
+ **kwargs,
197
199
  )
198
200
  Jd, Rlist = d.get_JR()
199
201
  return Jd, Rlist
@@ -274,10 +276,10 @@ class JDownfolder_pickle:
274
276
  def test():
275
277
  # pass
276
278
  # inpath = "/home/hexu/projects/NiCl2/vasp_inputs/TB2J_results"
277
- # inpath = "/home/hexu/projects/TB2J_example/CrI3/TB2J_results"
278
- inpath = "/home/hexu/projects/TB2J_projects/NiCl2/TB2J_NiCl/TB2J_results"
279
- fname = os.path.join(inpath, "TB2J.pickle")
280
- p = JDownfolder_pickle(
279
+ inpath = "/home/hexu/projects/TB2J_example/CrI3/TB2J_results"
280
+ # inpath = "/home/hexu/projects/TB2J_projects/NiCl2/TB2J_NiCl/TB2J_results"
281
+ _fname = os.path.join(inpath, "TB2J.pickle")
282
+ _p = JDownfolder_pickle(
281
283
  inpath=inpath, metals=["Ni"], ligands=["Cl"], outpath="TB2J_results_downfolded"
282
284
  )
283
285
 
TB2J/MAEGreen.py CHANGED
@@ -1,12 +1,17 @@
1
+ import gc
1
2
  from pathlib import Path
2
3
 
4
+ import matplotlib.pyplot as plt
3
5
  import numpy as np
4
6
  import tqdm
5
7
  from HamiltonIO.abacus.abacus_wrapper import AbacusSplitSOCParser
6
8
  from HamiltonIO.model.occupations import GaussOccupations
7
9
  from typing_extensions import DefaultDict
8
10
 
11
+ from TB2J.anisotropy import Anisotropy
9
12
  from TB2J.exchange import ExchangeNCL
13
+ from TB2J.external import p_map
14
+ from TB2J.mathutils.fibonacci_sphere import fibonacci_semisphere
10
15
 
11
16
  # from HamiltonIO.model.rotate_spin import rotate_Matrix_from_z_to_axis, rotate_Matrix_from_z_to_sperical
12
17
  # from TB2J.abacus.abacus_wrapper import AbacusWrapper, AbacusParser
@@ -22,12 +27,21 @@ def get_occupation(evals, kweights, nel, width=0.1):
22
27
 
23
28
  class MAEGreen(ExchangeNCL):
24
29
  def __init__(self, angles=None, **kwargs):
30
+ """
31
+ angles are defined as theta, phi pairs, where theta is the angle between the z-axis and the magnetization direction, and phi is the angle between the x-axis and the projection of the magnetization direction on the x-y plane.
32
+ """
25
33
  super().__init__(**kwargs)
26
34
  self.natoms = len(self.atoms)
27
35
  if angles is None or angles == "axis":
28
36
  self.set_angles_axis()
29
37
  elif angles == "scan":
30
38
  self.set_angles_scan()
39
+ elif angles == "fib":
40
+ self.set_angles_fib()
41
+ elif angles == "random":
42
+ self.set_angles_random()
43
+ elif angles == "miller":
44
+ self.set_angles_miller()
31
45
  else:
32
46
  self.thetas = angles[0]
33
47
  self.phis = angles[1]
@@ -38,9 +52,32 @@ class MAEGreen(ExchangeNCL):
38
52
  self.es_atom_orb = DefaultDict(lambda: 0)
39
53
 
40
54
  def set_angles_axis(self):
41
- """theta and phi are defined as the x, y, z, axis."""
42
- self.thetas = [0, np.pi / 2, np.pi / 2, np.pi / 2, np.pi]
43
- self.phis = [0, 0, np.pi / 2, np.pi / 4, 0]
55
+ """theta and phi are defined as the x, y, z, xy, yz, xz, xyz, x-yz, -xyz, -x-yz axis."""
56
+ self.thetas = [0, np.pi / 2, np.pi / 2, np.pi / 2, np.pi, 0, np.pi / 2, 0, 0, 0]
57
+ self.phis = [0, 0, np.pi / 2, np.pi / 4, 0, 0, 0, np.pi]
58
+
59
+ def set_angles_miller(self, nmax=2):
60
+ """theta and angles corresponding to the miller index. remove duplicates.
61
+ e.g. 002 and 001 are the same.
62
+ """
63
+ thetas = []
64
+ phis = []
65
+ for k in range(0, nmax + 1):
66
+ for j in range(-nmax, nmax + 1):
67
+ for i in range(-nmax, nmax + 1):
68
+ if i == 0 and j == 0 and k == 0:
69
+ continue
70
+ thetas.append(np.arccos(k / np.sqrt(i**2 + j**2 + k**2)))
71
+ if i == 0 and j == 0:
72
+ phis.append(0)
73
+ else:
74
+ phis.append(np.arctan2(j, i))
75
+ self.thetas = thetas
76
+ self.phis = phis
77
+ self.angle_pairs = list(zip(thetas, phis))
78
+ # remove duplicates of angles using sets.
79
+ self.angle_pairs = list(set(self.angle_pairs))
80
+ self.thetas, self.phis = zip(*self.angle_pairs)
44
81
 
45
82
  def set_angles_scan(self, step=15):
46
83
  self.thetas = []
@@ -50,6 +87,21 @@ class MAEGreen(ExchangeNCL):
50
87
  self.thetas.append(i * np.pi / 180)
51
88
  self.phis.append(j * np.pi / 180)
52
89
 
90
+ def set_angles_random(self, n=16):
91
+ # n random pairs of theta, phi
92
+ self.thetas = np.random.random(n) * np.pi
93
+ self.phis = np.random.random(n) * 2 * np.pi
94
+
95
+ def set_angles_fib(self, n=35):
96
+ self.thetas, self.phis = fibonacci_semisphere(n)
97
+ # thetas and phis are np.array
98
+ # add (theta, phi): (pi/2, pi/2) and (pi/2, pi/4)
99
+ # self.thetas += [np.pi / 2, np.pi / 2]
100
+ # self.phis += [np.pi / 2, np.pi / 4]
101
+ for i in range(8):
102
+ self.thetas = np.concatenate([self.thetas, [np.pi / 2]])
103
+ self.phis = np.concatenate([self.phis, [np.pi * i / 8]])
104
+
53
105
  def get_band_energy_vs_angles_from_eigen(
54
106
  self,
55
107
  thetas,
@@ -78,6 +130,13 @@ class MAEGreen(ExchangeNCL):
78
130
 
79
131
  def get_perturbed(self, e, thetas, phis):
80
132
  self.tbmodel.set_so_strength(0.0)
133
+ # maxsoc = self.tbmodel.get_max_Hsoc_abs()
134
+ # maxH0 = self.tbmodel.get_max_H0_spin_abs()
135
+ # if maxsoc > maxH0 * 0.01:
136
+ # print(f"""Warning: The SOC of the Hamiltonian has a maximum of {maxsoc} eV,
137
+ # comparing to the maximum of {maxH0} eV of the spin part of the Hamiltonian.
138
+ # The SOC is too strong, the perturbation theory may not be valid.""")
139
+
81
140
  G0K = self.G.get_Gk_all(e)
82
141
  Hsoc_k = self.tbmodel.get_Hk_soc(self.G.kpts)
83
142
  na = len(thetas)
@@ -92,12 +151,12 @@ class MAEGreen(ExchangeNCL):
92
151
  GdH = G0K[ik] @ dHi
93
152
  # dE += np.trace(GdH @ G0K[i].T.conj() @ dHi) * self.kweights[i]
94
153
  # diagonal of second order perturbation.
95
- dG2diag = np.diag(GdH @ GdH)
154
+ # dG2diag = np.diag(GdH @ GdH)
96
155
  # dG2 = np.einsum("ij,ji->ij", GdH, GdH)
97
156
  dG2 = GdH * GdH.T
98
157
  dG2sum = np.sum(dG2)
99
158
  # print(f"dG2sum-sum: {dG2sum}")
100
- dG2sum = np.sum(dG2diag)
159
+ # dG2sum = np.sum(dG2diag)
101
160
 
102
161
  # dG2sum = np.trace(GdH @ GdH)
103
162
  # print(f"dG2sum-Tr: {dG2sum}")
@@ -118,13 +177,11 @@ class MAEGreen(ExchangeNCL):
118
177
  + dE_atom_orb[1::2, ::2]
119
178
  + dE_atom_orb[::2, 1::2]
120
179
  )
180
+ dE_atom = np.sum(dE_atom_orb)
121
181
  mmat = self.mmats[iatom]
122
182
  dE_atom_orb = mmat.T @ dE_atom_orb @ mmat
123
-
124
183
  dE_angle_atom_orb[(iangle, iatom)] += dE_atom_orb
125
-
126
- dE_atom = np.sum(dG2diag[iorb]) * self.G.kweights[ik]
127
- # dE_atom = np.sum(dE_atom_orb)
184
+ # dE_atom = np.sum(dG2diag[iorb]) * self.G.kweights[ik]
128
185
  dE_angle_atom[iangle, iatom] += dE_atom
129
186
  return dE_angle, dE_angle_atom, dE_angle_atom_orb
130
187
 
@@ -143,15 +200,33 @@ class MAEGreen(ExchangeNCL):
143
200
  if with_eigen:
144
201
  self.es2 = self.get_band_energy_vs_angles_from_eigen(thetas, phis)
145
202
 
146
- for ie, e in enumerate(tqdm.tqdm(self.contour.path)):
147
- dE_angle, dE_angle_atom, dE_angle_atom_orb = self.get_perturbed(
148
- e, thetas, phis
149
- )
150
- self.es += dE_angle * self.contour.weights[ie]
151
- self.es_atom += dE_angle_atom * self.contour.weights[ie]
203
+ # for ie, e in enumerate(tqdm.tqdm(self.contour.path)):
204
+ # dE_angle, dE_angle_atom, dE_angle_atom_orb = self.get_perturbed(
205
+ # e, thetas, phis
206
+ # )
207
+ # self.es += dE_angle * self.contour.weights[ie]
208
+ # self.es_atom += dE_angle_atom * self.contour.weights[ie]
209
+ # for key, value in dE_angle_atom_orb.items():
210
+ # self.es_atom_orb[key] += (
211
+ # dE_angle_atom_orb[key] * self.contour.weights[ie]
212
+ # )
213
+
214
+ # rewrite the for loop above to use p_map
215
+ def func(e):
216
+ return self.get_perturbed(e, thetas, phis)
217
+
218
+ if self.nproc > 1:
219
+ results = p_map(func, self.contour.path, num_cpus=self.nproc)
220
+ else:
221
+ npole = len(self.contour.path)
222
+ results = map(func, tqdm.tqdm(self.contour.path, total=npole))
223
+ for i, result in enumerate(results):
224
+ dE_angle, dE_angle_atom, dE_angle_atom_orb = result
225
+ self.es += dE_angle * self.contour.weights[i]
226
+ self.es_atom += dE_angle_atom * self.contour.weights[i]
152
227
  for key, value in dE_angle_atom_orb.items():
153
228
  self.es_atom_orb[key] += (
154
- dE_angle_atom_orb[key] * self.contour.weights[ie]
229
+ dE_angle_atom_orb[key] * self.contour.weights[i]
155
230
  )
156
231
 
157
232
  self.es = -np.imag(self.es) / (2 * np.pi)
@@ -159,10 +234,25 @@ class MAEGreen(ExchangeNCL):
159
234
  for key, value in self.es_atom_orb.items():
160
235
  self.es_atom_orb[key] = -np.imag(value) / (2 * np.pi)
161
236
 
162
- def output(self, output_path="TB2J_anisotropy", with_eigen=True):
237
+ def fit_anisotropy_tensor(self):
238
+ self.ani = Anisotropy.fit_from_data(self.thetas, self.phis, self.es)
239
+ return self.ani
240
+
241
+ def output(
242
+ self,
243
+ output_path="TB2J_anisotropy",
244
+ with_eigen=True,
245
+ figure3d="MAE_3d.png",
246
+ figure_contourf="MAE_contourf.png",
247
+ ):
163
248
  Path(output_path).mkdir(exist_ok=True)
164
249
  fname = f"{output_path}/MAE.dat"
165
250
  fname_orb = f"{output_path}/MAE_orb.dat"
251
+ fname_tensor = f"{output_path}/MAE_tensor.dat"
252
+ if figure3d is not None:
253
+ fname_fig3d = f"{output_path}/{figure3d}"
254
+ if figure_contourf is not None:
255
+ fname_figcontourf = f"{output_path}/{figure_contourf}"
166
256
 
167
257
  # ouput with eigenvalues.
168
258
  if with_eigen:
@@ -178,15 +268,29 @@ class MAEGreen(ExchangeNCL):
178
268
  f.write("\n")
179
269
 
180
270
  with open(fname, "w") as f:
181
- f.write("# theta, phi, MAE(total), MAE(atom-wise) Unit: meV\n")
271
+ f.write("# theta (rad), phi(rad), MAE(total), MAE(atom-wise) Unit: meV\n")
182
272
  for i, (theta, phi, e, es) in enumerate(
183
273
  zip(self.thetas, self.phis, self.es, self.es_atom)
184
274
  ):
185
- f.write(f"{theta:.5f} {phi:.5f} {e*1e3:.8f} ")
275
+ f.write(f"{theta%np.pi:.5f} {phi%(2*np.pi):.5f} {e*1e3:.8f} ")
186
276
  for ea in es:
187
277
  f.write(f"{ea*1e3:.8f} ")
188
278
  f.write("\n")
189
279
 
280
+ self.ani = self.fit_anisotropy_tensor()
281
+ with open(fname_tensor, "w") as f:
282
+ f.write("# Anisotropy tensor in meV\n")
283
+ f.write(f"{self.ani.tensor_strings(include_isotropic=False)}\n")
284
+
285
+ if figure3d is not None:
286
+ self.ani.plot_3d(figname=fname_fig3d, show=False)
287
+
288
+ if figure_contourf is not None:
289
+ self.ani.plot_contourf(figname=fname_figcontourf, show=False)
290
+
291
+ plt.close()
292
+ gc.collect()
293
+
190
294
  with open(fname_orb, "w") as f:
191
295
  f.write("=" * 80 + "\n")
192
296
  f.write("Orbitals for each atom: \n")
TB2J/anisotropy.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import random
2
- import numpy as np
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
3
5
  import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ from numpy.linalg import matrix_rank
4
8
  from scipy.interpolate import LinearNDInterpolator
5
9
  from scipy.optimize import curve_fit
6
- from numpy.linalg import matrix_rank
7
- from dataclasses import dataclass
8
- from pathlib import Path
9
10
 
10
11
 
11
12
  @dataclass
@@ -18,8 +19,9 @@ class Anisotropy:
18
19
  @classmethod
19
20
  def from_T6(cls, T6):
20
21
  T = T6_to_T(T6)
21
- T -= np.trace(T) / 3 * np.eye(3)
22
- direction, amplitude = aniostropy_tensor_to_vector(T)
22
+ isotropic_part = np.trace(T) / 3
23
+ T -= isotropic_part * np.eye(3)
24
+ direction, amplitude = anisotropy_tensor_to_vector(T)
23
25
  return cls(
24
26
  direction=direction, amplitude=amplitude, isotropic_part=isotropic_part, T=T
25
27
  )
@@ -33,9 +35,9 @@ class Anisotropy:
33
35
 
34
36
  @classmethod
35
37
  def from_tensor(cls, T):
36
- isotropic_part = np.trace(T) / 3
38
+ isotropic_part = np.trace(T) / 3 * np.eye(3)
37
39
  T -= np.trace(T) / 3 * np.eye(3)
38
- direction, amplitude = aniostropy_tensor_to_vector(T)
40
+ direction, amplitude = anisotropy_tensor_to_vector(T)
39
41
  return cls(
40
42
  T=T, direction=direction, amplitude=amplitude, isotropic_part=isotropic_part
41
43
  )
@@ -44,13 +46,14 @@ class Anisotropy:
44
46
  if self.isotropic_part is None:
45
47
  self.isotropic_part = 0
46
48
  if self.T is None:
47
- self.T = anisotropy_vector_to_tensor(
48
- self.direction, self.amplitude
49
- ) + self.isotropic_part * np.eye(3)
49
+ self.T = (
50
+ anisotropy_vector_to_tensor(self.direction, self.amplitude)
51
+ + self.isotropic_part
52
+ )
50
53
  elif self.direction is None or self.amplitude is None:
51
- self.isotropic_part = np.trace(self.T) / 3
52
- # print(f'aniostropic tensor = {self.anisotropic_part}')
53
- self.direction, self.amplitude = aniostropy_tensor_to_vector(
54
+ self.isotropic_part = np.trace(self.T) / 3 * np.eye(3)
55
+ # print(f'anisotropic tensor = {self.anisotropic_part}')
56
+ self.direction, self.amplitude = anisotropy_tensor_to_vector(
54
57
  self.anisotropic_part
55
58
  )
56
59
  self.isotropic_part = np.trace(self.T) / 3
@@ -113,7 +116,6 @@ class Anisotropy:
113
116
  E = -self.amplitude * (S @ self.direction) ** 2
114
117
  if include_isotropic:
115
118
  E = E + self.isotropic_part
116
- print(f"E shape = {E.shape}")
117
119
  return E
118
120
 
119
121
  def energy_tensor_form(self, S=None, angle=None, include_isotropic=False):
@@ -126,7 +128,7 @@ class Anisotropy:
126
128
  return -S.T @ self.anisotropic_part @ S
127
129
 
128
130
  @classmethod
129
- def fit_from_data(cls, thetas, phis, values, test=False):
131
+ def fit_from_data(cls, thetas, phis, values, test=False, units="rad"):
130
132
  """
131
133
  Fit the anisotropic tensor to the data
132
134
  parameters:
@@ -137,14 +139,16 @@ class Anisotropy:
137
139
  the anisotropic object fitted from the data
138
140
  """
139
141
  angles = np.vstack([thetas, phis])
142
+ if units.lower().startswith("deg"):
143
+ angles = np.deg2rad(angles)
140
144
  params, cov = curve_fit(anisotropy_energy, angles, values)
141
145
  fitted_values = anisotropy_energy(angles, *params)
142
146
 
143
147
  delta = fitted_values - values
144
148
 
145
149
  # print(f'Max value = {np.max(values)}, Min value = {np.min(values)}')
146
- if np.abs(delta).max() > 1e-5:
147
- print(f"Warning: The fitting is not consistent with the data.")
150
+ if np.abs(delta).max() > 1e-4:
151
+ print("Warning: The fitting is not consistent with the data.")
148
152
  print(f"Max-min = {np.max(values) - np.min(values)}")
149
153
  print(f"delta = {np.max(np.abs(delta))}")
150
154
  T = T6_to_T(params)
@@ -157,7 +161,7 @@ class Anisotropy:
157
161
  angle=[thetas[i], phis[i]], include_isotropic=True
158
162
  )
159
163
  values2.append(E)
160
- delta2 = np.array(values2) - values
164
+ # delta2 = np.array(values2) - values
161
165
  # print(delta2)
162
166
 
163
167
  ax = plot_3D_scatter(angles, values - np.min(values), color="r")
@@ -183,7 +187,7 @@ class Anisotropy:
183
187
  delta = fitted_values - values
184
188
  print(f"Max value = {np.max(values)}, Min value = {np.min(values)}")
185
189
  print(f"Max-min = {np.max(values) - np.min(values)}")
186
- # print(f'delta = {delta}')
190
+ print(f"delta = {delta}")
187
191
  theta_a, phi_a, amplitude, isotropic_part = params
188
192
  direction = sphere_to_cartesian([theta_a, phi_a])
189
193
  return cls.from_direction_amplitude(
@@ -208,6 +212,25 @@ class Anisotropy:
208
212
  else:
209
213
  raise ValueError(f"Unknown method {method}")
210
214
 
215
+ @classmethod
216
+ def fit_from_xyz_data_file(cls, fname, method="tensor"):
217
+ """
218
+ Fit the anisotropic tensor to the data with x y z val form
219
+ parameters:
220
+ fname: the file name of the data
221
+ Return:
222
+ anisotropy: the anisotropic object
223
+ """
224
+ data = np.loadtxt(fname)
225
+ xyz, value = data[:, 0:3], data[:, 3]
226
+ theta, phi = np.array([cartesian_to_sphere(t) for t in xyz]).T
227
+ if method == "tensor":
228
+ return cls.fit_from_data(theta, phi, value)
229
+ elif method == "vector":
230
+ return cls.fit_from_data_vector_form(theta, phi, value)
231
+ else:
232
+ raise ValueError(f"Unknown method {method}")
233
+
211
234
  def plot_3d(self, ax=None, figname=None, show=True, surface=True):
212
235
  """
213
236
  plot the anisotropic energy in all directions in 3D
@@ -297,6 +320,55 @@ class Anisotropy:
297
320
  if show:
298
321
  plt.show()
299
322
 
323
+ def plot_contourf(self, ax=None, figname=None, show=False):
324
+ if ax is None:
325
+ fig, ax = plt.subplots()
326
+ X, Y = np.meshgrid(np.arange(0, 180, 1), np.arange(-180, 180, 1))
327
+ Z = np.zeros(X.shape)
328
+ ntheta, nphi = X.shape
329
+ for i in range(ntheta):
330
+ for j in range(nphi):
331
+ E = self.energy_tensor_form(angle=[X[i, j], Y[i, j]])
332
+ Z[i, j] = E
333
+ # find the X, Y for min and max of Z
334
+ X_max, Y_max = np.unravel_index(np.argmax(Z), Z.shape)
335
+ X_min, Y_min = np.unravel_index(np.argmin(Z), Z.shape)
336
+ X_max, Y_max = X[X_max, Y_max], Y[X_max, Y_max]
337
+ X_min, Y_min = X[X_min, Y_min], Y[X_min, Y_min]
338
+ c = ax.contourf(X, Y, Z, cmap="viridis", levels=200)
339
+ # print(X_max, Y_max, X_min, Y_min)
340
+ # ax.scatter(X_max, Y_max, color="r", marker="o")
341
+ # ax.scatter(X_min, Y_min, color="b", marker="o")
342
+ ax.set_xlabel("$\theta$ (degree)")
343
+ ax.set_ylabel("$\phi$ degree")
344
+ # ax.scatter(X_max, Y_max, color="r", marker="o")
345
+ # ax.scatter(X_min, Y_min, color="r", marker="o")
346
+
347
+ # colorbar
348
+ _cbar = plt.colorbar(c, ax=ax)
349
+
350
+ if figname is not None:
351
+ plt.savefig(figname)
352
+ plt.close()
353
+ if show:
354
+ print(f"Max = {X_max}, {Y_max}, Min = {X_min}, {Y_min}")
355
+ plt.show()
356
+ return ax
357
+
358
+ def tensor_strings(self, include_isotropic=False, multiplier=1):
359
+ """
360
+ convert the energy tensor to strings for easy printing
361
+ parameters:
362
+ include_isotropic: if include the isotropic part
363
+ multiplier: the multiplier for the tensor. Use for scaling the tensor by units.
364
+ """
365
+ if include_isotropic:
366
+ T = self.T
367
+ else:
368
+ T = self.T
369
+ strings = np.array2string(T * multiplier, precision=5, separator=" ")
370
+ return strings
371
+
300
372
 
301
373
  def plot_3D_scatter(angles, values, ax=None, **kwargs):
302
374
  if ax is None:
@@ -389,7 +461,7 @@ def sphere_to_cartesian(angles, r=1):
389
461
  return np.array([x, y, z])
390
462
 
391
463
 
392
- def cartesian_to_sphere(xyz):
464
+ def cartesian_to_sphere(xyz, unit="deg"):
393
465
  """
394
466
  Transform the cartesian coordinates to the spherical coordinates
395
467
  parameters:
@@ -401,8 +473,13 @@ def cartesian_to_sphere(xyz):
401
473
  r = np.linalg.norm(xyz)
402
474
  theta = np.arccos(z / r)
403
475
  phi = np.arctan2(y, x)
404
- theta = theta * 180 / np.pi
405
- phi = phi * 180 / np.pi
476
+ if unit.lower().startswith("deg"):
477
+ theta = theta * 180 / np.pi
478
+ phi = phi * 180 / np.pi
479
+ elif unit.lower.startswith("rad"):
480
+ pass
481
+ else:
482
+ raise ValueError("unit must be 'deg' or 'rad'")
406
483
  return np.array([theta, phi])
407
484
 
408
485
 
@@ -470,7 +547,7 @@ def fit_anisotropy(thetas, phis, values):
470
547
  params, cov = curve_fit(anisotropy_energy, angles, values)
471
548
  # check if the fitting is consistent with the data
472
549
  T = T6_to_T(params)
473
- direction, amp = anisostropy_tensor_to_vector(T)
550
+ direction, amp = anisotropy_tensor_to_vector(T)
474
551
  return direction, amp
475
552
 
476
553
 
@@ -518,7 +595,7 @@ def anisotropy_vector_to_tensor(direction, amplitude):
518
595
  return T
519
596
 
520
597
 
521
- def aniostropy_tensor_to_vector(T):
598
+ def anisotropy_tensor_to_vector(T):
522
599
  """
523
600
  Transform the anisotropic tensor to the anisotropic vector
524
601
  parameters:
@@ -553,7 +630,7 @@ def test_anisotorpy_vector_to_tensor():
553
630
  assert np.abs(diff) < 1e-10
554
631
 
555
632
  # test if the inverse transformation get the direction and amplitude back
556
- dir2, amp2 = aniostropy_tensor_to_vector(T)
633
+ dir2, amp2 = anisotropy_tensor_to_vector(T)
557
634
 
558
635
  # set the first element of the direction to be positive
559
636
  if direction[0] * dir2[0] < 0:
@@ -569,7 +646,7 @@ def test_anisotropy_tensor_to_vector():
569
646
  T = np.random.rand(3, 3)
570
647
  T = T + T.T
571
648
  T = T - np.trace(T) / 3
572
- direction, amplitude = aniostropy_tensor_to_vector(T)
649
+ direction, amplitude = anisotropy_tensor_to_vector(T)
573
650
  T2 = anisotropy_vector_to_tensor(direction, amplitude)
574
651
  print(f"T = {T}")
575
652
  print(f"T2 = {T2}")
@@ -597,9 +674,6 @@ def test_fit_anisotropy():
597
674
  f"theta = {theta[i]}, phi = {phi[i]}, value = {value[i]}, fitted value = {fitted_values[i]}, delta = {delta[i]}"
598
675
  )
599
676
 
600
- easy_axis = easy_axis_from_tensor(T6)
601
- print(f"easy_axis = {easy_axis}")
602
-
603
677
 
604
678
  def view_anisotropy_strain():
605
679
  strains1 = [(x, 0.0) for x in np.arange(0.000, 0.021, 0.002)]
TB2J/contour.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import numpy as np
2
2
  from scipy.special import roots_legendre
3
+
3
4
  from TB2J.utils import simpson_nonuniform_weight
4
5
 
5
6
 
@@ -15,6 +16,21 @@ class Contour:
15
16
  self._weights = simpson_nonuniform_weight(self.path)
16
17
  return self._weights
17
18
 
19
+ def integrate_func(self, func):
20
+ """
21
+ integrate f along the path
22
+ """
23
+ return np.dot(func(self.path), self.weights)
24
+
25
+ def integrate_values(self, values):
26
+ """
27
+ integrate f along the path
28
+ """
29
+ ret = 0
30
+ for i in range(len(values)):
31
+ ret += values[i] * self.weights[i]
32
+ return ret
33
+
18
34
  def build_path_semicircle(self, npoints, endpoint=True):
19
35
  R = (self.emax - self.emin) / 2.0
20
36
  R0 = (self.emin + self.emax) / 2.0