ion-CSP 2.1.5__py3-none-any.whl → 2.1.9__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 +3 -3
- ion_CSP/convert_SMILES.py +39 -11
- ion_CSP/empirical_estimate.py +288 -84
- ion_CSP/gen_opt.py +68 -22
- ion_CSP/identify_molecules.py +15 -0
- ion_CSP/log_and_time.py +55 -8
- ion_CSP/mlp_opt.py +52 -6
- ion_CSP/read_mlp_density.py +15 -1
- {run → ion_CSP/run}/main_EE.py +11 -13
- ion_CSP/task_manager.py +2 -2
- ion_CSP/upload_download.py +0 -1
- ion_CSP/vasp_processing.py +57 -28
- {ion_csp-2.1.5.dist-info → ion_csp-2.1.9.dist-info}/METADATA +44 -16
- ion_csp-2.1.9.dist-info/RECORD +43 -0
- {ion_csp-2.1.5.dist-info → ion_csp-2.1.9.dist-info}/licenses/LICENSE +1 -1
- {ion_csp-2.1.5.dist-info → ion_csp-2.1.9.dist-info}/top_level.txt +0 -1
- ion_csp-2.1.5.dist-info/RECORD +0 -44
- run/update_changelog.py +0 -68
- {run → ion_CSP/run}/__init__.py +0 -0
- {run → ion_CSP/run}/main_CSP.py +0 -0
- {run → ion_CSP/run}/run_convert_SMILES.py +0 -0
- {run → ion_CSP/run}/run_empirical_estimate.py +0 -0
- {run → ion_CSP/run}/run_gen_opt.py +0 -0
- {run → ion_CSP/run}/run_read_mlp_density.py +0 -0
- {run → ion_CSP/run}/run_upload_download.py +0 -0
- {run → ion_CSP/run}/run_vasp_processing.py +0 -0
- {ion_csp-2.1.5.dist-info → ion_csp-2.1.9.dist-info}/WHEEL +0 -0
- {ion_csp-2.1.5.dist-info → ion_csp-2.1.9.dist-info}/entry_points.txt +0 -0
ion_CSP/gen_opt.py
CHANGED
@@ -7,7 +7,7 @@ import subprocess
|
|
7
7
|
import importlib.resources
|
8
8
|
from typing import List
|
9
9
|
from ase.io import read
|
10
|
-
from dpdispatcher import Machine
|
10
|
+
from dpdispatcher import Machine, Resources
|
11
11
|
from pyxtal import pyxtal
|
12
12
|
from pyxtal.msg import Comp_CompatibilityError, Symm_CompatibilityError
|
13
13
|
from ion_CSP.log_and_time import redirect_dpdisp_logging
|
@@ -17,11 +17,13 @@ class CrystalGenerator:
|
|
17
17
|
def __init__(self, work_dir: str, ion_numbers: List[int], species: List[str]):
|
18
18
|
"""
|
19
19
|
Initialize the class based on the provided ionic crystal composition structure files and corresponding composition numbers.
|
20
|
+
|
21
|
+
:params
|
22
|
+
work_dir: The working directory where the ionic crystal structure files are located.
|
23
|
+
ion_numbers: A list of integers representing the number of each ion in the ionic crystal.
|
24
|
+
species: A list of strings representing the species of ions in the ionic crystal.
|
20
25
|
"""
|
21
26
|
redirect_dpdisp_logging(os.path.join(work_dir, "dpdispatcher.log"))
|
22
|
-
# self.script_dir = os.path.dirname(__file__)
|
23
|
-
# self.mlp_opt_file = os.path.join(self.script_dir, "mlp_opt.py")
|
24
|
-
# self.model_file = os.path.join(self.script_dir, "../../model/model.pt")
|
25
27
|
self.mlp_opt_file = importlib.resources.files("ion_CSP").joinpath("mlp_opt.py")
|
26
28
|
self.model_file = importlib.resources.files("ion_CSP.model").joinpath("model.pt")
|
27
29
|
# 获取当前脚本的路径以及同路径下离子晶体组分的结构文件, 并将这一路径作为工作路径来避免可能的错误
|
@@ -55,6 +57,10 @@ class CrystalGenerator:
|
|
55
57
|
"""
|
56
58
|
Private method:
|
57
59
|
Extract numbers from file names, convert them to integers, sort them by sequence, and return a list containing both indexes and file names
|
60
|
+
|
61
|
+
:params
|
62
|
+
directory: The directory where the files are located.
|
63
|
+
prefix_name: The prefix of the file names to be processed, e.g., 'POSCAR_'.
|
58
64
|
"""
|
59
65
|
# 获取dir文件夹中所有以prefix_name开头的文件,在此实例中为POSCAR_
|
60
66
|
files = [f for f in os.listdir(directory) if f.startswith(prefix_name)]
|
@@ -72,6 +78,9 @@ class CrystalGenerator:
|
|
72
78
|
):
|
73
79
|
"""
|
74
80
|
Based on the provided ion species and corresponding numbers, use pyxtal to randomly generate ion crystal structures based on crystal space groups.
|
81
|
+
:params
|
82
|
+
num_per_group: The number of POSCAR files to be generated for each space group, default is 100.
|
83
|
+
space_groups_limit: The maximum number of space groups to be searched, default is 230, which is the total number of space groups.
|
75
84
|
"""
|
76
85
|
# 如果目录不存在,则创建POSCAR_Files文件夹
|
77
86
|
os.makedirs(self.POSCAR_dir, exist_ok=True)
|
@@ -135,7 +144,14 @@ class CrystalGenerator:
|
|
135
144
|
)
|
136
145
|
|
137
146
|
def _single_phonopy_processing(self, filename):
|
138
|
-
|
147
|
+
"""
|
148
|
+
Private method:
|
149
|
+
Process a single POSCAR file using phonopy to generate symmetric primitive cells and conventional cells.
|
150
|
+
|
151
|
+
:params
|
152
|
+
filename: The name of the POSCAR file to be processed.
|
153
|
+
"""
|
154
|
+
# 按顺序处理POSCAR文件,首先复制一份无数字后缀的POSCAR文件
|
139
155
|
shutil.copy(f"{self.POSCAR_dir}/{filename}", f"{self.POSCAR_dir}/POSCAR")
|
140
156
|
try:
|
141
157
|
subprocess.run(["nohup", "phonopy", "--symmetry", "POSCAR"], check=True)
|
@@ -153,7 +169,7 @@ class CrystalGenerator:
|
|
153
169
|
# 检查生成的POSCAR中的原子数,如果不匹配则删除该POSCAR并在日志中记录
|
154
170
|
if cell_atoms != self.cell_atoms:
|
155
171
|
error_message = f"Atom number mismatch ({cell_atoms} vs {self.cell_atoms})"
|
156
|
-
|
172
|
+
print(f"{filename} - {error_message}")
|
157
173
|
|
158
174
|
# 新增:回溯空间群归属
|
159
175
|
poscar_index = int(filename.split('_')[1]) # 提取POSCAR编号
|
@@ -179,7 +195,15 @@ class CrystalGenerator:
|
|
179
195
|
os.remove(f"{self.primitive_cell_dir}/{filename}")
|
180
196
|
|
181
197
|
def _find_space_group(self, poscar_index: int) -> int:
|
182
|
-
"""
|
198
|
+
"""
|
199
|
+
Private method:
|
200
|
+
Find the space group for a given POSCAR index based on the group_counts.
|
201
|
+
|
202
|
+
:params
|
203
|
+
poscar_index: The index of the POSCAR file to find the space group for.
|
204
|
+
|
205
|
+
:return: The space group number corresponding to the POSCAR index.
|
206
|
+
"""
|
183
207
|
cumulative = 0
|
184
208
|
for idx, count in enumerate(self.group_counts, start=1):
|
185
209
|
if cumulative <= poscar_index < cumulative + count:
|
@@ -219,6 +243,11 @@ class CrystalGenerator:
|
|
219
243
|
def dpdisp_mlp_tasks(self, machine: str, resources: str, nodes: int = 1):
|
220
244
|
"""
|
221
245
|
Based on the dpdispatcher module, prepare and submit files for optimization on remote server or local machine.
|
246
|
+
|
247
|
+
params:
|
248
|
+
machine: The machine configuration file for dpdispatcher, can be in JSON or YAML format.
|
249
|
+
resources: The resources configuration file for dpdispatcher, can be in JSON or YAML format.
|
250
|
+
nodes: The number of nodes to be used for optimization, default is 1.
|
222
251
|
"""
|
223
252
|
# 调整工作目录,减少错误发生
|
224
253
|
os.chdir(self.primitive_cell_dir)
|
@@ -233,26 +262,31 @@ class CrystalGenerator:
|
|
233
262
|
machine = Machine.load_from_yaml(machine)
|
234
263
|
else:
|
235
264
|
raise KeyError("Not supported machine file type")
|
265
|
+
if resources.endswith(".json"):
|
266
|
+
resources = Resources.load_from_json(resources)
|
267
|
+
elif resources.endswith(".yaml"):
|
268
|
+
resources = Resources.load_from_yaml(resources)
|
269
|
+
else:
|
270
|
+
raise KeyError("Not supported resources file type")
|
236
271
|
# 由于dpdispatcher对于远程服务器以及本地运行的forward_common_files的默认存放位置不同,因此需要预先进行判断,从而不改动优化脚本
|
237
272
|
machine_inform = machine.serialize()
|
273
|
+
resources_inform = resources.serialize()
|
238
274
|
if machine_inform["context_type"] == "SSHContext":
|
239
275
|
# 如果调用远程服务器,则创建二级目录
|
240
276
|
parent = "data/"
|
241
277
|
elif machine_inform["context_type"] == "LocalContext":
|
242
278
|
# 如果在本地运行作业,则只在后续创建一级目录
|
243
279
|
parent = ""
|
244
|
-
|
245
|
-
|
246
|
-
|
280
|
+
if (
|
281
|
+
machine_inform["batch_type"] == "Shell"
|
282
|
+
and resources_inform["gpu_per_node"] != 0
|
283
|
+
):
|
284
|
+
# 如果是本地运行,则根据显存占用率阈值,等待可用的GPU
|
285
|
+
selected_gpu = _wait_for_gpu(memory_percent_threshold=40, wait_time=600)
|
286
|
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_gpu)
|
247
287
|
|
248
|
-
from dpdispatcher import
|
288
|
+
from dpdispatcher import Task, Submission
|
249
289
|
|
250
|
-
if resources.endswith(".json"):
|
251
|
-
resources = Resources.load_from_json(resources)
|
252
|
-
elif resources.endswith(".yaml"):
|
253
|
-
resources = Resources.load_from_yaml(resources)
|
254
|
-
else:
|
255
|
-
raise KeyError("Not supported resources file type")
|
256
290
|
# 依次读取primitive_cell文件夹中的所有POSCAR文件和对应的序号
|
257
291
|
primitive_cell_file_index_pairs = self._sequentially_read_files(
|
258
292
|
self.primitive_cell_dir, prefix_name="POSCAR_"
|
@@ -337,8 +371,14 @@ class CrystalGenerator:
|
|
337
371
|
logging.info("Batch optimization completed!!!")
|
338
372
|
|
339
373
|
|
340
|
-
def
|
341
|
-
"""
|
374
|
+
def _get_available_gpus(memory_percent_threshold=40):
|
375
|
+
"""
|
376
|
+
Private method:
|
377
|
+
Get available GPUs with memory usage below the specified threshold.
|
378
|
+
|
379
|
+
params:
|
380
|
+
memory_percent_threshold (int): The threshold for GPU memory usage percentage.
|
381
|
+
"""
|
342
382
|
try:
|
343
383
|
# 获取 nvidia-smi 的输出
|
344
384
|
output = subprocess.check_output(
|
@@ -368,10 +408,16 @@ def get_available_gpus(memory_percent_threshold=40):
|
|
368
408
|
return []
|
369
409
|
|
370
410
|
|
371
|
-
def
|
372
|
-
"""
|
411
|
+
def _wait_for_gpu(memory_percent_threshold=40, wait_time=300):
|
412
|
+
"""
|
413
|
+
Private method:
|
414
|
+
Wait until a GPU is available with memory usage below the specified threshold.
|
415
|
+
params:
|
416
|
+
memory_percent_threshold (int): The threshold for GPU memory usage percentage.
|
417
|
+
wait_time (int): The time to wait before checking again, in seconds.
|
418
|
+
"""
|
373
419
|
while True:
|
374
|
-
available_gpus =
|
420
|
+
available_gpus = _get_available_gpus(memory_percent_threshold)
|
375
421
|
logging.info(f"Available GPU: {available_gpus}")
|
376
422
|
if available_gpus:
|
377
423
|
selected_gpu = available_gpus[0]
|
ion_CSP/identify_molecules.py
CHANGED
@@ -7,6 +7,17 @@ from ase.neighborlist import NeighborList, natural_cutoffs
|
|
7
7
|
|
8
8
|
|
9
9
|
def identify_molecules(atoms) -> Tuple[List[Dict[str, int]], bool]:
|
10
|
+
"""
|
11
|
+
Identify independent molecules in a given set of atoms.
|
12
|
+
This function uses a depth-first search (DFS) approach to find connected components in the atomic structure,
|
13
|
+
treating each connected component as a separate molecule.
|
14
|
+
params:
|
15
|
+
atoms: ASE Atoms object containing the atomic structure.
|
16
|
+
returns:
|
17
|
+
A tuple containing:
|
18
|
+
- A list of dictionaries, each representing a molecule with element counts.
|
19
|
+
- A boolean flag indicating whether the identified molecules match the initial set of molecules.
|
20
|
+
"""
|
10
21
|
visited = set() # 用于记录已经访问过的原子索引
|
11
22
|
identified_molecules = [] # 用于存储识别到的独立分子
|
12
23
|
# 基于共价半径为每个原子生成径向截止
|
@@ -63,6 +74,10 @@ def identify_molecules(atoms) -> Tuple[List[Dict[str, int]], bool]:
|
|
63
74
|
def molecules_information(molecules: List[Dict[str, int]], molecules_flag: bool, initial_information: List[Dict[str, int]]):
|
64
75
|
"""
|
65
76
|
Set the output format of the molecule. Output simplified element information in the specified order of C, N, O, H, which may include other elements.
|
77
|
+
params:
|
78
|
+
molecules: A list of dictionaries representing identified molecules with element counts.
|
79
|
+
molecules_flag: A boolean flag indicating whether the identified molecules match the initial set of molecules.
|
80
|
+
initial_information: A list of dictionaries representing the initial set of molecules with element counts.
|
66
81
|
"""
|
67
82
|
# 定义固定顺序的元素
|
68
83
|
fixed_order = ['C', 'N', 'O', 'H']
|
ion_CSP/log_and_time.py
CHANGED
@@ -11,7 +11,14 @@ from dpdispatcher.dlog import dlog
|
|
11
11
|
|
12
12
|
|
13
13
|
def log_and_time(func):
|
14
|
-
"""
|
14
|
+
"""
|
15
|
+
Decorator for recording log information and script runtime
|
16
|
+
|
17
|
+
:params
|
18
|
+
func: The function to be decorated
|
19
|
+
|
20
|
+
:return: The decorated function with logging and timing capabilities
|
21
|
+
"""
|
15
22
|
@functools.wraps(func)
|
16
23
|
def wrapper(work_dir, *args, **kwargs):
|
17
24
|
# 使用inspect获取真实脚本文件名
|
@@ -54,20 +61,49 @@ def log_and_time(func):
|
|
54
61
|
|
55
62
|
|
56
63
|
def merge_config(default_config, user_config, key):
|
64
|
+
"""
|
65
|
+
Merge default configuration with user-provided configuration for a specific key.
|
66
|
+
|
67
|
+
:params
|
68
|
+
default_config: The default configuration dictionary.
|
69
|
+
user_config: The user-provided configuration dictionary.
|
70
|
+
key: The key for which the configuration should be merged.
|
71
|
+
|
72
|
+
:return: A merged configuration dictionary for the specified key.
|
73
|
+
"""
|
74
|
+
if key not in default_config:
|
75
|
+
raise KeyError(f"Key '{key}' not found in default configuration.")
|
76
|
+
if key not in user_config:
|
77
|
+
raise KeyError(f"Key '{key}' not found in user configuration.")
|
78
|
+
if not isinstance(default_config[key], dict) or not isinstance(user_config.get(key, {}), dict):
|
79
|
+
raise TypeError(f"Both default and user configurations for '{key}' must be dictionaries.")
|
80
|
+
# 合并两个参数配置,优先使用用户参数配置
|
57
81
|
return {**default_config[key], **user_config.get(key, {})}
|
58
82
|
|
59
83
|
|
60
84
|
class StatusLogger:
|
85
|
+
"""
|
86
|
+
A singleton class to log the status of a workflow, including RUNNING, SUCCESS, FAILURE, and KILLED.
|
87
|
+
It initializes a logger that writes to a log file and a YAML file to record the status of the workflow.
|
88
|
+
The logger captures the process ID and handles termination signals (SIGINT, SIGTERM).
|
89
|
+
"""
|
90
|
+
_name = "WorkflowLogger"
|
61
91
|
_instance = None
|
62
92
|
|
63
93
|
def __new__(cls, *args, **kwargs):
|
94
|
+
"""Ensure that only one instance of StatusLogger is created (Singleton Pattern)"""
|
64
95
|
if not cls._instance:
|
65
96
|
cls._instance = super(StatusLogger, cls).__new__(cls)
|
66
97
|
cls._instance.__init__(*args, **kwargs)
|
67
98
|
return cls._instance
|
68
99
|
|
69
100
|
def __init__(self, work_dir, task_name):
|
70
|
-
"""
|
101
|
+
"""
|
102
|
+
Initialize workflow status logger and generate the .log and .yaml file to record the status
|
103
|
+
|
104
|
+
:params
|
105
|
+
work_dir: The working directory where the log and yaml files will be created
|
106
|
+
task_name: The name of the task to be logged"""
|
71
107
|
# 使用单例模式,避免重复的日志记录,缺点是再重新给定task_name之后会覆盖原来的实例,只能顺序调用
|
72
108
|
self.task_name = task_name
|
73
109
|
log_file = os.path.join(work_dir, "workflow_status.log")
|
@@ -97,12 +133,17 @@ class StatusLogger:
|
|
97
133
|
self._init_yaml()
|
98
134
|
|
99
135
|
def set_running(self):
|
136
|
+
"""
|
137
|
+
Set the current task status to RUNNING and log the event.
|
138
|
+
This method increments the run count and updates the YAML file.
|
139
|
+
"""
|
100
140
|
self.current_status = "RUNNING"
|
101
141
|
self.logger.info(f"{self.task_name} Status: {self.current_status}")
|
102
142
|
self.run_count += 1
|
103
143
|
self._update_yaml()
|
104
144
|
|
105
145
|
def set_success(self):
|
146
|
+
"""Set the current task status to SUCCESS and log the event"""
|
106
147
|
self.current_status = "SUCCESS"
|
107
148
|
self.logger.info(f"{self.task_name} Status: {self.current_status}\n")
|
108
149
|
self._update_yaml()
|
@@ -112,12 +153,16 @@ class StatusLogger:
|
|
112
153
|
return self.current_status == "SUCCESS"
|
113
154
|
|
114
155
|
def set_failure(self):
|
156
|
+
"""Set the current task status to FAILURE and log the event"""
|
115
157
|
self.current_status = "FAILURE"
|
116
158
|
self.logger.error(f"{self.task_name} Status: {self.current_status}\n")
|
117
159
|
self._update_yaml()
|
118
160
|
|
119
161
|
def _signal_handler(self, signum, _):
|
120
|
-
"""
|
162
|
+
"""
|
163
|
+
Handle termination signals and log the event
|
164
|
+
:params
|
165
|
+
signum: The signal number received (e.g., SIGINT, SIGTERM)"""
|
121
166
|
if signum == 2:
|
122
167
|
self.logger.warning(
|
123
168
|
f"Process {os.getpid()} has been interrupted by 'Ctrl + C'\n"
|
@@ -134,6 +179,7 @@ class StatusLogger:
|
|
134
179
|
sys.exit(0)
|
135
180
|
|
136
181
|
def _set_killed(self):
|
182
|
+
"""Set the current task status to KILLED and log the event"""
|
137
183
|
self.current_status = "KILLED"
|
138
184
|
self.logger.warning(f"{self.task_name} Status: {self.current_status}\n")
|
139
185
|
self._update_yaml()
|
@@ -193,11 +239,12 @@ def redirect_dpdisp_logging(custom_log_path):
|
|
193
239
|
|
194
240
|
|
195
241
|
def get_work_dir_and_config():
|
196
|
-
"""
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
242
|
+
"""
|
243
|
+
Get the working directory and user configuration from command line arguments or interactive input.
|
244
|
+
If the working directory is not specified, it prompts the user to input it interactively.
|
245
|
+
It also reads the configuration from a 'config.yaml' file in the specified directory.
|
246
|
+
|
247
|
+
:return: A tuple containing the working directory and the user configuration dictionary.
|
201
248
|
"""
|
202
249
|
parser = argparse.ArgumentParser(
|
203
250
|
description="The full workflow of ionic crystal design for a certain ion combination, including generation, mlp optimization, screening, vasp optimization and analysis."
|
ion_CSP/mlp_opt.py
CHANGED
@@ -14,13 +14,22 @@ base_dir = os.path.dirname(__file__)
|
|
14
14
|
relative_path = './model.pt'
|
15
15
|
file_path = os.path.join(base_dir, relative_path)
|
16
16
|
calc = DP(file_path)
|
17
|
-
|
17
|
+
"""
|
18
18
|
structure optimization with DP model and ASE
|
19
19
|
PSTRESS and fmax should exist in input.dat
|
20
|
-
|
20
|
+
"""
|
21
21
|
|
22
22
|
def get_element_num(elements):
|
23
|
-
|
23
|
+
"""
|
24
|
+
Using the Atoms.symples to Know Element and Number
|
25
|
+
|
26
|
+
:params
|
27
|
+
elements: list of elements in the structure
|
28
|
+
|
29
|
+
:returns
|
30
|
+
element: list of unique elements in the structure
|
31
|
+
ele: dictionary with elements as keys and their counts as values
|
32
|
+
"""
|
24
33
|
element = []
|
25
34
|
ele = {}
|
26
35
|
element.append(elements[0])
|
@@ -32,7 +41,15 @@ def get_element_num(elements):
|
|
32
41
|
return element, ele
|
33
42
|
|
34
43
|
def write_CONTCAR(element, ele, lat, pos, index):
|
35
|
-
|
44
|
+
"""
|
45
|
+
Write CONTCAR file in VASP format
|
46
|
+
|
47
|
+
:params
|
48
|
+
element: list of elements in the structure
|
49
|
+
ele: dictionary of element counts
|
50
|
+
lat: lattice vectors
|
51
|
+
pos: atomic positions in direct coordinates
|
52
|
+
index: index for the output file"""
|
36
53
|
f = open(f'{base_dir}/CONTCAR_'+str(index),'w')
|
37
54
|
f.write('ASE-DPKit-Optimization\n')
|
38
55
|
f.write('1.0\n')
|
@@ -51,7 +68,21 @@ def write_CONTCAR(element, ele, lat, pos, index):
|
|
51
68
|
f.write('%15.10f %15.10f %15.10f\n' % tuple(dpos[i]))
|
52
69
|
|
53
70
|
def write_OUTCAR(element, ele, masses, volume, lat, pos, ene, force, stress, pstress, index):
|
54
|
-
|
71
|
+
"""
|
72
|
+
Write OUTCAR file in VASP format
|
73
|
+
:params
|
74
|
+
element: list of elements in the structure
|
75
|
+
ele: dictionary of element counts
|
76
|
+
masses: total mass of the atoms
|
77
|
+
volume: volume of the unit cell
|
78
|
+
lat: lattice vectors
|
79
|
+
pos: atomic positions in direct coordinates
|
80
|
+
ene: total energy of the system
|
81
|
+
force: forces on the atoms
|
82
|
+
stress: stress tensor components
|
83
|
+
pstress: external pressure
|
84
|
+
index: index for the output file
|
85
|
+
"""
|
55
86
|
f = open(f'{base_dir}/OUTCAR_'+str(index),'w')
|
56
87
|
for x in element:
|
57
88
|
f.write('VRHFIN =' + str(x) + '\n')
|
@@ -88,6 +119,13 @@ def write_OUTCAR(element, ele, masses, volume, lat, pos, ene, force, stress, pst
|
|
88
119
|
f.write('enthalpy TOTEN = %20.6f %20.6f\n' % (enthalpy, enthalpy/na))
|
89
120
|
|
90
121
|
def get_indexes():
|
122
|
+
"""
|
123
|
+
Get the indexes of POSCAR files in the current directory.
|
124
|
+
This function scans the current directory for files starting with 'POSCAR_' and extracts their numeric indexes.
|
125
|
+
|
126
|
+
:returns
|
127
|
+
A sorted list of indexes extracted from the POSCAR files.
|
128
|
+
"""
|
91
129
|
base_dir = os.path.dirname(__file__)
|
92
130
|
POSCAR_files = [f for f in os.listdir(base_dir) if f.startswith('POSCAR_')]
|
93
131
|
indexes = []
|
@@ -100,7 +138,11 @@ def get_indexes():
|
|
100
138
|
return indexes
|
101
139
|
|
102
140
|
def run_opt(index: int):
|
103
|
-
|
141
|
+
"""
|
142
|
+
Using the ASE&DP to Optimize Configures
|
143
|
+
:params
|
144
|
+
index: index of the POSCAR file to be optimized
|
145
|
+
"""
|
104
146
|
if os.path.isfile(f'{base_dir}/OUTCAR'):
|
105
147
|
os.system(f'mv {base_dir}/OUTCAR {base_dir}/OUTCAR-last')
|
106
148
|
fmax, pstress = 0.03, 0
|
@@ -143,6 +185,10 @@ def run_opt(index: int):
|
|
143
185
|
|
144
186
|
|
145
187
|
def main():
|
188
|
+
"""
|
189
|
+
Main function to run the optimization in parallel.
|
190
|
+
It initializes a multiprocessing pool and maps the run_opt function to the indexes of POSCAR files.
|
191
|
+
"""
|
146
192
|
ctx=multiprocessing.get_context("spawn")
|
147
193
|
pool=ctx.Pool(8)
|
148
194
|
indexes = get_indexes()
|
ion_CSP/read_mlp_density.py
CHANGED
@@ -9,6 +9,13 @@ from ion_CSP.identify_molecules import identify_molecules, molecules_information
|
|
9
9
|
class ReadMlpDensity:
|
10
10
|
|
11
11
|
def __init__(self, work_dir:str):
|
12
|
+
"""
|
13
|
+
This class is designed to read and process MLP optimized files, specifically CONTCAR files, to calculate and sort their densities.
|
14
|
+
The class also provides functionality to process these files using phonopy for symmetry analysis and primitive cell generation.
|
15
|
+
|
16
|
+
:params
|
17
|
+
work_dir: The working directory where the MLP optimized files are located.
|
18
|
+
"""
|
12
19
|
# 获取脚本的当前目录
|
13
20
|
self.base_dir = work_dir
|
14
21
|
os.chdir(self.base_dir)
|
@@ -39,7 +46,10 @@ class ReadMlpDensity:
|
|
39
46
|
"""
|
40
47
|
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.
|
41
48
|
|
42
|
-
:
|
49
|
+
:params
|
50
|
+
n_screen: The number of CONTCAR files with the highest density to be saved.
|
51
|
+
molecules_screen: If True, only consider ionic crystals with original ions.
|
52
|
+
detail_log: If True, print detailed information about the molecules identified in the CONTCAR files.
|
43
53
|
"""
|
44
54
|
os.chdir(self.base_dir)
|
45
55
|
# 获取所有以'CONTCAR_'开头的文件,并按数字顺序处理
|
@@ -135,6 +145,10 @@ class ReadMlpDensity:
|
|
135
145
|
def phonopy_processing_max_density(self, specific_directory :str = None):
|
136
146
|
"""
|
137
147
|
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.
|
148
|
+
|
149
|
+
:params
|
150
|
+
specific_directory: If specified, phonopy will process the files in this directory instead of the max_density directory.
|
151
|
+
If not specified, it will process the files in the max_density directory.
|
138
152
|
"""
|
139
153
|
if specific_directory:
|
140
154
|
self.phonopy_dir = os.path.join(self.base_dir, specific_directory)
|
{run → ion_CSP/run}/main_EE.py
RENAMED
@@ -1,4 +1,3 @@
|
|
1
|
-
import os
|
2
1
|
import logging
|
3
2
|
from ion_CSP.convert_SMILES import SmilesProcessing
|
4
3
|
from ion_CSP.empirical_estimate import EmpiricalEstimation
|
@@ -31,11 +30,10 @@ DEFAULT_CONFIG = {
|
|
31
30
|
@log_and_time
|
32
31
|
def main(work_dir, config):
|
33
32
|
logging.info(f"Using config: {config}")
|
34
|
-
empirical_estimate_dir = os.path.join(work_dir, "1_2_Gaussian_optimized")
|
35
33
|
tasks = {
|
36
34
|
"0_convertion": lambda: convertion_task(work_dir, config),
|
37
|
-
"0_estimation": lambda: estimation_task(
|
38
|
-
"0_update_combo": lambda: combination_task(
|
35
|
+
"0_estimation": lambda: estimation_task(work_dir, config),
|
36
|
+
"0_update_combo": lambda: combination_task(work_dir, config),
|
39
37
|
}
|
40
38
|
for task_name, task_func in tasks.items():
|
41
39
|
task_logger = StatusLogger(work_dir=work_dir, task_name=task_name)
|
@@ -52,7 +50,7 @@ def main(work_dir, config):
|
|
52
50
|
task_logger = StatusLogger(work_dir=work_dir, task_name="0_update_combo")
|
53
51
|
try:
|
54
52
|
task_logger.set_running()
|
55
|
-
combination_task(
|
53
|
+
combination_task(work_dir, config)
|
56
54
|
task_logger.set_success()
|
57
55
|
except Exception:
|
58
56
|
task_logger.set_failure()
|
@@ -94,14 +92,6 @@ def estimation_task(work_dir, config):
|
|
94
92
|
estimation.multiwfn_process_fchk_to_json()
|
95
93
|
# 由于后续晶体生成不支持 .log 文件,需要将 Gaussian 优化得到的 .log 文件最后一帧转为 .gjf 结构文件
|
96
94
|
estimation.gaussian_log_to_optimized_gjf()
|
97
|
-
# 如果依据密度排序,则需要经验公式根据配比生成离子晶体组合,读取 .json 文件并将静电势分析得到的各离子性质代入经验公式
|
98
|
-
if config["empirical_estimate"]["sort_by"] == "density":
|
99
|
-
# 最终将预测的离子晶体密度以及对应的组分输出到 .csv 文件并根据密度从大到小排序
|
100
|
-
estimation.empirical_estimate()
|
101
|
-
# 如果依据氮含量排序,则调用另一套根据 .gjf 文件中化学分布信息
|
102
|
-
elif config["empirical_estimate"]["sort_by"] == "nitrogen":
|
103
|
-
# 最终将预测的离子晶体氮含量以及对应的组分输出到 .csv 文件并根据氮含量从大到小排序
|
104
|
-
estimation.nitrogen_content_estimate()
|
105
95
|
|
106
96
|
def combination_task(work_dir, config):
|
107
97
|
# 在工作目录下准备 Gaussian 优化处理后具有 .gjf、.fchk 和 .log 文件的文件夹, 并提供对应的离子配比
|
@@ -111,6 +101,14 @@ def combination_task(work_dir, config):
|
|
111
101
|
ratios=config["empirical_estimate"]["ratios"],
|
112
102
|
sort_by=config["empirical_estimate"]["sort_by"],
|
113
103
|
)
|
104
|
+
# 如果依据密度排序,则需要经验公式根据配比生成离子晶体组合,读取 .json 文件并将静电势分析得到的各离子性质代入经验公式
|
105
|
+
if config["empirical_estimate"]["sort_by"] == "density":
|
106
|
+
# 最终将预测的离子晶体密度以及对应的组分输出到 .csv 文件并根据密度从大到小排序
|
107
|
+
combination.empirical_estimate()
|
108
|
+
# 如果依据氮含量排序,则调用另一套根据 .gjf 文件中化学分布信息
|
109
|
+
elif config["empirical_estimate"]["sort_by"] == "nitrogen":
|
110
|
+
# 最终将预测的离子晶体氮含量以及对应的组分输出到 .csv 文件并根据氮含量从大到小排序
|
111
|
+
combination.nitrogen_content_estimate()
|
114
112
|
# 基于排序依据 sort_by 对应的 .csv 文件创建 combo_n 文件夹,并复制相应的 .gjf 结构文件。
|
115
113
|
if config["empirical_estimate"]["make_combo_dir"]:
|
116
114
|
combination.make_combo_dir(
|
ion_CSP/task_manager.py
CHANGED
@@ -31,7 +31,7 @@ class TaskManager:
|
|
31
31
|
try:
|
32
32
|
return importlib.metadata.version("ion_CSP")
|
33
33
|
except importlib.metadata.PackageNotFoundError:
|
34
|
-
logging.error("
|
34
|
+
logging.error("Package not found")
|
35
35
|
return "unknown"
|
36
36
|
except Exception as e:
|
37
37
|
logging.error(f"Version detection failed: {e}")
|
@@ -225,7 +225,7 @@ class TaskManager:
|
|
225
225
|
pid_file = work_dir / "pid.txt"
|
226
226
|
|
227
227
|
# 动态加载模块
|
228
|
-
module_name = f"run.main_{module}"
|
228
|
+
module_name = f"ion_CSP.run.main_{module}"
|
229
229
|
spec = importlib.util.find_spec(module_name)
|
230
230
|
if not spec:
|
231
231
|
raise ImportError(f"Module {module_name} not found")
|
ion_CSP/upload_download.py
CHANGED