TB2Jflows 0.2__py3-none-any.whl → 0.2.1__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.
TB2Jflows/__init__.py CHANGED
@@ -1,2 +1,5 @@
1
- from TB2Jflows.ase_siesta import SiestaFlow
2
- from TB2Jflows.auto_siesta_TB2J import auto_siesta_TB2J
1
+ from TB2Jflows.ase_siesta import SiestaFlow as SiestaFlow
2
+ from TB2Jflows.auto_siesta_TB2J import auto_siesta_TB2J as auto_siesta_TB2J
3
+ from TB2Jflows.siesta_spinphonon import (
4
+ compute_spinphonon_coupling as compute_spinphonon_coupling,
5
+ )
TB2Jflows/ase_siesta.py CHANGED
@@ -5,11 +5,12 @@ from pathlib import Path
5
5
 
6
6
  import ase
7
7
  from ase.io.jsonio import decode, encode
8
- from pyDFTutils.siesta import MySiesta
9
8
  from TB2J.interfaces import gen_exchange_siesta
10
9
  from TB2J.io_merge import merge
11
10
  from TB2J.rotate_atoms import rotate_atom_spin, rotate_atom_xyz
12
11
 
12
+ from .mysiesta import MySiesta
13
+
13
14
 
14
15
  class SiestaFlow:
15
16
  def __init__(
@@ -24,7 +25,6 @@ class SiestaFlow:
24
25
  restart=True,
25
26
  metadata={},
26
27
  fdf_arguments={},
27
- relax_arguments={},
28
28
  split_soc=False,
29
29
  **kwargs,
30
30
  ):
@@ -33,6 +33,7 @@ class SiestaFlow:
33
33
  self.xc = xc
34
34
  self.kpts = kpts
35
35
  self.Udict = Udict
36
+
36
37
  # default fdf arguments
37
38
  self.fdf_arguments = {
38
39
  "MaxSCFIterations": 350,
@@ -51,7 +52,6 @@ class SiestaFlow:
51
52
  self.root_path = root_path
52
53
  self.restart = restart
53
54
  self.kwargs = kwargs
54
- self.relax_args = relax_arguments
55
55
 
56
56
  # paths
57
57
  self.metadata_path = os.path.join(self.root_path, "metadata.json")
@@ -160,7 +160,6 @@ class SiestaFlow:
160
160
  MaxStressTol=0.1,
161
161
  NumCGSteps=200,
162
162
  ):
163
- print(f"Relaxation in {path}")
164
163
  if (
165
164
  self.restart
166
165
  and self.metadata["already_relaxed"]
@@ -172,20 +171,20 @@ class SiestaFlow:
172
171
  old_spin = self.spin
173
172
  if use_collinear:
174
173
  self.spin = "collinear"
175
- calc = self.get_calculator(atoms, path=self.relax_path, label=label)
176
- calc.atoms = atoms
177
- atoms = calc.relax(
178
- atoms,
179
- TypeOfRun=TypeOfRun,
180
- VariableCell=VariableCell,
181
- ConstantVolume=ConstantVolume,
182
- RelaxCellOnly=RelaxCellOnly,
183
- MaxForceTol=MaxForceTol,
184
- MaxStressTol=MaxStressTol,
185
- NumCGSteps=NumCGSteps,
186
- )
187
- self.spin = old_spin
188
- self.relaxed_atoms = atoms
174
+ calc = self.get_calculator(atoms, path=self.relax_path, label=label)
175
+ calc.atoms = atoms
176
+ atoms = calc.relax(
177
+ atoms,
178
+ TypeOfRun=TypeOfRun,
179
+ VariableCell=VariableCell,
180
+ ConstantVolume=ConstantVolume,
181
+ RelaxCellOnly=RelaxCellOnly,
182
+ MaxForceTol=MaxForceTol,
183
+ MaxStressTol=MaxStressTol,
184
+ NumCGSteps=NumCGSteps,
185
+ )
186
+ self.spin = old_spin
187
+ self.relaxed_atoms = atoms
189
188
  self.update_metadata(
190
189
  {
191
190
  "already_relaxed": True,
@@ -359,9 +358,9 @@ class SiestaFlow:
359
358
  write_path=os.path.join(self.root_path, "TB2J_results_merged"),
360
359
  )
361
360
 
362
- def runall_collinear(self, atoms, relax=True, scf=True, TB2J=True, **kwargs):
361
+ def runall_collinear(self, atoms, relax=True, scf=True, TB2J=True, **kwargs):
363
362
  if relax:
364
- atoms = self.relax(atoms, **self.relax_args)
363
+ atoms = self.relax(atoms)
365
364
  if scf:
366
365
  self.scf_calculation_collinear(atoms, label="siesta")
367
366
  if TB2J:
@@ -371,7 +370,7 @@ class SiestaFlow:
371
370
  self, atoms, relax=True, scf=True, TB2J=True, rotate_type="structure", **kwargs
372
371
  ):
373
372
  if relax:
374
- atoms = self.relax(atoms, **self.relax_args)
373
+ atoms = self.relax(atoms)
375
374
  if scf:
376
375
  self.scf_calculation_with_rotations(
377
376
  atoms, rotate_type=rotate_type, label="siesta"
@@ -382,7 +381,7 @@ class SiestaFlow:
382
381
 
383
382
  def runall_split_soc(self, atoms, relax=True, scf=True, TB2J=True, **kwargs):
384
383
  if relax:
385
- atoms = self.relax(atoms, **self.relax_args)
384
+ atoms = self.relax(atoms)
386
385
  if scf:
387
386
  self.scf_calculatoin_split_soc(atoms, label="siesta")
388
387
  if TB2J:
@@ -52,7 +52,6 @@ def auto_siesta_TB2J(
52
52
  TB2J=True,
53
53
  rotate_type="structure",
54
54
  fincore=True,
55
- relax_kwargs={},
56
55
  siesta_kwargs={},
57
56
  TB2J_kwargs={},
58
57
  fdf_kwargs={},
@@ -100,7 +99,6 @@ def auto_siesta_TB2J(
100
99
  fincore=fincore,
101
100
  Udict=Udict,
102
101
  split_soc=split_soc,
103
- relax_arguments = relax_kwargs,
104
102
  **siesta_kwargs,
105
103
  )
106
104
  flow.write_metadata()
TB2Jflows/find_pp.py ADDED
@@ -0,0 +1,91 @@
1
+ import os
2
+
3
+ from ase.data import atomic_numbers
4
+
5
+
6
+ class PPFinder:
7
+ def __init__(self):
8
+ pass
9
+
10
+ def get_pp_path(self, element, xc, label, rel):
11
+ pass
12
+
13
+
14
+ class DojoFinder:
15
+ def __init__(self, path=None):
16
+ if path is None:
17
+ self.path = os.environ["DOJO_PATH"]
18
+ else:
19
+ self.path = path
20
+
21
+ def get_pp_path(
22
+ self, xc: str, typ="NC", rel="sr", version="04", accuracy="standard", fmt="psml"
23
+ ):
24
+ typ = typ.lower()
25
+ xc = xc.lower()
26
+ if xc == "lda":
27
+ xc = "pbe"
28
+ dirname = os.path.join(
29
+ self.path, f"{typ}-{rel}-{version}_{xc}_{accuracy}_{fmt}"
30
+ )
31
+ if not os.path.exists(dirname):
32
+ raise FileNotFoundError(f"File Not found: {dirname}")
33
+ return dirname
34
+
35
+ def get_pp_fname(
36
+ self,
37
+ element,
38
+ xc: str,
39
+ typ="NC",
40
+ rel="sr",
41
+ version="04",
42
+ accuracy="standard",
43
+ fincore=False,
44
+ fmt="psml",
45
+ ):
46
+ if 57 < atomic_numbers[element] <= 70:
47
+ if fincore:
48
+ fname = os.path.join(
49
+ self.get_pp_path(
50
+ xc=xc,
51
+ typ=typ,
52
+ rel=rel,
53
+ version=version,
54
+ accuracy=accuracy,
55
+ fmt=fmt,
56
+ ),
57
+ f"fincore/{element}.{fmt}",
58
+ )
59
+ else:
60
+ fname = os.path.join(
61
+ self.get_pp_path(
62
+ xc=xc,
63
+ typ=typ,
64
+ rel=rel,
65
+ version=version,
66
+ accuracy=accuracy,
67
+ fmt=fmt,
68
+ ),
69
+ f"withf/{element}.{fmt}",
70
+ )
71
+
72
+ else:
73
+ fname = os.path.join(
74
+ self.get_pp_path(
75
+ xc=xc, typ=typ, rel=rel, version=version, accuracy=accuracy, fmt=fmt
76
+ ),
77
+ f"{element}.{fmt}",
78
+ )
79
+ if not os.path.exists(fname):
80
+ raise FileNotFoundError(f"File Not found: {fname}")
81
+ return fname
82
+
83
+
84
+ def test():
85
+ finder = DojoFinder(path=os.path.expanduser("~/projects/pp/dojo"))
86
+ finder.get_pp_path(element="Sr", xc="pbesol")
87
+ finder.get_pp_path(element="Srg", xc="pbe")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ test()
TB2Jflows/mysiesta.py ADDED
@@ -0,0 +1,570 @@
1
+ import math
2
+ import os
3
+ import shutil
4
+ from os.path import isfile, islink, join
5
+
6
+ import numpy as np
7
+ from ase import Atoms
8
+ from ase.calculators.siesta import Siesta
9
+ from ase.calculators.siesta.parameters import PAOBasisBlock, Species, format_fdf
10
+ from ase.data import atomic_numbers
11
+ from ase.io import write
12
+ from ase.units import Bohr
13
+
14
+ from .find_pp import DojoFinder
15
+
16
+ # from pyDFTutils.siesta.pdos import gen_pdos_figure, plot_layer_pdos
17
+ from .siesta_basis import fincore_basis, withf_basis
18
+
19
+
20
+ def get_valence_charge(pseudopotential):
21
+ raise NotImplementedError(
22
+ "get_valence_charge is not available in the current environment."
23
+ )
24
+
25
+
26
+ def read_vca_synth_block(synth_block_filename, species_number):
27
+ raise NotImplementedError(
28
+ "read_vca_synth_block is not available in the current environment."
29
+ )
30
+
31
+
32
+ synthetic_atoms_dict_fincore = {
33
+ # Ce-Lu
34
+ "Ce": ((5, 5, 5, 4), (2, 6, 3, 0)),
35
+ "Pr": ((5, 5, 5, 4), (2, 6, 3, 0)),
36
+ "Nd": ((5, 5, 5, 4), (2, 6, 3, 0)),
37
+ "Pm": ((5, 5, 5, 4), (2, 6, 3, 0)),
38
+ "Sm": ((5, 5, 5, 4), (2, 6, 3, 0)),
39
+ "Eu": ((5, 5, 5, 4), (2, 6, 3, 0)),
40
+ "Gd": ((5, 5, 5, 4), (2, 6, 3, 0)),
41
+ "Tb": ((5, 5, 5, 4), (2, 6, 3, 0)),
42
+ "Dy": ((5, 5, 5, 4), (2, 6, 3, 0)),
43
+ "Ho": ((5, 5, 5, 4), (2, 6, 3, 0)),
44
+ "Er": ((5, 5, 5, 4), (2, 6, 3, 0)),
45
+ "Tm": ((5, 5, 5, 4), (2, 6, 3, 0)),
46
+ "Yb": ((6, 5, 5, 5), (2, 6, 1, 0)),
47
+ "Lu": ((5, 5, 5, 4), (2, 6, 3, 0)),
48
+ }
49
+
50
+
51
+ def read_siesta_xv(fd):
52
+ vectors = []
53
+ for i in range(3):
54
+ data = next(fd).split()
55
+ vectors.append([float(data[j]) * Bohr for j in range(3)])
56
+
57
+ # Read number of atoms (line 4)
58
+ natoms = int(next(fd).split()[0])
59
+
60
+ # Read remaining lines
61
+ speciesnumber, atomnumbers, xyz, V = [], [], [], []
62
+ for line in fd:
63
+ if len(line) > 5: # Ignore blank lines
64
+ data = line.split()
65
+ speciesnumber.append(int(data[0]))
66
+ atomnumbers.append(int(data[1]) % 200)
67
+ xyz.append([float(data[2 + j]) * Bohr for j in range(3)])
68
+ V.append([float(data[5 + j]) * Bohr for j in range(3)])
69
+
70
+ vectors = np.array(vectors)
71
+ atomnumbers = np.array(atomnumbers)
72
+ xyz = np.array(xyz)
73
+ atoms = Atoms(numbers=atomnumbers, positions=xyz, cell=vectors, pbc=True)
74
+ assert natoms == len(atoms)
75
+ return atoms
76
+
77
+
78
+ def read_xv(fname):
79
+ with open(fname) as myfile:
80
+ atoms = read_siesta_xv(myfile)
81
+ return atoms
82
+
83
+
84
+ def get_species(atoms, xc, rel="sr", accuracy="standard", fincore=False):
85
+ finder = DojoFinder()
86
+ elems = list(dict.fromkeys(atoms.get_chemical_symbols()).keys())
87
+ elem_dict = dict(zip(elems, range(1, len(elems) + 1)))
88
+ pseudo_path = finder.get_pp_path(xc=xc)
89
+ species = [
90
+ Species(
91
+ symbol=elem,
92
+ pseudopotential=finder.get_pp_fname(
93
+ elem, xc=xc, rel=rel, accuracy=accuracy, fincore=fincore
94
+ ),
95
+ ghost=False,
96
+ )
97
+ for elem in elem_dict.keys()
98
+ ]
99
+ return pseudo_path, species
100
+
101
+
102
+ def cart2sph(vec):
103
+ x, y, z = vec
104
+ r = np.linalg.norm(vec) # r
105
+ if r < 1e-10:
106
+ theta, phi = 0.0, 0.0
107
+ else:
108
+ # note that there are many conventions, here is the ISO convention.
109
+ phi = math.atan2(y, x) * 180 / math.pi # phi
110
+ theta = math.acos(z / r) * 180 / math.pi # theta
111
+ return r, theta, phi
112
+
113
+
114
+ class MySiesta(Siesta):
115
+ def __init__(
116
+ self,
117
+ atoms=None,
118
+ command=None,
119
+ xc="LDA",
120
+ spin="non-polarized",
121
+ basis_set="DZP",
122
+ species=None,
123
+ ghosts=[],
124
+ synthetic_atoms={},
125
+ input_basis_set={},
126
+ pseudo_path=None,
127
+ input_pp={},
128
+ pp_accuracy="standard",
129
+ fincore=False,
130
+ **kwargs,
131
+ ):
132
+ # non-perturnbative polarized orbital.
133
+ self.npt_elems = set()
134
+
135
+ if fincore:
136
+ self.synthetic_atoms = synthetic_atoms_dict_fincore.copy()
137
+ self.synthetic_atoms.update(synthetic_atoms)
138
+ else:
139
+ self.synthetic_atoms = synthetic_atoms
140
+
141
+ if fincore:
142
+ input_basis_set.update(fincore_basis)
143
+ else:
144
+ input_basis_set.update(withf_basis)
145
+
146
+ if atoms is not None:
147
+ finder = DojoFinder()
148
+ elems = list(dict.fromkeys(atoms.get_chemical_symbols()).keys())
149
+ self.elem_dict = dict(zip(elems, range(1, len(elems) + 1)))
150
+ symbols = atoms.get_chemical_symbols()
151
+
152
+ # ghosts
153
+ ghost_symbols = [symbols[i] for i in ghosts]
154
+ ghost_elems = list(dict.fromkeys(ghost_symbols).keys())
155
+ tags = [1 if i in ghosts else 0 for i in range(len(atoms))]
156
+ atoms.set_tags(tags)
157
+
158
+ if pseudo_path is None:
159
+ pseudo_path = finder.get_pp_path(xc=xc, accuracy=pp_accuracy)
160
+
161
+ if spin == "spin-orbit":
162
+ rel = "fr"
163
+ else:
164
+ rel = "sr"
165
+ species = []
166
+ for elem, index in self.elem_dict.items():
167
+ if elem not in input_basis_set:
168
+ bselem = basis_set
169
+ if elem in ["Li", "Be", "Na", "Mg", "Sm"]:
170
+ self.npt_elems.add(f"{elem}.{index}")
171
+ else:
172
+ bselem = PAOBasisBlock(input_basis_set[elem])
173
+ if elem not in input_pp:
174
+ pseudopotential = finder.get_pp_fname(
175
+ elem, xc=xc, rel=rel, accuracy=pp_accuracy, fincore=fincore
176
+ )
177
+ else:
178
+ pseudopotential = os.path.join(pseudo_path, input_pp[elem])
179
+
180
+ if elem in self.synthetic_atoms:
181
+ excess_charge = 0
182
+ else:
183
+ excess_charge = None
184
+
185
+ species.append(
186
+ Species(
187
+ symbol=elem,
188
+ pseudopotential=pseudopotential,
189
+ basis_set=bselem,
190
+ ghost=False,
191
+ excess_charge=excess_charge,
192
+ )
193
+ )
194
+ for elem in ghost_elems:
195
+ species.append(
196
+ Species(
197
+ symbol=elem,
198
+ pseudopotential=finder.get_pp_fname(
199
+ elem, xc=xc, rel=rel, accuracy=pp_accuracy, fincore=fincore
200
+ ),
201
+ tag=1,
202
+ ghost=True,
203
+ )
204
+ )
205
+
206
+ Siesta.__init__(
207
+ self,
208
+ xc=xc,
209
+ spin=spin,
210
+ atoms=atoms,
211
+ pseudo_path=pseudo_path,
212
+ species=species,
213
+ **kwargs,
214
+ )
215
+ self.set_npt_elements()
216
+ self.set_synthetic_atoms()
217
+
218
+ def _write_species(self, fd, atoms):
219
+ """Write input related the different species.
220
+
221
+ Parameters:
222
+ - f: An open file object.
223
+ - atoms: An atoms object.
224
+ """
225
+ species, species_numbers = self.species(atoms)
226
+
227
+ if self["pseudo_path"] is not None:
228
+ pseudo_path = self["pseudo_path"]
229
+ elif "SIESTA_PP_PATH" in os.environ:
230
+ pseudo_path = os.environ["SIESTA_PP_PATH"]
231
+ else:
232
+ mess = "Please set the environment variable 'SIESTA_PP_PATH'"
233
+ raise Exception(mess)
234
+
235
+ fd.write(format_fdf("NumberOfSpecies", len(species)))
236
+ fd.write(format_fdf("NumberOfAtoms", len(atoms)))
237
+
238
+ pao_basis = []
239
+ chemical_labels = []
240
+ basis_sizes = []
241
+ synth_blocks = []
242
+ for species_number, spec in enumerate(species):
243
+ species_number += 1
244
+ symbol = spec["symbol"]
245
+ atomic_number = atomic_numbers[symbol]
246
+
247
+ if spec["pseudopotential"] is None:
248
+ if self.pseudo_qualifier() == "":
249
+ label = symbol
250
+ pseudopotential = label + ".psf"
251
+ else:
252
+ label = ".".join([symbol, self.pseudo_qualifier()])
253
+ pseudopotential = label + ".psf"
254
+ else:
255
+ pseudopotential = spec["pseudopotential"]
256
+ label = os.path.basename(pseudopotential)
257
+ label = ".".join(label.split(".")[:-1])
258
+
259
+ if not os.path.isabs(pseudopotential):
260
+ pseudopotential = join(pseudo_path, pseudopotential)
261
+
262
+ if not os.path.exists(pseudopotential):
263
+ mess = "Pseudopotential '%s' not found" % pseudopotential
264
+ raise RuntimeError(mess)
265
+
266
+ name = os.path.basename(pseudopotential)
267
+ name = name.split(".")
268
+ name.insert(-1, str(species_number))
269
+ if spec["ghost"]:
270
+ name.insert(-1, "ghost")
271
+ atomic_number = -atomic_number
272
+
273
+ name = ".".join(name)
274
+ pseudo_targetpath = self.getpath(name)
275
+
276
+ if join(os.getcwd(), name) != pseudopotential:
277
+ if islink(pseudo_targetpath) or isfile(pseudo_targetpath):
278
+ os.remove(pseudo_targetpath)
279
+ symlink_pseudos = self["symlink_pseudos"]
280
+
281
+ symlink_pseudos = False
282
+
283
+ if symlink_pseudos is None:
284
+ symlink_pseudos = not os.name == "nt"
285
+
286
+ if symlink_pseudos:
287
+ os.symlink(pseudopotential, pseudo_targetpath)
288
+ else:
289
+ shutil.copy(pseudopotential, pseudo_targetpath)
290
+ if spec["excess_charge"] is not None:
291
+ atomic_number += 200
292
+ # n_atoms = sum(np.array(species_numbers) == species_number)
293
+
294
+ # if spec["excess_charge"] != 0:
295
+ # paec = float(spec["excess_charge"]) / n_atoms
296
+ # vc = get_valence_charge(pseudopotential)
297
+ # fraction = float(vc + paec) / vc
298
+ # pseudo_head = name[:-4]
299
+ # fractional_command = os.environ["SIESTA_UTIL_FRACTIONAL"]
300
+ # cmd = "%s %s %.7f" % (fractional_command, pseudo_head, fraction)
301
+ # os.system(cmd)
302
+ #
303
+ # pseudo_head += "-Fraction-%.5f" % fraction
304
+ # synth_pseudo = pseudo_head + ".psf"
305
+ # synth_block_filename = pseudo_head + ".synth"
306
+ # os.remove(name)
307
+ # shutil.copyfile(synth_pseudo, name)
308
+ # synth_block = read_vca_synth_block(
309
+ # synth_block_filename, species_number=species_number
310
+ # )
311
+ # synth_blocks.append(synth_block)
312
+ # else:
313
+ # synth_block = self.synthetic_atoms[symbol]
314
+ if symbol in self.synthetic_atoms:
315
+ synth_block = self.synthetic_atoms[symbol]
316
+ synth_blocks.append(synth_block)
317
+
318
+ if len(synth_blocks) > 0:
319
+ fd.write(format_fdf("SyntheticAtoms", list(synth_blocks)))
320
+
321
+ label = ".".join(np.array(name.split("."))[:-1])
322
+ string = " %d %d %s" % (species_number, atomic_number, label)
323
+ chemical_labels.append(string)
324
+ if isinstance(spec["basis_set"], PAOBasisBlock):
325
+ pao_basis.append(spec["basis_set"].script(label))
326
+ else:
327
+ basis_sizes.append((" " + label, spec["basis_set"]))
328
+ fd.write((format_fdf("ChemicalSpecieslabel", chemical_labels)))
329
+ fd.write("\n")
330
+ fd.write((format_fdf("PAO.Basis", pao_basis)))
331
+ fd.write((format_fdf("PAO.BasisSizes", basis_sizes)))
332
+ fd.write("\n")
333
+
334
+ def set_npt_elements(self):
335
+ if len(self.npt_elems) > 0:
336
+ npt_text = []
337
+ for name in self.npt_elems:
338
+ npt_text.append(f"{name} non-perturbative ")
339
+ # npt_text += "%endblock PAO.PolarizationScheme\n"
340
+ self["fdf_arguments"].update({"PAO.PolarizationScheme": npt_text})
341
+
342
+ def set_synthetic_atoms(self):
343
+ print("setting syn")
344
+ nsyn = len(self.synthetic_atoms)
345
+ if nsyn > 0:
346
+ syntext = []
347
+ # syntext.append(f"{nsyn}")
348
+ for name, content in self.synthetic_atoms.items():
349
+ if name in self.elem_dict:
350
+ syntext.append(f"{self.elem_dict[name]}")
351
+ syntext.append(" ".join([str(x) for x in content[0]]))
352
+ syntext.append(" ".join([str(x) for x in content[1]]))
353
+ if len(syntext) > 0:
354
+ self["fdf_arguments"].update({"SyntheticAtoms": syntext})
355
+
356
+ # print("setting syn:", syntext)
357
+
358
+ def set_fdf_arguments(self, fdf_arguments):
359
+ self["fdf_arguments"].update(fdf_arguments)
360
+
361
+ def set_mixer(
362
+ self,
363
+ method="pulay",
364
+ weight=0.05,
365
+ history=10,
366
+ restart=25,
367
+ restart_save=4,
368
+ linear_after=0,
369
+ linear_after_weight=0.1,
370
+ ):
371
+ pass
372
+
373
+ def update_fdf_arguments(self, fdf_arguments):
374
+ self["fdf_arguments"].update(fdf_arguments)
375
+
376
+ def add_Hubbard_U(
377
+ self,
378
+ specy,
379
+ n=3,
380
+ l=2, # noqa: E741
381
+ U=0,
382
+ J=0,
383
+ rc=0.0,
384
+ Fermi_cut=0.0,
385
+ scale_factor="0.95",
386
+ ):
387
+ if "Udict" not in self.__dict__:
388
+ self.Udict = dict()
389
+ idx = self.elem_dict[specy]
390
+ specy_label = f"{specy}.{idx}"
391
+ self.Udict[specy_label] = {
392
+ "n": n,
393
+ "l": l,
394
+ "U": U,
395
+ "J": J,
396
+ "rc": rc,
397
+ "Fermi_cut": Fermi_cut,
398
+ "scale_factor": scale_factor,
399
+ }
400
+ self.set_Hubbard_U(self.Udict)
401
+
402
+ def set_Hubbard_U(self, Udict):
403
+ """
404
+ Udict: {'Fe': {'n':n, 'l':l, 'U':U, 'J', J, 'rc':rc, 'Fermi_cut':Fermi_cut }}
405
+ """
406
+ Ublock = []
407
+ for key, val in Udict.items():
408
+ Ublock.append(" %s %s " % (key, 1))
409
+ if val["n"] is not None:
410
+ Ublock.append(" n=%s %s" % (val["n"], val["l"]))
411
+ else:
412
+ Ublock.append("%s" % (val["l"]))
413
+ Ublock.append(" %s %s" % (val["U"], val["J"]))
414
+ if "rc" in val:
415
+ Ublock.append(" %s %s" % (val["rc"], val["Fermi_cut"]))
416
+ Ublock.append(" %s" % val["scale_factor"])
417
+
418
+ self.update_fdf_arguments(
419
+ {"LDAU.Proj": Ublock, "LDAU.ProjectorGenerationMethod": 2}
420
+ )
421
+
422
+ def set_Udict(self, Udict):
423
+ """
424
+ Udict: e.g. {"Fe":{"n":3, "l":2, "U":3.0, "J":0.0}, ...}
425
+ or {"Fe":{[3, 2, 3, 0]}
426
+
427
+ """
428
+ for specy, val in Udict.items():
429
+ if isinstance(val, dict):
430
+ self.add_Hubbard_U(specy, **val)
431
+ else:
432
+ self.add_Hubbard_U(specy, *val)
433
+
434
+ def write_Hubbard_block(self, f):
435
+ pass
436
+
437
+ def relax(
438
+ self,
439
+ atoms,
440
+ TypeOfRun="Broyden",
441
+ VariableCell=True,
442
+ ConstantVolume=False,
443
+ RelaxCellOnly=False,
444
+ MaxForceTol=0.001,
445
+ MaxStressTol=1,
446
+ NumCGSteps=40,
447
+ relaxed_file="relaxed.vasp",
448
+ ):
449
+ pbc = atoms.get_pbc()
450
+ initial_magnetic_moments = atoms.get_initial_magnetic_moments()
451
+ self.update_fdf_arguments(
452
+ {
453
+ "MD.TypeOfRun": TypeOfRun,
454
+ "MD.VariableCell": VariableCell,
455
+ "MD.ConstantVolume": ConstantVolume,
456
+ "MD.RelaxCellOnly": RelaxCellOnly,
457
+ "MD.MaxForceTol": "%s eV/Ang" % MaxForceTol,
458
+ "MD.MaxStressTol": "%s GPa" % MaxStressTol,
459
+ "MD.NumCGSteps": NumCGSteps,
460
+ }
461
+ )
462
+ self.calculate(atoms)
463
+ # self.read(self.prefix + '.XV')
464
+ self.atoms = read_xv(os.path.join(self.directory, self.prefix + ".XV"))
465
+ self.atoms.set_pbc(pbc)
466
+ self.atoms.set_initial_magnetic_moments(initial_magnetic_moments)
467
+ atoms = self.atoms
468
+ self.update_fdf_arguments(
469
+ {
470
+ "MD.NumCGSteps": 0,
471
+ }
472
+ )
473
+ if relaxed_file is not None:
474
+ write(relaxed_file, atoms, vasp5=True, sort=False)
475
+ return self.atoms
476
+
477
+ def scf_calculation(self, atoms, dos=True, kpts=[7, 7, 7], **kwargs):
478
+ if dos:
479
+ k1, k2, k3 = kpts
480
+ self.update_fdf_arguments(
481
+ {
482
+ "WriteEigenvalues": ".true.",
483
+ "ProjectedDensityOfStates": ["-70.00 30.0 0.015 3000 eV"],
484
+ "PDOS.kgrid_Monkhorst_Pack": [
485
+ f"{k1} 0 0 0.0",
486
+ f"0 {k2} 0 0.0",
487
+ f"0 0 {k3} 0.0",
488
+ ],
489
+ }
490
+ )
491
+ self.calculate(atoms, **kwargs)
492
+
493
+ def _write_structure(self, f, atoms):
494
+ """Translate the Atoms object to fdf-format.
495
+
496
+ Parameters:
497
+ - f: An open file object.
498
+ - atoms: An atoms object.
499
+ """
500
+ cell = atoms.cell
501
+ f.write("\n")
502
+
503
+ if cell.rank in [1, 2]:
504
+ raise ValueError(
505
+ "Expected 3D unit cell or no unit cell. You may "
506
+ "wish to add vacuum along some directions."
507
+ )
508
+
509
+ # Write lattice vectors
510
+ if np.any(cell):
511
+ f.write(format_fdf("LatticeConstant", "1.0 Ang"))
512
+ f.write("%block LatticeVectors\n")
513
+ for i in range(3):
514
+ for j in range(3):
515
+ s = (" %.15f" % cell[i, j]).rjust(16) + " "
516
+ f.write(s)
517
+ f.write("\n")
518
+ f.write("%endblock LatticeVectors\n")
519
+ f.write("\n")
520
+
521
+ self._write_atomic_coordinates(f, atoms)
522
+
523
+ # Write magnetic moments.
524
+ magmoms = atoms.get_initial_magnetic_moments()
525
+
526
+ # The DM.InitSpin block must be written to initialize to
527
+ # no spin. SIESTA default is FM initialization, if the
528
+ # block is not written, but we must conform to the
529
+ # atoms object.
530
+ if magmoms is not None:
531
+ if len(magmoms) == 0:
532
+ f.write("#Empty block forces ASE initialization.\n")
533
+
534
+ f.write("%block DM.InitSpin\n")
535
+ if len(magmoms) != 0 and isinstance(magmoms[0], np.ndarray):
536
+ for n, Mcart in enumerate(magmoms):
537
+ M = cart2sph(Mcart)
538
+ if M[0] != 0:
539
+ f.write(
540
+ " %d %.14f %.14f %.14f \n" % (n + 1, M[0], M[1], M[2])
541
+ )
542
+ elif len(magmoms) != 0 and isinstance(magmoms[0], float):
543
+ for n, M in enumerate(magmoms):
544
+ if M != 0:
545
+ f.write(" %d %.14f \n" % (n + 1, M))
546
+ f.write("%endblock DM.InitSpin\n")
547
+ f.write("\n")
548
+
549
+ def my_read_results(self):
550
+ """Read the results."""
551
+ # self.read_number_of_grid_points()
552
+ self.read_energy()
553
+ self.read_forces_stress()
554
+ # self.read_eigenvalues()
555
+ self.read_kpoints()
556
+ self.read_dipole()
557
+ self.read_pseudo_density()
558
+ # self.read_hsx()
559
+ self.read_dim()
560
+ # if self.results['hsx'] is not None:
561
+ # self.read_pld(self.results['hsx'].norbitals,
562
+ # len(self.atoms))
563
+ # self.atoms.cell = self.results['pld'].cell * Bohr
564
+ # else:
565
+ # self.results['pld'] = None
566
+
567
+ # self.read_wfsx()
568
+ self.read_ion(self.atoms)
569
+
570
+ self.read_bands()
@@ -0,0 +1,47 @@
1
+ withf_basis = {
2
+ # "La": "5\nn=5 0 1\n3.889 \n1.0 \nn=6 0 2\n9.113 6.906 \n1.0 1.0 \nn=5 1 1\n4.548 \n1.0 \nn=6 1 1\n9.113 \n1.0 \nn=5 2 2\n7.55 5.398 \n1.0 1.0 \n",
3
+ "Ce": "6\nn=5 0 1\n3.805 \n1.0 \nn=6 0 2\n9.041 6.837 \n1.0 1.0 \nn=5 1 1\n4.458 \n1.0 \nn=6 1 1\n9.041 \n1.0 \nn=5 2 2\n7.489 5.339 \n1.0 1.0 \nn=4 3 2\n4.114 2.494 \n1.0 1.0 \n",
4
+ "Pr": "6\nn=5 0 1\n3.737 \n1.0 \nn=6 0 2\n8.879 6.702 \n1.0 1.0 \nn=5 1 1\n4.387 \n1.0 \nn=6 1 1\n8.879 \n1.0 \nn=5 2 2\n7.46 5.307 \n1.0 1.0 \nn=4 3 2\n3.944 2.369 \n1.0 1.0 \n",
5
+ "Nd": "6\nn=5 0 1\n3.67 \n1.0 \nn=6 0 2\n8.808 6.641 \n1.0 1.0 \nn=5 1 1\n4.317 \n1.0 \nn=6 1 1\n8.808 \n1.0 \nn=5 2 2\n7.445 5.285 \n1.0 1.0 \nn=4 3 2\n3.805 2.271 \n1.0 1.0 \n",
6
+ "Pm": "6\nn=5 0 1\n3.611 \n1.0 \nn=6 0 2\n8.756 6.588 \n1.0 1.0 \nn=5 1 1\n4.257 \n1.0 \nn=6 1 1\n8.756 \n1.0 \nn=5 2 2\n7.43 5.269 \n1.0 1.0 \nn=4 3 2\n3.692 2.189 \n1.0 1.0 \n",
7
+ "Sm": "6\nn=5 0 1\n3.554 \n1.0 \nn=6 0 2\n8.668 6.516 \n1.0 1.0 \nn=5 1 1\n4.197 \n1.0 \nn=6 1 1\n8.668 \n1.0 \nn=5 2 2\n7.445 5.269 \n1.0 1.0 \nn=4 3 2\n3.59 2.115 \n1.0 1.0 \n",
8
+ "Eu": "6\nn=5 0 1\n3.497 \n1.0 \nn=6 0 2\n8.599 6.458 \n1.0 1.0 \nn=5 1 1\n4.139 \n1.0 \nn=6 1 1\n8.599 \n1.0 \nn=5 2 2\n7.46 5.275 \n1.0 1.0 \nn=4 3 2\n3.504 2.052 \n1.0 1.0 \n",
9
+ "Gd": "6\nn=5 0 1\n3.442 \n1.0 \nn=6 0 2\n8.514 6.374 \n1.0 1.0 \nn=5 1 1\n4.089 \n1.0 \nn=6 1 1\n8.514 \n1.0 \nn=5 2 2\n7.489 5.285 \n1.0 1.0 \nn=4 3 2\n3.435 1.997 \n1.0 1.0 \n",
10
+ "Tb": "6\nn=5 0 1\n3.394 \n1.0 \nn=6 0 2\n8.395 6.279 \n1.0 1.0 \nn=5 1 1\n4.04 \n1.0 \nn=6 1 1\n8.395 \n1.0 \nn=5 2 2\n7.52 5.301 \n1.0 1.0 \nn=4 3 2\n3.366 1.948 \n1.0 1.0 \n",
11
+ "Dy": "6\nn=5 0 1\n3.346 \n1.0 \nn=6 0 2\n8.295 6.186 \n1.0 1.0 \nn=5 1 1\n3.992 \n1.0 \nn=6 1 1\n8.295 \n1.0 \nn=5 2 2\n7.565 5.323 \n1.0 1.0 \nn=4 3 2\n3.306 1.901 \n1.0 1.0 \n",
12
+ "Ho": "6\nn=5 0 1\n3.293 \n1.0 \nn=6 0 2\n8.179 6.093 \n1.0 1.0 \nn=5 1 1\n3.944 \n1.0 \nn=6 1 1\n8.179 \n1.0 \nn=5 2 2\n7.61 5.349 \n1.0 1.0 \nn=4 3 2\n3.254 1.86 \n1.0 1.0 \n",
13
+ "Er": "6\nn=5 0 1\n3.247 \n1.0 \nn=6 0 2\n8.049 5.972 \n1.0 1.0 \nn=5 1 1\n3.897 \n1.0 \nn=6 1 1\n8.049 \n1.0 \nn=5 2 2\n7.656 5.387 \n1.0 1.0 \nn=4 3 2\n3.208 1.821 \n1.0 1.0 \n",
14
+ "Tm": "6\nn=5 0 1\n3.202 \n1.0 \nn=6 0 2\n7.921 5.872 \n1.0 1.0 \nn=5 1 1\n3.858 \n1.0 \nn=6 1 1\n7.921 \n1.0 \nn=5 2 2\n7.718 5.419 \n1.0 1.0 \nn=4 3 2\n3.163 1.785 \n1.0 1.0 \n",
15
+ "Yb": "6\nn=5 0 1\n3.163 \n1.0 \nn=6 0 2\n7.796 5.761 \n1.0 1.0 \nn=5 1 1\n3.812 \n1.0 \nn=6 1 1\n7.796 \n1.0 \nn=5 2 2\n7.78 5.468 \n1.0 1.0 \nn=4 3 2\n3.119 1.753 \n1.0 1.0 \n",
16
+ }
17
+
18
+ fincore_basis = {
19
+ # "La": "4\nn=5 0 1\n3.889 \n1.0 \nn=6 0 2\n9.113 6.906 \n1.0 1.0 \nn=5 1 1\n4.548 \n1.0 \nn=6 1 1\n9.113 \n1.0 \nn=5 2 2\n7.55 5.398 \n1.0 1.0 \n",
20
+ "Ce": "5\nn=5 0 1\n3.805 \n1.0 \nn=6 0 2\n8.968 6.776 \n1.0 1.0 \nn=5 1 1\n4.458 \n1.0 \nn=6 1 1\n8.968 \n1.0 \nn=5 2 2\n7.489 5.339 \n1.0 1.0 \n",
21
+ "Pr": "5\nn=5 0 1\n3.737 \n1.0 \nn=6 0 2\n8.791 6.621 \n1.0 1.0 \nn=5 1 1\n4.387 \n1.0 \nn=6 1 1\n8.791 \n1.0 \nn=5 2 2\n7.46 5.307 \n1.0 1.0 \n",
22
+ "Nd": "5\nn=5 0 1\n3.67 \n1.0 \nn=6 0 2\n8.634 6.49 \n1.0 1.0 \nn=5 1 1\n4.317 \n1.0 \nn=6 1 1\n8.634 \n1.0 \nn=5 2 2\n7.445 5.285 \n1.0 1.0 \n",
23
+ "Pm": "5\nn=5 0 1\n3.611 \n1.0 \nn=6 0 2\n8.446 6.33 \n1.0 1.0 \nn=5 1 1\n4.257 \n1.0 \nn=6 1 1\n8.446 \n1.0 \nn=5 2 2\n7.43 5.269 \n1.0 1.0 \n",
24
+ "Sm": "5\nn=5 0 1\n3.554 \n1.0 \nn=6 0 2\n8.328 6.223 \n1.0 1.0 \nn=5 1 1\n4.197 \n1.0 \nn=6 1 1\n8.328 \n1.0 \nn=5 2 2\n7.445 5.269 \n1.0 1.0 \n",
25
+ "Eu": "5\nn=5 0 1\n3.497 \n1.0 \nn=6 0 2\n8.278 6.186 \n1.0 1.0 \nn=5 1 1\n4.139 \n1.0 \nn=6 1 1\n8.278 \n1.0 \nn=5 2 2\n7.46 5.275 \n1.0 1.0 \n",
26
+ "Gd": "5\nn=5 0 1\n3.442 \n1.0 \nn=6 0 2\n8.278 6.179 \n1.0 1.0 \nn=5 1 1\n4.089 \n1.0 \nn=6 1 1\n8.278 \n1.0 \nn=5 2 2\n7.489 5.285 \n1.0 1.0 \n",
27
+ "Tb": "5\nn=5 0 1\n3.394 \n1.0 \nn=6 0 2\n8.147 6.063 \n1.0 1.0 \nn=5 1 1\n4.04 \n1.0 \nn=6 1 1\n8.147 \n1.0 \nn=5 2 2\n7.52 5.301 \n1.0 1.0 \n",
28
+ "Dy": "5\nn=5 0 1\n3.346 \n1.0 \nn=6 0 2\n7.969 5.919 \n1.0 1.0 \nn=5 1 1\n3.992 \n1.0 \nn=6 1 1\n7.969 \n1.0 \nn=5 2 2\n7.565 5.323 \n1.0 1.0 \n",
29
+ "Ho": "5\nn=5 0 1\n3.293 \n1.0 \nn=6 0 2\n7.827 5.79 \n1.0 1.0 \nn=5 1 1\n3.944 \n1.0 \nn=6 1 1\n7.827 \n1.0 \nn=5 2 2\n7.61 5.349 \n1.0 1.0 \n",
30
+ "Er": "5\nn=5 0 1\n3.247 \n1.0 \nn=6 0 2\n7.626 5.624 \n1.0 1.0 \nn=5 1 1\n3.897 \n1.0 \nn=6 1 1\n7.626 \n1.0 \nn=5 2 2\n7.656 5.387 \n1.0 1.0 \n",
31
+ "Tm": "5\nn=5 0 1\n3.202 \n1.0 \nn=6 0 2\n7.474 5.49 \n1.0 1.0 \nn=5 1 1\n3.858 \n1.0 \nn=6 1 1\n7.474 \n1.0 \nn=5 2 2\n7.718 5.419 \n1.0 1.0 \n",
32
+ "Yb": "5\nn=5 0 1\n3.163 \n1.0 \nn=6 0 2\n7.312 5.36 \n1.0 1.0 \nn=5 1 1\n3.812 \n1.0 \nn=6 1 1\n7.312 \n1.0 \nn=5 2 2\n7.78 5.468 \n1.0 1.0 \n",
33
+ }
34
+
35
+
36
+ def get_basis(element, fincore=False):
37
+ if not fincore:
38
+ return withf_basis[element]
39
+ else:
40
+ return fincore_basis[element]
41
+
42
+
43
+ if __name__ == "__main__":
44
+ print(get_basis("La"))
45
+ print(get_basis("La", fincore=True))
46
+ print(get_basis("Ce"))
47
+ print(get_basis("Ce", fincore=True))
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spin-phonon coupling calculation workflow using SIESTA and TB2J.
4
+
5
+ This module generates distorted structures from phonon modes and computes
6
+ exchange parameters for each structure.
7
+
8
+ Purpose:
9
+ Compute exchange parameters as a function of phonon mode amplitude
10
+ to study spin-phonon coupling.
11
+
12
+ How to run:
13
+ python -m TB2Jflows.siesta_spinphonon
14
+
15
+ Expected behavior:
16
+ For each amplitude, generates a distorted structure and runs SIESTA+TB2J
17
+ to compute exchange parameters.
18
+ """
19
+
20
+ from pathlib import Path
21
+ from typing import Callable, List, Optional, Union
22
+
23
+ # Import from phononkit
24
+ from phononkit import generate_structure_for_mode, load_from_phonopy_yaml
25
+
26
+ from TB2Jflows import auto_siesta_TB2J
27
+
28
+
29
+ def compute_spinphonon_coupling(
30
+ phonon_yaml: str,
31
+ qpoint: list,
32
+ mode_index: int,
33
+ amplitudes: list,
34
+ supercell: Union[List[int], List[List[int]], None] = None,
35
+ base_path: str = "./spinphonon_results",
36
+ spin: str = "collinear",
37
+ magnetic_elements: Optional[List[str]] = None,
38
+ Udict: Optional[dict] = None,
39
+ xc: str = "PBE",
40
+ kmesh: Optional[List[int]] = None,
41
+ relax: bool = False,
42
+ scf: bool = True,
43
+ TB2J: bool = True,
44
+ rotate_type: str = "structure",
45
+ fincore: bool = True,
46
+ siesta_kwargs: Optional[dict] = None,
47
+ TB2J_kwargs: Optional[dict] = None,
48
+ fdf_kwargs: Optional[dict] = None,
49
+ prepare_atoms_callback: Optional[Callable] = None,
50
+ ):
51
+ """
52
+ Compute exchange parameters for phonon-distorted structures.
53
+
54
+ This function generates distorted structures from a phonon mode at different
55
+ amplitudes and computes exchange parameters using SIESTA and TB2J for each.
56
+
57
+ Parameters:
58
+ phonon_yaml: Path to phonopy YAML file containing phonon data
59
+ qpoint: Q-point coordinates [qx, qy, qz] in reduced coordinates
60
+ (e.g., [0, 0, 0] for Gamma point)
61
+ mode_index: Index of the phonon mode at the specified q-point (0-based)
62
+ amplitudes: List of displacement amplitudes in Angstroms
63
+ (e.g., [0.1, 0.2, 0.3, 0.4, 0.5])
64
+ supercell: Supercell specification (optional):
65
+ - 3-vector [n1, n2, n3] for diagonal matrix (e.g., [2, 2, 2])
66
+ - Full 3x3 matrix [[n1, 0, 0], [0, n2, 0], [0, 0, n3]]
67
+ - If None, uses primitive cell
68
+ base_path: Base directory for output files
69
+ spin: Spin type ('collinear', 'noncollinear', 'spinorbit')
70
+ magnetic_elements: List of magnetic element symbols (if None, auto-detect)
71
+ Udict: Dictionary of Hubbard U parameters {element: U_value}
72
+ xc: Exchange-correlation functional
73
+ kmesh: K-point mesh [nk1, nk2, nk3] (if None, auto-generate)
74
+ relax: Whether to perform structural relaxation
75
+ scf: Whether to run SCF calculation
76
+ TB2J: Whether to run TB2J calculation
77
+ rotate_type: Type of rotation for non-collinear calculations
78
+ fincore: Whether to use frozen core approximation
79
+ siesta_kwargs: Additional SIESTA parameters
80
+ TB2J_kwargs: Additional TB2J parameters
81
+ fdf_kwargs: Additional FDF file parameters
82
+ prepare_atoms_callback: Optional callback function to modify atoms
83
+ before calculation (e.g., set magnetic moments)
84
+
85
+ Returns:
86
+ dict: Dictionary mapping amplitudes to their calculation directories
87
+
88
+ Examples:
89
+ >>> # Compute exchange for Gamma-point mode 3 at various amplitudes
90
+ >>> results = compute_spinphonon_coupling(
91
+ ... phonon_yaml='phonopy.yaml',
92
+ ... qpoint=[0, 0, 0],
93
+ ... mode_index=3,
94
+ ... amplitudes=[0.1, 0.2, 0.3, 0.4, 0.5],
95
+ ... supercell=[2, 2, 2],
96
+ ... magnetic_elements=['Fe', 'Co'],
97
+ ... Udict={'Fe': 4.0, 'Co': 4.5}
98
+ ... )
99
+
100
+ >>> # Use results
101
+ >>> for amp, calc_dir in results.items():
102
+ ... print(f"Amplitude {amp} Å: {calc_dir}")
103
+ """
104
+ # Set defaults
105
+ if Udict is None:
106
+ Udict = {}
107
+ if siesta_kwargs is None:
108
+ siesta_kwargs = {}
109
+ if TB2J_kwargs is None:
110
+ TB2J_kwargs = {}
111
+ if fdf_kwargs is None:
112
+ fdf_kwargs = {}
113
+
114
+ # Create base directory
115
+ path_obj = Path(base_path)
116
+ path_obj.mkdir(parents=True, exist_ok=True)
117
+
118
+ # Load phonon data to get the structure
119
+ print(f"Loading phonon data from: {phonon_yaml}")
120
+ structure, modes = load_from_phonopy_yaml(phonon_yaml)
121
+
122
+ # Auto-detect magnetic elements if not provided
123
+ if magnetic_elements is None:
124
+ # For now, assume all unique elements with initial magnetic moments
125
+ # User should provide this explicitly
126
+ magnetic_elements = list(set(structure.get_chemical_symbols()))
127
+ print(f"Warning: Auto-detected magnetic elements: {magnetic_elements}")
128
+ print("Consider providing magnetic_elements explicitly")
129
+
130
+ # Dictionary to store results
131
+ results = {}
132
+
133
+ # Loop over amplitudes
134
+ for amplitude in amplitudes:
135
+ print("=" * 70)
136
+ print(f"Processing amplitude: {amplitude} Å")
137
+ print("=" * 70)
138
+
139
+ # Generate distorted structure
140
+ print("Generating distorted structure...")
141
+ print(f" Q-point: {qpoint}")
142
+ print(f" Mode index: {mode_index}")
143
+ print(f" Amplitude: {amplitude} Å")
144
+ if supercell is not None:
145
+ print(f" Supercell: {supercell}")
146
+
147
+ try:
148
+ distorted_atoms = generate_structure_for_mode(
149
+ yaml_path=phonon_yaml,
150
+ qpoint=qpoint,
151
+ mode_index=mode_index,
152
+ amplitude=amplitude,
153
+ supercell=supercell,
154
+ )
155
+ print(f" Generated structure with {len(distorted_atoms)} atoms")
156
+
157
+ # Apply callback if provided
158
+ if prepare_atoms_callback is not None:
159
+ distorted_atoms = prepare_atoms_callback(distorted_atoms)
160
+
161
+ except Exception as e:
162
+ print(f"Error generating structure for amplitude {amplitude}: {e}")
163
+ continue
164
+
165
+ # Create calculation directory
166
+ calc_dir = path_obj / f"amplitude_{amplitude:.4f}"
167
+ calc_dir.mkdir(parents=True, exist_ok=True)
168
+
169
+ print(f"Running SIESTA+TB2J calculation in: {calc_dir}")
170
+
171
+ # Run auto_siesta_TB2J
172
+ try:
173
+ auto_siesta_TB2J(
174
+ path=str(calc_dir),
175
+ atoms=distorted_atoms,
176
+ spin=spin,
177
+ elems=magnetic_elements,
178
+ Udict=Udict,
179
+ xc=xc,
180
+ kmesh=kmesh,
181
+ split_soc=False,
182
+ relax=relax,
183
+ scf=scf,
184
+ TB2J=TB2J,
185
+ rotate_type=rotate_type,
186
+ fincore=fincore,
187
+ siesta_kwargs=siesta_kwargs,
188
+ TB2J_kwargs=TB2J_kwargs,
189
+ fdf_kwargs=fdf_kwargs,
190
+ )
191
+ results[amplitude] = str(calc_dir)
192
+ print(f"✓ Completed calculation for amplitude {amplitude} Å")
193
+ except Exception as e:
194
+ print(f"✗ Error running calculation for amplitude {amplitude}: {e}")
195
+ continue
196
+
197
+ # Summary
198
+ print("\n" + "=" * 70)
199
+ print("Spin-Phonon Coupling Calculation Summary")
200
+ print("=" * 70)
201
+ print(f"Total amplitudes processed: {len(results)}/{len(amplitudes)}")
202
+ print(f"Results stored in: {path_obj}")
203
+ print("\nCompleted calculations:")
204
+ for amp, calc_dir in sorted(results.items()):
205
+ print(f" {amp:.4f} Å → {calc_dir}")
206
+
207
+ return results
208
+
209
+
210
+ def main():
211
+ """
212
+ Example usage of compute_spinphonon_coupling.
213
+
214
+ This example shows how to compute exchange parameters for a phonon mode
215
+ at multiple amplitudes. Users should modify the parameters below for
216
+ their specific calculation.
217
+ """
218
+ # Example parameters - MODIFY THESE FOR YOUR CALCULATION
219
+ phonon_yaml = "phonopy.yaml"
220
+ qpoint = [0, 0, 0] # Gamma point
221
+ mode_index = 3 # Fourth mode (0-indexed)
222
+ amplitudes = [0.1, 0.2, 0.3, 0.4, 0.5] # Amplitudes in Angstroms
223
+ supercell = [2, 2, 2] # 2x2x2 supercell
224
+ magnetic_elements = ["Fe"] # Magnetic elements
225
+ Udict = {"Fe": 4.0} # Hubbard U parameters
226
+
227
+ compute_spinphonon_coupling(
228
+ phonon_yaml=phonon_yaml,
229
+ qpoint=qpoint,
230
+ mode_index=mode_index,
231
+ amplitudes=amplitudes,
232
+ supercell=supercell,
233
+ base_path="./spinphonon_results",
234
+ spin="collinear",
235
+ magnetic_elements=magnetic_elements,
236
+ Udict=Udict,
237
+ xc="PBE",
238
+ kmesh=None, # Auto-generate
239
+ relax=False,
240
+ scf=True,
241
+ TB2J=True,
242
+ rotate_type="structure",
243
+ )
244
+
245
+ print("\n" + "=" * 70)
246
+ print("Calculation complete!")
247
+ print("=" * 70)
248
+ print("Next steps:")
249
+ print("1. Check the results in the output directories")
250
+ print("2. Extract exchange parameters from TB2J output files")
251
+ print("3. Plot exchange parameters vs. amplitude to study spin-phonon coupling")
252
+
253
+
254
+ if __name__ == "__main__":
255
+ main()
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: TB2Jflows
3
+ Version: 0.2.1
4
+ Summary: TB2Jflows: Workflows for automatically calculation of exchange parameters using TB2J
5
+ Author-email: Xu He <mailhexu@gmail.com>
6
+ License: BSD-2-clause
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Requires-Python: >=3.6
16
+ Requires-Dist: ase
17
+ Requires-Dist: phononkit
18
+ Requires-Dist: sisl
19
+ Requires-Dist: tb2j
20
+ Description-Content-Type: text/markdown
21
+
22
+ # TB2Jflows
23
+ Workflows for automatically calculation of exchange parameters from DFT
24
+
25
+ ## Installation
26
+
27
+ First download the package from the github page. Run the following command in the TB2Jflows directory
28
+
29
+ ```
30
+ pip install . --user
31
+ ```
32
+
33
+ will install TB2Jflows and the dependencies.
34
+
35
+ You need the following things to
36
+
37
+ - Siesta built with psml and netcdf.
38
+ - Pseudopotentials from PseudoDojo Dataset.
39
+ - Configure the command to run siesta and the path to the pseudopotentials, e.g.
40
+
41
+ ```
42
+ export ASE_IESTA_COMMAND="mpirun siesta < PREFIX.fdf > PREFIX.out 2> PREFIX.err"
43
+ export DOJO_PATH='$HOME/.local/pp/dojo'
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ Below is an example of calculating the exchange parameters of SrMnO3:
49
+
50
+ ```python
51
+ from ase.io import read
52
+ from TB2Jflows import SiestaFlow
53
+
54
+
55
+ def calculate_siesta_TB2J_SrMnO3():
56
+ atoms = read('SrMnO3.STRUCT_OUT')
57
+ atoms.set_initial_magnetic_moments([0, 3, 0, 0, 0])
58
+ flow = SiestaFlow(atoms,
59
+ spin='spin-orbit',
60
+ restart=True,
61
+ root_path='SrMnO3')
62
+ flow.write_metadata()
63
+ atoms = flow.relax(atoms)
64
+ flow.scf_calculation_with_rotations(atoms)
65
+ flow.run_TB2J(magnetic_elements='Mn',
66
+ nz=50,
67
+ kmesh=[7, 7, 7],
68
+ Rcut=18,
69
+ np=10,
70
+ use_cache=True)
71
+ flow.run_TB2J_merge()
72
+
73
+
74
+ if __name__ == '__main__':
75
+ calculate_siesta_TB2J_SrMnO3()
76
+ ~
77
+ ```
78
+
@@ -0,0 +1,12 @@
1
+ TB2Jflows/__init__.py,sha256=hRwR95ChyiFkUTX2djY2hQI9ph7x2YIJxiOehvxe79I,242
2
+ TB2Jflows/ase_siesta.py,sha256=d8Li-zwUGlulT0haEIKLSSgsveMU6YzV0thrFnDhV2U,14359
3
+ TB2Jflows/auto_siesta_TB2J.py,sha256=c5dviznEbXyO3BJbh-LNBlC9efT7YbdC7ydWxyCJZ5E,3113
4
+ TB2Jflows/find_pp.py,sha256=Od8Zb-Rd0kDC6ALBa39knYFNVKfNxhZx9OLocOS8YUA,2435
5
+ TB2Jflows/mysiesta.py,sha256=IUgfNu_X9rmX_CkqNsHrwFk3Hp8o5Wlp7Fd19PaCZPw,19518
6
+ TB2Jflows/run_abacus.py,sha256=96tZ2n02FFLl8IZf4W_4TvkCcsd6oS1FVEIqhZpY76o,2692
7
+ TB2Jflows/siesta_basis.py,sha256=ysGZUfjwuoOGImj0k-pDicKKG_o6Me0hzJqf5p5x9MQ,5319
8
+ TB2Jflows/siesta_spinphonon.py,sha256=jgTTtyWoJMnhyEJdWMnJ7pjxc6EM_aKEeu_-EgbtUFs,9037
9
+ tb2jflows-0.2.1.dist-info/METADATA,sha256=CnYSE36Rfm7HM-JRw304EkMi567VmfVlQRJyawYe65g,2195
10
+ tb2jflows-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ tb2jflows-0.2.1.dist-info/licenses/LICENSE,sha256=KNu68sa-XR_2jZJKhDcSnxoNve8jtHgkw_w9PjP1YOk,1315
12
+ tb2jflows-0.2.1.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.3)
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,31 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: TB2Jflows
3
- Version: 0.2
4
- Summary: TB2Jflows: Workflows for automatically calculation of exchange parameters using TB2J
5
- Author: Xu He
6
- Author-email: mailhexu@gmail.com
7
- License: BSD-2-clause
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Operating System :: OS Independent
11
- Classifier: Intended Audience :: Science/Research
12
- Classifier: Topic :: Scientific/Engineering :: Chemistry
13
- Classifier: Topic :: Scientific/Engineering :: Physics
14
- Classifier: License :: OSI Approved :: BSD License
15
- Requires-Python: >=3.6
16
- License-File: LICENSE
17
- Requires-Dist: TB2J
18
- Requires-Dist: ase
19
- Requires-Dist: sisl
20
- Requires-Dist: pyDFTutils
21
- Dynamic: author
22
- Dynamic: author-email
23
- Dynamic: classifier
24
- Dynamic: description
25
- Dynamic: license
26
- Dynamic: license-file
27
- Dynamic: requires-dist
28
- Dynamic: requires-python
29
- Dynamic: summary
30
-
31
- TB2Jflows: Workflows for automatically calculation of exchange parameters using TB2J
@@ -1,9 +0,0 @@
1
- TB2Jflows/__init__.py,sha256=0aRVj9v-u64LxJl7Cyxcr4YUjpeMkBGPuv-nJbxbHOg,100
2
- TB2Jflows/ase_siesta.py,sha256=tHeiKWszra8sjcJ_7Pq0cOBUC1ZEluRNEeHGj35Oz6M,14476
3
- TB2Jflows/auto_siesta_TB2J.py,sha256=o1A5Qf0Ru35BY5jwSAIQ2rPXD1mb572LEAQfgNfEVnM,3174
4
- TB2Jflows/run_abacus.py,sha256=96tZ2n02FFLl8IZf4W_4TvkCcsd6oS1FVEIqhZpY76o,2692
5
- tb2jflows-0.2.dist-info/licenses/LICENSE,sha256=KNu68sa-XR_2jZJKhDcSnxoNve8jtHgkw_w9PjP1YOk,1315
6
- tb2jflows-0.2.dist-info/METADATA,sha256=l2Lsj-MqAlL7fizCsUsD94RaqGpg6D-F-1HtmaOesL8,967
7
- tb2jflows-0.2.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
8
- tb2jflows-0.2.dist-info/top_level.txt,sha256=iYRLHB7ZeHb59fEZLnbqJDymBKWPqfVgmvqd9S51Txw,10
9
- tb2jflows-0.2.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- TB2Jflows