Skip to content

Commit 6a94875

Browse files
authored
Feature: tools for selective dynamics with ABACUS + Phonopy (#6755)
* Feature: tools for selective dynamics with ABACUS + Phonopy * delete useless example * update selective_dynamics.py
1 parent a38ba1b commit 6a94875

File tree

3 files changed

+370
-0
lines changed

3 files changed

+370
-0
lines changed

tools/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
SIAB: codes to generate numerical atomic orbitals.
2+
23
molden: generate molden style file for Multiwfn analysis.
34

45
plot-tools: band structure, dos and pdos, dipole and adsorption.
6+
57
rt-tddft-tools: tools for real-time tddft.
8+
69
average_pot: python script used to calculate and plot the average electrostatic potential.
10+
711
stm: generate figures related to Scanning tunneling microscope technique.
812

913
generate_orbital.sh: script used to generate numerical atomic orbitals (NAO).
14+
1015
opt_abfs_bash: related to generating NAO basis set.
16+
1117
opt_lcao_bash: related to generating NAO basis set.
18+
1219
opt_orb_pytorch: related to generating NAO basis set.
20+
1321
opt_orb_pytorch_dpsi: related to generating NAO basis set.
22+
1423
qo: generate quasiatomic orbital (qo).
24+
25+
selective_dynamics: used to do selective dynamics with ABACUS + Phonopy.

tools/selective_dynamics/README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Selective Dynamics for ABACUS+Phonopy
2+
3+
## Requirements
4+
5+
- [ase-abacus](https://gitlab.com/1041176461/ase-abacus)
6+
- [Phonopy](https://github.com/phonopy/phonopy)
7+
8+
## Usage
9+
10+
### Setting
11+
12+
There is a setting file named `config.yaml`:
13+
```yaml
14+
origin_structure: 'STRU' # the original structure
15+
selected_indices: [18, 19] # the atom index that you concerned about, start from 0
16+
tasks_per_batch: 12 # how much jobs per batch
17+
wait_time: 600 # s, sleep time between batches
18+
19+
setting_conf: |
20+
SYMMETRY = .FALSE.
21+
DIM = 1 1 1
22+
DISPLACEMENT_DISTANCE = 0.03
23+
24+
mesh.conf: |
25+
DIM = 1 1 1
26+
MESH = 31 31 31
27+
TMAX = 2000
28+
TSTEP = 2
29+
30+
input: |
31+
INPUT_PARAMETERS
32+
#Parameters (1.General)
33+
suffix phonon
34+
calculation scf
35+
symmetry 1
36+
nspin 1
37+
pseudo_dir /fs2/home/chenkg/2_liuyu/3_abacus/PP_ORB/pseudo
38+
orbital_dir /fs2/home/chenkg/2_liuyu/3_abacus/PP_ORB/efficiency
39+
kpoint_file ../KPT
40+
41+
#Parameters (2.Iteration)
42+
ecutwfc 100
43+
scf_thr 1e-8
44+
scf_nmax 100
45+
46+
#Parameters (3.Basis)
47+
basis_type lcao
48+
ks_solver genelpa
49+
gamma_only 0
50+
51+
#Parameters (4.Smearing)
52+
smearing_method gaussian
53+
smearing_sigma 0.001
54+
55+
#Parameters (5.Mixing)
56+
mixing_type broyden
57+
mixing_beta 0.7
58+
59+
cal_force 1
60+
cal_stress 1
61+
62+
kpt: |
63+
K_POINTS
64+
0
65+
Gamma
66+
5 5 1 0 0 0
67+
68+
job_script: |
69+
#!/bin/bash
70+
#SBATCH -p cp6
71+
#SBATCH -N 1
72+
#SBATCH -J abacus
73+
#SBATCH -n 28
74+
75+
source /fs2/home/chenkg/2_liuyu/3_abacus/abacus_env.sh
76+
export OMP_NUM_THREADS=28
77+
78+
mpirun -n 1 abacus
79+
```
80+
81+
- origin_structure: The `STRU` filename, which contains both the fixed atoms and the free atoms.
82+
- selected_indices: The indexs of the free atoms. Note that the index starts from 0.
83+
- tasks_per_batch: How much jobs submitted per batch.
84+
- wait_time: Sleep time between batches, the unit is second.
85+
- setting_conf: The `setting.conf` file for Phonopy.
86+
- mesh.conf: The `mesh.conf` file for Phonopy.
87+
- input: The `INPUT` file for ABACUS.
88+
- kpt: The `KPT` file for ABACUS.
89+
- job_script: The script used to submit jobs.
90+
91+
### Submit jobs
92+
93+
Use the following command
94+
```bash
95+
python3 path_to_selective_dynamics.py --submit
96+
```
97+
to generate displaced structures and submit jobs.
98+
99+
### Postprocess
100+
101+
Use the following command
102+
```bash
103+
python3 path_to_selective_dynamics.py --post
104+
```
105+
to generate `FORCE_SETS` and results of phonon calculations.
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import re
4+
import sys
5+
import glob
6+
import time
7+
import yaml
8+
import shutil
9+
import subprocess
10+
from ase import Atoms
11+
from ase.io import read, write
12+
13+
14+
def split_stru_with_index(input_file, selected_indices, output1, output2):
15+
16+
atoms = read(input_file, verbose=True)
17+
pp = atoms.info["pp"]
18+
basis = atoms.info["basis"]
19+
20+
for i, atom in enumerate(atoms):
21+
atom.tag = i
22+
23+
atoms1 = atoms[selected_indices]
24+
25+
26+
remaining_indices = [i for i in range(len(atoms)) if i not in selected_indices]
27+
atoms2 = atoms[remaining_indices]
28+
29+
30+
write(output1, atoms1, format='abacus', pp=pp, basis=basis)
31+
write(output2, atoms2, format='abacus', pp=pp, basis=basis)
32+
33+
print(f" Success! \n A.stru: {len(atoms1)} atoms\n B.stru: {len(atoms2)} atoms\n")
34+
return selected_indices, remaining_indices, pp, basis
35+
36+
37+
def merge_stru_by_index(file1, file2, indices1, indices2, pp, basis, output_file):
38+
39+
atoms1 = read(file1)
40+
atoms2 = read(file2)
41+
42+
43+
total_atoms = len(atoms1) + len(atoms2)
44+
merged_atoms = [None] * total_atoms
45+
46+
for i, idx in enumerate(indices1):
47+
merged_atoms[idx] = atoms1[i]
48+
49+
for i, idx in enumerate(indices2):
50+
merged_atoms[idx] = atoms2[i]
51+
52+
53+
cell = atoms2.get_cell()
54+
pbc = atoms2.get_pbc()
55+
56+
positions = [atom.position for atom in merged_atoms]
57+
symbols = [atom.symbol for atom in merged_atoms]
58+
59+
merged = Atoms(symbols=symbols, positions=positions, cell=cell, pbc=pbc)
60+
61+
write(output_file, merged, format='abacus', pp=pp, basis=basis)
62+
print(f"success! total {len(merged)} atoms")
63+
64+
65+
def parse_forces_from_file(indices, file_path="running_scf.log"):
66+
67+
with open(file_path, 'r', encoding='utf-8') as f:
68+
contents = f.read()
69+
70+
lines = contents.split('\n')
71+
72+
start_index = -1
73+
for i, line in enumerate(lines):
74+
if "#TOTAL-FORCE (eV/Angstrom)#" in line:
75+
start_index = i+4
76+
break
77+
78+
results = []
79+
for i in indices:
80+
results.append(lines[start_index+i])
81+
82+
result_text = '\n'.join(results)
83+
return result_text
84+
85+
86+
def load_config(config_file="config.yaml"):
87+
88+
with open(config_file, 'r', encoding='utf-8') as f:
89+
return yaml.safe_load(f)
90+
91+
def submit_jobs():
92+
93+
# loading settings
94+
config = load_config()
95+
96+
# split structure into A and B
97+
origin_structure = config['origin_structure']
98+
selected_indices = config['selected_indices']
99+
indices1, indices2, pp, basis = split_stru_with_index(origin_structure, selected_indices,
100+
'A.stru', 'B.stru')
101+
102+
103+
# setting.conf for phonopy
104+
setting_conf_content = config['setting.conf']
105+
with open("setting.conf", "w", encoding="utf-8") as f:
106+
f.write(setting_conf_content)
107+
108+
109+
# generate perturbed structures
110+
result = subprocess.run("phonopy setting.conf --abacus -d -c A.stru",
111+
shell=True, capture_output=True, text=True)
112+
print(result.stdout)
113+
114+
115+
all_files = sorted(glob.glob("STRU-*"))
116+
TOTAL_TASKS = len(all_files)
117+
TASKS_PER_BATCH = config['tasks_per_batch']
118+
WAIT_TIME = config['wait_time']
119+
TOTAL_BATCHES = (TOTAL_TASKS + TASKS_PER_BATCH - 1) // TASKS_PER_BATCH
120+
print(f' TOTAL_TASKS = {TOTAL_TASKS}\n TASKS_PER_BATCH = {TASKS_PER_BATCH}\n TOTAL_BATCHES = {TOTAL_BATCHES}\n')
121+
122+
123+
# job script
124+
job_script_content = config['job_script']
125+
with open("job.sh", "w", encoding="utf-8") as f:
126+
f.write(job_script_content)
127+
os.chmod("job.sh", 0o755)
128+
129+
# KPT
130+
kpt_content = config['kpt']
131+
with open("KPT", "w", encoding="utf-8") as f:
132+
f.write(kpt_content)
133+
134+
input_content = config['input']
135+
136+
num_digits = len(str(TOTAL_TASKS))
137+
for batch in range(1, TOTAL_BATCHES + 1):
138+
print("-" * 30)
139+
print(f"Current batch: {batch}/{TOTAL_BATCHES}")
140+
print("-" * 30)
141+
142+
for task in range(1, TASKS_PER_BATCH + 1):
143+
index = task - 1 + (batch - 1) * TASKS_PER_BATCH
144+
145+
if index >= TOTAL_TASKS:
146+
break
147+
148+
149+
new_index = f"{index+1:0{num_digits+1}d}"
150+
151+
dir_name = f"job_{new_index}"
152+
os.makedirs(dir_name, exist_ok=True)
153+
os.chdir(dir_name)
154+
155+
156+
# STRU
157+
perturbed_stru = f"../{all_files[index]}"
158+
merge_stru_by_index(perturbed_stru, '../B.stru', indices1, indices2,
159+
pp, basis, 'STRU')
160+
161+
# INPUT
162+
with open("INPUT", "w") as f:
163+
f.write(input_content)
164+
165+
166+
# submit job
167+
try:
168+
result = subprocess.run(["sbatch", "../job.sh"],
169+
check=True, capture_output=True, text=True)
170+
print(f"submit job: {new_index}")
171+
except subprocess.CalledProcessError as e:
172+
print(f"Failed - {e}")
173+
174+
os.chdir("..")
175+
176+
# sleep if not the last batch
177+
if batch < TOTAL_BATCHES:
178+
print(f"wait {WAIT_TIME} seconds...")
179+
time.sleep(WAIT_TIME)
180+
181+
print("-" * 30)
182+
183+
print("All job submitted!")
184+
print("-" * 30)
185+
186+
def postprocess():
187+
188+
# loading settings
189+
config = load_config()
190+
191+
selected_indices = config['selected_indices']
192+
natom = len(selected_indices)
193+
194+
all_files = sorted(glob.glob("STRU-*"))
195+
TOTAL_TASKS = len(all_files)
196+
num_digits = len(str(TOTAL_TASKS))
197+
198+
for task in range(1, TOTAL_TASKS + 1):
199+
200+
new_index = f"{task:0{num_digits+1}d}"
201+
dir_name = f"job_{new_index}"
202+
os.chdir(dir_name)
203+
204+
out_folders = glob.glob("OUT.*")
205+
out_folder = out_folders[0]
206+
os.chdir(out_folder)
207+
208+
atom_forces = parse_forces_from_file(selected_indices)
209+
210+
# phonon.log
211+
log = f"""TOTAL ATOM NUMBER = {natom}
212+
213+
#TOTAL-FORCE (eV/Angstrom)#
214+
-------------------------------------------------------------------------
215+
Atoms Force_x Force_y Force_z
216+
-------------------------------------------------------------------------\n"""
217+
218+
with open("phonon.log", "w") as f:
219+
f.write(log)
220+
f.write(str(atom_forces))
221+
f.write("\n -------------------------------------------------------------------------")
222+
223+
224+
os.chdir("../..")
225+
226+
result = subprocess.run("phonopy -f job_*/OUT*/phonon.log",
227+
shell=True, check=True, capture_output=True, text=True)
228+
229+
230+
mesh_content = config['mesh.conf']
231+
with open("mesh.conf", "w") as f:
232+
f.write(mesh_content)
233+
234+
result = subprocess.run("phonopy -t mesh.conf",
235+
shell=True, check=True, capture_output=True, text=True)
236+
237+
238+
239+
240+
def main():
241+
if len(sys.argv) != 2:
242+
print("usage: python3 selective_dynamics.py --submit")
243+
print("usage: python3 selective_dynamics.py --post")
244+
sys.exit(1)
245+
246+
if sys.argv[1] == "--submit":
247+
submit_jobs()
248+
elif sys.argv[1] == "--post":
249+
postprocess()
250+
else:
251+
print(f"No such parameter: {sys.argv[1]}")
252+
253+
if __name__ == "__main__":
254+
main()

0 commit comments

Comments
 (0)