ion-CSP 2.0.2__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.
- ion_CSP/__init__.py +8 -0
- ion_CSP/app.py +201 -0
- ion_CSP/convert_SMILES.py +291 -0
- ion_CSP/empirical_estimate.py +505 -0
- ion_CSP/gen_opt.py +378 -0
- ion_CSP/identify_molecules.py +88 -0
- ion_CSP/log_and_time.py +234 -0
- ion_CSP/mlp_opt.py +154 -0
- ion_CSP/read_mlp_density.py +144 -0
- ion_CSP/steps_opt_monitor.sh +110 -0
- ion_CSP/upload_download.py +487 -0
- ion_CSP/vasp_processing.py +299 -0
- ion_csp-2.0.2.dist-info/METADATA +83 -0
- ion_csp-2.0.2.dist-info/RECORD +18 -0
- ion_csp-2.0.2.dist-info/WHEEL +5 -0
- ion_csp-2.0.2.dist-info/entry_points.txt +2 -0
- ion_csp-2.0.2.dist-info/licenses/LICENSE +21 -0
- ion_csp-2.0.2.dist-info/top_level.txt +1 -0
ion_CSP/log_and_time.py
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import time
|
4
|
+
import yaml
|
5
|
+
import signal
|
6
|
+
import logging
|
7
|
+
import argparse
|
8
|
+
from dpdispatcher.dlog import dlog
|
9
|
+
|
10
|
+
|
11
|
+
def log_and_time(func):
|
12
|
+
"""Decorator for recording log information and script runtime"""
|
13
|
+
|
14
|
+
def wrapper(script_name, work_dir, *args, **kwargs):
|
15
|
+
# 获取脚本所在目录, 在该目录下生成日志
|
16
|
+
log_file_path = os.path.join(work_dir, f"{script_name}_output.log")
|
17
|
+
# 配置日志记录
|
18
|
+
logging.basicConfig(
|
19
|
+
filename=log_file_path, # 日志文件名
|
20
|
+
level=logging.INFO, # 指定日志级别
|
21
|
+
format="%(asctime)s - %(levelname)s - %(message)s", # 日志格式
|
22
|
+
)
|
23
|
+
# 获取程序开始执行时的CPU时间和Wall Clock时间
|
24
|
+
start_cpu, start_clock = time.process_time(), time.perf_counter()
|
25
|
+
# 记录程序开始信息
|
26
|
+
logging.info(f"Start running: {script_name}")
|
27
|
+
# 调用实际的函数, 如果出现错误, 报错的同时也将错误信息记录到日志中
|
28
|
+
result = None
|
29
|
+
try:
|
30
|
+
result = func(work_dir, *args, **kwargs)
|
31
|
+
except Exception as e:
|
32
|
+
logging.error(f"Error occurred: {e}", exc_info=True)
|
33
|
+
raise
|
34
|
+
print(
|
35
|
+
f"The script {script_name} has run successfully, and the output content has been recorded in the output.log file in the same directory."
|
36
|
+
)
|
37
|
+
# 获取程序结束时的CPU时间和Wall Clock时间
|
38
|
+
end_cpu, end_clock = time.process_time(), time.perf_counter()
|
39
|
+
# 计算CPU时间和Wall Clock时间的差值
|
40
|
+
cpu_time, wall_time = end_cpu - start_cpu, end_clock - start_clock
|
41
|
+
# 记录程序结束信息
|
42
|
+
logging.info(
|
43
|
+
f"End running: {script_name}\nWall time: {wall_time:.4f} sec, CPU time: {cpu_time:.4f} sec\n"
|
44
|
+
)
|
45
|
+
return result
|
46
|
+
|
47
|
+
return wrapper
|
48
|
+
|
49
|
+
|
50
|
+
def merge_config(default_config, user_config, key):
|
51
|
+
return {**default_config[key], **user_config.get(key, {})}
|
52
|
+
|
53
|
+
|
54
|
+
class StatusLogger:
|
55
|
+
_instance = None
|
56
|
+
|
57
|
+
def __new__(cls, *args, **kwargs):
|
58
|
+
if not cls._instance:
|
59
|
+
cls._instance = super(StatusLogger, cls).__new__(cls)
|
60
|
+
cls._instance.__init__(*args, **kwargs)
|
61
|
+
return cls._instance
|
62
|
+
|
63
|
+
def __init__(self, work_dir, task_name):
|
64
|
+
"""Initialize workflow status logger and generate the .log and .yaml file to record the status"""
|
65
|
+
# 使用单例模式,避免重复的日志记录,缺点是再重新给定task_name之后会覆盖原来的实例,只能顺序调用
|
66
|
+
self.task_name = task_name
|
67
|
+
log_file = os.path.join(work_dir, "workflow_status.log")
|
68
|
+
yaml_file = os.path.join(work_dir, "workflow_status.yaml")
|
69
|
+
self.yaml_file = yaml_file
|
70
|
+
self._init_yaml()
|
71
|
+
if hasattr(self, "initialized"):
|
72
|
+
return
|
73
|
+
# 创建 logger 对象
|
74
|
+
self.logger = logging.getLogger("WorkflowLogger")
|
75
|
+
self.logger.setLevel(logging.INFO)
|
76
|
+
# 创建文件处理器
|
77
|
+
file_handler = logging.FileHandler(log_file)
|
78
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
79
|
+
file_handler.setFormatter(formatter)
|
80
|
+
# 添加处理器到 logger
|
81
|
+
self.logger.addHandler(file_handler)
|
82
|
+
# 控制日志信息的传播,不将logger的日志信息传播到全局
|
83
|
+
self.logger.propagate = False
|
84
|
+
# 注册信号处理器
|
85
|
+
signal.signal(signal.SIGINT, self._signal_handler) # 捕捉 Ctrl + C
|
86
|
+
signal.signal(signal.SIGTERM, self._signal_handler) # 捕捉 kill 命令
|
87
|
+
# 记录当前进程的 PID
|
88
|
+
self.logger.info(f"Process started with PID: {os.getpid()}")
|
89
|
+
# 初始化工作状态
|
90
|
+
self.initialized = True
|
91
|
+
self._init_yaml()
|
92
|
+
|
93
|
+
def set_running(self):
|
94
|
+
self.current_status = "RUNNING"
|
95
|
+
self.logger.info(f"{self.task_name} Status: {self.current_status}")
|
96
|
+
self.run_count += 1
|
97
|
+
self._update_yaml()
|
98
|
+
|
99
|
+
def set_success(self):
|
100
|
+
self.current_status = "SUCCESS"
|
101
|
+
self.logger.info(f"{self.task_name} Status: {self.current_status}\n")
|
102
|
+
self._update_yaml()
|
103
|
+
|
104
|
+
def is_successful(self):
|
105
|
+
"""Check if the current task status is SUCCESS"""
|
106
|
+
return self.current_status == "SUCCESS"
|
107
|
+
|
108
|
+
def set_failure(self):
|
109
|
+
self.current_status = "FAILURE"
|
110
|
+
self.logger.error(f"{self.task_name} Status: {self.current_status}\n")
|
111
|
+
self._update_yaml()
|
112
|
+
|
113
|
+
def _signal_handler(self, signum, _):
|
114
|
+
"""Handle termination signals and log the event"""
|
115
|
+
if signum == 2:
|
116
|
+
self.logger.warning(
|
117
|
+
f"Process {os.getpid()} has been interrupted by 'Ctrl + C'\n"
|
118
|
+
)
|
119
|
+
elif signum == 15:
|
120
|
+
self.logger.warning(
|
121
|
+
f"Process {os.getpid()} has been killed by 'kill <pid>' order\n"
|
122
|
+
)
|
123
|
+
else:
|
124
|
+
self.logger.warning(
|
125
|
+
f"Process {os.getpid()} received signal {signum}. Exiting ...\n"
|
126
|
+
)
|
127
|
+
self._set_killed()
|
128
|
+
sys.exit(0)
|
129
|
+
|
130
|
+
def _set_killed(self):
|
131
|
+
self.current_status = "KILLED"
|
132
|
+
self.logger.warning(f"{self.task_name} Status: {self.current_status}\n")
|
133
|
+
self._update_yaml()
|
134
|
+
|
135
|
+
def _init_yaml(self):
|
136
|
+
"""Initialize the workflow_status.yaml file"""
|
137
|
+
# 初始化状态信息
|
138
|
+
status_info = {}
|
139
|
+
# 读取现有的 .yaml 文件
|
140
|
+
if os.path.exists(self.yaml_file):
|
141
|
+
with open(self.yaml_file, "r") as yaml_file:
|
142
|
+
status_info = yaml.safe_load(yaml_file) or {}
|
143
|
+
# 更新或添加当前任务的信息
|
144
|
+
if self.task_name not in status_info:
|
145
|
+
self.run_count = 0
|
146
|
+
self.current_status = "INITIAL"
|
147
|
+
status_info[self.task_name] = {
|
148
|
+
"run_count": self.run_count,
|
149
|
+
"current_status": self.current_status,
|
150
|
+
}
|
151
|
+
else:
|
152
|
+
self.run_count = status_info[self.task_name]["run_count"]
|
153
|
+
self.current_status = status_info[self.task_name]["current_status"]
|
154
|
+
# 写回更新后的内容
|
155
|
+
self._write_yaml(status_info=status_info)
|
156
|
+
|
157
|
+
def _update_yaml(self):
|
158
|
+
"""Update the workflow_status.yaml file"""
|
159
|
+
with open(self.yaml_file, "r") as yaml_file:
|
160
|
+
status_info = yaml.safe_load(yaml_file)
|
161
|
+
status_info[self.task_name]["run_count"] = self.run_count
|
162
|
+
status_info[self.task_name]["current_status"] = self.current_status
|
163
|
+
# 写回更新后的内容
|
164
|
+
self._write_yaml(status_info=status_info)
|
165
|
+
|
166
|
+
def _write_yaml(self, status_info):
|
167
|
+
"""Write the status_info into the workflow_status.yaml file"""
|
168
|
+
with open(self.yaml_file, "w") as yaml_file:
|
169
|
+
yaml.dump(status_info, yaml_file)
|
170
|
+
|
171
|
+
|
172
|
+
def redirect_dpdisp_logging(custom_log_path):
|
173
|
+
# 移除所有文件处理器
|
174
|
+
for handler in list(dlog.handlers):
|
175
|
+
if isinstance(handler, logging.FileHandler):
|
176
|
+
dlog.removeHandler(handler)
|
177
|
+
# 创建新文件处理器并继承原始格式
|
178
|
+
new_handler = logging.FileHandler(custom_log_path)
|
179
|
+
# 复制原始处理器的格式
|
180
|
+
for h in dlog.handlers:
|
181
|
+
if isinstance(h, logging.StreamHandler):
|
182
|
+
new_handler.setFormatter(h.formatter)
|
183
|
+
break
|
184
|
+
# 添加新处理器
|
185
|
+
dlog.addHandler(new_handler)
|
186
|
+
dlog.info(f"LOG INIT:dpdispatcher log direct to {custom_log_path}")
|
187
|
+
|
188
|
+
|
189
|
+
def get_work_dir_and_config():
|
190
|
+
"""获取工作目录并加载配置文件
|
191
|
+
Returns:
|
192
|
+
tuple: (工作目录路径, 合并后的配置字典)
|
193
|
+
Raises:
|
194
|
+
SystemExit: 当输入无效时退出程序
|
195
|
+
"""
|
196
|
+
parser = argparse.ArgumentParser(
|
197
|
+
description="The full workflow of ionic crystal design for a certain ion combination, including generation, mlp optimization, screening, vasp optimization and analysis."
|
198
|
+
)
|
199
|
+
parser.add_argument(
|
200
|
+
"work_dir",
|
201
|
+
type=str,
|
202
|
+
nargs="?", # 使参数变为可选
|
203
|
+
default=None,
|
204
|
+
help="The working directory to run. If not specified, interactive input will be used",
|
205
|
+
)
|
206
|
+
args = parser.parse_args()
|
207
|
+
|
208
|
+
# 交互式输入逻辑
|
209
|
+
if args.work_dir is None:
|
210
|
+
while True:
|
211
|
+
work_dir = input(
|
212
|
+
"Please enter the working directory: "
|
213
|
+
).strip()
|
214
|
+
if os.path.exists(work_dir) and os.path.isdir(work_dir):
|
215
|
+
args.work_dir = work_dir
|
216
|
+
break
|
217
|
+
print(f"Error: Directory '{work_dir}' does not exist. Please try again.")
|
218
|
+
|
219
|
+
# 配置文件读取逻辑
|
220
|
+
config_path = os.path.join(args.work_dir, "config.yaml")
|
221
|
+
try:
|
222
|
+
with open(config_path, "r") as file:
|
223
|
+
user_config = yaml.safe_load(file)
|
224
|
+
except FileNotFoundError:
|
225
|
+
print(f"Error: config.yaml not found in {args.work_dir}")
|
226
|
+
sys.exit(1)
|
227
|
+
except yaml.YAMLError as e:
|
228
|
+
print("Error parsing YAML file:")
|
229
|
+
print(f" Line {e.problem_mark.line + 1}, Column {e.problem_mark.column + 1}")
|
230
|
+
print(f" Details: {str(e)}")
|
231
|
+
sys.exit(1)
|
232
|
+
|
233
|
+
return args.work_dir, user_config
|
234
|
+
|
ion_CSP/mlp_opt.py
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
import os
|
4
|
+
import time
|
5
|
+
import numpy as np
|
6
|
+
import multiprocessing
|
7
|
+
from ase.io.vasp import read_vasp
|
8
|
+
from ase.optimize import LBFGS
|
9
|
+
from ase.constraints import UnitCellFilter
|
10
|
+
from deepmd.calculator import DP
|
11
|
+
|
12
|
+
# 根据脚本位置确定model.pt文件的位置, 减少错误发生
|
13
|
+
base_dir = os.path.dirname(__file__)
|
14
|
+
relative_path = './model.pt'
|
15
|
+
file_path = os.path.join(base_dir, relative_path)
|
16
|
+
calc = DP(file_path)
|
17
|
+
'''
|
18
|
+
structure optimization with DP model and ASE
|
19
|
+
PSTRESS and fmax should exist in input.dat
|
20
|
+
'''
|
21
|
+
|
22
|
+
def get_element_num(elements):
|
23
|
+
'''Using the Atoms.symples to Know Element&Num'''
|
24
|
+
element = []
|
25
|
+
ele = {}
|
26
|
+
element.append(elements[0])
|
27
|
+
for x in elements:
|
28
|
+
if x not in element :
|
29
|
+
element.append(x)
|
30
|
+
for x in element:
|
31
|
+
ele[x] = elements.count(x)
|
32
|
+
return element, ele
|
33
|
+
|
34
|
+
def write_CONTCAR(element, ele, lat, pos, index):
|
35
|
+
'''Write CONTCAR'''
|
36
|
+
f = open(f'{base_dir}/CONTCAR_'+str(index),'w')
|
37
|
+
f.write('ASE-DPKit-Optimization\n')
|
38
|
+
f.write('1.0\n')
|
39
|
+
for i in range(3):
|
40
|
+
f.write('%15.10f %15.10f %15.10f\n' % tuple(lat[i]))
|
41
|
+
for x in element:
|
42
|
+
f.write(x + ' ')
|
43
|
+
f.write('\n')
|
44
|
+
for x in element:
|
45
|
+
f.write(str(ele[x]) + ' ')
|
46
|
+
f.write('\n')
|
47
|
+
f.write('Direct\n')
|
48
|
+
na = sum(ele.values())
|
49
|
+
dpos = np.dot(pos,np.linalg.inv(lat))
|
50
|
+
for i in range(na):
|
51
|
+
f.write('%15.10f %15.10f %15.10f\n' % tuple(dpos[i]))
|
52
|
+
|
53
|
+
def write_OUTCAR(element, ele, masses, volume, lat, pos, ene, force, stress, pstress, index):
|
54
|
+
'''Write OUTCAR'''
|
55
|
+
f = open(f'{base_dir}/OUTCAR_'+str(index),'w')
|
56
|
+
for x in element:
|
57
|
+
f.write('VRHFIN =' + str(x) + '\n')
|
58
|
+
f.write('ions per type =')
|
59
|
+
for x in element:
|
60
|
+
f.write('%5d' % ele[x])
|
61
|
+
f.write('\nDirection XX YY ZZ XY YZ ZX\n')
|
62
|
+
f.write('in kB')
|
63
|
+
f.write('%15.6f' % stress[0])
|
64
|
+
f.write('%15.6f' % stress[1])
|
65
|
+
f.write('%15.6f' % stress[2])
|
66
|
+
f.write('%15.6f' % stress[3])
|
67
|
+
f.write('%15.6f' % stress[4])
|
68
|
+
f.write('%15.6f' % stress[5])
|
69
|
+
f.write('\n')
|
70
|
+
ext_pressure = np.sum(stress[0] + stress[1] + stress[2])/3.0 - pstress
|
71
|
+
f.write('external pressure = %20.6f kB Pullay stress = %20.6f kB\n'% (ext_pressure, pstress))
|
72
|
+
f.write('volume of cell : %20.6f\n' % volume)
|
73
|
+
f.write('direct lattice vectors\n')
|
74
|
+
for i in range(3):
|
75
|
+
f.write('%10.6f %10.6f %10.6f\n' % tuple(lat[i]))
|
76
|
+
f.write('POSITION TOTAL-FORCE(eV/Angst)\n')
|
77
|
+
f.write('-------------------------------------------------------------------\n')
|
78
|
+
na = sum(ele.values())
|
79
|
+
for i in range(na):
|
80
|
+
f.write('%15.6f %15.6f %15.6f' % tuple(pos[i]))
|
81
|
+
f.write('%15.6f %15.6f %15.6f\n' % tuple(force[i]))
|
82
|
+
f.write('-------------------------------------------------------------------\n')
|
83
|
+
# 1.66054这一转换因子用于将原子质量单位转换为克,以便在宏观尺度上计算密度g/cm³
|
84
|
+
atoms_density = 1.66054 * masses / volume
|
85
|
+
f.write('density = %20.6f\n' % atoms_density)
|
86
|
+
f.write('energy without entropy= %20.6f %20.6f\n' % (ene, ene/na))
|
87
|
+
enthalpy = ene + pstress * volume / 1602.17733
|
88
|
+
f.write('enthalpy TOTEN = %20.6f %20.6f\n' % (enthalpy, enthalpy/na))
|
89
|
+
|
90
|
+
def get_indexes():
|
91
|
+
base_dir = os.path.dirname(__file__)
|
92
|
+
POSCAR_files = [f for f in os.listdir(base_dir) if f.startswith('POSCAR_')]
|
93
|
+
indexes = []
|
94
|
+
for filename in POSCAR_files:
|
95
|
+
index_part = filename[len('POSCAR_'):]
|
96
|
+
if index_part.isdigit():
|
97
|
+
index = int(index_part)
|
98
|
+
indexes.append(index)
|
99
|
+
indexes.sort(key=lambda indexes: indexes)
|
100
|
+
return indexes
|
101
|
+
|
102
|
+
def run_opt(index: int):
|
103
|
+
'''Using the ASE&DP to Optimize Configures'''
|
104
|
+
if os.path.isfile(f'{base_dir}/OUTCAR'):
|
105
|
+
os.system(f'mv {base_dir}/OUTCAR {base_dir}/OUTCAR-last')
|
106
|
+
fmax, pstress = 0.03, 0
|
107
|
+
|
108
|
+
print('Start to Optimize Structures by DP----------')
|
109
|
+
|
110
|
+
Opt_Step = 2000
|
111
|
+
start = time.time()
|
112
|
+
# pstress kbar
|
113
|
+
# kBar to eV/A^3
|
114
|
+
# 1 eV/A^3 = 160.21766028 GPa
|
115
|
+
# 1 / 160.21766028 ~ 0.006242
|
116
|
+
aim_stress = 1.0 * pstress * 0.01 * 0.6242 / 10.0
|
117
|
+
atoms = read_vasp('POSCAR_'+str(index))
|
118
|
+
atoms.calc = calc
|
119
|
+
ucf = UnitCellFilter(atoms, scalar_pressure=aim_stress)
|
120
|
+
# optimization
|
121
|
+
opt = LBFGS(ucf)
|
122
|
+
opt.run(fmax=fmax,steps=Opt_Step)
|
123
|
+
# atoms will be optimized and updated during the opt.run process
|
124
|
+
atoms_lat = atoms.cell
|
125
|
+
atoms_pos = atoms.positions
|
126
|
+
atoms_force = atoms.get_forces()
|
127
|
+
atoms_stress = atoms.get_stress()
|
128
|
+
# eV/A^3 to GPa
|
129
|
+
atoms_stress = atoms_stress/(0.01*0.6242)
|
130
|
+
atoms_symbols = atoms.get_chemical_symbols()
|
131
|
+
atoms_ene = atoms.get_potential_energy()
|
132
|
+
atoms_masses = sum(atoms.get_masses())
|
133
|
+
atoms_vol = atoms.get_volume()
|
134
|
+
element, ele = get_element_num(atoms_symbols)
|
135
|
+
|
136
|
+
write_CONTCAR(element, ele, atoms_lat, atoms_pos, index)
|
137
|
+
write_OUTCAR(element, ele, atoms_masses, atoms_vol, atoms_lat, atoms_pos, atoms_ene, atoms_force, -10.0 * atoms_stress, pstress, index)
|
138
|
+
|
139
|
+
stop = time.time()
|
140
|
+
_cwd = os.getcwd()
|
141
|
+
_cwd = os.path.basename(_cwd)
|
142
|
+
print(f'{_cwd} is done, time: {stop-start}')
|
143
|
+
|
144
|
+
|
145
|
+
def main():
|
146
|
+
ctx=multiprocessing.get_context("spawn")
|
147
|
+
pool=ctx.Pool(8)
|
148
|
+
indexes = get_indexes()
|
149
|
+
pool.map(func=run_opt, iterable=indexes)
|
150
|
+
pool.close()
|
151
|
+
pool.join()
|
152
|
+
|
153
|
+
if __name__=='__main__':
|
154
|
+
main()
|
@@ -0,0 +1,144 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
import logging
|
4
|
+
import subprocess
|
5
|
+
from ase.io.vasp import read_vasp
|
6
|
+
from ion_CSP.identify_molecules import identify_molecules, molecules_information
|
7
|
+
|
8
|
+
class ReadMlpDensity:
|
9
|
+
|
10
|
+
def __init__(self, work_dir:str):
|
11
|
+
# 获取脚本的当前目录
|
12
|
+
self.base_dir = work_dir
|
13
|
+
os.chdir(self.base_dir)
|
14
|
+
# 寻找同一目录下的2_mlp_optimized文件夹
|
15
|
+
self.folder_dir = os.path.join(self.base_dir, '2_mlp_optimized')
|
16
|
+
self.max_density_dir = os.path.join(self.folder_dir, 'max_density')
|
17
|
+
self.primitive_cell_dir = os.path.join(self.folder_dir, 'primitive_cell')
|
18
|
+
print(f"Processing MLP CONTCARs in {self.folder_dir}")
|
19
|
+
logging.info(f"Processing MLP CONTCARs in {self.folder_dir}")
|
20
|
+
|
21
|
+
def _sequentially_read_files(self, directory: str, prefix_name: str = 'POSCAR_'):
|
22
|
+
"""
|
23
|
+
Private method:
|
24
|
+
Extract numbers from file names, convert them to integers, sort them by sequence, and return a list containing both indexes and file names
|
25
|
+
"""
|
26
|
+
# 获取dir文件夹中所有以prefix_name开头的文件,在此实例中为POSCAR_
|
27
|
+
files = [f for f in os.listdir(directory) if f.startswith(prefix_name)]
|
28
|
+
file_index_pairs = []
|
29
|
+
for filename in files:
|
30
|
+
index_part = filename[len(prefix_name):] # 选取去除前缀'POSCAR_'的数字
|
31
|
+
if index_part.isdigit(): # 确保剩余部分全是数字
|
32
|
+
index = int(index_part)
|
33
|
+
file_index_pairs.append((index, filename))
|
34
|
+
file_index_pairs.sort(key=lambda pair: pair[0])
|
35
|
+
return file_index_pairs
|
36
|
+
|
37
|
+
def read_density_and_sort(self, n_screen: int = 10, molecules_screen: bool = True, detail_log: bool = False):
|
38
|
+
"""
|
39
|
+
Obtain the atomic mass and unit cell volume from the optimized CONTCAR file, and obtain the ion crystal density. Finally, take n CONTCAR files with the highest density and save them separately for viewing.
|
40
|
+
|
41
|
+
:param n_screen: 取前n个最大密度的文件
|
42
|
+
"""
|
43
|
+
os.chdir(self.base_dir)
|
44
|
+
# 获取所有以'CONTCAR_'开头的文件,并按数字顺序处理
|
45
|
+
CONTCAR_file_index_pairs = self._sequentially_read_files(self.folder_dir, prefix_name='CONTCAR_')
|
46
|
+
# 逐个处理文件
|
47
|
+
density_index_list = []
|
48
|
+
for _, CONTCAR_filename in CONTCAR_file_index_pairs:
|
49
|
+
atoms = read_vasp(os.path.join(self.folder_dir, CONTCAR_filename))
|
50
|
+
molecules, molecules_flag, initial_information = identify_molecules(atoms)
|
51
|
+
if detail_log:
|
52
|
+
molecules_information(molecules, molecules_flag, initial_information)
|
53
|
+
if molecules_screen:
|
54
|
+
if molecules_flag:
|
55
|
+
atoms_volume = atoms.get_volume() # 体积单位为立方埃(ų)
|
56
|
+
atoms_masses = sum(atoms.get_masses()) # 质量单位为原子质量单位(amu)
|
57
|
+
# 1.66054这一转换因子用于将原子质量单位转换为克,以便在宏观尺度上计算密度g/cm³
|
58
|
+
density = 1.66054 * atoms_masses / atoms_volume
|
59
|
+
density_index_list.append((density, CONTCAR_filename))
|
60
|
+
else:
|
61
|
+
atoms_volume = atoms.get_volume() # 体积单位为立方埃(ų)
|
62
|
+
atoms_masses = sum(atoms.get_masses()) # 质量单位为原子质量单位(amu)
|
63
|
+
# 1.66054这一转换因子用于将原子质量单位转换为克,以便在宏观尺度上计算密度g/cm³
|
64
|
+
density = 1.66054 * atoms_masses / atoms_volume
|
65
|
+
density_index_list.append((density, CONTCAR_filename))
|
66
|
+
if molecules_screen:
|
67
|
+
print(f'Total optimized ionic crystals: {len(CONTCAR_file_index_pairs)}')
|
68
|
+
print(f'Screened ionic crystals with original ions: {len(density_index_list)}')
|
69
|
+
logging.info(f'Total optimized ionic crystals: {len(CONTCAR_file_index_pairs)}')
|
70
|
+
logging.info(f'Screened ionic crystals with original ions: {len(density_index_list)}')
|
71
|
+
# 根据密度降序排序
|
72
|
+
sorted_filename = sorted(density_index_list, key=lambda x: x[0], reverse=True)
|
73
|
+
# 将前n个最大密度的CONTCAR文件进行重命名并保存到max_density文件夹
|
74
|
+
if os.path.exists(self.max_density_dir):
|
75
|
+
backup_dir = os.path.join(self.folder_dir, 'backup', 'max_density')
|
76
|
+
os.makedirs(backup_dir, exist_ok=True)
|
77
|
+
for item in os.listdir(self.max_density_dir):
|
78
|
+
shutil.move(os.path.join(self.max_density_dir, item), os.path.join(backup_dir, item))
|
79
|
+
|
80
|
+
os.makedirs(self.max_density_dir, exist_ok=True)
|
81
|
+
for density, CONTCAR_filename in sorted_filename[:n_screen]:
|
82
|
+
# 生成新的包含密度值的文件名,并重命名文件
|
83
|
+
# 密度转换为字符串,保留4位小数
|
84
|
+
density_str = f'{density:.4f}'
|
85
|
+
# 保留 CONTCAR 的序数信息,方便回推检查
|
86
|
+
number = CONTCAR_filename.split("_")[1]
|
87
|
+
OUTCAR_filename = f'OUTCAR_{number}'
|
88
|
+
new_CONTCAR_filename = f'CONTCAR_{density_str}_{number}'
|
89
|
+
new_OUTCAR_filename = f'OUTCAR_{density_str}_{number}'
|
90
|
+
shutil.copy(f'{self.folder_dir}/{CONTCAR_filename}', f'{self.max_density_dir}/{new_CONTCAR_filename}')
|
91
|
+
shutil.copy(f'{self.folder_dir}/{OUTCAR_filename}', f'{self.max_density_dir}/{new_OUTCAR_filename}')
|
92
|
+
print(f'New CONTCAR and OUTCAR of {density_str}_{number} are renamed and saved')
|
93
|
+
logging.info(f'New CONTCAR and OUTCAR of {density_str}_{number} are renamed and saved')
|
94
|
+
|
95
|
+
def phonopy_processing_max_density(self, specific_directory :str = None):
|
96
|
+
"""
|
97
|
+
Use phonopy to check and generate symmetric primitive cells, reducing the complexity of subsequent optimization calculations, and preventing pyxtal.from_random from generating double proportioned supercells.
|
98
|
+
"""
|
99
|
+
if specific_directory:
|
100
|
+
self.phonopy_dir = os.path.join(self.base_dir, specific_directory)
|
101
|
+
self.primitive_cell_dir = os.path.join(os.path.dirname(self.phonopy_dir), 'primitive_cell')
|
102
|
+
else:
|
103
|
+
self.phonopy_dir = self.max_density_dir
|
104
|
+
if os.path.exists(self.primitive_cell_dir):
|
105
|
+
backup_dir = os.path.join(self.folder_dir, 'backup', 'primitive_cell')
|
106
|
+
os.makedirs(backup_dir, exist_ok=True)
|
107
|
+
for item in os.listdir(self.primitive_cell_dir):
|
108
|
+
shutil.move(os.path.join(self.primitive_cell_dir, item), os.path.join(backup_dir, item))
|
109
|
+
|
110
|
+
os.makedirs(self.primitive_cell_dir, exist_ok=True)
|
111
|
+
CONTCAR_files = [f for f in os.listdir(self.phonopy_dir) if f.startswith('CONTCAR_')]
|
112
|
+
# 改变工作目录,便于运行shell命令进行phonopy对称性检查和原胞与常规胞的生成
|
113
|
+
os.chdir(self.phonopy_dir)
|
114
|
+
logging.info('Start running phonopy processing ...')
|
115
|
+
try:
|
116
|
+
for new_CONTCAR_filename in CONTCAR_files:
|
117
|
+
# 按顺序处理POSCAR文件,首先复制一份无数字后缀的POSCAR文件
|
118
|
+
shutil.copy(f'{self.phonopy_dir}/{new_CONTCAR_filename}', f'{self.phonopy_dir}/POSCAR')
|
119
|
+
with open(f'{self.primitive_cell_dir}/phonopy.log', 'a') as log:
|
120
|
+
# 使用phonopy模块处理POSCAR结构文件,获取对称化的原胞和常规胞。
|
121
|
+
# 应用晶体的对称操作优化后的原胞可以最好地符合晶体的对称性,减少后续优化计算的复杂性。
|
122
|
+
log.write(f'\nProcessing file: {new_CONTCAR_filename}\n')
|
123
|
+
result = subprocess.run(['nohup', 'phonopy', '--symmetry', 'POSCAR'], stderr=subprocess.STDOUT)
|
124
|
+
log.write(f'Finished processing file: {new_CONTCAR_filename} with return code: {result.returncode}\n')
|
125
|
+
# 将phonopy生成的PPOSCAR(对称化原胞)和BPOSCAR(对称化常规胞)放到对应的文件夹中,并将文件名改回POSCAR_index
|
126
|
+
shutil.move(f'{self.phonopy_dir}/PPOSCAR', f'{self.primitive_cell_dir}/{new_CONTCAR_filename}')
|
127
|
+
# 复制对应的OUTCAR文件到primitive_cell目录下
|
128
|
+
density_number = new_CONTCAR_filename.split("CONTCAR_")[1]
|
129
|
+
new_OUTCAR_filename = f'OUTCAR_{density_number}'
|
130
|
+
shutil.copy(f'{self.max_density_dir}/{new_OUTCAR_filename}', f'{self.primitive_cell_dir}/{new_OUTCAR_filename}')
|
131
|
+
for_vasp_opt_dir = os.path.join(self.base_dir, '3_for_vasp_opt')
|
132
|
+
if os.path.exists(for_vasp_opt_dir):
|
133
|
+
shutil.rmtree(for_vasp_opt_dir)
|
134
|
+
shutil.copytree(self.primitive_cell_dir, for_vasp_opt_dir)
|
135
|
+
logging.info('The phonopy processing has been completed!!\nThe symmetrized primitive cells have been saved in POSCAR format to the primitive_cell folder.\nThe output content of phonopy has been saved to the phonopy.log file in the same directory.')
|
136
|
+
# 在 phonopy 成功进行对称化处理后,删除 2_mlp_optimized/max_density 文件夹以节省空间
|
137
|
+
shutil.rmtree(self.max_density_dir)
|
138
|
+
except FileNotFoundError:
|
139
|
+
logging.error(
|
140
|
+
"There are no CONTCAR structure files after screening.\nPlease check if the ions correspond to the crystals and adjust the screening criteria"
|
141
|
+
)
|
142
|
+
raise FileNotFoundError(
|
143
|
+
"There are no CONTCAR structure files after screening.\nPlease check if the ions correspond to the crystals and adjust the screening criteria"
|
144
|
+
)
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# 设置变量
|
4
|
+
FOLDER=$1
|
5
|
+
BASE_DIR="$FOLDER/3_for_vasp_opt"
|
6
|
+
INCAR_1="INCAR_1"
|
7
|
+
INCAR_2="INCAR_2"
|
8
|
+
POTCAR_H="POTCAR_H"
|
9
|
+
POTCAR_C="POTCAR_C"
|
10
|
+
POTCAR_N="POTCAR_N"
|
11
|
+
POTCAR_O="POTCAR_O"
|
12
|
+
SUB_SCRIPT="JLU_184_sub.sh"
|
13
|
+
FLAG_FILE="${BASE_DIR}/flag_${FOLDER}.txt"
|
14
|
+
|
15
|
+
# 检查必要文件是否存在
|
16
|
+
if [[ ! -f "$INCAR_1" || ! -f "$INCAR_2" || ! -f "$POTCAR_H" || ! -f "$POTCAR_C" || ! -f "$POTCAR_N" || ! -f "$POTCAR_O" || ! -f "$SUB_SCRIPT" ]]; then
|
17
|
+
echo "Necessary files are missing, please check the path."
|
18
|
+
exit 1
|
19
|
+
fi
|
20
|
+
|
21
|
+
# 创建 POTCAR 文件
|
22
|
+
create_potcar_from_poscar() {
|
23
|
+
poscar_file="$1"
|
24
|
+
output_file="POTCAR"
|
25
|
+
> "$output_file" # 清空文件
|
26
|
+
|
27
|
+
# 读取 POSCAR 文件的第六行(元素行)
|
28
|
+
read -r element_line < <(sed -n '6p' "$poscar_file")
|
29
|
+
|
30
|
+
# 将元素转换为数组
|
31
|
+
IFS=' ' read -r -a elements <<< "$element_line"
|
32
|
+
|
33
|
+
# 根据元素拼接 POTCAR
|
34
|
+
for element in "${elements[@]}"; do
|
35
|
+
case $element in
|
36
|
+
H) cat "$POTCAR_H" >> "$output_file" ;;
|
37
|
+
C) cat "$POTCAR_C" >> "$output_file" ;;
|
38
|
+
N) cat "$POTCAR_N" >> "$output_file" ;;
|
39
|
+
O) cat "$POTCAR_O" >> "$output_file" ;;
|
40
|
+
*) echo "Warning: POTCAR for element $element not found." ;;
|
41
|
+
esac
|
42
|
+
done
|
43
|
+
}
|
44
|
+
|
45
|
+
# 提交第一步优化任务
|
46
|
+
for contcar in ${BASE_DIR}/CONTCAR_*; do
|
47
|
+
if [[ $contcar =~ CONTCAR_(.*) ]]; then
|
48
|
+
sample=${BASH_REMATCH[1]}
|
49
|
+
sample_dir="${BASE_DIR}/${sample}"
|
50
|
+
mkdir -p "$sample_dir"
|
51
|
+
cp "$contcar" "${sample_dir}/POSCAR"
|
52
|
+
cp "$INCAR_1" "${sample_dir}/INCAR"
|
53
|
+
create_potcar_from_poscar "${sample_dir}/POSCAR" # 根据 POSCAR 创建 POTCAR
|
54
|
+
cp "POTCAR" "${sample_dir}/" # 使用自动生成的 POTCAR
|
55
|
+
cp "$SUB_SCRIPT" "${sample_dir}/"
|
56
|
+
rm "POTCAR"
|
57
|
+
(cd "${sample_dir}" && bsub -J "${FOLDER}_1" < "${SUB_SCRIPT}")
|
58
|
+
fi
|
59
|
+
done
|
60
|
+
|
61
|
+
echo "All first step tasks have been submitted."
|
62
|
+
|
63
|
+
# 提交监控任务
|
64
|
+
monitor_job_id=$(bsub -J "m_$FOLDER" -n 1 -q normal -o /dev/null -e /dev/null <<EOF
|
65
|
+
#!/bin/bash
|
66
|
+
|
67
|
+
# 设置变量
|
68
|
+
FOLDER=$FOLDER
|
69
|
+
BASE_DIR="$BASE_DIR"
|
70
|
+
INCAR_2="$INCAR_2"
|
71
|
+
SUB_SCRIPT="$SUB_SCRIPT"
|
72
|
+
FLAG_FILE="$FLAG_FILE"
|
73
|
+
|
74
|
+
# 监控任务
|
75
|
+
while true; do
|
76
|
+
# 检查第一步任务是否完成
|
77
|
+
if ! bjobs -J "${FOLDER}_1" | grep -q "RUN\|PEND"; then
|
78
|
+
echo "The first step task has been completed, ready to submit the second step task..."
|
79
|
+
break
|
80
|
+
fi
|
81
|
+
sleep 60 # 每60秒检查一次
|
82
|
+
done
|
83
|
+
|
84
|
+
# 提交第二步优化任务
|
85
|
+
for sample in \$(ls \${BASE_DIR}); do
|
86
|
+
if [ -f "\${BASE_DIR}/\${sample}/CONTCAR" ]; then
|
87
|
+
sample_dir="\${BASE_DIR}/\${sample}"
|
88
|
+
mkdir -p "\${sample_dir}/fine"
|
89
|
+
cp "\${sample_dir}/CONTCAR" "\${sample_dir}/fine/POSCAR"
|
90
|
+
cp "\$INCAR_2" "\${sample_dir}/fine/INCAR"
|
91
|
+
cp "\${sample_dir}/POTCAR" "\${sample_dir}/fine/"
|
92
|
+
cp "\$SUB_SCRIPT" "\${sample_dir}/fine/"
|
93
|
+
(cd "\${sample_dir}/fine" && bsub -J "\${FOLDER}_2" < "\${SUB_SCRIPT}")
|
94
|
+
fi
|
95
|
+
done
|
96
|
+
|
97
|
+
# 等待第二步任务完成
|
98
|
+
while true; do
|
99
|
+
if ! bjobs -J "\${FOLDER}_2" | grep -q "RUN\|PEND"; then
|
100
|
+
echo "All second step tasks have been completed, and the flag file is generated ..."
|
101
|
+
touch "\$FLAG_FILE"
|
102
|
+
break
|
103
|
+
fi
|
104
|
+
sleep 60 # 每60秒检查一次
|
105
|
+
done
|
106
|
+
|
107
|
+
EOF
|
108
|
+
)
|
109
|
+
|
110
|
+
echo "Monitoring task submitted, task ID: $monitor_job_id"
|