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/__init__.py
ADDED
ion_CSP/app.py
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import time
|
5
|
+
import signal
|
6
|
+
import logging
|
7
|
+
import subprocess
|
8
|
+
from pathlib import Path
|
9
|
+
from datetime import datetime
|
10
|
+
|
11
|
+
|
12
|
+
class TaskManager:
|
13
|
+
def __init__(self):
|
14
|
+
self.env = "LOCAL"
|
15
|
+
self.workspace = Path.cwd()
|
16
|
+
self.log_base = self.workspace / "logs"
|
17
|
+
self._detect_env()
|
18
|
+
self._setup_logging()
|
19
|
+
|
20
|
+
def _detect_env(self):
|
21
|
+
"""环境检测"""
|
22
|
+
if Path("/.dockerenv").exists() or "DOCKER" in os.environ:
|
23
|
+
self.env = "DOCKER"
|
24
|
+
self.workspace = Path("/app")
|
25
|
+
self.log_base = Path("/app/logs")
|
26
|
+
self.workspace.mkdir(exist_ok=True)
|
27
|
+
self.log_base.mkdir(exist_ok=True)
|
28
|
+
|
29
|
+
def _setup_logging(self):
|
30
|
+
"""日志配置"""
|
31
|
+
logging.basicConfig(
|
32
|
+
level=logging.INFO,
|
33
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
34
|
+
handlers=[
|
35
|
+
logging.FileHandler(self.log_base / "system.log"),
|
36
|
+
logging.StreamHandler(),
|
37
|
+
],
|
38
|
+
)
|
39
|
+
|
40
|
+
def normalize_path(self, path):
|
41
|
+
"""路径标准化"""
|
42
|
+
path = Path(path).resolve()
|
43
|
+
if self.env == "DOCKER":
|
44
|
+
return str(path.relative_to(self.workspace))
|
45
|
+
return str(path)
|
46
|
+
|
47
|
+
def _get_pid(self, module, work_dir):
|
48
|
+
"""获取进程PID"""
|
49
|
+
log_file = Path(work_dir) / f"main_{module}_console.log"
|
50
|
+
if not log_file.exists():
|
51
|
+
return None
|
52
|
+
try:
|
53
|
+
with open(log_file, "r") as f:
|
54
|
+
for line in f:
|
55
|
+
if "PYTHON_PID:" in line:
|
56
|
+
return int(line.split(":")[-1].strip())
|
57
|
+
except Exception as e:
|
58
|
+
logging.error(f"Error reading PID from log: {e}")
|
59
|
+
return None
|
60
|
+
|
61
|
+
def task_runner(self, module, work_dir):
|
62
|
+
"""任务执行器"""
|
63
|
+
work_dir = Path(work_dir)
|
64
|
+
work_dir.mkdir(exist_ok=True)
|
65
|
+
|
66
|
+
console_log = work_dir / f"main_{module}_console.log"
|
67
|
+
pid_file = work_dir / "pid.txt"
|
68
|
+
|
69
|
+
# 启动子进程
|
70
|
+
cmd = ["python", "-m", f"src.main_{module}", str(work_dir)]
|
71
|
+
|
72
|
+
with open(console_log, "w") as f:
|
73
|
+
process = subprocess.Popen(
|
74
|
+
cmd,
|
75
|
+
stdout=f,
|
76
|
+
stderr=subprocess.STDOUT,
|
77
|
+
preexec_fn=os.setsid if os.name != "nt" else None,
|
78
|
+
)
|
79
|
+
|
80
|
+
# 等待PID文件创建
|
81
|
+
time.sleep(1)
|
82
|
+
try:
|
83
|
+
with open(pid_file, "w") as f:
|
84
|
+
f.write(str(process.pid))
|
85
|
+
except Exception as e:
|
86
|
+
logging.error(f"Error writing PID file: {e}")
|
87
|
+
process.terminate()
|
88
|
+
return
|
89
|
+
# 创建符号链接
|
90
|
+
output_log = work_dir / f"main_{module}.py_output.log"
|
91
|
+
print(f"Original log file: {output_log}")
|
92
|
+
std_log = self.log_base / f"{module}_{process.pid}.log"
|
93
|
+
try:
|
94
|
+
std_log.symlink_to(output_log)
|
95
|
+
os.remove(pid_file)
|
96
|
+
except FileExistsError:
|
97
|
+
os.remove(std_log)
|
98
|
+
std_log.symlink_to(output_log)
|
99
|
+
|
100
|
+
logging.info(f"Started {module} module (PID: {process.pid})")
|
101
|
+
print(f"Task started (PID: {process.pid})")
|
102
|
+
print(f"Normalized log file: {std_log}")
|
103
|
+
|
104
|
+
def terminate_task(self, pid):
|
105
|
+
"""终止任务"""
|
106
|
+
try:
|
107
|
+
os.killpg(os.getpgid(pid), signal.SIGTERM)
|
108
|
+
print(f"Successfully terminated PID {pid}")
|
109
|
+
except ProcessLookupError:
|
110
|
+
print(f"No process found with PID {pid}")
|
111
|
+
except Exception as e:
|
112
|
+
print(f"Error terminating process: {e}")
|
113
|
+
|
114
|
+
def view_logs(self, page_size=10):
|
115
|
+
"""查看日志"""
|
116
|
+
log_files = sorted(
|
117
|
+
self.log_base.glob("**/*.log"), key=os.path.getmtime, reverse=True
|
118
|
+
)
|
119
|
+
if not log_files:
|
120
|
+
print("No logs found")
|
121
|
+
return
|
122
|
+
total_files = len(log_files)
|
123
|
+
total_pages = (total_files + page_size - 1) // page_size # 计算总页数
|
124
|
+
|
125
|
+
current_page = 0
|
126
|
+
while True:
|
127
|
+
start_index = current_page * page_size
|
128
|
+
end_index = start_index + page_size
|
129
|
+
print("\nAvailable logs:")
|
130
|
+
|
131
|
+
# 显示当前页的日志文件
|
132
|
+
for i, f in enumerate(log_files[start_index:end_index], start_index + 1):
|
133
|
+
print(
|
134
|
+
f"{i}) {f.name} ({datetime.fromtimestamp(f.stat().st_mtime).strftime('%Y-%m-%d %H:%M')})"
|
135
|
+
)
|
136
|
+
|
137
|
+
print("\nPage {} of {}".format(current_page + 1, total_pages))
|
138
|
+
if current_page > 0:
|
139
|
+
print("Enter 'p' to go to the previous page.")
|
140
|
+
if current_page < total_pages - 1:
|
141
|
+
print("Enter 'n' to go to the next page.")
|
142
|
+
print("Enter log number to view (q to cancel): ")
|
143
|
+
|
144
|
+
choice = input().strip()
|
145
|
+
if choice.isdigit():
|
146
|
+
choice_index = int(choice) - 1
|
147
|
+
if 0 <= choice_index < total_files:
|
148
|
+
os.system(f"less {log_files[choice_index]}")
|
149
|
+
else:
|
150
|
+
print("Invalid selection")
|
151
|
+
elif choice == "n" and current_page < total_pages - 1:
|
152
|
+
current_page += 1
|
153
|
+
elif choice == "p" and current_page > 0:
|
154
|
+
current_page -= 1
|
155
|
+
elif choice == "q":
|
156
|
+
break
|
157
|
+
else:
|
158
|
+
print("Invalid command")
|
159
|
+
|
160
|
+
def main_menu(self):
|
161
|
+
"""主菜单循环"""
|
162
|
+
while True:
|
163
|
+
os.system("clear" if os.name == "posix" else "cls")
|
164
|
+
print("========== Task Execution System ==========")
|
165
|
+
print(f"Current Environment: {self.env}")
|
166
|
+
print(f"Current Directory: {self.workspace}")
|
167
|
+
print(f"Log Base Directory: {self.log_base}")
|
168
|
+
print("=" * 50)
|
169
|
+
print("1) Run EE Module")
|
170
|
+
print("2) Run CSP Module")
|
171
|
+
print("3) View Logs")
|
172
|
+
print("4) Terminate Task")
|
173
|
+
print("q) Exit")
|
174
|
+
print("=" * 50)
|
175
|
+
|
176
|
+
choice = input("Please select one of the operation: ").strip()
|
177
|
+
if choice == "1":
|
178
|
+
work_dir = input("Enter EE working directory: ").strip()
|
179
|
+
self.task_runner("EE", work_dir)
|
180
|
+
elif choice == "2":
|
181
|
+
work_dir = input("Enter CSP working directory: ").strip()
|
182
|
+
self.task_runner("CSP", work_dir)
|
183
|
+
elif choice == "3":
|
184
|
+
self.view_logs()
|
185
|
+
elif choice == "4":
|
186
|
+
pid = input("Enter PID to terminate: ").strip()
|
187
|
+
if pid.isdigit():
|
188
|
+
self.terminate_task(int(pid))
|
189
|
+
else:
|
190
|
+
print("Invalid PID format")
|
191
|
+
elif choice == "q":
|
192
|
+
print("Exiting system...")
|
193
|
+
sys.exit(0)
|
194
|
+
else:
|
195
|
+
print("Invalid selection")
|
196
|
+
input("\nPress Enter to continue...")
|
197
|
+
|
198
|
+
|
199
|
+
if __name__ == "__main__":
|
200
|
+
manager = TaskManager()
|
201
|
+
manager.main_menu()
|
@@ -0,0 +1,291 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
import logging
|
4
|
+
import pandas as pd
|
5
|
+
from typing import List
|
6
|
+
from rdkit import Chem
|
7
|
+
from rdkit.Chem import AllChem
|
8
|
+
from dpdispatcher import Machine, Resources, Task, Submission
|
9
|
+
from ion_CSP.log_and_time import redirect_dpdisp_logging
|
10
|
+
|
11
|
+
|
12
|
+
class SmilesProcessing:
|
13
|
+
|
14
|
+
def __init__(self, work_dir: str, csv_file: str, converted_folder: str = '1_1_SMILES_gjf', optimized_dir: str = '1_2_Gaussian_optimized'):
|
15
|
+
"""
|
16
|
+
args:
|
17
|
+
work_dir: the path of the working directory.
|
18
|
+
csv_file: the csv file name in the working directory.
|
19
|
+
"""
|
20
|
+
redirect_dpdisp_logging(os.path.join(work_dir, "dpdispatcher.log"))
|
21
|
+
# 读取csv文件并处理数据, csv文件的表头包括 SMILES, Charge, Refcode或Number
|
22
|
+
self.base_dir = work_dir
|
23
|
+
os.chdir(work_dir)
|
24
|
+
if not csv_file:
|
25
|
+
raise Exception('Necessary .csv file not provided!')
|
26
|
+
csv_path = os.path.join(self.base_dir, csv_file)
|
27
|
+
self.converted_dir = os.path.join(
|
28
|
+
self.base_dir, converted_folder, os.path.splitext(csv_file)[0]
|
29
|
+
)
|
30
|
+
self.gaussian_optimized_dir = os.path.join(self.base_dir, optimized_dir)
|
31
|
+
self.param_dir = os.path.join(os.path.dirname(__file__), "../../param")
|
32
|
+
original_df = pd.read_csv(csv_path)
|
33
|
+
logging.info(f"Processing {csv_path}")
|
34
|
+
# 对SMILES码去重
|
35
|
+
df = original_df.drop_duplicates(subset="SMILES")
|
36
|
+
try:
|
37
|
+
# 根据Refcode进行排序
|
38
|
+
df = df.sort_values(by="Refcode")
|
39
|
+
self.base_name = "Refcode"
|
40
|
+
except KeyError:
|
41
|
+
# 如果不存在Refcode,则根据Number进行排序
|
42
|
+
df = df.sort_values(by="Number")
|
43
|
+
self.base_name = "Number"
|
44
|
+
# 根据Charge分组
|
45
|
+
grouped = df.groupby("Charge")
|
46
|
+
duplicate_message = f"\nOriginal SMILES dataset: {len(original_df)}\nAfter SMILES deduplication\n Valid SMILES: {len(df)}\n Duplicate SMILES: {len(original_df) - len(df)}"
|
47
|
+
logging.info(duplicate_message)
|
48
|
+
self.csv = csv_file.split(".csv")[0]
|
49
|
+
self.df = df
|
50
|
+
self.grouped = grouped
|
51
|
+
|
52
|
+
def _convert_SMILES(
|
53
|
+
self, dir: str, smiles: str, basename: str, charge: int
|
54
|
+
):
|
55
|
+
"""
|
56
|
+
Private method: Use the rdkit module to read SMILES code and convert it into the required file types such as gjf, xyz, mol, etc.
|
57
|
+
|
58
|
+
args:
|
59
|
+
dir: The directory used for outputting files, regardless of existence of the directory.
|
60
|
+
smiles: SMILES code to be converted.
|
61
|
+
basename: The reference code or number corresponding to SMILES code.
|
62
|
+
charge: The charge carried by ions.
|
63
|
+
return:
|
64
|
+
result_code: Result code 0 or -1, representing success and failure respectively.
|
65
|
+
basename: The corresponding basename.
|
66
|
+
"""
|
67
|
+
mol = Chem.MolFromSmiles(smiles)
|
68
|
+
mol = Chem.AddHs(mol)
|
69
|
+
try:
|
70
|
+
# 生成3D坐标
|
71
|
+
AllChem.EmbedMolecule(mol)
|
72
|
+
AllChem.UFFOptimizeMolecule(mol)
|
73
|
+
# 获取原子信息
|
74
|
+
conf = mol.GetConformer()
|
75
|
+
num_atoms = mol.GetNumAtoms()
|
76
|
+
# 计算电荷与分子多重度
|
77
|
+
num_charge, num_unpaired_electrons = 0, 0
|
78
|
+
for atom in mol.GetAtoms():
|
79
|
+
num_charge += atom.GetFormalCharge()
|
80
|
+
num_unpaired_electrons += atom.GetNumRadicalElectrons()
|
81
|
+
if num_charge != charge:
|
82
|
+
logging.error(
|
83
|
+
f"{basename}: charge wrong! calculated {num_charge} and given {charge}"
|
84
|
+
)
|
85
|
+
multiplicity = 2 * num_unpaired_electrons + 1
|
86
|
+
# 根据type参数判断要生成什么类型的结构文件, 目前只支持gjf, xyz, mol格式
|
87
|
+
filename = f"{dir}/{basename}.gjf"
|
88
|
+
# 创建gjf文件内容
|
89
|
+
gjf_content = f"%nprocshared=8\n%chk={basename}.chk\n#p B3LYP/6-31G** opt\n\n{basename}\n\n{num_charge} {multiplicity}\n"
|
90
|
+
for atom in range(num_atoms):
|
91
|
+
pos = conf.GetAtomPosition(atom)
|
92
|
+
atom_symbol = mol.GetAtomWithIdx(atom).GetSymbol()
|
93
|
+
gjf_content += (
|
94
|
+
f"{atom_symbol} {pos.x:.6f} {pos.y:.6f} {pos.z:.6f}\n"
|
95
|
+
)
|
96
|
+
# 写入gjf文件
|
97
|
+
with open(filename, "w") as gjf_file:
|
98
|
+
# gjf文件末尾需要空行,否则Gaussian会报End of file in ZSymb错误(l101.exe)
|
99
|
+
gjf_file.write(f"{gjf_content}\n\n")
|
100
|
+
result_code = 0
|
101
|
+
except Exception as e: # 捕获运行过程中的错误
|
102
|
+
logging.error(
|
103
|
+
f"Error occurred while optimizing molecule of {basename} with charge {charge}: {e}"
|
104
|
+
)
|
105
|
+
result_code = 1
|
106
|
+
# 第一项返回值为结果码0或1, 分别代表成功和失败; 第二项返回值为对应的refcode或序号
|
107
|
+
return result_code, basename
|
108
|
+
|
109
|
+
def charge_group(self):
|
110
|
+
"""
|
111
|
+
Create folders by grouping according to charges and convert SMILES codes into corresponding structural files.
|
112
|
+
"""
|
113
|
+
# 分别记录生成结构成功和失败的refcode或序号
|
114
|
+
success, fail = [], []
|
115
|
+
for charge, group in self.grouped:
|
116
|
+
# 根据文件类型与电荷分组创建对应的文件夹
|
117
|
+
charge_dir = (
|
118
|
+
f"{self.converted_dir}/charge_{charge}"
|
119
|
+
)
|
120
|
+
os.makedirs(charge_dir, exist_ok=True)
|
121
|
+
# 通过SMILE_to函数依次处理SMILES码
|
122
|
+
for _, row in group.iterrows():
|
123
|
+
result_code, basename = self._convert_SMILES(
|
124
|
+
dir=charge_dir,
|
125
|
+
smiles=row["SMILES"],
|
126
|
+
basename=row[self.base_name],
|
127
|
+
charge=row["Charge"]
|
128
|
+
)
|
129
|
+
# 根据私有方法_convert_SMILES的返回值记录refcode对应的分子是否能够成功生成结构文件
|
130
|
+
if result_code == 0:
|
131
|
+
success.append(basename)
|
132
|
+
elif result_code == 1:
|
133
|
+
fail.append(basename)
|
134
|
+
# 将统计信息输出并记录到log文件中
|
135
|
+
generation_message = f"\nDuring the .gjf file generation process\n Successfully generated .gjf files: {len(success)}\n Errors encounted: {len(fail)}\n Error {self.base_name}: {fail}"
|
136
|
+
logging.info(generation_message)
|
137
|
+
|
138
|
+
def screen(
|
139
|
+
self,
|
140
|
+
charge_screen: int = 0,
|
141
|
+
group_screen: str = "",
|
142
|
+
group_name: str = "",
|
143
|
+
group_screen_invert: bool = False,
|
144
|
+
):
|
145
|
+
"""
|
146
|
+
Screen based on the provided functional groups and charges.
|
147
|
+
"""
|
148
|
+
# 另外筛选出符合条件的离子
|
149
|
+
screened = self.df
|
150
|
+
if group_screen:
|
151
|
+
if group_screen_invert:
|
152
|
+
screened = screened[
|
153
|
+
~screened["SMILES"].str.contains(group_screen, regex=False)
|
154
|
+
]
|
155
|
+
else:
|
156
|
+
screened = screened[
|
157
|
+
screened["SMILES"].str.contains(group_screen, regex=False)
|
158
|
+
]
|
159
|
+
if charge_screen:
|
160
|
+
screened = screened[screened["Charge"] == charge_screen]
|
161
|
+
screened_message = f"\nNumber of ions with charge of [{charge_screen}] and {group_name} group: {len(screened)}\n"
|
162
|
+
logging.info(screened_message)
|
163
|
+
# 另外创建文件夹, 并依次处理SMILES码
|
164
|
+
screened_dir = f"{self.converted_dir}/{group_name}_{charge_screen}"
|
165
|
+
os.makedirs(screened_dir, exist_ok=True)
|
166
|
+
for _, row in screened.iterrows():
|
167
|
+
self._convert_SMILES(
|
168
|
+
dir=screened_dir,
|
169
|
+
smiles=row["SMILES"],
|
170
|
+
basename=row[self.base_name],
|
171
|
+
charge=row["Charge"]
|
172
|
+
)
|
173
|
+
|
174
|
+
def dpdisp_gaussian_tasks(self,
|
175
|
+
folders: List[str] = [],
|
176
|
+
machine: str = "",
|
177
|
+
resources: str = "",
|
178
|
+
nodes: int = 1,
|
179
|
+
):
|
180
|
+
"""
|
181
|
+
Based on the dpdispatcher module, prepare and submit files for optimization on remote server or local machine.
|
182
|
+
"""
|
183
|
+
if os.path.exists(self.gaussian_optimized_dir):
|
184
|
+
logging.error(f'The directory {self.gaussian_optimized_dir} has already existed.')
|
185
|
+
return
|
186
|
+
if not folders:
|
187
|
+
logging.error('No available folders for dpdispatcher to process Gaussian tasks.')
|
188
|
+
return
|
189
|
+
# 调整工作目录,减少错误发生
|
190
|
+
os.chdir(self.converted_dir)
|
191
|
+
# 读取machine和resources的参数
|
192
|
+
if machine.endswith(".json"):
|
193
|
+
machine = Machine.load_from_json(machine)
|
194
|
+
elif machine.endswith(".yaml"):
|
195
|
+
machine = Machine.load_from_yaml(machine)
|
196
|
+
else:
|
197
|
+
raise KeyError("Not supported machine file type")
|
198
|
+
if resources.endswith(".json"):
|
199
|
+
resources = Resources.load_from_json(resources)
|
200
|
+
elif resources.endswith(".yaml"):
|
201
|
+
resources = Resources.load_from_yaml(resources)
|
202
|
+
else:
|
203
|
+
raise KeyError("Not supported resources file type")
|
204
|
+
# 由于dpdispatcher对于远程服务器以及本地运行的forward_common_files的默认存放位置不同,因此需要预先进行判断,从而不改动优化脚本
|
205
|
+
machine_inform = machine.serialize()
|
206
|
+
if machine_inform["context_type"] == "SSHContext":
|
207
|
+
# 如果调用远程服务器,则创建二级目录
|
208
|
+
parent = "data/"
|
209
|
+
elif machine_inform["context_type"] == "LocalContext":
|
210
|
+
# 如果在本地运行作业,则只在后续创建一级目录
|
211
|
+
parent = ""
|
212
|
+
|
213
|
+
for folder in folders:
|
214
|
+
folder_dir = os.path.join(self.converted_dir, folder)
|
215
|
+
if not os.path.exists(folder_dir):
|
216
|
+
logging.error(f'Provided folder {folder} is not in the directory {folder_dir}')
|
217
|
+
continue
|
218
|
+
# 获取文件夹中所有以 .gjf 结尾的文件
|
219
|
+
gjf_files = [
|
220
|
+
f for f in os.listdir(folder_dir) if f.endswith(".gjf")
|
221
|
+
]
|
222
|
+
# 创建一个嵌套列表来存储每个节点的任务并将文件平均依次分配给每个节点
|
223
|
+
# 例如:对于10个结构文件任务分发给4个节点的情况,则4个节点领到的任务分别[0, 4, 8], [1, 5, 9], [2, 6], [3, 7]
|
224
|
+
node_jobs = [[] for _ in range(nodes)]
|
225
|
+
for index, file in enumerate(gjf_files):
|
226
|
+
node_index = index % nodes
|
227
|
+
node_jobs[node_index].append(index)
|
228
|
+
task_list = []
|
229
|
+
for pop in range(nodes):
|
230
|
+
forward_files = ["g16_sub.sh"]
|
231
|
+
backward_files = ["log", "err"]
|
232
|
+
# 将所有参数文件各复制一份到每个 task_dir 目录下
|
233
|
+
task_dir = os.path.join(self.converted_dir, f"{parent}pop{pop}")
|
234
|
+
os.makedirs(task_dir, exist_ok=True)
|
235
|
+
for file in forward_files:
|
236
|
+
shutil.copyfile(f"{self.param_dir}/{file}", f"{task_dir}/{file}")
|
237
|
+
for job_i in node_jobs[pop]:
|
238
|
+
# 将分配好的 .gjf 文件添加到对应的上传文件中
|
239
|
+
forward_files.append(gjf_files[job_i])
|
240
|
+
base_name, _ = os.path.splitext(gjf_files[job_i])
|
241
|
+
# 每个 .gjf 文件在优化后都取回对应的 .log、.fchk 输出文件
|
242
|
+
for ext in ['log', 'fchk']:
|
243
|
+
backward_files.append(f'{base_name}.{ext}')
|
244
|
+
shutil.copyfile(
|
245
|
+
f"{folder_dir}/{gjf_files[job_i]}",
|
246
|
+
f"{task_dir}/{gjf_files[job_i]}",
|
247
|
+
)
|
248
|
+
|
249
|
+
remote_task_dir = f"{parent}pop{pop}"
|
250
|
+
command = "chmod +x g16_sub.sh && ./g16_sub.sh"
|
251
|
+
task = Task(
|
252
|
+
command=command,
|
253
|
+
task_work_path=remote_task_dir,
|
254
|
+
forward_files=forward_files,
|
255
|
+
backward_files=backward_files,
|
256
|
+
)
|
257
|
+
task_list.append(task)
|
258
|
+
|
259
|
+
submission = Submission(
|
260
|
+
work_base=self.converted_dir,
|
261
|
+
machine=machine,
|
262
|
+
resources=resources,
|
263
|
+
task_list=task_list,
|
264
|
+
)
|
265
|
+
submission.run_submission()
|
266
|
+
|
267
|
+
# 创建用于存放优化后文件的 gaussian_optimized 目录
|
268
|
+
optimized_folder_dir = os.path.join(self.gaussian_optimized_dir, folder)
|
269
|
+
os.makedirs(optimized_folder_dir, exist_ok=True)
|
270
|
+
for pop in range(nodes):
|
271
|
+
# 从传回目录下的 pop 文件夹中将结果文件取到 gaussian_optimized 目录
|
272
|
+
task_dir = os.path.join(self.converted_dir, f"{parent}pop{pop}")
|
273
|
+
# 按照给定的 .gjf 结构文件读取 .log、 文件并复制
|
274
|
+
for job_i in node_jobs[pop]:
|
275
|
+
base_name, _ = os.path.splitext(gjf_files[job_i])
|
276
|
+
# 在优化后都取回每个 .gjf 文件对应的 .log、.fchk 输出文件
|
277
|
+
for ext in ['gjf', 'log', 'fchk']:
|
278
|
+
shutil.copyfile(
|
279
|
+
f"{task_dir}/{base_name}.{ext}",
|
280
|
+
f"{optimized_folder_dir}/{base_name}.{ext}"
|
281
|
+
)
|
282
|
+
# 在成功完成Gaussian优化后,删除 1_1_SMILES_gjf/{csv}/{parent}/pop{n} 文件夹以节省空间
|
283
|
+
shutil.rmtree(task_dir)
|
284
|
+
shutil.copyfile(
|
285
|
+
os.path.join(self.base_dir, "config.yaml"),
|
286
|
+
os.path.join(optimized_folder_dir, "config.yaml"),
|
287
|
+
)
|
288
|
+
if machine_inform["context_type"] == "SSHContext":
|
289
|
+
# 如果调用远程服务器,则删除data级目录
|
290
|
+
shutil.rmtree(os.path.join(self.converted_dir, parent))
|
291
|
+
logging.info("Batch Gaussian optimization completed!!!")
|