TB2Jflows 0.1.0__py3-none-any.whl → 0.2.0__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__(
@@ -32,6 +33,7 @@ class SiestaFlow:
32
33
  self.xc = xc
33
34
  self.kpts = kpts
34
35
  self.Udict = Udict
36
+
35
37
  # default fdf arguments
36
38
  self.fdf_arguments = {
37
39
  "MaxSCFIterations": 350,
TB2Jflows/find_pp.py CHANGED
@@ -1,67 +1,91 @@
1
1
  import os
2
- from ase.data import chemical_symbols, atomic_numbers
3
2
 
3
+ from ase.data import atomic_numbers
4
4
 
5
- class PPFinder():
5
+
6
+ class PPFinder:
6
7
  def __init__(self):
7
8
  pass
8
9
 
9
10
  def get_pp_path(self, element, xc, label, rel):
10
11
  pass
11
12
 
12
- class DojoFinder():
13
+
14
+ class DojoFinder:
13
15
  def __init__(self, path=None):
14
16
  if path is None:
15
- self.path = os.environ['DOJO_PATH']
17
+ self.path = os.environ["DOJO_PATH"]
16
18
  else:
17
19
  self.path = path
18
20
 
19
- def get_pp_path(self,
20
- xc : str,
21
- typ='NC',
22
- rel='sr',
23
- version='04',
24
- accuracy='standard',
25
- fmt='psml'):
26
- typ=typ.lower()
27
- xc=xc.lower()
28
- if xc=="lda":
29
- xc="pw"
30
- dirname = os.path.join(self.path, f"{typ}-{rel}-{version}_{xc}_{accuracy}_{fmt}")
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
31
  if not os.path.exists(dirname):
32
32
  raise FileNotFoundError(f"File Not found: {dirname}")
33
33
  return dirname
34
34
 
35
- def get_pp_fname(self,
36
- element,
37
- xc : str,
38
- typ='NC',
39
- rel='sr',
40
- version='04',
41
- accuracy='standard',
42
- fincore=False,
43
- fmt='psml'):
44
- if 57<atomic_numbers[element]<=70:
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:
45
47
  if fincore:
46
- fname = os.path.join(self.get_pp_path(xc=xc, typ=typ, rel=rel,
47
- version=version, accuracy=accuracy, fmt=fmt), f"fincore/{element}.{fmt}")
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
+ )
48
59
  else:
49
- fname = os.path.join(self.get_pp_path(xc=xc, typ=typ, rel=rel,
50
- version=version, accuracy=accuracy, fmt=fmt), f"withf/{element}.{fmt}")
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
+ )
51
71
 
52
72
  else:
53
- fname = os.path.join(self.get_pp_path(xc=xc, typ=typ, rel=rel,
54
- version=version, accuracy=accuracy, fmt=fmt), f"{element}.{fmt}")
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
+ )
55
79
  if not os.path.exists(fname):
56
80
  raise FileNotFoundError(f"File Not found: {fname}")
57
81
  return fname
58
82
 
59
83
 
60
-
61
84
  def test():
62
- finder=DojoFinder(path=os.path.expanduser('~/projects/pp/dojo'))
63
- fname=finder.get_pp_path(element='Sr', xc='pbesol')
64
- fname=finder.get_pp_path(element='Srg', xc='pbe')
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
+
65
89
 
66
- if __name__=='__main__':
90
+ if __name__ == "__main__":
67
91
  test()
TB2Jflows/mysiesta.py CHANGED
@@ -1,35 +1,52 @@
1
- import os
2
1
  import math
2
+ import os
3
+ import shutil
4
+ from os.path import isfile, islink, join
5
+
3
6
  import numpy as np
4
7
  from ase import Atoms
5
8
  from ase.calculators.siesta import Siesta
6
- from ase.calculators.calculator import FileIOCalculator, ReadError
7
- from ase.calculators.siesta.parameters import format_fdf
8
- from ase.calculators.siesta.parameters import Species, PAOBasisBlock
9
+ from ase.calculators.siesta.parameters import PAOBasisBlock, Species, format_fdf
10
+ from ase.data import atomic_numbers
9
11
  from ase.io import write
10
- import copy
12
+ from ase.units import Bohr
11
13
 
12
- import shutil
13
- from os.path import join, isfile, islink
14
- import numpy as np
15
- from ase.units import Ry, eV, Bohr
16
- from ase.data import atomic_numbers
17
- #from ase.io.siesta import read_siesta_xv
18
- #from ase.calculators.siesta.import_functions import \
19
- # get_valence_charge, read_vca_synth_block
20
- from ase.calculators.calculator import FileIOCalculator, ReadError
21
- from ase.calculators.calculator import Parameters, all_changes
22
- from ase.calculators.siesta.parameters import PAOBasisBlock, Species
23
- from ase.calculators.siesta.parameters import format_fdf
24
- #from pyDFTutils.siesta.pdos import gen_pdos_figure, plot_layer_pdos
14
+ from .find_pp import DojoFinder
25
15
 
26
- from .siesta_basis import get_basis, fincore_basis, withf_basis
27
- from .findpp import DojoFinder
16
+ # from pyDFTutils.siesta.pdos import gen_pdos_figure, plot_layer_pdos
17
+ from .siesta_basis import fincore_basis, withf_basis
28
18
 
29
19
 
30
- synthetic_atoms_dict_fincore={
31
- "Yb":((6,5,5,5), (2,6,1,0))
32
- }
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
+
33
50
 
34
51
  def read_siesta_xv(fd):
35
52
  vectors = []
@@ -46,69 +63,80 @@ def read_siesta_xv(fd):
46
63
  if len(line) > 5: # Ignore blank lines
47
64
  data = line.split()
48
65
  speciesnumber.append(int(data[0]))
49
- atomnumbers.append(int(data[1])%200)
66
+ atomnumbers.append(int(data[1]) % 200)
50
67
  xyz.append([float(data[2 + j]) * Bohr for j in range(3)])
51
68
  V.append([float(data[5 + j]) * Bohr for j in range(3)])
52
69
 
53
70
  vectors = np.array(vectors)
54
71
  atomnumbers = np.array(atomnumbers)
55
72
  xyz = np.array(xyz)
56
- atoms = Atoms(numbers=atomnumbers, positions=xyz, cell=vectors,
57
- pbc=True)
73
+ atoms = Atoms(numbers=atomnumbers, positions=xyz, cell=vectors, pbc=True)
58
74
  assert natoms == len(atoms)
59
75
  return atoms
60
76
 
77
+
61
78
  def read_xv(fname):
62
79
  with open(fname) as myfile:
63
- atoms=read_siesta_xv(myfile)
80
+ atoms = read_siesta_xv(myfile)
64
81
  return atoms
65
82
 
66
83
 
67
- def get_species(atoms, xc, rel='sr', accuracy='standard'):
84
+ def get_species(atoms, xc, rel="sr", accuracy="standard", fincore=False):
68
85
  finder = DojoFinder()
69
86
  elems = list(dict.fromkeys(atoms.get_chemical_symbols()).keys())
70
87
  elem_dict = dict(zip(elems, range(1, len(elems) + 1)))
71
88
  pseudo_path = finder.get_pp_path(xc=xc)
72
89
  species = [
73
- Species(symbol=elem,
74
- pseudopotential=finder.get_pp_fname(
75
- elem, xc=xc, rel=rel, accuracy=accuracy, fincore=fincore),
76
- ghost=False) for elem in elem_dict.keys()
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()
77
98
  ]
78
99
  return pseudo_path, species
79
100
 
80
101
 
81
102
  def cart2sph(vec):
82
103
  x, y, z = vec
83
- r = np.linalg.norm(vec) # r
104
+ r = np.linalg.norm(vec) # r
84
105
  if r < 1e-10:
85
106
  theta, phi = 0.0, 0.0
86
107
  else:
87
108
  # note that there are many conventions, here is the ISO convention.
88
- phi = math.atan2(y, x) * 180/math.pi # phi
89
- theta = math.acos(z/r) * 180/math.pi # theta
109
+ phi = math.atan2(y, x) * 180 / math.pi # phi
110
+ theta = math.acos(z / r) * 180 / math.pi # theta
90
111
  return r, theta, phi
91
112
 
92
113
 
93
114
  class MySiesta(Siesta):
94
- def __init__(self,
95
- atoms=None,
96
- command=None,
97
- xc='LDA',
98
- spin='non-polarized',
99
- basis_set='DZP',
100
- species=None,
101
- ghosts=[],
102
- synthetic_atoms={},
103
- input_basis_set={},
104
- pseudo_path=None,
105
- input_pp={},
106
- pp_accuracy='standard',
107
- fincore=False,
108
- **kwargs):
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
+ ):
109
132
  # non-perturnbative polarized orbital.
110
133
  self.npt_elems = set()
111
- self.synthetic_atoms=synthetic_atoms
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
112
140
 
113
141
  if fincore:
114
142
  input_basis_set.update(fincore_basis)
@@ -130,52 +158,60 @@ class MySiesta(Siesta):
130
158
  if pseudo_path is None:
131
159
  pseudo_path = finder.get_pp_path(xc=xc, accuracy=pp_accuracy)
132
160
 
133
- if spin == 'spin-orbit':
134
- rel = 'fr'
161
+ if spin == "spin-orbit":
162
+ rel = "fr"
135
163
  else:
136
- rel = 'sr'
164
+ rel = "sr"
137
165
  species = []
138
166
  for elem, index in self.elem_dict.items():
139
167
  if elem not in input_basis_set:
140
168
  bselem = basis_set
141
- if elem in ['Li', 'Be', 'Na', 'Mg', 'Sm']:
169
+ if elem in ["Li", "Be", "Na", "Mg", "Sm"]:
142
170
  self.npt_elems.add(f"{elem}.{index}")
143
171
  else:
144
172
  bselem = PAOBasisBlock(input_basis_set[elem])
145
173
  if elem not in input_pp:
146
174
  pseudopotential = finder.get_pp_fname(
147
- elem, xc=xc, rel=rel, accuracy=pp_accuracy, fincore=fincore)
175
+ elem, xc=xc, rel=rel, accuracy=pp_accuracy, fincore=fincore
176
+ )
148
177
  else:
149
- pseudopotential = os.path.join(
150
- pseudo_path, input_pp[elem])
178
+ pseudopotential = os.path.join(pseudo_path, input_pp[elem])
151
179
 
152
180
  if elem in self.synthetic_atoms:
153
181
  excess_charge = 0
154
182
  else:
155
183
  excess_charge = None
156
184
 
157
- species.append(Species(symbol=elem,
158
- pseudopotential=pseudopotential,
159
- basis_set=bselem,
160
- ghost=False,
161
- excess_charge=excess_charge))
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
+ )
162
194
  for elem in ghost_elems:
163
195
  species.append(
164
- Species(symbol=elem,
165
- pseudopotential=finder.get_pp_fname(
166
- elem, xc=xc, rel=rel, accuracy=pp_accuracy,
167
- fincore=fincore),
168
- tag=1,
169
- ghost=True))
170
-
171
-
172
- Siesta.__init__(self,
173
- xc=xc,
174
- spin=spin,
175
- atoms=atoms,
176
- pseudo_path=pseudo_path,
177
- species=species,
178
- **kwargs)
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
+ )
179
215
  self.set_npt_elements()
180
216
  self.set_synthetic_atoms()
181
217
 
@@ -188,16 +224,16 @@ class MySiesta(Siesta):
188
224
  """
189
225
  species, species_numbers = self.species(atoms)
190
226
 
191
- if self['pseudo_path'] is not None:
192
- pseudo_path = self['pseudo_path']
193
- elif 'SIESTA_PP_PATH' in os.environ:
194
- pseudo_path = os.environ['SIESTA_PP_PATH']
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"]
195
231
  else:
196
232
  mess = "Please set the environment variable 'SIESTA_PP_PATH'"
197
233
  raise Exception(mess)
198
234
 
199
- fd.write(format_fdf('NumberOfSpecies', len(species)))
200
- fd.write(format_fdf('NumberOfAtoms', len(atoms)))
235
+ fd.write(format_fdf("NumberOfSpecies", len(species)))
236
+ fd.write(format_fdf("NumberOfAtoms", len(atoms)))
201
237
 
202
238
  pao_basis = []
203
239
  chemical_labels = []
@@ -205,20 +241,20 @@ class MySiesta(Siesta):
205
241
  synth_blocks = []
206
242
  for species_number, spec in enumerate(species):
207
243
  species_number += 1
208
- symbol = spec['symbol']
244
+ symbol = spec["symbol"]
209
245
  atomic_number = atomic_numbers[symbol]
210
246
 
211
- if spec['pseudopotential'] is None:
212
- if self.pseudo_qualifier() == '':
247
+ if spec["pseudopotential"] is None:
248
+ if self.pseudo_qualifier() == "":
213
249
  label = symbol
214
- pseudopotential = label + '.psf'
250
+ pseudopotential = label + ".psf"
215
251
  else:
216
- label = '.'.join([symbol, self.pseudo_qualifier()])
217
- pseudopotential = label + '.psf'
252
+ label = ".".join([symbol, self.pseudo_qualifier()])
253
+ pseudopotential = label + ".psf"
218
254
  else:
219
- pseudopotential = spec['pseudopotential']
255
+ pseudopotential = spec["pseudopotential"]
220
256
  label = os.path.basename(pseudopotential)
221
- label = '.'.join(label.split('.')[:-1])
257
+ label = ".".join(label.split(".")[:-1])
222
258
 
223
259
  if not os.path.isabs(pseudopotential):
224
260
  pseudopotential = join(pseudo_path, pseudopotential)
@@ -228,138 +264,138 @@ class MySiesta(Siesta):
228
264
  raise RuntimeError(mess)
229
265
 
230
266
  name = os.path.basename(pseudopotential)
231
- name = name.split('.')
267
+ name = name.split(".")
232
268
  name.insert(-1, str(species_number))
233
- if spec['ghost']:
234
- name.insert(-1, 'ghost')
269
+ if spec["ghost"]:
270
+ name.insert(-1, "ghost")
235
271
  atomic_number = -atomic_number
236
272
 
237
- name = '.'.join(name)
273
+ name = ".".join(name)
238
274
  pseudo_targetpath = self.getpath(name)
239
275
 
240
276
  if join(os.getcwd(), name) != pseudopotential:
241
277
  if islink(pseudo_targetpath) or isfile(pseudo_targetpath):
242
278
  os.remove(pseudo_targetpath)
243
- symlink_pseudos = self['symlink_pseudos']
279
+ symlink_pseudos = self["symlink_pseudos"]
244
280
 
245
281
  symlink_pseudos = False
246
282
 
247
283
  if symlink_pseudos is None:
248
- symlink_pseudos = not os.name == 'nt'
284
+ symlink_pseudos = not os.name == "nt"
249
285
 
250
286
  if symlink_pseudos:
251
287
  os.symlink(pseudopotential, pseudo_targetpath)
252
288
  else:
253
289
  shutil.copy(pseudopotential, pseudo_targetpath)
254
- if not spec['excess_charge'] is None:
290
+ if spec["excess_charge"] is not None:
255
291
  atomic_number += 200
256
- n_atoms = sum(np.array(species_numbers) == species_number)
257
-
258
- if spec['excess_charge'] != 0:
259
- paec = float(spec['excess_charge']) / n_atoms
260
- vc = get_valence_charge(pseudopotential)
261
- fraction = float(vc + paec) / vc
262
- pseudo_head = name[:-4]
263
- fractional_command = os.environ['SIESTA_UTIL_FRACTIONAL']
264
- cmd = '%s %s %.7f' % (fractional_command,
265
- pseudo_head,
266
- fraction)
267
- os.system(cmd)
268
-
269
- pseudo_head += '-Fraction-%.5f' % fraction
270
- synth_pseudo = pseudo_head + '.psf'
271
- synth_block_filename = pseudo_head + '.synth'
272
- os.remove(name)
273
- shutil.copyfile(synth_pseudo, name)
274
- synth_block = read_vca_synth_block(
275
- synth_block_filename,
276
- species_number=species_number)
277
- synth_blocks.append(synth_block)
278
- else:
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:
279
315
  synth_block = self.synthetic_atoms[symbol]
280
-
281
-
316
+ synth_blocks.append(synth_block)
282
317
 
283
318
  if len(synth_blocks) > 0:
284
- fd.write(format_fdf('SyntheticAtoms', list(synth_blocks)))
319
+ fd.write(format_fdf("SyntheticAtoms", list(synth_blocks)))
285
320
 
286
- label = '.'.join(np.array(name.split('.'))[:-1])
287
- string = ' %d %d %s' % (species_number, atomic_number, label)
321
+ label = ".".join(np.array(name.split("."))[:-1])
322
+ string = " %d %d %s" % (species_number, atomic_number, label)
288
323
  chemical_labels.append(string)
289
- if isinstance(spec['basis_set'], PAOBasisBlock):
290
- pao_basis.append(spec['basis_set'].script(label))
324
+ if isinstance(spec["basis_set"], PAOBasisBlock):
325
+ pao_basis.append(spec["basis_set"].script(label))
291
326
  else:
292
- basis_sizes.append((" " + label, spec['basis_set']))
293
- fd.write((format_fdf('ChemicalSpecieslabel', chemical_labels)))
294
- fd.write('\n')
295
- fd.write((format_fdf('PAO.Basis', pao_basis)))
296
- fd.write((format_fdf('PAO.BasisSizes', basis_sizes)))
297
- fd.write('\n')
298
-
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")
299
333
 
300
334
  def set_npt_elements(self):
301
335
  if len(self.npt_elems) > 0:
302
336
  npt_text = []
303
337
  for name in self.npt_elems:
304
- npt_text.append(
305
- f"{name} non-perturbative ")
338
+ npt_text.append(f"{name} non-perturbative ")
306
339
  # npt_text += "%endblock PAO.PolarizationScheme\n"
307
- self['fdf_arguments'].update({"PAO.PolarizationScheme": npt_text})
340
+ self["fdf_arguments"].update({"PAO.PolarizationScheme": npt_text})
308
341
 
309
342
  def set_synthetic_atoms(self):
310
343
  print("setting syn")
311
- nsyn=len(self.synthetic_atoms)
312
- if nsyn> 0:
344
+ nsyn = len(self.synthetic_atoms)
345
+ if nsyn > 0:
313
346
  syntext = []
314
- #syntext.append(f"{nsyn}")
347
+ # syntext.append(f"{nsyn}")
315
348
  for name, content in self.synthetic_atoms.items():
316
- syntext.append(
317
- f"{self.elem_dict[name]}")
318
- syntext.append(
319
- " ".join([str(x) for x in content[0]]))
320
- syntext.append(
321
- " ".join([str(x) for x in content[1]]))
322
- self['fdf_arguments'].update({"SyntheticAtoms": syntext})
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})
323
355
 
324
- #print("setting syn:", syntext)
356
+ # print("setting syn:", syntext)
325
357
 
326
358
  def set_fdf_arguments(self, fdf_arguments):
327
- self['fdf_arguments'].update(fdf_arguments)
328
-
329
- def set_mixer(self,
330
- method='pulay',
331
- weight=0.05,
332
- history=10,
333
- restart=25,
334
- restart_save=4,
335
- linear_after=0,
336
- linear_after_weight=0.1):
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
+ ):
337
371
  pass
338
372
 
339
373
  def update_fdf_arguments(self, fdf_arguments):
340
- fdf = self['fdf_arguments'].update(fdf_arguments)
341
-
342
- def add_Hubbard_U(self,
343
- specy,
344
- n=3,
345
- l=2,
346
- U=0,
347
- J=0,
348
- rc=0.0,
349
- Fermi_cut=0.0,
350
- scale_factor='0.95'):
351
- if not 'Udict' in self.__dict__:
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__:
352
388
  self.Udict = dict()
353
- idx=self.elem_dict[specy]
354
- specy_label=f"{specy}.{idx}"
389
+ idx = self.elem_dict[specy]
390
+ specy_label = f"{specy}.{idx}"
355
391
  self.Udict[specy_label] = {
356
- 'n': n,
357
- 'l': l,
358
- 'U': U,
359
- 'J': J,
360
- 'rc': rc,
361
- 'Fermi_cut': Fermi_cut,
362
- 'scale_factor': scale_factor
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,
363
399
  }
364
400
  self.set_Hubbard_U(self.Udict)
365
401
 
@@ -369,18 +405,19 @@ class MySiesta(Siesta):
369
405
  """
370
406
  Ublock = []
371
407
  for key, val in Udict.items():
372
- Ublock.append(' %s %s ' % (key, 1))
373
- if val['n'] is not None:
374
- Ublock.append(' n=%s %s' % (val['n'], val['l']))
408
+ Ublock.append(" %s %s " % (key, 1))
409
+ if val["n"] is not None:
410
+ Ublock.append(" n=%s %s" % (val["n"], val["l"]))
375
411
  else:
376
- Ublock.append('%s' % (val['l']))
377
- Ublock.append(' %s %s' % (val['U'], val['J']))
378
- if 'rc' in val:
379
- Ublock.append(' %s %s' % (val['rc'], val['Fermi_cut']))
380
- Ublock.append(' %s' % val['scale_factor'])
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"])
381
417
 
382
418
  self.update_fdf_arguments(
383
- {'LDAU.Proj': Ublock, 'LDAU.ProjectorGenerationMethod': 2})
419
+ {"LDAU.Proj": Ublock, "LDAU.ProjectorGenerationMethod": 2}
420
+ )
384
421
 
385
422
  def set_Udict(self, Udict):
386
423
  """
@@ -400,47 +437,57 @@ class MySiesta(Siesta):
400
437
  def relax(
401
438
  self,
402
439
  atoms,
403
- TypeOfRun='Broyden',
440
+ TypeOfRun="Broyden",
404
441
  VariableCell=True,
405
442
  ConstantVolume=False,
406
443
  RelaxCellOnly=False,
407
444
  MaxForceTol=0.001,
408
445
  MaxStressTol=1,
409
446
  NumCGSteps=40,
410
- relaxed_file="relaxed.vasp"
447
+ relaxed_file="relaxed.vasp",
411
448
  ):
412
449
  pbc = atoms.get_pbc()
413
450
  initial_magnetic_moments = atoms.get_initial_magnetic_moments()
414
- self.update_fdf_arguments({
415
- 'MD.TypeOfRun': TypeOfRun,
416
- 'MD.VariableCell': VariableCell,
417
- 'MD.ConstantVolume': ConstantVolume,
418
- 'MD.RelaxCellOnly': RelaxCellOnly,
419
- 'MD.MaxForceTol': "%s eV/Ang" % MaxForceTol,
420
- 'MD.MaxStressTol': "%s GPa" % MaxStressTol,
421
- 'MD.NumCGSteps': NumCGSteps,
422
- })
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
+ )
423
462
  self.calculate(atoms)
424
- #self.read(self.prefix + '.XV')
425
- self.atoms=read_xv(os.path.join(self.directory, self.prefix + '.XV'))
463
+ # self.read(self.prefix + '.XV')
464
+ self.atoms = read_xv(os.path.join(self.directory, self.prefix + ".XV"))
426
465
  self.atoms.set_pbc(pbc)
427
466
  self.atoms.set_initial_magnetic_moments(initial_magnetic_moments)
428
467
  atoms = self.atoms
429
- self.update_fdf_arguments({
430
- 'MD.NumCGSteps': 0,
431
- })
468
+ self.update_fdf_arguments(
469
+ {
470
+ "MD.NumCGSteps": 0,
471
+ }
472
+ )
432
473
  if relaxed_file is not None:
433
474
  write(relaxed_file, atoms, vasp5=True, sort=False)
434
475
  return self.atoms
435
476
 
436
- def scf_calculation(self, atoms, dos=True, kpts=[7,7,7], **kwargs):
477
+ def scf_calculation(self, atoms, dos=True, kpts=[7, 7, 7], **kwargs):
437
478
  if dos:
438
479
  k1, k2, k3 = kpts
439
- self.update_fdf_arguments({'WriteEigenvalues': '.true.',
440
- 'ProjectedDensityOfStates': ['-70.00 30.0 0.015 3000 eV'],
441
- 'PDOS.kgrid_Monkhorst_Pack': [f'{k1} 0 0 0.0',
442
- f'0 {k2} 0 0.0',
443
- f'0 0 {k3} 0.0']})
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
+ )
444
491
  self.calculate(atoms, **kwargs)
445
492
 
446
493
  def _write_structure(self, f, atoms):
@@ -451,23 +498,25 @@ class MySiesta(Siesta):
451
498
  - atoms: An atoms object.
452
499
  """
453
500
  cell = atoms.cell
454
- f.write('\n')
501
+ f.write("\n")
455
502
 
456
503
  if cell.rank in [1, 2]:
457
- raise ValueError('Expected 3D unit cell or no unit cell. You may '
458
- 'wish to add vacuum along some directions.')
504
+ raise ValueError(
505
+ "Expected 3D unit cell or no unit cell. You may "
506
+ "wish to add vacuum along some directions."
507
+ )
459
508
 
460
509
  # Write lattice vectors
461
510
  if np.any(cell):
462
- f.write(format_fdf('LatticeConstant', '1.0 Ang'))
463
- f.write('%block LatticeVectors\n')
511
+ f.write(format_fdf("LatticeConstant", "1.0 Ang"))
512
+ f.write("%block LatticeVectors\n")
464
513
  for i in range(3):
465
514
  for j in range(3):
466
- s = (' %.15f' % cell[i, j]).rjust(16) + ' '
515
+ s = (" %.15f" % cell[i, j]).rjust(16) + " "
467
516
  f.write(s)
468
- f.write('\n')
469
- f.write('%endblock LatticeVectors\n')
470
- f.write('\n')
517
+ f.write("\n")
518
+ f.write("%endblock LatticeVectors\n")
519
+ f.write("\n")
471
520
 
472
521
  self._write_atomic_coordinates(f, atoms)
473
522
 
@@ -480,26 +529,26 @@ class MySiesta(Siesta):
480
529
  # atoms object.
481
530
  if magmoms is not None:
482
531
  if len(magmoms) == 0:
483
- f.write('#Empty block forces ASE initialization.\n')
532
+ f.write("#Empty block forces ASE initialization.\n")
484
533
 
485
- f.write('%block DM.InitSpin\n')
534
+ f.write("%block DM.InitSpin\n")
486
535
  if len(magmoms) != 0 and isinstance(magmoms[0], np.ndarray):
487
536
  for n, Mcart in enumerate(magmoms):
488
537
  M = cart2sph(Mcart)
489
538
  if M[0] != 0:
490
- f.write(' %d %.14f %.14f %.14f \n' %
491
- (n + 1, M[0], M[1], M[2]))
539
+ f.write(
540
+ " %d %.14f %.14f %.14f \n" % (n + 1, M[0], M[1], M[2])
541
+ )
492
542
  elif len(magmoms) != 0 and isinstance(magmoms[0], float):
493
543
  for n, M in enumerate(magmoms):
494
544
  if M != 0:
495
- f.write(' %d %.14f \n' % (n + 1, M))
496
- f.write('%endblock DM.InitSpin\n')
497
- f.write('\n')
545
+ f.write(" %d %.14f \n" % (n + 1, M))
546
+ f.write("%endblock DM.InitSpin\n")
547
+ f.write("\n")
498
548
 
499
549
  def my_read_results(self):
500
- """Read the results.
501
- """
502
- #self.read_number_of_grid_points()
550
+ """Read the results."""
551
+ # self.read_number_of_grid_points()
503
552
  self.read_energy()
504
553
  self.read_forces_stress()
505
554
  # self.read_eigenvalues()
TB2Jflows/siesta_basis.py CHANGED
@@ -1,6 +1,5 @@
1
-
2
1
  withf_basis = {
3
- #"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",
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",
4
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",
5
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",
6
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",
@@ -13,11 +12,11 @@ withf_basis = {
13
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",
14
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",
15
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",
16
- "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"
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",
17
16
  }
18
17
 
19
- fincore_basis ={
20
- #"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",
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",
21
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",
22
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",
23
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",
@@ -30,21 +29,19 @@ fincore_basis ={
30
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",
31
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",
32
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",
33
- "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"
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",
34
33
  }
35
34
 
35
+
36
36
  def get_basis(element, fincore=False):
37
37
  if not fincore:
38
38
  return withf_basis[element]
39
- elif basis == "fincore":
40
- return fincore_basis[element]
41
39
  else:
42
- raise ValueError("Invalid basis set: {}".format(basis))
40
+ return fincore_basis[element]
41
+
43
42
 
44
43
  if __name__ == "__main__":
45
44
  print(get_basis("La"))
46
45
  print(get_basis("La", fincore=True))
47
46
  print(get_basis("Ce"))
48
47
  print(get_basis("Ce", fincore=True))
49
-
50
-
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TB2Jflows
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: TB2Jflows: Workflows for automatically calculation of exchange parameters using TB2J
5
5
  Author-email: Xu He <mailhexu@gmail.com>
6
6
  License: BSD-2-clause
@@ -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.0.dist-info/METADATA,sha256=3Sy89-TX_nkmlZOxbCekV4sVrreG5q-cl9iNm1iX6Ho,2170
10
+ tb2jflows-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ tb2jflows-0.2.0.dist-info/licenses/LICENSE,sha256=KNu68sa-XR_2jZJKhDcSnxoNve8jtHgkw_w9PjP1YOk,1315
12
+ tb2jflows-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,11 +0,0 @@
1
- TB2Jflows/__init__.py,sha256=0aRVj9v-u64LxJl7Cyxcr4YUjpeMkBGPuv-nJbxbHOg,100
2
- TB2Jflows/ase_siesta.py,sha256=1b6j0dFNnXCeLnCTWoLAJ158JQul8UcORIttGOhNydw,14366
3
- TB2Jflows/auto_siesta_TB2J.py,sha256=c5dviznEbXyO3BJbh-LNBlC9efT7YbdC7ydWxyCJZ5E,3113
4
- TB2Jflows/find_pp.py,sha256=iTqxLPuR88Jsuh4lc7hPEzFczhoOB0x6K5MWqYTEeiQ,2134
5
- TB2Jflows/mysiesta.py,sha256=DV9dnYibrpPzegKy_MvoMOOrmoom2NPKblhVNBUZkeg,18943
6
- TB2Jflows/run_abacus.py,sha256=96tZ2n02FFLl8IZf4W_4TvkCcsd6oS1FVEIqhZpY76o,2692
7
- TB2Jflows/siesta_basis.py,sha256=UIO0VF_558jIK6kGctqEwHyyV_t7KEWGOtv4Obw4fyg,5408
8
- tb2jflows-0.1.0.dist-info/METADATA,sha256=mnUu8VXO3Dndka3rbgqWJ0hiWxOXmWyF9AjauzpVQZk,2170
9
- tb2jflows-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- tb2jflows-0.1.0.dist-info/licenses/LICENSE,sha256=KNu68sa-XR_2jZJKhDcSnxoNve8jtHgkw_w9PjP1YOk,1315
11
- tb2jflows-0.1.0.dist-info/RECORD,,