TB2J 0.7.7.1__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.
@@ -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-