TB2J 0.7.7.2__py3-none-any.whl → 0.8.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.
- TB2J/__init__.py +1 -1
- TB2J/abacus/__init__.py +1 -0
- TB2J/abacus/abacus_api.py +191 -0
- TB2J/abacus/abacus_wrapper.py +223 -0
- TB2J/abacus/gen_exchange_abacus.py +102 -0
- TB2J/abacus/orbital_api.py +70 -0
- TB2J/abacus/stru_api.py +2020 -0
- TB2J/abacus/test_read_HRSR.py +44 -0
- TB2J/abacus/test_read_stru.py +30 -0
- TB2J/basis.py +67 -0
- TB2J/exchange.py +3 -14
- TB2J/utils.py +5 -0
- TB2J-0.8.0.data/scripts/abacus2J.py +151 -0
- {TB2J-0.7.7.2.dist-info → TB2J-0.8.0.dist-info}/METADATA +1 -1
- {TB2J-0.7.7.2.dist-info → TB2J-0.8.0.dist-info}/RECORD +26 -16
- {TB2J-0.7.7.2.data → TB2J-0.8.0.data}/scripts/TB2J_downfold.py +0 -0
- {TB2J-0.7.7.2.data → TB2J-0.8.0.data}/scripts/TB2J_eigen.py +0 -0
- {TB2J-0.7.7.2.data → TB2J-0.8.0.data}/scripts/TB2J_magnon.py +0 -0
- {TB2J-0.7.7.2.data → TB2J-0.8.0.data}/scripts/TB2J_magnon_dos.py +0 -0
- {TB2J-0.7.7.2.data → TB2J-0.8.0.data}/scripts/TB2J_merge.py +0 -0
- {TB2J-0.7.7.2.data → TB2J-0.8.0.data}/scripts/TB2J_rotate.py +0 -0
- {TB2J-0.7.7.2.data → TB2J-0.8.0.data}/scripts/siesta2J.py +0 -0
- {TB2J-0.7.7.2.data → TB2J-0.8.0.data}/scripts/wann2J.py +0 -0
- {TB2J-0.7.7.2.dist-info → TB2J-0.8.0.dist-info}/LICENSE +0 -0
- {TB2J-0.7.7.2.dist-info → TB2J-0.8.0.dist-info}/WHEEL +0 -0
- {TB2J-0.7.7.2.dist-info → TB2J-0.8.0.dist-info}/top_level.txt +0 -0
TB2J/abacus/stru_api.py
ADDED
@@ -0,0 +1,2020 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
Created on Wed Jun 13 10:31:30 2018
|
4
|
+
@author: shenzx
|
5
|
+
|
6
|
+
Modified on Wed Aug 01 11:44:51 2022
|
7
|
+
@author: Ji Yu-yang
|
8
|
+
"""
|
9
|
+
|
10
|
+
import re
|
11
|
+
import warnings
|
12
|
+
import numpy as np
|
13
|
+
import os
|
14
|
+
import shutil
|
15
|
+
from pathlib import Path
|
16
|
+
|
17
|
+
from ase import Atoms
|
18
|
+
from ase.units import Bohr, Hartree, GPa, mol, _me, Rydberg
|
19
|
+
from ase.utils import lazymethod, lazyproperty, reader, writer
|
20
|
+
from ase.calculators.singlepoint import SinglePointDFTCalculator, arrays_to_kpoints
|
21
|
+
|
22
|
+
_re_float = r"[-+]?\d+\.*\d*(?:[Ee][-+]\d+)?"
|
23
|
+
AU_to_MASS = mol * _me * 1e3
|
24
|
+
UNIT_V = np.sqrt(Hartree / AU_to_MASS)
|
25
|
+
|
26
|
+
# --------WRITE---------
|
27
|
+
|
28
|
+
# WRITE ABACUS INPUT -START-
|
29
|
+
|
30
|
+
|
31
|
+
@writer
|
32
|
+
def write_input(fd, parameters=None):
|
33
|
+
"""Write the INPUT file for ABACUS
|
34
|
+
|
35
|
+
Parameters
|
36
|
+
----------
|
37
|
+
fd: str
|
38
|
+
The file object to write to
|
39
|
+
parameters: dict
|
40
|
+
The dictionary of all paramters for the calculation.
|
41
|
+
"""
|
42
|
+
from copy import deepcopy
|
43
|
+
|
44
|
+
params = deepcopy(parameters)
|
45
|
+
params["dft_functional"] = (
|
46
|
+
params.pop("xc") if params.get("xc") else params.get("dft_functional", "pbe")
|
47
|
+
)
|
48
|
+
for key in [
|
49
|
+
"pp",
|
50
|
+
"basis",
|
51
|
+
"pseudo_dir",
|
52
|
+
"basis_dir",
|
53
|
+
"orbital_dir",
|
54
|
+
"offsite_basis_dir",
|
55
|
+
"kpts",
|
56
|
+
"knumber",
|
57
|
+
"kmode",
|
58
|
+
"knumbers",
|
59
|
+
"scaled",
|
60
|
+
]:
|
61
|
+
params.pop(key, None)
|
62
|
+
|
63
|
+
lines = []
|
64
|
+
lines.append("INPUT_PARAMETERS")
|
65
|
+
lines.append("# Created by Atomic Simulation Enviroment")
|
66
|
+
for key, val in params.items():
|
67
|
+
if val is not None:
|
68
|
+
lines.append(str(key) + " " * (40 - len(key)) + str(val))
|
69
|
+
lines.append("")
|
70
|
+
fd.write("\n".join(lines))
|
71
|
+
|
72
|
+
|
73
|
+
# WRITE ABACUS INPUT -END-
|
74
|
+
|
75
|
+
# WRITE ABACUS KPT -START-
|
76
|
+
|
77
|
+
|
78
|
+
@writer
|
79
|
+
def write_kpt(fd=None, parameters=None, atoms=None):
|
80
|
+
"""Write the KPT file for ABACUS
|
81
|
+
|
82
|
+
Parameters
|
83
|
+
----------
|
84
|
+
fd: str
|
85
|
+
The file object to write to
|
86
|
+
parameters: dict
|
87
|
+
The dictionary of all paramters for the calculation.
|
88
|
+
If `gamma_only` or `kspacing` in `parameters`, it will not output any files by ase
|
89
|
+
atoms: Atoms
|
90
|
+
It should be set, when `parameters['kpts']` is `dict`. Parameters of `cell.bandpath` and
|
91
|
+
`ase.calculators.calculator.kpts2sizeandoffsets` are supported to be keys
|
92
|
+
of the dictionary `parameters['kpts']`, and k-points will be generated by ASE.
|
93
|
+
"""
|
94
|
+
|
95
|
+
gamma_only = parameters.get("gamma_only", 0)
|
96
|
+
kspacing = parameters.get("kspacing", 0.0)
|
97
|
+
kpts = parameters.get("kpts", None)
|
98
|
+
koffset = parameters.get("koffset", 0)
|
99
|
+
if gamma_only is not None and gamma_only == 1:
|
100
|
+
return
|
101
|
+
elif kspacing is not None and kspacing > 0.0:
|
102
|
+
return
|
103
|
+
elif kpts is not None:
|
104
|
+
if isinstance(kpts, dict) and "path" not in kpts:
|
105
|
+
from ase.calculators.calculator import kpts2sizeandoffsets
|
106
|
+
|
107
|
+
kgrid, shift = kpts2sizeandoffsets(atoms=atoms, **kpts)
|
108
|
+
koffset = []
|
109
|
+
for i, x in enumerate(shift):
|
110
|
+
assert x == 0 or abs(x * kgrid[i] - 0.5) < 1e-14
|
111
|
+
koffset.append(0 if x == 0 else 1)
|
112
|
+
else:
|
113
|
+
kgrid = kpts
|
114
|
+
else:
|
115
|
+
kgrid = "gamma"
|
116
|
+
|
117
|
+
if isinstance(koffset, int):
|
118
|
+
koffset = [koffset] * 3
|
119
|
+
|
120
|
+
if isinstance(kgrid, dict) or hasattr(kgrid, "kpts"):
|
121
|
+
from ase.calculators.calculator import kpts2ndarray
|
122
|
+
|
123
|
+
kmode = "Direct"
|
124
|
+
kgrid = kpts2ndarray(kgrid, atoms=atoms)
|
125
|
+
elif isinstance(kgrid, str) and (kgrid == "gamma"):
|
126
|
+
kmode = "Gamma"
|
127
|
+
knumber = 0
|
128
|
+
kgrid = [0, 0, 0]
|
129
|
+
elif "gamma" in kpts:
|
130
|
+
kmode = "Gamma" if kpts["gamma"] else "MP"
|
131
|
+
else:
|
132
|
+
kmode = parameters.get("kmode", "Gamma")
|
133
|
+
|
134
|
+
lines = []
|
135
|
+
lines.append("K_POINTS")
|
136
|
+
if kmode in ["Gamma", "MP"]:
|
137
|
+
knumber = 0
|
138
|
+
lines.append(f"{knumber}")
|
139
|
+
lines.append(f"{kmode}")
|
140
|
+
lines.append(" ".join(map(str, kgrid)) + " " + " ".join(map(str, koffset)))
|
141
|
+
elif kmode in ["Direct", "Cartesian"]:
|
142
|
+
knumber = parameters.get("knumber", len(kgrid))
|
143
|
+
lines.append(f"{knumber}")
|
144
|
+
lines.append(f"{kmode}")
|
145
|
+
assert isinstance(knumber, int) and knumber > 0
|
146
|
+
for n in range(knumber):
|
147
|
+
lines.append(
|
148
|
+
f"{kgrid[n][0]:0<12f} {kgrid[n][1]:0<12f} {kgrid[n][2]:0<12f} {1/knumber}"
|
149
|
+
)
|
150
|
+
elif kmode in ["Line"]:
|
151
|
+
knumber = parameters.get("knumber", len(kgrid))
|
152
|
+
lines.append(f"{knumber}")
|
153
|
+
lines.append(f"{kmode}")
|
154
|
+
knumbers = parameters.get("knumbers", [10] * (knumber - 1) + [1])
|
155
|
+
for n in range(knumber):
|
156
|
+
lines.append(
|
157
|
+
f"{kgrid[n][0]:0<12f} {kgrid[n][1]:0<12f} {kgrid[n][2]:0<12f} {knumbers[n]}"
|
158
|
+
)
|
159
|
+
else:
|
160
|
+
raise ValueError(
|
161
|
+
"The value of kmode is not right, set to "
|
162
|
+
"Gamma, MP, Direct, Cartesian, or Line."
|
163
|
+
)
|
164
|
+
lines.append("")
|
165
|
+
fd.write("\n".join(lines))
|
166
|
+
|
167
|
+
|
168
|
+
# WRITE ABACUS KPT -END-
|
169
|
+
|
170
|
+
|
171
|
+
def _copy_files(file_list, src, dst, env, name):
|
172
|
+
if not src:
|
173
|
+
# environment variable for PP paths
|
174
|
+
if env in os.environ:
|
175
|
+
src = os.environ[env]
|
176
|
+
else:
|
177
|
+
src = "./"
|
178
|
+
# raise NotFoundErr(
|
179
|
+
# f"Can not set directory of {name} according to environment variable {env}")
|
180
|
+
|
181
|
+
for val in file_list:
|
182
|
+
src_file = os.path.join(src, val.strip())
|
183
|
+
dst_file = os.path.join(dst, val.strip())
|
184
|
+
if os.path.exists(dst_file):
|
185
|
+
continue
|
186
|
+
elif os.path.exists(src_file):
|
187
|
+
shutil.copyfile(src_file, dst_file)
|
188
|
+
else:
|
189
|
+
raise FileNotFoundError(f"Can't find {name} for ABACUS calculation")
|
190
|
+
|
191
|
+
|
192
|
+
# WRITE ABACUS PP -START-
|
193
|
+
def copy_pp(pp_list, pseudo_dir=None, directory="./"):
|
194
|
+
"""Copy pseudo-potential files from `pseudo_dir` to `directory`
|
195
|
+
|
196
|
+
Parameters
|
197
|
+
----------
|
198
|
+
pp_list: list
|
199
|
+
List of pseudo-potential files, e.g. ['Si.UPF', 'C.UPF']
|
200
|
+
pseudo_dir: str
|
201
|
+
The src directory includes pseudo-potential files. If None,
|
202
|
+
it will get directory from environment variable `ABACUS_PP_PATH`
|
203
|
+
directory: str
|
204
|
+
The dst directory
|
205
|
+
"""
|
206
|
+
_copy_files(
|
207
|
+
pp_list, pseudo_dir, directory, "ABACUS_PP_PATH", "pseudo-potential files"
|
208
|
+
)
|
209
|
+
|
210
|
+
|
211
|
+
# WRITE ABACUS PP -END-
|
212
|
+
|
213
|
+
|
214
|
+
# WRITE ABACUS basis -START-
|
215
|
+
def copy_basis(basis_list, basis_dir=None, directory="./"):
|
216
|
+
"""Copy LCAO basis files from `basis_dir` to `directory`
|
217
|
+
|
218
|
+
Parameters
|
219
|
+
----------
|
220
|
+
basis_list: list
|
221
|
+
List of LCAO basis files, e.g. ['Si.orb', 'C.orb']
|
222
|
+
basis_dir: str
|
223
|
+
The src directory includes LCAO basis files. If None,
|
224
|
+
it will get directory from environment variable `ABACUS_ORBITAL_PATH`
|
225
|
+
directory: str
|
226
|
+
The dst directory
|
227
|
+
"""
|
228
|
+
_copy_files(basis_list, basis_dir, directory, "ABACUS_ORBITAL_PATH", "basis files")
|
229
|
+
|
230
|
+
|
231
|
+
# WRITE ABACUS basis -END-
|
232
|
+
|
233
|
+
# WRITE ABACUS basis -START-
|
234
|
+
|
235
|
+
|
236
|
+
def copy_offsite_basis(offsite_basis_list, offsite_basis_dir=None, directory="./"):
|
237
|
+
"""Copy off-site ABFs basis files from `basis_dir` to `directory`
|
238
|
+
|
239
|
+
Parameters
|
240
|
+
----------
|
241
|
+
offsite_basis_list: list
|
242
|
+
List of off-site ABFs basis files, e.g. ['abfs_Si.dat', 'abfs_C.dat']
|
243
|
+
offsite_basis_dir: str
|
244
|
+
The src directory includes off-site ABFs basis files. If None,
|
245
|
+
it will get directory from environment variable `ABACUS_ABFS_PATH`
|
246
|
+
directory: str
|
247
|
+
The dst directory
|
248
|
+
"""
|
249
|
+
_copy_files(
|
250
|
+
offsite_basis_list,
|
251
|
+
offsite_basis_dir,
|
252
|
+
directory,
|
253
|
+
"ABACUS_ABFS_PATH",
|
254
|
+
"off-site ABFs basis files",
|
255
|
+
)
|
256
|
+
|
257
|
+
|
258
|
+
# WRITE ABACUS basis -END-
|
259
|
+
|
260
|
+
|
261
|
+
# WRITE ABACUS STRU -START-
|
262
|
+
|
263
|
+
|
264
|
+
def judge_exist_stru(stru=None):
|
265
|
+
if stru is None:
|
266
|
+
return False
|
267
|
+
else:
|
268
|
+
return True
|
269
|
+
|
270
|
+
|
271
|
+
def read_ase_stru(stru=None, coordinates_type="Cartesian"):
|
272
|
+
from ase.constraints import FixAtoms, FixCartesian
|
273
|
+
|
274
|
+
fix_cart = np.ones((len(stru), 3), dtype=int).tolist()
|
275
|
+
for constr in stru.constraints:
|
276
|
+
for i in constr.index:
|
277
|
+
if isinstance(constr, FixAtoms):
|
278
|
+
fix_cart[i] = [0, 0, 0]
|
279
|
+
elif isinstance(constr, FixCartesian):
|
280
|
+
fix_cart[i] = constr.mask
|
281
|
+
else:
|
282
|
+
UserWarning("Only `FixAtoms` and `FixCartesian` are supported now.")
|
283
|
+
|
284
|
+
if judge_exist_stru(stru):
|
285
|
+
atoms_list = []
|
286
|
+
atoms_sort = []
|
287
|
+
atoms_position = []
|
288
|
+
atoms_masses = []
|
289
|
+
atoms_magnetism = []
|
290
|
+
atoms_fix = []
|
291
|
+
atoms_all = stru.get_chemical_symbols()
|
292
|
+
|
293
|
+
# sort atoms according to atoms
|
294
|
+
atoms_dict = {}
|
295
|
+
for idx, atoms_all_name in enumerate(atoms_all):
|
296
|
+
if atoms_all_name not in atoms_dict:
|
297
|
+
atoms_dict[atoms_all_name] = []
|
298
|
+
atoms_dict[atoms_all_name].append(idx)
|
299
|
+
for symbol in atoms_dict:
|
300
|
+
atoms_sort.extend(atoms_dict[symbol])
|
301
|
+
atoms_list = list(
|
302
|
+
atoms_dict.keys()
|
303
|
+
) # Python >= 3.7 for keeping the order of keys
|
304
|
+
|
305
|
+
for atoms_list_name in atoms_list:
|
306
|
+
atoms_position.append([])
|
307
|
+
atoms_masses.append([])
|
308
|
+
atoms_magnetism.append(0)
|
309
|
+
atoms_fix.append([])
|
310
|
+
|
311
|
+
# get position, masses, magnetism from ase atoms
|
312
|
+
# TODO: property 'magmoms' is not implemented in ABACUS
|
313
|
+
if coordinates_type == "Cartesian":
|
314
|
+
for i in range(len(atoms_list)):
|
315
|
+
for j in range(len(atoms_all)):
|
316
|
+
if atoms_all[j] == atoms_list[i]:
|
317
|
+
atoms_position[i].append(list(stru.get_positions()[j]))
|
318
|
+
atoms_masses[i] = stru.get_masses()[j]
|
319
|
+
# atoms_magnetism[i] += np.array(stru[j].magmom)
|
320
|
+
atoms_fix[i].append(fix_cart[j])
|
321
|
+
# atoms_magnetism[i] = np.linalg.norm(atoms_magnetism[i])
|
322
|
+
|
323
|
+
elif coordinates_type == "Direct":
|
324
|
+
for i in range(len(atoms_list)):
|
325
|
+
for j in range(len(atoms_all)):
|
326
|
+
if atoms_all[j] == atoms_list[i]:
|
327
|
+
atoms_position[i].append(list(stru.get_scaled_positions()[j]))
|
328
|
+
atoms_masses[i] = stru.get_masses()[j]
|
329
|
+
# atoms_magnetism[i] += np.array(stru[j].magmom)
|
330
|
+
atoms_fix[i].append(fix_cart[j])
|
331
|
+
# atoms_magnetism[i] = np.linalg.norm(atoms_magnetism[i])
|
332
|
+
|
333
|
+
else:
|
334
|
+
raise ValueError(
|
335
|
+
"'coordinates_type' is ERROR," "please set to 'Cartesian' or 'Direct'"
|
336
|
+
)
|
337
|
+
|
338
|
+
return (
|
339
|
+
atoms_list,
|
340
|
+
atoms_sort,
|
341
|
+
atoms_masses,
|
342
|
+
atoms_position,
|
343
|
+
atoms_magnetism,
|
344
|
+
atoms_fix,
|
345
|
+
)
|
346
|
+
|
347
|
+
|
348
|
+
def write_input_stru_sort(atoms_sort=None):
|
349
|
+
if atoms_sort is None:
|
350
|
+
return "Please set right atoms sort"
|
351
|
+
else:
|
352
|
+
with open("ase_sort.dat", "w") as fd:
|
353
|
+
for idx in atoms_sort:
|
354
|
+
fd.write("%s\n" % idx)
|
355
|
+
|
356
|
+
|
357
|
+
def write_input_stru_core(
|
358
|
+
fd,
|
359
|
+
stru=None,
|
360
|
+
pp=None,
|
361
|
+
basis=None,
|
362
|
+
offsite_basis=None,
|
363
|
+
coordinates_type="Cartesian",
|
364
|
+
atoms_list=None,
|
365
|
+
atoms_position=None,
|
366
|
+
atoms_masses=None,
|
367
|
+
atoms_magnetism=None,
|
368
|
+
fix=None,
|
369
|
+
init_vel=False,
|
370
|
+
):
|
371
|
+
if not judge_exist_stru(stru):
|
372
|
+
return "No input structure!"
|
373
|
+
|
374
|
+
elif atoms_list is None:
|
375
|
+
return "Please set right atoms list"
|
376
|
+
elif atoms_position is None:
|
377
|
+
return "Please set right atoms position"
|
378
|
+
elif atoms_masses is None:
|
379
|
+
return "Please set right atoms masses"
|
380
|
+
elif atoms_magnetism is None:
|
381
|
+
return "Please set right atoms magnetism"
|
382
|
+
else:
|
383
|
+
fd.write("ATOMIC_SPECIES\n")
|
384
|
+
for i, elem in enumerate(atoms_list):
|
385
|
+
if pp:
|
386
|
+
pseudofile = pp.get(elem, "")
|
387
|
+
else:
|
388
|
+
pseudofile = ""
|
389
|
+
temp1 = " " * (4 - len(atoms_list[i]))
|
390
|
+
temp2 = " " * (14 - len(str(atoms_masses[i])))
|
391
|
+
atomic_species = (
|
392
|
+
atoms_list[i] + temp1 + str(atoms_masses[i]) + temp2 + pseudofile
|
393
|
+
)
|
394
|
+
|
395
|
+
fd.write(atomic_species)
|
396
|
+
fd.write("\n")
|
397
|
+
|
398
|
+
if basis:
|
399
|
+
fd.write("\n")
|
400
|
+
fd.write("NUMERICAL_ORBITAL\n")
|
401
|
+
for i, elem in enumerate(atoms_list):
|
402
|
+
orbitalfile = basis[elem]
|
403
|
+
fd.write(orbitalfile)
|
404
|
+
fd.write("\n")
|
405
|
+
|
406
|
+
if offsite_basis:
|
407
|
+
fd.write("\n")
|
408
|
+
fd.write("ABFS_ORBITAL\n")
|
409
|
+
for i, elem in enumerate(atoms_list):
|
410
|
+
orbitalfile = offsite_basis[elem]
|
411
|
+
fd.write(orbitalfile)
|
412
|
+
fd.write("\n")
|
413
|
+
# modified output by QuantumMisaka to synchroize with ATOMKIT
|
414
|
+
fd.write("\n")
|
415
|
+
fd.write("LATTICE_CONSTANT\n")
|
416
|
+
fd.write(f"{1/Bohr:.6f}\n")
|
417
|
+
fd.write("\n")
|
418
|
+
|
419
|
+
fd.write("LATTICE_VECTORS\n")
|
420
|
+
for i in range(3):
|
421
|
+
for j in range(3):
|
422
|
+
temp3 = str("{:0<12f}".format(stru.get_cell()[i][j])) + " " * 3
|
423
|
+
fd.write(temp3)
|
424
|
+
fd.write(" ")
|
425
|
+
fd.write("\n")
|
426
|
+
fd.write("\n")
|
427
|
+
|
428
|
+
fd.write("ATOMIC_POSITIONS\n")
|
429
|
+
fd.write(coordinates_type)
|
430
|
+
fd.write("\n")
|
431
|
+
fd.write("\n")
|
432
|
+
k = 0
|
433
|
+
for i in range(len(atoms_list)):
|
434
|
+
fd.write(atoms_list[i])
|
435
|
+
fd.write("\n")
|
436
|
+
fd.write(str("{:0<12f}".format(float(atoms_magnetism[i]))))
|
437
|
+
fd.write("\n")
|
438
|
+
fd.write(str(len(atoms_position[i])))
|
439
|
+
fd.write("\n")
|
440
|
+
|
441
|
+
for j in range(len(atoms_position[i])):
|
442
|
+
temp4 = str("{:0<12f}".format(atoms_position[i][j][0])) + " "
|
443
|
+
temp5 = str("{:0<12f}".format(atoms_position[i][j][1])) + " "
|
444
|
+
temp6 = str("{:0<12f}".format(atoms_position[i][j][2])) + " "
|
445
|
+
sym_pos = (
|
446
|
+
temp4
|
447
|
+
+ temp5
|
448
|
+
+ temp6
|
449
|
+
+ f"{fix[i][j][0]:.0f} {fix[i][j][1]:.0f} {fix[i][j][2]:.0f} "
|
450
|
+
)
|
451
|
+
if init_vel: # velocity in unit A/fs ?
|
452
|
+
sym_pos += f"v {stru.get_velocities()[j][0]} {stru.get_velocities()[j][1]} {stru.get_velocities()[j][2]} "
|
453
|
+
|
454
|
+
if isinstance(stru[k].magmom, float):
|
455
|
+
sym_pos += f"mag {stru[k].magmom} "
|
456
|
+
elif isinstance(stru[k].magmom, list) or isinstance(
|
457
|
+
stru[k].magmom, np.ndarray
|
458
|
+
):
|
459
|
+
if len(stru[k].magmom) == 3:
|
460
|
+
sym_pos += f"mag {stru[k].magmom[0]} {stru[k].magmom[1]} {stru[k].magmom[2]} "
|
461
|
+
elif len(stru[k].magmom) == 1:
|
462
|
+
sym_pos += f"mag {stru[k].magmom[0]} "
|
463
|
+
k += 1
|
464
|
+
fd.write(sym_pos)
|
465
|
+
fd.write("\n")
|
466
|
+
fd.write("\n")
|
467
|
+
|
468
|
+
|
469
|
+
@writer
|
470
|
+
def write_abacus(
|
471
|
+
fd, atoms=None, pp=None, basis=None, offsite_basis=None, scaled=True, init_vel=False
|
472
|
+
):
|
473
|
+
"""Write the STRU file for ABACUS
|
474
|
+
|
475
|
+
Parameters
|
476
|
+
----------
|
477
|
+
fd: str
|
478
|
+
The file object to write to
|
479
|
+
atoms: atoms.Atoms
|
480
|
+
The Atoms object for the requested calculation
|
481
|
+
pp: dict
|
482
|
+
The pseudo-potential file of each elements, e.g. for SiC, {'Si':'Si.UPF', 'C':'C.UPF'}
|
483
|
+
basis: dict
|
484
|
+
The basis file of each elements for LCAO calculations, e.g. for SiC, {'Si':'Si.orb', 'C':'C.orb'}
|
485
|
+
offsite_basis: dict
|
486
|
+
The offsite basis file of each elements for HSE calculations with off-site ABFs, e.g. for SiC, {'Si':'Si.orb', 'C':'C.orb'}
|
487
|
+
scaled: bool
|
488
|
+
If output STRU file with scaled positions
|
489
|
+
init_vel: bool
|
490
|
+
if initialize velocities in STRU file
|
491
|
+
"""
|
492
|
+
|
493
|
+
if scaled:
|
494
|
+
coordinates_type = "Direct"
|
495
|
+
else:
|
496
|
+
coordinates_type = "Cartesian"
|
497
|
+
|
498
|
+
if not judge_exist_stru(atoms):
|
499
|
+
return "No input structure!"
|
500
|
+
|
501
|
+
else:
|
502
|
+
(
|
503
|
+
atoms_list,
|
504
|
+
atoms_sort,
|
505
|
+
atoms_masses,
|
506
|
+
atoms_position,
|
507
|
+
atoms_magnetism,
|
508
|
+
atoms_fix,
|
509
|
+
) = read_ase_stru(atoms, coordinates_type)
|
510
|
+
|
511
|
+
write_input_stru_core(
|
512
|
+
fd,
|
513
|
+
atoms,
|
514
|
+
pp,
|
515
|
+
basis,
|
516
|
+
offsite_basis,
|
517
|
+
coordinates_type,
|
518
|
+
atoms_list,
|
519
|
+
atoms_position,
|
520
|
+
atoms_masses,
|
521
|
+
atoms_magnetism,
|
522
|
+
atoms_fix,
|
523
|
+
init_vel,
|
524
|
+
)
|
525
|
+
write_input_stru_sort(atoms_sort)
|
526
|
+
|
527
|
+
|
528
|
+
# WRITE ABACUS STRU -END-
|
529
|
+
|
530
|
+
# --------READ---------
|
531
|
+
|
532
|
+
# Read KPT file -START-
|
533
|
+
|
534
|
+
|
535
|
+
@reader
|
536
|
+
def read_kpt(fd, cell=None):
|
537
|
+
"""Read ABACUS KPT file and return results dict.
|
538
|
+
|
539
|
+
If `cell` is not None, a BandPath object will be returned.
|
540
|
+
"""
|
541
|
+
contents = fd.read()
|
542
|
+
contents = re.compile(r"#.*|//.*").sub("", contents)
|
543
|
+
lines = [i.strip() for i in contents.split("\n")]
|
544
|
+
kmode = None
|
545
|
+
knumber = None
|
546
|
+
kpts = None
|
547
|
+
koffset = None
|
548
|
+
knumbers = None
|
549
|
+
weights = None
|
550
|
+
kmode = lines[2]
|
551
|
+
knumber = int(lines[1].split()[0])
|
552
|
+
if kmode in ["Gamma", "MP"]:
|
553
|
+
kpts = np.array(lines[3].split()[:3], dtype=int)
|
554
|
+
koffset = np.array(lines[3].split()[3:], dtype=float)
|
555
|
+
return {"mode": kmode, "number": knumber, "kpts": kpts, "offset": koffset}
|
556
|
+
elif kmode in ["Cartesian", "Direct", "Line"]:
|
557
|
+
klines = np.array(
|
558
|
+
[line.split() for line in lines[3 : 3 + knumber]], dtype=float
|
559
|
+
)
|
560
|
+
kpts = klines[:, :3]
|
561
|
+
if kmode in ["Cartesian", "Direct"]:
|
562
|
+
weights = klines[:, 3]
|
563
|
+
return {"mode": kmode, "number": knumber, "kpts": kpts, "weights": weights}
|
564
|
+
else:
|
565
|
+
knumbers = klines[:, 3].astype(int)
|
566
|
+
if cell is not None:
|
567
|
+
from ase.dft.kpoints import bandpath
|
568
|
+
|
569
|
+
return bandpath(kpts, cell, npoints=knumbers.sum())
|
570
|
+
else:
|
571
|
+
return {
|
572
|
+
"mode": kmode,
|
573
|
+
"number": knumber,
|
574
|
+
"kpts": kpts,
|
575
|
+
"knumbers": knumbers,
|
576
|
+
}
|
577
|
+
else:
|
578
|
+
raise ValueError(
|
579
|
+
"The value of kmode is not right, please set to Gamma, MP, Direct, Cartesian, or Line."
|
580
|
+
)
|
581
|
+
|
582
|
+
|
583
|
+
# Read KPT file -END-
|
584
|
+
|
585
|
+
# Read INPUT file -START-
|
586
|
+
|
587
|
+
|
588
|
+
@reader
|
589
|
+
def read_input(fd):
|
590
|
+
"""Read ABACUS INPUT file and return parameters dict."""
|
591
|
+
result = {}
|
592
|
+
|
593
|
+
first_line = fd.readline().strip()
|
594
|
+
if first_line != "INPUT_PARAMETERS":
|
595
|
+
raise ValueError("Missing INPUT_PARAMETERS keyword in INPUT file.")
|
596
|
+
for line in fd:
|
597
|
+
if line.startswith("#"):
|
598
|
+
continue
|
599
|
+
items = line.strip().split()
|
600
|
+
if not items:
|
601
|
+
continue
|
602
|
+
key = items[0]
|
603
|
+
value = " ".join(items[1:])
|
604
|
+
result[key] = value
|
605
|
+
|
606
|
+
return result
|
607
|
+
|
608
|
+
|
609
|
+
# Read INPUT file -End-
|
610
|
+
|
611
|
+
# READ ABACUS STRU -START-
|
612
|
+
|
613
|
+
# Read UPF file -START-
|
614
|
+
|
615
|
+
|
616
|
+
@reader
|
617
|
+
def read_pp_upf(fd):
|
618
|
+
"""Read PP UPF file and return parameters dict."""
|
619
|
+
result = {}
|
620
|
+
|
621
|
+
for line in fd:
|
622
|
+
if "<UPF version=" in line:
|
623
|
+
result["version"] = (
|
624
|
+
line.split("=")[-1].strip('"').strip().strip(r"\>").strip('"')
|
625
|
+
)
|
626
|
+
if "element" in line:
|
627
|
+
result["element"] = (
|
628
|
+
line.split("=")[-1].strip('"').strip().strip('"').strip()
|
629
|
+
)
|
630
|
+
if "pseudo_type" in line:
|
631
|
+
result["pseudo_type"] = line.split("=")[-1].strip('"').strip().strip('"')
|
632
|
+
if "relativistic" in line:
|
633
|
+
result["relativistic"] = line.split("=")[-1].strip('"').strip().strip('"')
|
634
|
+
if "is_ultrasoft" in line:
|
635
|
+
result["is_ultrasoft"] = (
|
636
|
+
False
|
637
|
+
if line.split("=")[-1].strip('"').strip().strip('"') == "F"
|
638
|
+
else True
|
639
|
+
)
|
640
|
+
if "is_paw" in line:
|
641
|
+
result["is_paw"] = (
|
642
|
+
False
|
643
|
+
if line.split("=")[-1].strip('"').strip().strip('"') == "F"
|
644
|
+
else True
|
645
|
+
)
|
646
|
+
if "is_coulomb" in line:
|
647
|
+
result["is_coulomb"] = (
|
648
|
+
False
|
649
|
+
if line.split("=")[-1].strip('"').strip().strip('"') == "F"
|
650
|
+
else True
|
651
|
+
)
|
652
|
+
if "core_correction" in line:
|
653
|
+
result["core_correction"] = (
|
654
|
+
False
|
655
|
+
if line.split("=")[-1].strip('"').strip().strip('"') == "F"
|
656
|
+
else True
|
657
|
+
)
|
658
|
+
if "functional" in line:
|
659
|
+
result["functional"] = line.split("=")[-1].strip('"').strip().strip('"')
|
660
|
+
if "z_valence" in line:
|
661
|
+
result["z_valence"] = float(
|
662
|
+
line.split("=")[-1].strip('" ').strip().strip('"')
|
663
|
+
)
|
664
|
+
if "l_max" in line:
|
665
|
+
result["l_max"] = int(line.split("=")[-1].strip('"').strip().strip('"'))
|
666
|
+
|
667
|
+
return result
|
668
|
+
|
669
|
+
|
670
|
+
# Read UPF file -END-
|
671
|
+
|
672
|
+
|
673
|
+
@reader
|
674
|
+
def read_abacus(fd, latname=None, verbose=False):
|
675
|
+
"""Read structure information from abacus structure file.
|
676
|
+
|
677
|
+
If `latname` is not None, 'LATTICE_VECTORS' should be removed in structure files of ABACUS.
|
678
|
+
Allowed values: 'sc', 'fcc', 'bcc', 'hexagonal', 'trigonal', 'st', 'bct', 'so', 'baco', 'fco', 'bco', 'sm', 'bacm', 'triclinic'
|
679
|
+
|
680
|
+
If `verbose` is True, pseudo-potential, basis and other information along with the Atoms object will be output as a dict.
|
681
|
+
"""
|
682
|
+
|
683
|
+
from ase.constraints import FixCartesian
|
684
|
+
|
685
|
+
contents = fd.read()
|
686
|
+
title_str = r"(?:LATTICE_CONSTANT|NUMERICAL_DESCRIPTOR|NUMERICAL_ORBITAL|ABFS_ORBITAL|LATTICE_VECTORS|LATTICE_PARAMETERS|ATOMIC_POSITIONS)"
|
687
|
+
|
688
|
+
# remove comments and empty lines
|
689
|
+
contents = re.compile(r"#.*|//.*").sub("", contents)
|
690
|
+
contents = re.compile(r"\n{2,}").sub("\n", contents)
|
691
|
+
|
692
|
+
# specie, mass, pps
|
693
|
+
specie_pattern = re.compile(rf"ATOMIC_SPECIES\s*\n([\s\S]+?)\s*\n{title_str}")
|
694
|
+
specie_lines = np.array(
|
695
|
+
[line.split() for line in specie_pattern.search(contents).group(1).split("\n")]
|
696
|
+
)
|
697
|
+
symbols = specie_lines[:, 0]
|
698
|
+
ntype = len(symbols)
|
699
|
+
mass = specie_lines[:, 1].astype(float)
|
700
|
+
try:
|
701
|
+
atom_potential = dict(zip(symbols, specie_lines[:, 2].tolist()))
|
702
|
+
except IndexError:
|
703
|
+
atom_potential = None
|
704
|
+
|
705
|
+
# basis
|
706
|
+
aim_title = "NUMERICAL_ORBITAL"
|
707
|
+
aim_title_sub = title_str.replace("|" + aim_title, "")
|
708
|
+
orb_pattern = re.compile(rf"{aim_title}\s*\n([\s\S]+?)\s*\n{aim_title_sub}")
|
709
|
+
orb_lines = orb_pattern.search(contents)
|
710
|
+
if orb_lines:
|
711
|
+
atom_basis = dict(zip(symbols, orb_lines.group(1).split("\n")))
|
712
|
+
else:
|
713
|
+
atom_basis = None
|
714
|
+
|
715
|
+
# ABFs basis
|
716
|
+
aim_title = "ABFS_ORBITAL"
|
717
|
+
aim_title_sub = title_str.replace("|" + aim_title, "")
|
718
|
+
abf_pattern = re.compile(rf"{aim_title}\s*\n([\s\S]+?)\s*\n{aim_title_sub}")
|
719
|
+
abf_lines = abf_pattern.search(contents)
|
720
|
+
if abf_lines:
|
721
|
+
atom_offsite_basis = dict(zip(symbols, abf_lines.group(1).split("\n")))
|
722
|
+
else:
|
723
|
+
atom_offsite_basis = None
|
724
|
+
|
725
|
+
# deepks for ABACUS
|
726
|
+
aim_title = "NUMERICAL_DESCRIPTOR"
|
727
|
+
aim_title_sub = title_str.replace("|" + aim_title, "")
|
728
|
+
deep_pattern = re.compile(rf"{aim_title}\s*\n([\s\S]+?)\s*\n{aim_title_sub}")
|
729
|
+
deep_lines = deep_pattern.search(contents)
|
730
|
+
if deep_lines:
|
731
|
+
atom_descriptor = deep_lines.group(1)
|
732
|
+
else:
|
733
|
+
atom_descriptor = None
|
734
|
+
|
735
|
+
# lattice constant
|
736
|
+
aim_title = "LATTICE_CONSTANT"
|
737
|
+
aim_title_sub = title_str.replace("|" + aim_title, "")
|
738
|
+
a0_pattern = re.compile(rf"{aim_title}\s*\n([\s\S]+?)\s*\n{aim_title_sub}")
|
739
|
+
a0_lines = a0_pattern.search(contents)
|
740
|
+
atom_lattice_scale = float(a0_lines.group(1))
|
741
|
+
|
742
|
+
# lattice vector
|
743
|
+
if latname:
|
744
|
+
aim_title = "LATTICE_PARAMETERS"
|
745
|
+
aim_title_sub = title_str.replace("|" + aim_title, "")
|
746
|
+
lparam_pattern = re.compile(rf"{aim_title}\s*\n([\s\S]+?)\s*\n{aim_title_sub}")
|
747
|
+
lparam_lines = lparam_pattern.search(contents)
|
748
|
+
atom_lattice = get_lattice_from_latname(lparam_lines, latname)
|
749
|
+
else:
|
750
|
+
aim_title = "LATTICE_VECTORS"
|
751
|
+
aim_title_sub = title_str.replace("|" + aim_title, "")
|
752
|
+
vec_pattern = re.compile(rf"{aim_title}\s*\n([\s\S]+?)\s*\n{aim_title_sub}")
|
753
|
+
vec_lines = vec_pattern.search(contents)
|
754
|
+
if vec_lines:
|
755
|
+
atom_lattice = np.array(
|
756
|
+
[
|
757
|
+
line.split()
|
758
|
+
for line in vec_pattern.search(contents).group(1).split("\n")
|
759
|
+
]
|
760
|
+
).astype(float)
|
761
|
+
else:
|
762
|
+
raise Exception(
|
763
|
+
f"Parameter `latname` or `LATTICE_VECTORS` in {fd.name} must be set."
|
764
|
+
)
|
765
|
+
atom_lattice = atom_lattice * atom_lattice_scale * Bohr
|
766
|
+
|
767
|
+
aim_title = "ATOMIC_POSITIONS"
|
768
|
+
type_pattern = re.compile(rf"{aim_title}\s*\n(\w+)\s*\n")
|
769
|
+
# type of coordinates
|
770
|
+
atom_pos_type = type_pattern.search(contents).group(1)
|
771
|
+
assert atom_pos_type in [
|
772
|
+
"Direct",
|
773
|
+
"Cartesian",
|
774
|
+
], "Only two type of atomic coordinates are supported: 'Direct' or 'Cartesian'."
|
775
|
+
|
776
|
+
block_pattern = re.compile(rf"{atom_pos_type}\s*\n([\s\S]+)")
|
777
|
+
block = block_pattern.search(contents).group()
|
778
|
+
if block[-1] != "\n":
|
779
|
+
block += "\n"
|
780
|
+
atom_magnetism = []
|
781
|
+
atom_symbol = []
|
782
|
+
# atom_mass = []
|
783
|
+
atom_block = []
|
784
|
+
for i, symbol in enumerate(symbols):
|
785
|
+
pattern = re.compile(rf"{symbol}\s*\n({_re_float})\s*\n(\d+)")
|
786
|
+
sub_block = pattern.search(block)
|
787
|
+
number = int(sub_block.group(2))
|
788
|
+
|
789
|
+
# symbols, magnetism
|
790
|
+
sym = [symbol] * number
|
791
|
+
masses = [mass] * number
|
792
|
+
atom_mags = [float(sub_block.group(1))] * number
|
793
|
+
for j in range(number):
|
794
|
+
atom_symbol.append(sym[j])
|
795
|
+
# atom_mass.append(masses[j])
|
796
|
+
atom_magnetism.append(atom_mags[j])
|
797
|
+
|
798
|
+
if i == ntype - 1:
|
799
|
+
lines_pattern = re.compile(
|
800
|
+
rf"{symbol}\s*\n{_re_float}\s*\n\d+\s*\n([\s\S]+)\s*\n"
|
801
|
+
)
|
802
|
+
else:
|
803
|
+
lines_pattern = re.compile(
|
804
|
+
rf"{symbol}\s*\n{_re_float}\s*\n\d+\s*\n([\s\S]+?)\s*\n\w+\s*\n{_re_float}"
|
805
|
+
)
|
806
|
+
lines = lines_pattern.search(block)
|
807
|
+
for j in [line.split() for line in lines.group(1).split("\n")]:
|
808
|
+
atom_block.append(j)
|
809
|
+
atom_block = np.array(atom_block)
|
810
|
+
atom_magnetism = np.array(atom_magnetism)
|
811
|
+
|
812
|
+
# position
|
813
|
+
atom_positions = atom_block[:, 0:3].astype(float)
|
814
|
+
natoms = len(atom_positions)
|
815
|
+
|
816
|
+
# fix_cart
|
817
|
+
if (atom_block[:, 3] == ["m"] * natoms).all():
|
818
|
+
atom_xyz = ~atom_block[:, 4:7].astype(bool)
|
819
|
+
else:
|
820
|
+
atom_xyz = ~atom_block[:, 3:6].astype(bool)
|
821
|
+
fix_cart = [FixCartesian(ci, xyz) for ci, xyz in enumerate(atom_xyz)]
|
822
|
+
|
823
|
+
def _get_index(labels, num):
|
824
|
+
index = None
|
825
|
+
res = []
|
826
|
+
for l in labels:
|
827
|
+
if l in atom_block:
|
828
|
+
index = np.where(atom_block == l)[-1][0]
|
829
|
+
if index is not None:
|
830
|
+
res = atom_block[:, index + 1 : index + 1 + num].astype(float)
|
831
|
+
|
832
|
+
return res, index
|
833
|
+
|
834
|
+
# velocity
|
835
|
+
v_labels = ["v", "vel", "velocity"]
|
836
|
+
atom_vel, v_index = _get_index(v_labels, 3)
|
837
|
+
|
838
|
+
# magnetism
|
839
|
+
m_labels = ["mag", "magmom"]
|
840
|
+
if "angle1" in atom_block or "angle2" in atom_block:
|
841
|
+
warnings.warn(
|
842
|
+
"Non-colinear angle-settings are not yet supported for this interface."
|
843
|
+
)
|
844
|
+
mags, m_index = _get_index(m_labels, 1)
|
845
|
+
try: # non-colinear
|
846
|
+
if m_index:
|
847
|
+
atom_magnetism = atom_block[:, m_index + 1 : m_index + 4].astype(float)
|
848
|
+
except IndexError: # colinear
|
849
|
+
if m_index:
|
850
|
+
atom_magnetism = mags
|
851
|
+
|
852
|
+
# to ase
|
853
|
+
if atom_pos_type == "Direct":
|
854
|
+
atoms = Atoms(
|
855
|
+
symbols=atom_symbol,
|
856
|
+
cell=atom_lattice,
|
857
|
+
scaled_positions=atom_positions,
|
858
|
+
pbc=True,
|
859
|
+
)
|
860
|
+
elif atom_pos_type == "Cartesian":
|
861
|
+
atoms = Atoms(
|
862
|
+
symbols=atom_symbol,
|
863
|
+
cell=atom_lattice,
|
864
|
+
positions=atom_positions * atom_lattice_scale * Bohr,
|
865
|
+
pbc=True,
|
866
|
+
)
|
867
|
+
|
868
|
+
# atom_mass = np.array(atom_mass).flatten()
|
869
|
+
# if atom_mass.any():
|
870
|
+
# atoms.set_masses(atom_mass)
|
871
|
+
if v_index:
|
872
|
+
atoms.set_velocities(atom_vel * UNIT_V)
|
873
|
+
|
874
|
+
atoms.set_initial_magnetic_moments(atom_magnetism)
|
875
|
+
atoms.set_constraint(fix_cart)
|
876
|
+
|
877
|
+
if verbose:
|
878
|
+
atoms.info["pp"] = atom_potential
|
879
|
+
atoms.info["basis"] = atom_basis
|
880
|
+
atoms.info["offsite_basis"] = atom_offsite_basis
|
881
|
+
atoms.info["descriptor"] = atom_descriptor
|
882
|
+
|
883
|
+
return atoms
|
884
|
+
|
885
|
+
|
886
|
+
def get_lattice_from_latname(lines, latname=None):
|
887
|
+
from math import sqrt
|
888
|
+
|
889
|
+
if lines:
|
890
|
+
lines = lines.group(1).split(" ")
|
891
|
+
|
892
|
+
if latname == "sc":
|
893
|
+
return np.eye(3)
|
894
|
+
elif latname == "fcc":
|
895
|
+
return np.array([[-0.5, 0, 0.5], [0, 0.5, 0.5], [-0.5, 0.5, 0]])
|
896
|
+
elif latname == "bcc":
|
897
|
+
return np.array([[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5]])
|
898
|
+
elif latname == "hexagonal":
|
899
|
+
x = float(lines[0])
|
900
|
+
return np.array([[1.0, 0, 0], [-0.5, sqrt(3) / 2, 0], [0, 0, x]])
|
901
|
+
elif latname == "trigonal":
|
902
|
+
x = float(lines[0])
|
903
|
+
tx = sqrt((1 - x) / 2)
|
904
|
+
ty = sqrt((1 - x) / 6)
|
905
|
+
tz = sqrt((1 + 2 * x) / 3)
|
906
|
+
return np.array([[tx, -ty, tz], [0, 2 * ty, tz], [-tx, -ty, tz]])
|
907
|
+
elif latname == "st":
|
908
|
+
x = float(lines[0])
|
909
|
+
return np.array([[1.0, 0, 0], [0, 1, 0], [0, 0, x]])
|
910
|
+
elif latname == "bct":
|
911
|
+
x = float(lines[0])
|
912
|
+
return np.array([[0.5, -0.5, x], [0.5, 0.5, x], [0.5, 0.5, x]])
|
913
|
+
elif latname == "baco":
|
914
|
+
x, y = list(map(float, lines))
|
915
|
+
return np.array([[0.5, x / 2, 0], [-0.5, x / 2, 0], [0, 0, y]])
|
916
|
+
elif latname == "fco":
|
917
|
+
x, y = list(map(float, lines))
|
918
|
+
return np.array([[0.5, 0, y / 2], [0.5, x / 2, 0], [0.5, x / 2, 0]])
|
919
|
+
elif latname == "bco":
|
920
|
+
x, y = list(map(float, lines))
|
921
|
+
return np.array(
|
922
|
+
[[0.5, x / 2, y / 2], [-0.5, x / 2, y / 2], [-0.5, -x / 2, y / 2]]
|
923
|
+
)
|
924
|
+
elif latname == "bco":
|
925
|
+
x, y, z = list(map(float, lines))
|
926
|
+
return np.array([[1, 0, 0], [x * z, x * sqrt(1 - z**2), 0], [0, 0, y]])
|
927
|
+
elif latname == "bacm":
|
928
|
+
x, y, z = list(map(float, lines))
|
929
|
+
return np.array(
|
930
|
+
[[0.5, 0, -y / 2], [x * z, x * sqrt(1 - z**2), 0], [0.5, 0, y / 2]]
|
931
|
+
)
|
932
|
+
elif latname == "triclinic":
|
933
|
+
x, y, m, n, l = list(map(float, lines))
|
934
|
+
fac = sqrt(1 + 2 * m * n * l - m**2 - n**2 - l**2) / sqrt(1 - m**2)
|
935
|
+
return np.array(
|
936
|
+
[
|
937
|
+
[1, 0, 0],
|
938
|
+
[x * m, x * sqrt(1 - m**2), 0],
|
939
|
+
[y * n, y * (l - n * m / sqrt(1 - m**2)), y * fac],
|
940
|
+
]
|
941
|
+
)
|
942
|
+
|
943
|
+
|
944
|
+
# READ ABACUS STRU -END-
|
945
|
+
|
946
|
+
|
947
|
+
# READ ABACUS OUT -START-
|
948
|
+
class AbacusOutChunk:
|
949
|
+
"""Base class for AbacusOutChunks"""
|
950
|
+
|
951
|
+
def __init__(self, contents):
|
952
|
+
"""Constructor
|
953
|
+
|
954
|
+
Parameters
|
955
|
+
----------
|
956
|
+
contents: str
|
957
|
+
The contents of the output file
|
958
|
+
"""
|
959
|
+
self.contents = contents
|
960
|
+
|
961
|
+
def parse_scalar(self, pattern):
|
962
|
+
"""Parse a scalar property from the chunk according to specific pattern
|
963
|
+
|
964
|
+
Parameters
|
965
|
+
----------
|
966
|
+
pattern: str
|
967
|
+
The pattern used to parse
|
968
|
+
|
969
|
+
Returns
|
970
|
+
-------
|
971
|
+
float
|
972
|
+
The scalar value of the property
|
973
|
+
"""
|
974
|
+
pattern_compile = re.compile(pattern)
|
975
|
+
res = pattern_compile.search(self.contents)
|
976
|
+
if res:
|
977
|
+
return float(res.group(1))
|
978
|
+
else:
|
979
|
+
return None
|
980
|
+
|
981
|
+
@lazyproperty
|
982
|
+
def coordinate_system(self):
|
983
|
+
"""Parse coordinate system (Cartesian or Direct) from the output file"""
|
984
|
+
# for '|', it will match all the patterns which results in '' or None
|
985
|
+
class_pattern = re.compile(r"(DIRECT) COORDINATES|(CARTESIAN) COORDINATES")
|
986
|
+
coord_class = list(class_pattern.search(self.contents).groups())
|
987
|
+
_remove_empty(coord_class)
|
988
|
+
|
989
|
+
return coord_class[0]
|
990
|
+
|
991
|
+
@lazymethod
|
992
|
+
def _parse_site(self):
|
993
|
+
"""Parse sites for all the structures in the output file"""
|
994
|
+
pos_pattern = re.compile(
|
995
|
+
rf"(CARTESIAN COORDINATES \( UNIT = {_re_float} Bohr \)\.+\n\s*atom\s*x\s*y\s*z\s*mag(\s*vx\s*vy\s*vz\s*|\s*)\n[\s\S]+?)\n\n|(DIRECT COORDINATES\n\s*atom\s*x\s*y\s*z\s*mag(\s*vx\s*vy\s*vz\s*|\s*)\n[\s\S]+?)\n\n"
|
996
|
+
)
|
997
|
+
|
998
|
+
return pos_pattern.findall(self.contents)
|
999
|
+
|
1000
|
+
|
1001
|
+
class AbacusOutHeaderChunk(AbacusOutChunk):
|
1002
|
+
"""General information that the header of the running_*.log file contains"""
|
1003
|
+
|
1004
|
+
def __init__(self, contents):
|
1005
|
+
"""Constructor
|
1006
|
+
|
1007
|
+
Parameters
|
1008
|
+
----------
|
1009
|
+
contents: str
|
1010
|
+
The contents of the output file
|
1011
|
+
"""
|
1012
|
+
super().__init__(contents)
|
1013
|
+
|
1014
|
+
@lazyproperty
|
1015
|
+
def out_dir(self):
|
1016
|
+
out_pattern = re.compile(r"global_out_dir\s*=\s*([\s\S]+?)/")
|
1017
|
+
return out_pattern.search(self.contents).group(1)
|
1018
|
+
|
1019
|
+
@lazyproperty
|
1020
|
+
def lattice_constant(self):
|
1021
|
+
"""The lattice constant from the header of the running_*.log"""
|
1022
|
+
a0_pattern_str = rf"lattice constant \(Angstrom\)\s*=\s*({_re_float})"
|
1023
|
+
return self.parse_scalar(a0_pattern_str)
|
1024
|
+
|
1025
|
+
@lazyproperty
|
1026
|
+
def initial_cell(self):
|
1027
|
+
"""The initial cell from the header of the running_*.log file"""
|
1028
|
+
cell_pattern = re.compile(
|
1029
|
+
rf"Lattice vectors: \(Cartesian coordinate: in unit of a_0\)\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n"
|
1030
|
+
)
|
1031
|
+
lattice = np.reshape(cell_pattern.findall(self.contents)[0], (3, 3)).astype(
|
1032
|
+
float
|
1033
|
+
)
|
1034
|
+
|
1035
|
+
return lattice * self.lattice_constant
|
1036
|
+
|
1037
|
+
@lazyproperty
|
1038
|
+
def initial_site(self):
|
1039
|
+
def str_to_sites(val_in):
|
1040
|
+
val = np.array(val_in)
|
1041
|
+
labels = val[:, 0]
|
1042
|
+
pos = val[:, 1:4].astype(float)
|
1043
|
+
if val.shape[1] == 5:
|
1044
|
+
mag = val[:, 4].astype(float)
|
1045
|
+
vel = np.zeros((3,), dtype=float)
|
1046
|
+
elif val.shape[1] == 8:
|
1047
|
+
mag = val[:, 4].astype(float)
|
1048
|
+
vel = val[:, 5:8].astype(float)
|
1049
|
+
return labels, pos, mag, vel
|
1050
|
+
|
1051
|
+
def parse_block(pos_block):
|
1052
|
+
data = list(pos_block)
|
1053
|
+
_remove_empty(data)
|
1054
|
+
site = list(map(list, site_pattern.findall(data[0])))
|
1055
|
+
list(map(_remove_empty, site))
|
1056
|
+
labels, pos, mag, vel = str_to_sites(site)
|
1057
|
+
if self.coordinate_system == "CARTESIAN":
|
1058
|
+
unit = float(unit_pattern.search(self.contents).group(1)) * Bohr
|
1059
|
+
positions = pos * unit
|
1060
|
+
elif self.coordinate_system == "DIRECT":
|
1061
|
+
positions = pos
|
1062
|
+
return labels, positions, mag, vel
|
1063
|
+
|
1064
|
+
site_pattern = re.compile(
|
1065
|
+
rf"tau[cd]_([a-zA-Z]+)\d+\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})|tau[cd]_([a-zA-Z]+)\d+\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})"
|
1066
|
+
)
|
1067
|
+
unit_pattern = re.compile(rf"UNIT = ({_re_float}) Bohr")
|
1068
|
+
|
1069
|
+
return parse_block(self._parse_site()[0])
|
1070
|
+
|
1071
|
+
@lazyproperty
|
1072
|
+
def initial_atoms(self):
|
1073
|
+
"""Create an atoms object for the initial structure from the
|
1074
|
+
header of the running_*.log file"""
|
1075
|
+
labels, positions, mag, vel = self.initial_site
|
1076
|
+
if self.coordinate_system == "CARTESIAN":
|
1077
|
+
atoms = Atoms(
|
1078
|
+
symbols=labels,
|
1079
|
+
positions=positions,
|
1080
|
+
cell=self.initial_cell,
|
1081
|
+
pbc=True,
|
1082
|
+
velocities=vel * UNIT_V,
|
1083
|
+
)
|
1084
|
+
elif self.coordinate_system == "DIRECT":
|
1085
|
+
atoms = Atoms(
|
1086
|
+
symbols=labels,
|
1087
|
+
scaled_positions=positions,
|
1088
|
+
cell=self.initial_cell,
|
1089
|
+
pbc=True,
|
1090
|
+
velocities=vel * UNIT_V,
|
1091
|
+
)
|
1092
|
+
atoms.set_initial_magnetic_moments(mag)
|
1093
|
+
|
1094
|
+
return atoms
|
1095
|
+
|
1096
|
+
@lazyproperty
|
1097
|
+
def is_relaxation(self):
|
1098
|
+
"""Determine if the calculation is an atomic position optimization or not"""
|
1099
|
+
return "RELAXATION" in self.contents
|
1100
|
+
|
1101
|
+
@lazyproperty
|
1102
|
+
def is_nscf(self):
|
1103
|
+
"""Determine if the calculation is a NSCF calculation"""
|
1104
|
+
return "NONSELF-CONSISTENT" in self.contents
|
1105
|
+
|
1106
|
+
@lazyproperty
|
1107
|
+
def is_cell_relaxation(self):
|
1108
|
+
"""Determine if the calculation is an variable cell optimization or not"""
|
1109
|
+
return "RELAX CELL" in self.contents
|
1110
|
+
|
1111
|
+
@lazyproperty
|
1112
|
+
def is_md(self):
|
1113
|
+
"""Determine if calculation is a molecular dynamics calculation"""
|
1114
|
+
return "STEP OF MOLECULAR DYNAMICS" in self.contents
|
1115
|
+
|
1116
|
+
@lazymethod
|
1117
|
+
def _parse_k_points(self):
|
1118
|
+
"""Get the list of k-points used in the calculation"""
|
1119
|
+
|
1120
|
+
def str_to_kpoints(val_in):
|
1121
|
+
lines = (
|
1122
|
+
re.search(
|
1123
|
+
rf"KPOINTS\s*DIRECT_X\s*DIRECT_Y\s*DIRECT_Z\s*WEIGHT([\s\S]+?)DONE",
|
1124
|
+
val_in,
|
1125
|
+
)
|
1126
|
+
.group(1)
|
1127
|
+
.strip()
|
1128
|
+
.split("\n")
|
1129
|
+
)
|
1130
|
+
data = []
|
1131
|
+
for line in lines:
|
1132
|
+
data.append(line.strip().split()[1:5])
|
1133
|
+
data = np.array(data, dtype=float)
|
1134
|
+
kpoints = data[:, :3]
|
1135
|
+
weights = data[:, 3]
|
1136
|
+
return kpoints, weights
|
1137
|
+
|
1138
|
+
k_pattern = re.compile(
|
1139
|
+
r"minimum distributed K point number\s*=\s*\d+([\s\S]+?DONE : INIT K-POINTS Time)"
|
1140
|
+
)
|
1141
|
+
sub_contents = k_pattern.search(self.contents).group(1)
|
1142
|
+
k_points, k_point_weights = str_to_kpoints(sub_contents)
|
1143
|
+
|
1144
|
+
return k_points[: int(self.n_k_points)], k_point_weights[: int(self.n_k_points)]
|
1145
|
+
|
1146
|
+
@lazyproperty
|
1147
|
+
def n_atoms(self):
|
1148
|
+
"""The number of atoms for the material"""
|
1149
|
+
pattern_str = r"TOTAL ATOM NUMBER = (\d+)"
|
1150
|
+
|
1151
|
+
return int(self.parse_scalar(pattern_str))
|
1152
|
+
|
1153
|
+
@lazyproperty
|
1154
|
+
def n_bands(self):
|
1155
|
+
"""The number of Kohn-Sham states for the chunk"""
|
1156
|
+
pattern_str = r"NBANDS = (\d+)"
|
1157
|
+
|
1158
|
+
return int(self.parse_scalar(pattern_str))
|
1159
|
+
|
1160
|
+
@lazyproperty
|
1161
|
+
def n_electrons(self):
|
1162
|
+
"""The number of valence electrons for the chunk"""
|
1163
|
+
pattern_str = r"AUTOSET number of electrons: = (\d+)"
|
1164
|
+
res = self.parse_scalar(pattern_str)
|
1165
|
+
if res:
|
1166
|
+
return int(res)
|
1167
|
+
else:
|
1168
|
+
return None
|
1169
|
+
|
1170
|
+
@lazyproperty
|
1171
|
+
def n_occupied_bands(self):
|
1172
|
+
"""The number of occupied Kohn-Sham states for the chunk"""
|
1173
|
+
pattern_str = r"occupied bands = (\d+)"
|
1174
|
+
|
1175
|
+
return int(self.parse_scalar(pattern_str))
|
1176
|
+
|
1177
|
+
@lazyproperty
|
1178
|
+
def n_spins(self):
|
1179
|
+
"""The number of spin channels for the chunk"""
|
1180
|
+
pattern_str = r"nspin = (\d+)"
|
1181
|
+
|
1182
|
+
return 1 if int(self.parse_scalar(pattern_str)) in [1, 4] else 2
|
1183
|
+
|
1184
|
+
@lazyproperty
|
1185
|
+
def n_k_points(self):
|
1186
|
+
"""The number of spin channels for the chunk"""
|
1187
|
+
nks = (
|
1188
|
+
self.parse_scalar(r"nkstot_ibz = (\d+)")
|
1189
|
+
if self.parse_scalar(r"nkstot_ibz = (\d+)")
|
1190
|
+
else self.parse_scalar(r"nkstot = (\d+)")
|
1191
|
+
)
|
1192
|
+
|
1193
|
+
return int(nks)
|
1194
|
+
|
1195
|
+
@lazyproperty
|
1196
|
+
def k_points(self):
|
1197
|
+
"""All k-points listed in the calculation"""
|
1198
|
+
return self._parse_k_points()[0]
|
1199
|
+
|
1200
|
+
@lazyproperty
|
1201
|
+
def k_point_weights(self):
|
1202
|
+
"""The k-point weights for the calculation"""
|
1203
|
+
return self._parse_k_points()[1]
|
1204
|
+
|
1205
|
+
@lazyproperty
|
1206
|
+
def header_summary(self):
|
1207
|
+
"""Dictionary summarizing the information inside the header"""
|
1208
|
+
return {
|
1209
|
+
"lattice_constant": self.lattice_constant,
|
1210
|
+
"initial_atoms": self.initial_atoms,
|
1211
|
+
"initial_cell": self.initial_cell,
|
1212
|
+
"is_nscf": self.is_nscf,
|
1213
|
+
"is_relaxation": self.is_relaxation,
|
1214
|
+
"is_cell_relaxation": self.is_cell_relaxation,
|
1215
|
+
"is_md": self.is_md,
|
1216
|
+
"n_atoms": self.n_atoms,
|
1217
|
+
"n_bands": self.n_bands,
|
1218
|
+
"n_occupied_bands": self.n_occupied_bands,
|
1219
|
+
"n_spins": self.n_spins,
|
1220
|
+
"n_k_points": self.n_k_points,
|
1221
|
+
"k_points": self.k_points,
|
1222
|
+
"k_point_weights": self.k_point_weights,
|
1223
|
+
"out_dir": self.out_dir,
|
1224
|
+
}
|
1225
|
+
|
1226
|
+
|
1227
|
+
class AbacusOutCalcChunk(AbacusOutChunk):
|
1228
|
+
"""A part of the running_*.log file correponding to a single calculated structure"""
|
1229
|
+
|
1230
|
+
def __init__(self, contents, header, index=-1):
|
1231
|
+
"""Constructor
|
1232
|
+
|
1233
|
+
Parameters
|
1234
|
+
----------
|
1235
|
+
lines: str
|
1236
|
+
The contents of the output file
|
1237
|
+
header: dict
|
1238
|
+
A summary of the relevant information from the running_*.log header
|
1239
|
+
index: slice or int
|
1240
|
+
index of image. `index = 0` is the first calculated image rather initial image
|
1241
|
+
"""
|
1242
|
+
super().__init__(contents)
|
1243
|
+
self._header = header.header_summary
|
1244
|
+
self.index = index
|
1245
|
+
|
1246
|
+
@lazymethod
|
1247
|
+
def _parse_cells(self):
|
1248
|
+
"""Parse all the cells from the output file"""
|
1249
|
+
if self._header["is_relaxation"]:
|
1250
|
+
return [self.initial_cell for i in range(self.ion_steps)]
|
1251
|
+
elif self._header["is_cell_relaxation"]:
|
1252
|
+
cell_pattern = re.compile(
|
1253
|
+
rf"Lattice vectors: \(Cartesian coordinate: in unit of a_0\)\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n"
|
1254
|
+
)
|
1255
|
+
_lattice = np.reshape(
|
1256
|
+
cell_pattern.findall(self.contents), (-1, 3, 3)
|
1257
|
+
).astype(float)
|
1258
|
+
if self.ion_steps and _lattice.shape[0] != self.ion_steps:
|
1259
|
+
lattice = np.zeros((self.ion_steps, 3, 3), dtype=float)
|
1260
|
+
_indices = np.where(self._parse_relaxation_convergency())[0]
|
1261
|
+
for i in range(len(_indices)):
|
1262
|
+
if i == 0:
|
1263
|
+
lattice[: _indices[i] + 1] = self.initial_cell
|
1264
|
+
else:
|
1265
|
+
lattice[_indices[i - 1] + 1 : _indices[i] + 1] = _lattice[i - 1]
|
1266
|
+
return lattice * self._header["lattice_constant"]
|
1267
|
+
else:
|
1268
|
+
return self.initial_cell
|
1269
|
+
|
1270
|
+
@lazyproperty
|
1271
|
+
def ion_steps(self):
|
1272
|
+
"The number of ion steps"
|
1273
|
+
return len(self._parse_ionic_block())
|
1274
|
+
|
1275
|
+
@lazymethod
|
1276
|
+
def _parse_forces(self):
|
1277
|
+
"""Parse all the forces from the output file"""
|
1278
|
+
force_pattern = re.compile(
|
1279
|
+
r"TOTAL\-FORCE\s*\(eV/Angstrom\)\n\n.*\s*atom\s*x\s*y\s*z\n([\s\S]+?)\n\n"
|
1280
|
+
)
|
1281
|
+
forces = force_pattern.findall(self.contents)
|
1282
|
+
if not forces:
|
1283
|
+
force_pattern = re.compile(
|
1284
|
+
r"TOTAL\-FORCE\s*\(eV/Angstrom\)\s*[\-]{2,}\n([\s\S]+?)\n[\-]{2,}"
|
1285
|
+
)
|
1286
|
+
|
1287
|
+
return force_pattern.findall(self.contents)
|
1288
|
+
|
1289
|
+
@lazymethod
|
1290
|
+
def _parse_stress(self):
|
1291
|
+
"""Parse the stress from the output file"""
|
1292
|
+
stress_pattern = re.compile(
|
1293
|
+
rf"(?:TOTAL\-|MD\s*)STRESS\s*\(KBAR\)\n\n.*\n\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n"
|
1294
|
+
)
|
1295
|
+
stresses = stress_pattern.findall(self.contents)
|
1296
|
+
if not stresses:
|
1297
|
+
stress_pattern = re.compile(
|
1298
|
+
r"(?:TOTAL\-|MD\s*)STRESS\s*\(KBAR\)\s*[\-]{2,}\n"
|
1299
|
+
+ rf"\s*({_re_float})\s*({_re_float})\s*({_re_float})\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n\s*({_re_float})\s*({_re_float})\s*({_re_float})\n"
|
1300
|
+
)
|
1301
|
+
|
1302
|
+
return stress_pattern.findall(self.contents)
|
1303
|
+
|
1304
|
+
@lazymethod
|
1305
|
+
def _parse_eigenvalues(self):
|
1306
|
+
"""Parse the eigenvalues and occupations of the system."""
|
1307
|
+
scf_eig_pattern = re.compile(
|
1308
|
+
r"(STATE ENERGY\(eV\) AND OCCUPATIONS\s*NSPIN\s*==\s*\d+[\s\S]+?(?:\n\n\s*EFERMI|\n\n\n))"
|
1309
|
+
)
|
1310
|
+
scf_eig_all = scf_eig_pattern.findall(self.contents)
|
1311
|
+
|
1312
|
+
nscf_eig_pattern = re.compile(
|
1313
|
+
r"(band eigenvalue in this processor \(eV\)\s*:\n[\s\S]+?\n\n\n)"
|
1314
|
+
)
|
1315
|
+
nscf_eig_all = nscf_eig_pattern.findall(self.contents)
|
1316
|
+
|
1317
|
+
return {"scf": scf_eig_all, "nscf": nscf_eig_all}
|
1318
|
+
|
1319
|
+
@lazymethod
|
1320
|
+
def _parse_energy(self):
|
1321
|
+
"""Parse the energy from the output file."""
|
1322
|
+
_out_dir = self._header["out_dir"].strip("/")
|
1323
|
+
|
1324
|
+
energy_pattern = re.compile(
|
1325
|
+
rf"{_out_dir}\/\s*final etot is\s*({_re_float})\s*eV"
|
1326
|
+
)
|
1327
|
+
res = energy_pattern.findall(self.contents)
|
1328
|
+
if res:
|
1329
|
+
return res
|
1330
|
+
else:
|
1331
|
+
energy_pattern = re.compile(rf"\s*final etot is\s*({_re_float})\s*eV")
|
1332
|
+
res = energy_pattern.findall(self.contents)
|
1333
|
+
return res
|
1334
|
+
|
1335
|
+
# energy_pattern = re.compile(
|
1336
|
+
# rf'{_out_dir}\/\s*final etot is\s*({_re_float})\s*eV') if 'RELAXATION' in self.contents or 'RELAX CELL' in self.contents else re.compile(rf'\s*final etot is\s*({_re_float})\s*eV')
|
1337
|
+
|
1338
|
+
# return energy_pattern.findall(self.contents)
|
1339
|
+
|
1340
|
+
@lazymethod
|
1341
|
+
def _parse_efermi(self):
|
1342
|
+
"""Parse the Fermi energy from the output file."""
|
1343
|
+
fermi_pattern = re.compile(rf"EFERMI\s*=\s*({_re_float})\s*eV")
|
1344
|
+
|
1345
|
+
return fermi_pattern.findall(self.contents)
|
1346
|
+
|
1347
|
+
@lazymethod
|
1348
|
+
def _parse_ionic_block(self):
|
1349
|
+
"""Parse the ionic block from the output file"""
|
1350
|
+
step_pattern = re.compile(
|
1351
|
+
rf"(?:[NON]*SELF-|STEP OF|RELAX CELL)([\s\S]+?)charge density convergence is achieved"
|
1352
|
+
)
|
1353
|
+
|
1354
|
+
return step_pattern.findall(self.contents)
|
1355
|
+
|
1356
|
+
@lazymethod
|
1357
|
+
def _parse_relaxation_convergency(self):
|
1358
|
+
"""Parse the convergency of atomic position optimization from the output file"""
|
1359
|
+
if "Ion relaxation" in self.contents:
|
1360
|
+
pattern = re.compile(
|
1361
|
+
r"Ion relaxation is converged!|Ion relaxation is not converged yet"
|
1362
|
+
)
|
1363
|
+
return (
|
1364
|
+
np.array(pattern.findall(self.contents))
|
1365
|
+
== "Ion relaxation is converged!"
|
1366
|
+
)
|
1367
|
+
else:
|
1368
|
+
pattern = re.compile(
|
1369
|
+
r"Relaxation is converged!|Relaxation is not converged yet!"
|
1370
|
+
)
|
1371
|
+
return (
|
1372
|
+
np.array(pattern.findall(self.contents)) == "Relaxation is converged!"
|
1373
|
+
)
|
1374
|
+
|
1375
|
+
@lazymethod
|
1376
|
+
def _parse_cell_relaxation_convergency(self):
|
1377
|
+
"""Parse the convergency of variable cell optimization from the output file"""
|
1378
|
+
pattern = re.compile(
|
1379
|
+
r"Lattice relaxation is converged!|Lattice relaxation is not converged yet"
|
1380
|
+
)
|
1381
|
+
lat_arr = (
|
1382
|
+
np.array(pattern.findall(self.contents))
|
1383
|
+
== "Lattice relaxation is converged!"
|
1384
|
+
)
|
1385
|
+
res = np.zeros((self.ion_steps), dtype=bool)
|
1386
|
+
if lat_arr[-1] == True:
|
1387
|
+
res[-1] = 1
|
1388
|
+
|
1389
|
+
return res.astype(bool)
|
1390
|
+
|
1391
|
+
@lazymethod
|
1392
|
+
def _parse_md(self):
|
1393
|
+
"""Parse the molecular dynamics information from the output file"""
|
1394
|
+
md_pattern = re.compile(
|
1395
|
+
rf"Energy\s*\(Ry\)\s*Potential\s*\(Ry\)\s*Kinetic\s*\(Ry\)\s*Temperature\s*\(K\)\s*(?:Pressure\s*\(kbar\)\s*\n|\n)\s*({_re_float})\s*({_re_float})"
|
1396
|
+
)
|
1397
|
+
result = md_pattern.findall(self.contents)
|
1398
|
+
self.md_e_unit = Rydberg
|
1399
|
+
if result:
|
1400
|
+
return result
|
1401
|
+
else:
|
1402
|
+
md_pattern = re.compile(
|
1403
|
+
rf"Energy\s*Potential\s*Kinetic\s*Temperature\s*(?:Pressure \(KBAR\)\s*\n|\n)\s*({_re_float})\s*({_re_float})"
|
1404
|
+
)
|
1405
|
+
self.md_e_unit = Hartree
|
1406
|
+
return md_pattern.findall(self.contents)
|
1407
|
+
|
1408
|
+
@lazymethod
|
1409
|
+
def get_site(self):
|
1410
|
+
"""Get site from the output file according to index"""
|
1411
|
+
|
1412
|
+
def str_to_sites(val_in):
|
1413
|
+
val = np.array(val_in)
|
1414
|
+
labels = val[:, 0]
|
1415
|
+
pos = val[:, 1:4].astype(float)
|
1416
|
+
if val.shape[1] == 5:
|
1417
|
+
mag = val[:, 4].astype(float)
|
1418
|
+
vel = np.zeros((3,), dtype=float)
|
1419
|
+
elif val.shape[1] == 8:
|
1420
|
+
mag = val[:, 4].astype(float)
|
1421
|
+
vel = val[:, 5:8].astype(float)
|
1422
|
+
return labels, pos, mag, vel
|
1423
|
+
|
1424
|
+
def parse_block(pos_block):
|
1425
|
+
data = list(pos_block)
|
1426
|
+
_remove_empty(data)
|
1427
|
+
site = list(map(list, site_pattern.findall(data[0])))
|
1428
|
+
list(map(_remove_empty, site))
|
1429
|
+
labels, pos, mag, vel = str_to_sites(site)
|
1430
|
+
if self.coordinate_system == "CARTESIAN":
|
1431
|
+
unit = float(unit_pattern.search(self.contents).group(1)) * Bohr
|
1432
|
+
positions = pos * unit
|
1433
|
+
elif self.coordinate_system == "DIRECT":
|
1434
|
+
positions = pos
|
1435
|
+
return labels, positions, mag, vel
|
1436
|
+
|
1437
|
+
site_pattern = re.compile(
|
1438
|
+
rf"tau[cd]_([a-zA-Z]+)\d+\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})|tau[cd]_([a-zA-Z]+)\d+\s+({_re_float})\s+({_re_float})\s+({_re_float})\s+({_re_float})"
|
1439
|
+
)
|
1440
|
+
unit_pattern = re.compile(rf"UNIT = ({_re_float}) Bohr")
|
1441
|
+
|
1442
|
+
sites = self._parse_site()
|
1443
|
+
if self.get_relaxation_convergency():
|
1444
|
+
sites.append(sites[-1])
|
1445
|
+
return parse_block(sites[self.index])
|
1446
|
+
|
1447
|
+
@lazymethod
|
1448
|
+
def get_forces(self):
|
1449
|
+
"""Get forces from the output file according to index"""
|
1450
|
+
|
1451
|
+
def str_to_force(val_in):
|
1452
|
+
data = []
|
1453
|
+
val = [v.strip().split() for v in val_in.split("\n")]
|
1454
|
+
for v in val:
|
1455
|
+
data.append(np.array(v[1:], dtype=float))
|
1456
|
+
return np.array(data)
|
1457
|
+
|
1458
|
+
try:
|
1459
|
+
forces = self._parse_forces()[self.index]
|
1460
|
+
return str_to_force(forces)
|
1461
|
+
except IndexError:
|
1462
|
+
return
|
1463
|
+
|
1464
|
+
@lazymethod
|
1465
|
+
def get_forces_sort(self):
|
1466
|
+
"""Get forces from the output file according to index"""
|
1467
|
+
|
1468
|
+
def str_to_force(val_in):
|
1469
|
+
data = []
|
1470
|
+
val = [v.strip().split() for v in val_in.split("\n")]
|
1471
|
+
for v in val:
|
1472
|
+
data.append(np.array(v[1:], dtype=float))
|
1473
|
+
return np.array(data)
|
1474
|
+
|
1475
|
+
try:
|
1476
|
+
forces = self._parse_forces()[self.index]
|
1477
|
+
if Path("ase_sort.dat").exists():
|
1478
|
+
atoms_sort = np.loadtxt("ase_sort.dat", dtype=int)
|
1479
|
+
return str_to_force(forces)[np.argsort(atoms_sort)]
|
1480
|
+
else:
|
1481
|
+
return str_to_force(forces)
|
1482
|
+
except IndexError:
|
1483
|
+
return
|
1484
|
+
|
1485
|
+
@lazymethod
|
1486
|
+
def get_stress(self):
|
1487
|
+
"""Get the stress from the output file according to index"""
|
1488
|
+
from ase.stress import full_3x3_to_voigt_6_stress
|
1489
|
+
|
1490
|
+
try:
|
1491
|
+
stress = (
|
1492
|
+
-0.1
|
1493
|
+
* GPa
|
1494
|
+
* np.array(self._parse_stress()[self.index])
|
1495
|
+
.reshape((3, 3))
|
1496
|
+
.astype(float)
|
1497
|
+
)
|
1498
|
+
return full_3x3_to_voigt_6_stress(stress)
|
1499
|
+
except IndexError:
|
1500
|
+
return
|
1501
|
+
|
1502
|
+
@lazymethod
|
1503
|
+
def get_eigenvalues(self):
|
1504
|
+
"""Get the eigenvalues and occupations of the system according to index."""
|
1505
|
+
|
1506
|
+
# SCF
|
1507
|
+
def str_to_energy_occupation(val_in):
|
1508
|
+
def extract_data(val):
|
1509
|
+
def func(i):
|
1510
|
+
res = np.array(
|
1511
|
+
list(
|
1512
|
+
map(
|
1513
|
+
lambda x: x.strip().split(),
|
1514
|
+
re.search(
|
1515
|
+
rf"{i+1}/{nks} kpoint \(Cartesian\)\s*=.*\n([\s\S]+?)\n\n",
|
1516
|
+
val,
|
1517
|
+
)
|
1518
|
+
.group(1)
|
1519
|
+
.split("\n"),
|
1520
|
+
)
|
1521
|
+
),
|
1522
|
+
dtype=float,
|
1523
|
+
)
|
1524
|
+
return res[:, 1].astype(float), res[:, 2].astype(float)
|
1525
|
+
|
1526
|
+
return np.asarray(list(map(func, [i for i in range(nks)])))
|
1527
|
+
|
1528
|
+
nspin = int(
|
1529
|
+
re.search(
|
1530
|
+
r"STATE ENERGY\(eV\) AND OCCUPATIONS\s*NSPIN\s*==\s*(\d+)", val_in
|
1531
|
+
).group(1)
|
1532
|
+
)
|
1533
|
+
nks = int(re.search(r"\d+/(\d+) kpoint \(Cartesian\)", val_in).group(1))
|
1534
|
+
eigenvalues = np.full(
|
1535
|
+
(
|
1536
|
+
self._header["n_spins"],
|
1537
|
+
self._header["n_k_points"],
|
1538
|
+
self._header["n_bands"],
|
1539
|
+
),
|
1540
|
+
np.nan,
|
1541
|
+
)
|
1542
|
+
occupations = np.full(
|
1543
|
+
(
|
1544
|
+
self._header["n_spins"],
|
1545
|
+
self._header["n_k_points"],
|
1546
|
+
self._header["n_bands"],
|
1547
|
+
),
|
1548
|
+
np.nan,
|
1549
|
+
)
|
1550
|
+
if nspin in [1, 4]:
|
1551
|
+
energies, occs = (
|
1552
|
+
extract_data(val_in)[:, 0, :],
|
1553
|
+
extract_data(val_in)[:, 1, :],
|
1554
|
+
)
|
1555
|
+
eigenvalues[0] = energies
|
1556
|
+
occupations[0] = occs
|
1557
|
+
elif nspin == 2:
|
1558
|
+
val_up = re.search(r"SPIN UP :([\s\S]+?)\n\nSPIN", val_in).group()
|
1559
|
+
energies, occs = (
|
1560
|
+
extract_data(val_up)[:, 0, :],
|
1561
|
+
extract_data(val_up)[:, 1, :],
|
1562
|
+
)
|
1563
|
+
eigenvalues[0] = energies
|
1564
|
+
occupations[0] = occs
|
1565
|
+
|
1566
|
+
val_dw = re.search(
|
1567
|
+
r"SPIN DOWN :([\s\S]+?)(?:\n\n\s*EFERMI|\n\n\n)", val_in
|
1568
|
+
).group()
|
1569
|
+
energies, occs = (
|
1570
|
+
extract_data(val_dw)[:, 0, :],
|
1571
|
+
extract_data(val_dw)[:, 1, :],
|
1572
|
+
)
|
1573
|
+
eigenvalues[1] = energies
|
1574
|
+
occupations[1] = occs
|
1575
|
+
return eigenvalues, occupations
|
1576
|
+
|
1577
|
+
# NSCF
|
1578
|
+
def str_to_bandstructure(val_in):
|
1579
|
+
def extract_data(val):
|
1580
|
+
def func(i):
|
1581
|
+
res = np.array(
|
1582
|
+
list(
|
1583
|
+
map(
|
1584
|
+
lambda x: x.strip().split(),
|
1585
|
+
re.search(
|
1586
|
+
rf"k\-points{i+1}\(\d+\):.*\n([\s\S]+?)\n\n", val
|
1587
|
+
)
|
1588
|
+
.group(1)
|
1589
|
+
.split("\n"),
|
1590
|
+
)
|
1591
|
+
)
|
1592
|
+
)
|
1593
|
+
return res[:, 2].astype(float), res[:, 3].astype(float)
|
1594
|
+
|
1595
|
+
return np.asarray(list(map(func, [i for i in range(nks)])))
|
1596
|
+
|
1597
|
+
nks = int(re.search(r"k\-points\d+\((\d+)\)", val_in).group(1))
|
1598
|
+
eigenvalues = np.full(
|
1599
|
+
(
|
1600
|
+
self._header["n_spins"],
|
1601
|
+
self._header["n_k_points"],
|
1602
|
+
self._header["n_bands"],
|
1603
|
+
),
|
1604
|
+
np.nan,
|
1605
|
+
)
|
1606
|
+
occupations = np.full(
|
1607
|
+
(
|
1608
|
+
self._header["n_spins"],
|
1609
|
+
self._header["n_k_points"],
|
1610
|
+
self._header["n_bands"],
|
1611
|
+
),
|
1612
|
+
np.nan,
|
1613
|
+
)
|
1614
|
+
if re.search("spin up", val_in) and re.search("spin down", val_in):
|
1615
|
+
val = re.search(r"spin up :\n([\s\S]+?)\n\n\n", val_in).group()
|
1616
|
+
energies, occs = (
|
1617
|
+
extract_data(val)[:, 0, :],
|
1618
|
+
extract_data(val_in)[:, 1, :],
|
1619
|
+
)
|
1620
|
+
eigenvalues[0] = energies[: int(nks / 2)]
|
1621
|
+
eigenvalues[1] = energies[int(nks / 2) :]
|
1622
|
+
occupations[0] = occs[: int(nks / 2)]
|
1623
|
+
occupations[1] = occs[int(nks / 2) :]
|
1624
|
+
else:
|
1625
|
+
energies, occs = (
|
1626
|
+
extract_data(val_in)[:, 0, :],
|
1627
|
+
extract_data(val_in)[:, 1, :],
|
1628
|
+
)
|
1629
|
+
eigenvalues[0] = energies
|
1630
|
+
occupations[0] = occs
|
1631
|
+
return eigenvalues, occupations
|
1632
|
+
|
1633
|
+
try:
|
1634
|
+
return str_to_energy_occupation(
|
1635
|
+
self._parse_eigenvalues()["scf"][self.index]
|
1636
|
+
)
|
1637
|
+
except IndexError:
|
1638
|
+
try:
|
1639
|
+
return str_to_bandstructure(
|
1640
|
+
self._parse_eigenvalues()["nscf"][self.index]
|
1641
|
+
)
|
1642
|
+
except IndexError:
|
1643
|
+
return np.full(
|
1644
|
+
(
|
1645
|
+
self._header["n_spins"],
|
1646
|
+
self._header["n_k_points"],
|
1647
|
+
self._header["n_bands"],
|
1648
|
+
),
|
1649
|
+
np.nan,
|
1650
|
+
), np.full(
|
1651
|
+
(
|
1652
|
+
self._header["n_spins"],
|
1653
|
+
self._header["n_k_points"],
|
1654
|
+
self._header["n_bands"],
|
1655
|
+
),
|
1656
|
+
np.nan,
|
1657
|
+
)
|
1658
|
+
|
1659
|
+
@lazymethod
|
1660
|
+
def get_energy(self):
|
1661
|
+
"""Get the energy from the output file according to index."""
|
1662
|
+
try:
|
1663
|
+
return float(self._parse_energy()[self.index])
|
1664
|
+
except IndexError:
|
1665
|
+
return None
|
1666
|
+
|
1667
|
+
@lazymethod
|
1668
|
+
def get_efermi(self):
|
1669
|
+
"""Get the Fermi energy from the output file according to index."""
|
1670
|
+
try:
|
1671
|
+
return float(self._parse_efermi()[self.index])
|
1672
|
+
except IndexError:
|
1673
|
+
return
|
1674
|
+
|
1675
|
+
@lazymethod
|
1676
|
+
def get_relaxation_convergency(self):
|
1677
|
+
"""Get the convergency of atomic position optimization from the output file"""
|
1678
|
+
return self._parse_relaxation_convergency()[self.index]
|
1679
|
+
|
1680
|
+
@lazymethod
|
1681
|
+
def get_cell_relaxation_convergency(self):
|
1682
|
+
"""Get the convergency of variable cell optimization from the output file"""
|
1683
|
+
return self._parse_cell_relaxation_convergency()[self.index]
|
1684
|
+
|
1685
|
+
@lazymethod
|
1686
|
+
def get_md_energy(self):
|
1687
|
+
"""Get the total energy of each md step"""
|
1688
|
+
|
1689
|
+
try:
|
1690
|
+
return float(self._parse_md()[self.index][0]) * self.md_e_unit
|
1691
|
+
except IndexError:
|
1692
|
+
return
|
1693
|
+
|
1694
|
+
@lazymethod
|
1695
|
+
def get_md_potential(self):
|
1696
|
+
"""Get the potential energy of each md step"""
|
1697
|
+
|
1698
|
+
try:
|
1699
|
+
return float(self._parse_md()[self.index][1]) * self.md_e_unit
|
1700
|
+
except IndexError:
|
1701
|
+
return
|
1702
|
+
|
1703
|
+
@lazymethod
|
1704
|
+
def get_md_steps(self):
|
1705
|
+
"""Get steps of molecular dynamics"""
|
1706
|
+
step_pattern = re.compile(r"STEP OF MOLECULAR DYNAMICS\s*:\s*(\d+)")
|
1707
|
+
|
1708
|
+
return list(map(int, step_pattern.findall(self.contents)))
|
1709
|
+
|
1710
|
+
@lazymethod
|
1711
|
+
def get_dipole(self):
|
1712
|
+
"""Get electrical dipole"""
|
1713
|
+
|
1714
|
+
if self._header["is_md"]:
|
1715
|
+
data = np.zeros((len(self.get_md_steps()), 3), dtype=float)
|
1716
|
+
out_dir = Path(self._header["out_dir"])
|
1717
|
+
data_files = [
|
1718
|
+
out_dir / f"SPIN{i+1}_DIPOLE" for i in range(self._header["n_spins"])
|
1719
|
+
]
|
1720
|
+
for file in data_files:
|
1721
|
+
if file.exists():
|
1722
|
+
data = data + np.loadtxt(file, float)[:, 1:4]
|
1723
|
+
|
1724
|
+
return data[self.index]
|
1725
|
+
else:
|
1726
|
+
return
|
1727
|
+
|
1728
|
+
@lazyproperty
|
1729
|
+
def forces(self):
|
1730
|
+
"""The forces for the chunk"""
|
1731
|
+
return self.get_forces()
|
1732
|
+
|
1733
|
+
@lazyproperty
|
1734
|
+
def forces_sort(self):
|
1735
|
+
"""The forces for the chunk"""
|
1736
|
+
return self.get_forces_sort()
|
1737
|
+
|
1738
|
+
@lazyproperty
|
1739
|
+
def stress(self):
|
1740
|
+
"""The stress for the chunk"""
|
1741
|
+
return self.get_stress()
|
1742
|
+
|
1743
|
+
@lazyproperty
|
1744
|
+
def dipole(self):
|
1745
|
+
"""The dipole for the chunk"""
|
1746
|
+
return self.get_dipole()
|
1747
|
+
|
1748
|
+
@lazyproperty
|
1749
|
+
def energy(self):
|
1750
|
+
"""The energy for the chunk"""
|
1751
|
+
if self._header["is_md"]:
|
1752
|
+
return self.get_md_potential()
|
1753
|
+
else:
|
1754
|
+
return self.get_energy()
|
1755
|
+
|
1756
|
+
@lazyproperty
|
1757
|
+
def free_energy(self):
|
1758
|
+
"""The free energy for the chunk"""
|
1759
|
+
if self._header["is_md"]:
|
1760
|
+
return self.get_md_energy()
|
1761
|
+
else:
|
1762
|
+
return self.get_energy()
|
1763
|
+
|
1764
|
+
@lazyproperty
|
1765
|
+
def eigenvalues(self):
|
1766
|
+
"""The eigenvalues for the chunk"""
|
1767
|
+
return self.get_eigenvalues()[0]
|
1768
|
+
|
1769
|
+
@lazyproperty
|
1770
|
+
def occupations(self):
|
1771
|
+
"""The occupations for the chunk"""
|
1772
|
+
return self.get_eigenvalues()[1]
|
1773
|
+
|
1774
|
+
@lazyproperty
|
1775
|
+
def kpts(self):
|
1776
|
+
"""The SinglePointKPoint objects for the chunk"""
|
1777
|
+
return arrays_to_kpoints(
|
1778
|
+
self.eigenvalues, self.occupations, self._header["k_point_weights"]
|
1779
|
+
)
|
1780
|
+
|
1781
|
+
@lazyproperty
|
1782
|
+
def E_f(self):
|
1783
|
+
"""The Fermi energy for the chunk"""
|
1784
|
+
return self.get_efermi()
|
1785
|
+
|
1786
|
+
@lazyproperty
|
1787
|
+
def _ionic_block(self):
|
1788
|
+
"""The ionic block for the chunk"""
|
1789
|
+
|
1790
|
+
return self._parse_ionic_block()[self.index]
|
1791
|
+
|
1792
|
+
@lazyproperty
|
1793
|
+
def magmom(self):
|
1794
|
+
"""The Fermi energy for the chunk"""
|
1795
|
+
magmom_pattern = re.compile(
|
1796
|
+
rf"total magnetism \(Bohr mag/cell\)\s*=\s*({_re_float})"
|
1797
|
+
)
|
1798
|
+
|
1799
|
+
try:
|
1800
|
+
return float(magmom_pattern.findall(self._ionic_block)[-1])
|
1801
|
+
except IndexError:
|
1802
|
+
return
|
1803
|
+
|
1804
|
+
@lazyproperty
|
1805
|
+
def n_iter(self):
|
1806
|
+
"""The number of SCF iterations needed to converge the SCF cycle for the chunk"""
|
1807
|
+
step_pattern = re.compile(rf"ELEC\s*=\s*[+]?(\d+)")
|
1808
|
+
|
1809
|
+
try:
|
1810
|
+
return int(step_pattern.findall(self._ionic_block)[-1])
|
1811
|
+
except IndexError:
|
1812
|
+
return
|
1813
|
+
|
1814
|
+
@lazyproperty
|
1815
|
+
def converged(self):
|
1816
|
+
"""True if the chunk is a fully converged final structure"""
|
1817
|
+
if self._header["is_cell_relaxation"]:
|
1818
|
+
return self.get_cell_relaxation_convergency()
|
1819
|
+
elif self._header["is_relaxation"]:
|
1820
|
+
return self.get_relaxation_convergency()
|
1821
|
+
elif self._header["is_nscf"]:
|
1822
|
+
return "Total Time" in self.contents
|
1823
|
+
else:
|
1824
|
+
return "charge density convergence is achieved" in self.contents
|
1825
|
+
|
1826
|
+
@lazyproperty
|
1827
|
+
def initial_atoms(self):
|
1828
|
+
"""The initial structure defined in the running_*.log file"""
|
1829
|
+
return self._header["initial_atoms"]
|
1830
|
+
|
1831
|
+
@lazyproperty
|
1832
|
+
def initial_cell(self):
|
1833
|
+
"""The initial lattice vectors defined in the running_*.log file"""
|
1834
|
+
return self._header["initial_cell"]
|
1835
|
+
|
1836
|
+
@lazyproperty
|
1837
|
+
def n_atoms(self):
|
1838
|
+
"""The number of atoms for the material"""
|
1839
|
+
return self._header["n_atoms"]
|
1840
|
+
|
1841
|
+
@lazyproperty
|
1842
|
+
def n_bands(self):
|
1843
|
+
"""The number of Kohn-Sham states for the chunk"""
|
1844
|
+
return self._header["n_bands"]
|
1845
|
+
|
1846
|
+
@lazyproperty
|
1847
|
+
def n_occupied_bands(self):
|
1848
|
+
"""The number of occupied Kohn-Sham states for the chunk"""
|
1849
|
+
return self._header["n_occupied_bands"]
|
1850
|
+
|
1851
|
+
@lazyproperty
|
1852
|
+
def n_spins(self):
|
1853
|
+
"""The number of spin channels for the chunk"""
|
1854
|
+
return self._header["n_spins"]
|
1855
|
+
|
1856
|
+
@lazyproperty
|
1857
|
+
def n_k_points(self):
|
1858
|
+
"""The number of k_points for the chunk"""
|
1859
|
+
return self._header["n_k_points"]
|
1860
|
+
|
1861
|
+
@lazyproperty
|
1862
|
+
def k_points(self):
|
1863
|
+
"""k_points for the chunk"""
|
1864
|
+
return self._header["k_points"]
|
1865
|
+
|
1866
|
+
@lazyproperty
|
1867
|
+
def k_point_weights(self):
|
1868
|
+
"""k_point_weights for the chunk"""
|
1869
|
+
return self._header["k_point_weights"]
|
1870
|
+
|
1871
|
+
@property
|
1872
|
+
def results(self):
|
1873
|
+
"""Convert an AbacusOutChunk to a Results Dictionary"""
|
1874
|
+
results = {
|
1875
|
+
"energy": self.energy,
|
1876
|
+
"free_energy": self.free_energy,
|
1877
|
+
"forces": self.forces_sort,
|
1878
|
+
"stress": self.stress,
|
1879
|
+
"magmom": self.magmom,
|
1880
|
+
"fermi_level": self.E_f,
|
1881
|
+
"n_iter": self.n_iter,
|
1882
|
+
"eigenvalues": self.eigenvalues,
|
1883
|
+
"occupations": self.occupations,
|
1884
|
+
"ibz_kpoints": self.k_points,
|
1885
|
+
"kpoint_weights": self.k_point_weights,
|
1886
|
+
"dipole": self.dipole,
|
1887
|
+
}
|
1888
|
+
|
1889
|
+
return {key: value for key, value in results.items() if value is not None}
|
1890
|
+
|
1891
|
+
@lazyproperty
|
1892
|
+
def atoms(self):
|
1893
|
+
"""Convert AbacusOutChunk to Atoms object and add all non-standard outputs to atoms.info"""
|
1894
|
+
"""Create an atoms object for the subsequent structures
|
1895
|
+
calculated in the output file"""
|
1896
|
+
atoms = None
|
1897
|
+
if self._header["is_md"]:
|
1898
|
+
_stru_dir = Path(self._header["out_dir"]) / "STRU"
|
1899
|
+
md_stru_dir = (
|
1900
|
+
_stru_dir if _stru_dir.exists() else Path(self._header["out_dir"])
|
1901
|
+
)
|
1902
|
+
atoms = read_abacus(
|
1903
|
+
open(md_stru_dir / f"STRU_MD_{self.get_md_steps()[self.index]}", "r")
|
1904
|
+
)
|
1905
|
+
|
1906
|
+
# self._header['is_relaxation'] or self._header['is_cell_relaxation']:
|
1907
|
+
elif self.ion_steps > 1:
|
1908
|
+
labels, positions, mag, vel = self.get_site()
|
1909
|
+
if self.coordinate_system == "CARTESIAN":
|
1910
|
+
atoms = Atoms(
|
1911
|
+
symbols=labels,
|
1912
|
+
positions=positions,
|
1913
|
+
cell=self._parse_cells()[self.index],
|
1914
|
+
pbc=True,
|
1915
|
+
velocities=vel * UNIT_V,
|
1916
|
+
)
|
1917
|
+
elif self.coordinate_system == "DIRECT":
|
1918
|
+
atoms = Atoms(
|
1919
|
+
symbols=labels,
|
1920
|
+
scaled_positions=positions,
|
1921
|
+
cell=self._parse_cells()[self.index],
|
1922
|
+
pbc=True,
|
1923
|
+
velocities=vel * UNIT_V,
|
1924
|
+
)
|
1925
|
+
|
1926
|
+
else:
|
1927
|
+
atoms = self.initial_atoms.copy()
|
1928
|
+
|
1929
|
+
calc = SinglePointDFTCalculator(
|
1930
|
+
atoms,
|
1931
|
+
energy=self.energy,
|
1932
|
+
free_energy=self.free_energy,
|
1933
|
+
efermi=self.E_f,
|
1934
|
+
forces=self.forces,
|
1935
|
+
stress=self.stress,
|
1936
|
+
magmom=self.magmom,
|
1937
|
+
dipole=self.dipole,
|
1938
|
+
ibzkpts=self.k_points,
|
1939
|
+
kpts=self.kpts,
|
1940
|
+
)
|
1941
|
+
|
1942
|
+
calc.name = "Abacus"
|
1943
|
+
atoms.calc = calc
|
1944
|
+
|
1945
|
+
return atoms
|
1946
|
+
|
1947
|
+
|
1948
|
+
def _slice2indices(s, n=None):
|
1949
|
+
"""Convert a slice object into indices"""
|
1950
|
+
if isinstance(s, slice):
|
1951
|
+
return range(*s.indices(n))
|
1952
|
+
elif isinstance(s, int):
|
1953
|
+
return [s]
|
1954
|
+
elif isinstance(s, list):
|
1955
|
+
return s
|
1956
|
+
else:
|
1957
|
+
raise ValueError(
|
1958
|
+
"Indices must be scalar integer, list of integers, or slice object"
|
1959
|
+
)
|
1960
|
+
|
1961
|
+
|
1962
|
+
@reader
|
1963
|
+
def _get_abacus_chunks(fd, index=-1, non_convergence_ok=False):
|
1964
|
+
"""Import ABACUS output files with all data available, i.e.
|
1965
|
+
relaxations, MD information, force information ..."""
|
1966
|
+
contents = fd.read()
|
1967
|
+
header_pattern = re.compile(
|
1968
|
+
r"READING GENERAL INFORMATION([\s\S]+?([NON]*SELF-CONSISTENT|STEP OF MOLECULAR DYNAMICS|STEP OF [ION]*\s*RELAXATION|RELAX CELL))"
|
1969
|
+
)
|
1970
|
+
header_chunk = AbacusOutHeaderChunk(header_pattern.search(contents).group(1))
|
1971
|
+
|
1972
|
+
time_pattern = re.compile(
|
1973
|
+
r"Total\s*Time\s*:\s*[0-9]+\s*h\s*[0-9]+\s*mins\s*[0-9]+\s*secs"
|
1974
|
+
)
|
1975
|
+
time = time_pattern.findall(contents)[-1]
|
1976
|
+
|
1977
|
+
calc_pattern = re.compile(
|
1978
|
+
rf"(([NON]*SELF-CONSISTENT|STEP OF MOLECULAR DYNAMICS|STEP OF [ION]*\s*RELAXATION|RELAX CELL)[\s\S]+?(?={time}))"
|
1979
|
+
)
|
1980
|
+
calc_contents = calc_pattern.search(contents).group(1)
|
1981
|
+
final_chunk = AbacusOutCalcChunk(calc_contents, header_chunk, -1)
|
1982
|
+
|
1983
|
+
if not non_convergence_ok and not final_chunk.converged:
|
1984
|
+
raise ValueError("The calculation did not complete successfully")
|
1985
|
+
|
1986
|
+
_steps = final_chunk.ion_steps if final_chunk.ion_steps else 1
|
1987
|
+
indices = _slice2indices(index, _steps)
|
1988
|
+
|
1989
|
+
return [AbacusOutCalcChunk(calc_contents, header_chunk, i) for i in indices]
|
1990
|
+
|
1991
|
+
|
1992
|
+
@reader
|
1993
|
+
def read_abacus_out(fd, index=-1, non_convergence_ok=False):
|
1994
|
+
"""Import ABACUS output files with all data available, i.e.
|
1995
|
+
relaxations, MD information, force information ..."""
|
1996
|
+
chunks = _get_abacus_chunks(fd, index, non_convergence_ok)
|
1997
|
+
|
1998
|
+
return [chunk.atoms for chunk in chunks]
|
1999
|
+
|
2000
|
+
|
2001
|
+
@reader
|
2002
|
+
def read_abacus_results(fd, index=-1, non_convergence_ok=False):
|
2003
|
+
"""Import ABACUS output files and summarize all relevant information
|
2004
|
+
into a dictionary"""
|
2005
|
+
chunks = _get_abacus_chunks(fd, index, non_convergence_ok)
|
2006
|
+
|
2007
|
+
return [chunk.results for chunk in chunks]
|
2008
|
+
|
2009
|
+
|
2010
|
+
def _remove_empty(a: list):
|
2011
|
+
"""Remove '' and [] in `a`"""
|
2012
|
+
while "" in a:
|
2013
|
+
a.remove("")
|
2014
|
+
while [] in a:
|
2015
|
+
a.remove([])
|
2016
|
+
while None in a:
|
2017
|
+
a.remove(None)
|
2018
|
+
|
2019
|
+
|
2020
|
+
# READ ABACUS OUT -END-
|