ion-CSP 2.2.0__tar.gz → 2.2.2__tar.gz
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-2.2.0 → ion_csp-2.2.2}/CHANGELOG.md +24 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/PKG-INFO +2 -2
- {ion_csp-2.2.0 → ion_csp-2.2.2}/pyproject.toml +1 -1
- {ion_csp-2.2.0 → ion_csp-2.2.2}/requirements.txt +1 -1
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/__init__.py +2 -2
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/convert_SMILES.py +5 -3
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/gen_opt.py +6 -6
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/mlp_opt.py +14 -9
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/read_mlp_density.py +7 -3
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/main_EE.py +2 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/task_manager.py +4 -2
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP.egg-info/PKG-INFO +2 -2
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP.egg-info/SOURCES.txt +1 -1
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP.egg-info/requires.txt +1 -1
- ion_csp-2.2.2/tests/test_identify_molecules.py +171 -0
- ion_csp-2.2.0/tests/test1_task_manager1.py +0 -121
- {ion_csp-2.2.0 → ion_csp-2.2.2}/.dockerignore +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/.github/workflows/conda-env-build.yml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/.github/workflows/pypi-publish.yml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/.gitignore +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/.vscode/settings.json +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/CODE_OF_CONDUCT.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/CONTRIBUTING.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/Dockerfile +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/LICENSE +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/MANIFEST.in +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/README.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/SECURITY.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/config/complete_config.yaml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/config/simple_config.yaml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/config/usage.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/docs/example_usage_CSP.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/docs/example_usage_EE.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/docs/index.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/docs/usage.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/environment.yml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/example_1/config.yaml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/example_1/ions.csv +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/example_2/combo_1/ACEGUL.gjf +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/example_2/combo_1/AGIDOM.gjf +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/example_2/combo_1/config.yaml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/example_usage.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/server/example_local_machine.yaml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/server/example_local_resources.yaml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/server/example_remote_machine.yaml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/examples/server/example_remote_resources.yaml +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/makefile +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/model/model.pt +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/model/options/README.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/model/options/model.ckpt-4000000.pt +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/INCAR_0 +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/INCAR_1 +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/INCAR_2 +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/INCAR_3 +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/POTCAR_C +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/POTCAR_H +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/POTCAR_N +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/POTCAR_O +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/g16_sub.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/sub_final.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/sub_ori.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/param/sub_supple.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/scripts/CLI.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/scripts/CLI.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/scripts/main_CSP.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/scripts/main_EE.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/scripts/update_changelog.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/setup.cfg +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/setup.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/__init__.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/__main__.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/empirical_estimate.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/identify_molecules.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/log_and_time.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/model/model.pt +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/model/options/README.md +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/model/options/model.ckpt-4000000.pt +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/INCAR_0 +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/INCAR_1 +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/INCAR_2 +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/INCAR_3 +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/POTCAR_C +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/POTCAR_H +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/POTCAR_N +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/POTCAR_O +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/g16_sub.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/sub_final.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/sub_ori.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/param/sub_supple.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/__init__.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/main_CSP.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/run_convert_SMILES.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/run_empirical_estimate.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/run_gen_opt.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/run_read_mlp_density.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/run_upload_download.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/run/run_vasp_processing.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/steps_opt_monitor.sh +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/upload_download.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP/vasp_processing.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP.egg-info/dependency_links.txt +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP.egg-info/entry_points.txt +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/src/ion_CSP.egg-info/top_level.txt +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/tests/__init__.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/tests/test_log_and_time.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/tests/test_task_manager.py +0 -0
- {ion_csp-2.2.0 → ion_csp-2.2.2}/uv.lock +0 -0
@@ -2,6 +2,30 @@
|
|
2
2
|
|
3
3
|
## Latest Changes
|
4
4
|
|
5
|
+
### d914d90 (2025-07-03)
|
6
|
+
|
7
|
+
Update the version to 2.2.2. The redundant output content of the console is greatly reduced to control the size of the console. log file. Specifically, the redundant log records are reduced in gen_opt when exceptions occur in the structure generation process, and the redundant log records in gen_opt and read_mlp_density are reduced when calling the phonopy toolkit.
|
8
|
+
|
9
|
+
## V2.2.1
|
10
|
+
|
11
|
+
### 0a77f51 (2025-07-01)
|
12
|
+
|
13
|
+
Due to the unsatisfactory performance of Mattersim in testing, we switched back to fine-tuning the machine learning potential of DP. The performance of ExpCellFilter in testing is easier to identify experimental structures compared to UnitCellFilter, so it is changed to call ExpCellFilter in mlp_opt.py.
|
14
|
+
|
15
|
+
### 1754cc7 (2025-06-30)
|
16
|
+
|
17
|
+
Update version number to 2.2.1, fix file copying logic in main_EE workflow, update mlp_opt.py to use MatterSimCalculator to develop calculation efficiency.
|
18
|
+
|
19
|
+
### d6abfb5 (2025-06-28)
|
20
|
+
|
21
|
+
Optimize the environment variable settings in mlp_opt.py to disable numpy's multithreading and ensure the stability of multi-threaded computation.
|
22
|
+
|
23
|
+
## V2.2.0
|
24
|
+
|
25
|
+
### 18b9eba (2025-06-28)
|
26
|
+
|
27
|
+
Optimize the file copying logic in vasp_processing to ensure that copying operations are only performed when the file exists; Update sorting parameters to support 'NC_ratio' and modify the relevant CSV file names accordingly.
|
28
|
+
|
5
29
|
### 14ffac2 (2025-06-27)
|
6
30
|
|
7
31
|
Update version number to 2.2.0, optimize file replication logic in VASP processing, add error handling to ensure errors are recorded when files do not exist, modify README and CHANGELOG to reflect the latest changes.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ion_CSP
|
3
|
-
Version: 2.2.
|
3
|
+
Version: 2.2.2
|
4
4
|
Summary: Crystal Structure Design Software Based on Molecular/Ionic Configuration.
|
5
5
|
Home-page: https://github.com/bagabaga007/ion_CSP
|
6
6
|
Author: Ze Yang
|
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
15
15
|
Requires-Dist: ase==3.23.0
|
16
16
|
Requires-Dist: scipy==1.15.2
|
17
|
-
Requires-Dist: deepmd-kit
|
17
|
+
Requires-Dist: deepmd-kit>=3.0.1
|
18
18
|
Requires-Dist: torch==2.5.0
|
19
19
|
Requires-Dist: dpdispatcher==0.6.7
|
20
20
|
Requires-Dist: numpy==1.26.4
|
@@ -131,7 +131,7 @@ class SmilesProcessing:
|
|
131
131
|
f"{self.converted_dir}/charge_{charge}"
|
132
132
|
)
|
133
133
|
os.makedirs(charge_dir, exist_ok=True)
|
134
|
-
# 通过
|
134
|
+
# 通过_convert_SMILES函数依次处理SMILES码
|
135
135
|
for _, row in group.iterrows():
|
136
136
|
result_code, basename = self._convert_SMILES(
|
137
137
|
dir=charge_dir,
|
@@ -238,8 +238,10 @@ class SmilesProcessing:
|
|
238
238
|
for folder in folders:
|
239
239
|
folder_dir = os.path.join(self.converted_dir, folder)
|
240
240
|
if not os.path.exists(folder_dir):
|
241
|
-
|
242
|
-
|
241
|
+
folder_dir = os.path.join(self.base_dir, folder)
|
242
|
+
if not os.path.exists(folder_dir):
|
243
|
+
logging.error(f'Provided folder {folder} is not either in the work directory or the converted directory.\n')
|
244
|
+
continue
|
243
245
|
# 获取文件夹中所有以 .gjf 结尾的文件
|
244
246
|
gjf_files = [
|
245
247
|
f for f in os.listdir(folder_dir) if f.endswith(".gjf")
|
@@ -116,9 +116,8 @@ class CrystalGenerator:
|
|
116
116
|
pyxtal_structure.to_file(POSCAR_path, fmt="poscar")
|
117
117
|
total_count += 1
|
118
118
|
group_count += 1
|
119
|
+
# 捕获对于某一空间群生成结构的运行时间过长、组成兼容性错误、对称性兼容性错误等异常,使结构生成能够完全进行而不中断
|
119
120
|
except (RuntimeError, Comp_CompatibilityError, Symm_CompatibilityError) as e:
|
120
|
-
# 捕获对于某一空间群生成结构的运行时间过长、组成兼容性错误、对称性兼容性错误等异常,使结构生成能够完全进行而不中断
|
121
|
-
logging.error(f"Generating structure error: {e}")
|
122
121
|
# 记录异常类型并跳出当前空间群的生成循环
|
123
122
|
exception_message = type(e).__name__
|
124
123
|
break
|
@@ -154,7 +153,11 @@ class CrystalGenerator:
|
|
154
153
|
# 按顺序处理POSCAR文件,首先复制一份无数字后缀的POSCAR文件
|
155
154
|
shutil.copy(f"{self.POSCAR_dir}/{filename}", f"{self.POSCAR_dir}/POSCAR")
|
156
155
|
try:
|
157
|
-
subprocess.run(
|
156
|
+
subprocess.run(
|
157
|
+
["nohup", "phonopy", "--symmetry", "POSCAR"],
|
158
|
+
check=True,
|
159
|
+
stdout=subprocess.DEVNULL
|
160
|
+
)
|
158
161
|
except subprocess.CalledProcessError as e:
|
159
162
|
# 新增:捕获phonopy执行错误
|
160
163
|
logging.error(f"Phonopy execution failed for {filename}: {str(e)}")
|
@@ -168,9 +171,6 @@ class CrystalGenerator:
|
|
168
171
|
|
169
172
|
# 检查生成的POSCAR中的原子数,如果不匹配则删除该POSCAR并在日志中记录
|
170
173
|
if cell_atoms != self.cell_atoms:
|
171
|
-
error_message = f"Atom number mismatch ({cell_atoms} vs {self.cell_atoms})"
|
172
|
-
print(f"{filename} - {error_message}")
|
173
|
-
|
174
174
|
# 新增:回溯空间群归属
|
175
175
|
poscar_index = int(filename.split('_')[1]) # 提取POSCAR编号
|
176
176
|
space_group = self._find_space_group(poscar_index)
|
@@ -1,23 +1,27 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
# -*- coding: utf-8 -*-
|
3
3
|
import os
|
4
|
+
os.environ["MKL_NUM_THREADS"] = "1"
|
5
|
+
os.environ["NUMEXPR_NUM_THREADS"] = "1"
|
6
|
+
os.environ["OMP_NUM_THREADS"] = "1"
|
7
|
+
|
4
8
|
import time
|
5
9
|
import numpy as np
|
6
10
|
import multiprocessing
|
7
11
|
from ase.io.vasp import read_vasp
|
8
12
|
from ase.optimize import LBFGS
|
9
|
-
from ase.constraints import UnitCellFilter
|
13
|
+
# from ase.constraints import UnitCellFilter
|
14
|
+
from ase.filters import ExpCellFilter
|
10
15
|
from deepmd.calculator import DP
|
16
|
+
# from mattersim.forcefield import MatterSimCalculator
|
11
17
|
|
12
18
|
# 根据脚本位置确定model.pt文件的位置, 减少错误发生
|
13
19
|
base_dir = os.path.dirname(__file__)
|
14
20
|
relative_path = './model.pt'
|
15
21
|
file_path = os.path.join(base_dir, relative_path)
|
16
22
|
calc = DP(file_path)
|
17
|
-
""
|
18
|
-
|
19
|
-
PSTRESS and fmax should exist in input.dat
|
20
|
-
"""
|
23
|
+
# calc = MatterSimCalculator(device="cuda")
|
24
|
+
|
21
25
|
|
22
26
|
def get_element_num(elements):
|
23
27
|
"""
|
@@ -51,7 +55,7 @@ def write_CONTCAR(element, ele, lat, pos, index):
|
|
51
55
|
pos: atomic positions in direct coordinates
|
52
56
|
index: index for the output file"""
|
53
57
|
f = open(f'{base_dir}/CONTCAR_'+str(index),'w')
|
54
|
-
f.write('ASE-
|
58
|
+
f.write('ASE-MLP-Optimization\n')
|
55
59
|
f.write('1.0\n')
|
56
60
|
for i in range(3):
|
57
61
|
f.write('%15.10f %15.10f %15.10f\n' % tuple(lat[i]))
|
@@ -139,7 +143,7 @@ def get_indexes():
|
|
139
143
|
|
140
144
|
def run_opt(index: int):
|
141
145
|
"""
|
142
|
-
Using the ASE&
|
146
|
+
Using the ASE & MLP to Optimize Configures
|
143
147
|
:params
|
144
148
|
index: index of the POSCAR file to be optimized
|
145
149
|
"""
|
@@ -147,7 +151,7 @@ def run_opt(index: int):
|
|
147
151
|
os.system(f'mv {base_dir}/OUTCAR {base_dir}/OUTCAR-last')
|
148
152
|
fmax, pstress = 0.03, 0
|
149
153
|
|
150
|
-
print('Start to Optimize Structures by
|
154
|
+
print('Start to Optimize Structures by MLP----------')
|
151
155
|
|
152
156
|
Opt_Step = 2000
|
153
157
|
start = time.time()
|
@@ -158,7 +162,8 @@ def run_opt(index: int):
|
|
158
162
|
aim_stress = 1.0 * pstress * 0.01 * 0.6242 / 10.0
|
159
163
|
atoms = read_vasp('POSCAR_'+str(index))
|
160
164
|
atoms.calc = calc
|
161
|
-
ucf = UnitCellFilter(atoms, scalar_pressure=aim_stress)
|
165
|
+
# ucf = UnitCellFilter(atoms, scalar_pressure=aim_stress)
|
166
|
+
ucf = ExpCellFilter(atoms, scalar_pressure=aim_stress)
|
162
167
|
# optimization
|
163
168
|
opt = LBFGS(ucf)
|
164
169
|
opt.run(fmax=fmax,steps=Opt_Step)
|
@@ -8,7 +8,7 @@ from ion_CSP.identify_molecules import identify_molecules, molecules_information
|
|
8
8
|
|
9
9
|
class ReadMlpDensity:
|
10
10
|
|
11
|
-
def __init__(self, work_dir:str):
|
11
|
+
def __init__(self, work_dir:str, folder:str = '2_mlp_optimized'):
|
12
12
|
"""
|
13
13
|
This class is designed to read and process MLP optimized files, specifically CONTCAR files, to calculate and sort their densities.
|
14
14
|
The class also provides functionality to process these files using phonopy for symmetry analysis and primitive cell generation.
|
@@ -20,7 +20,7 @@ class ReadMlpDensity:
|
|
20
20
|
self.base_dir = work_dir
|
21
21
|
os.chdir(self.base_dir)
|
22
22
|
# 寻找同一目录下的2_mlp_optimized文件夹
|
23
|
-
self.folder_dir = os.path.join(self.base_dir,
|
23
|
+
self.folder_dir = os.path.join(self.base_dir, folder)
|
24
24
|
self.max_density_dir = os.path.join(self.folder_dir, 'max_density')
|
25
25
|
self.primitive_cell_dir = os.path.join(self.folder_dir, 'primitive_cell')
|
26
26
|
print(f"Processing MLP CONTCARs in {self.folder_dir}")
|
@@ -174,7 +174,11 @@ class ReadMlpDensity:
|
|
174
174
|
# 使用phonopy模块处理POSCAR结构文件,获取对称化的原胞和常规胞。
|
175
175
|
# 应用晶体的对称操作优化后的原胞可以最好地符合晶体的对称性,减少后续优化计算的复杂性。
|
176
176
|
log.write(f'\nProcessing file: {new_CONTCAR_filename}\n')
|
177
|
-
result = subprocess.run(
|
177
|
+
result = subprocess.run(
|
178
|
+
["nohup", "phonopy", "--symmetry", "POSCAR"],
|
179
|
+
check=True,
|
180
|
+
stdout=subprocess.DEVNULL
|
181
|
+
)
|
178
182
|
log.write(f'Finished processing file: {new_CONTCAR_filename} with return code: {result.returncode}\n')
|
179
183
|
# 将phonopy生成的PPOSCAR(对称化原胞)和BPOSCAR(对称化常规胞)放到对应的文件夹中,并将文件名改回POSCAR_index
|
180
184
|
shutil.move(f'{self.phonopy_dir}/PPOSCAR', f'{self.primitive_cell_dir}/{new_CONTCAR_filename}')
|
@@ -109,6 +109,8 @@ def combination_task(work_dir, config):
|
|
109
109
|
elif config["empirical_estimate"]["sort_by"] == "nitrogen":
|
110
110
|
# 最终将预测的离子晶体氮含量以及对应的组分输出到 .csv 文件并根据氮含量从大到小排序
|
111
111
|
combination.nitrogen_content_estimate()
|
112
|
+
elif config["empirical_estimate"]["sort_by"] == "NC_ratio":
|
113
|
+
combination.carbon_nitrogen_ratio_estimate()
|
112
114
|
# 基于排序依据 sort_by 对应的 .csv 文件创建 combo_n 文件夹,并复制相应的 .gjf 结构文件。
|
113
115
|
if config["empirical_estimate"]["make_combo_dir"]:
|
114
116
|
combination.make_combo_dir(
|
@@ -219,7 +219,9 @@ class TaskManager:
|
|
219
219
|
def task_runner(self, module: str, work_dir: str):
|
220
220
|
"""任务执行器 - Task execution handler"""
|
221
221
|
work_dir = Path(work_dir)
|
222
|
-
|
222
|
+
if not os.path.exists(work_dir):
|
223
|
+
print(f"Work directory {work_dir} does not exist")
|
224
|
+
return
|
223
225
|
|
224
226
|
console_log = work_dir / f"main_{module}_console.log"
|
225
227
|
pid_file = work_dir / "pid.txt"
|
@@ -262,7 +264,7 @@ class TaskManager:
|
|
262
264
|
std_log.symlink_to(output_log)
|
263
265
|
|
264
266
|
print('Starting task ......')
|
265
|
-
time.sleep(
|
267
|
+
time.sleep(3)
|
266
268
|
logging.info(f"Started {module} module (PID: {process.pid})")
|
267
269
|
print(f"Task started (PID: {process.pid})")
|
268
270
|
print(f"Normalized log file: {std_log}")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ion_CSP
|
3
|
-
Version: 2.2.
|
3
|
+
Version: 2.2.2
|
4
4
|
Summary: Crystal Structure Design Software Based on Molecular/Ionic Configuration.
|
5
5
|
Home-page: https://github.com/bagabaga007/ion_CSP
|
6
6
|
Author: Ze Yang
|
@@ -14,7 +14,7 @@ Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
15
15
|
Requires-Dist: ase==3.23.0
|
16
16
|
Requires-Dist: scipy==1.15.2
|
17
|
-
Requires-Dist: deepmd-kit
|
17
|
+
Requires-Dist: deepmd-kit>=3.0.1
|
18
18
|
Requires-Dist: torch==2.5.0
|
19
19
|
Requires-Dist: dpdispatcher==0.6.7
|
20
20
|
Requires-Dist: numpy==1.26.4
|
@@ -100,6 +100,6 @@ src/ion_CSP/run/run_read_mlp_density.py
|
|
100
100
|
src/ion_CSP/run/run_upload_download.py
|
101
101
|
src/ion_CSP/run/run_vasp_processing.py
|
102
102
|
tests/__init__.py
|
103
|
-
tests/
|
103
|
+
tests/test_identify_molecules.py
|
104
104
|
tests/test_log_and_time.py
|
105
105
|
tests/test_task_manager.py
|
@@ -0,0 +1,171 @@
|
|
1
|
+
import os
|
2
|
+
import logging
|
3
|
+
import pytest
|
4
|
+
from ase import Atoms
|
5
|
+
from ion_CSP.identify_molecules import identify_molecules, molecules_information
|
6
|
+
|
7
|
+
# 配置日志
|
8
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
9
|
+
|
10
|
+
|
11
|
+
def create_gjf_file(path: str, filename: str, atoms: Atoms):
|
12
|
+
"""手动创建临时的GJF文件"""
|
13
|
+
filepath = os.path.join(path, filename)
|
14
|
+
with open(filepath, "w") as f:
|
15
|
+
f.write("#N\n\n") # Gaussian 文件头
|
16
|
+
f.write("Generated by ASE test\n\n")
|
17
|
+
f.write("0 1\n")
|
18
|
+
for atom in atoms:
|
19
|
+
f.write(f"{atom.symbol} {atom.x} {atom.y} {atom.z}\n")
|
20
|
+
f.write("\n\n") # 文件结束
|
21
|
+
return filepath
|
22
|
+
|
23
|
+
|
24
|
+
@pytest.fixture
|
25
|
+
def setup_test_environment(tmp_path):
|
26
|
+
"""设置测试环境,创建临时目录并切换工作目录"""
|
27
|
+
os.chdir(tmp_path)
|
28
|
+
return tmp_path
|
29
|
+
|
30
|
+
|
31
|
+
def test_single_water_molecule(setup_test_environment):
|
32
|
+
"""测试单个水分子"""
|
33
|
+
# 创建初始GJF文件
|
34
|
+
water = Atoms("OH2", positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
35
|
+
create_gjf_file(setup_test_environment, "water.gjf", water)
|
36
|
+
|
37
|
+
# 运行识别函数
|
38
|
+
merged_molecules, molecules_flag, initial_info = identify_molecules(water)
|
39
|
+
|
40
|
+
# 验证结果
|
41
|
+
expected_molecule = frozenset([("O", 1), ("H", 2)])
|
42
|
+
assert len(merged_molecules) == 1
|
43
|
+
assert expected_molecule in merged_molecules
|
44
|
+
assert merged_molecules[expected_molecule] == 1
|
45
|
+
assert molecules_flag is True
|
46
|
+
assert initial_info == [{"O": 1, "H": 2}]
|
47
|
+
|
48
|
+
|
49
|
+
def test_two_isolated_water_molecules(setup_test_environment):
|
50
|
+
"""测试两个独立的水分子"""
|
51
|
+
# 创建两个初始GJF文件
|
52
|
+
water1 = Atoms("OH2", positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
53
|
+
water2 = Atoms("OH2", positions=[[10, 0, 0], [11, 0, 0], [10, 1, 0]])
|
54
|
+
create_gjf_file(setup_test_environment, "water1.gjf", water1)
|
55
|
+
create_gjf_file(setup_test_environment, "water2.gjf", water2)
|
56
|
+
|
57
|
+
# 合并两个分子
|
58
|
+
combined = water1 + water2
|
59
|
+
|
60
|
+
# 运行识别函数
|
61
|
+
merged_molecules, molecules_flag, initial_info = identify_molecules(combined)
|
62
|
+
|
63
|
+
# 验证结果
|
64
|
+
expected_molecule = frozenset([("O", 1), ("H", 2)])
|
65
|
+
assert len(merged_molecules) == 1
|
66
|
+
assert expected_molecule in merged_molecules
|
67
|
+
assert merged_molecules[expected_molecule] == 2
|
68
|
+
assert molecules_flag is True
|
69
|
+
|
70
|
+
|
71
|
+
def test_water_and_methane_molecules(setup_test_environment):
|
72
|
+
"""测试水分子和甲烷分子"""
|
73
|
+
# 创建初始GJF文件
|
74
|
+
water = Atoms("OH2", positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
75
|
+
methane = Atoms(
|
76
|
+
"CH4", positions=[[10, 0, 0], [11, 0, 0], [10, 1, 0], [10, 0, 1], [9, 0, 0]]
|
77
|
+
)
|
78
|
+
create_gjf_file(setup_test_environment, "water.gjf", water)
|
79
|
+
create_gjf_file(setup_test_environment, "methane.gjf", methane)
|
80
|
+
|
81
|
+
# 合并两个分子
|
82
|
+
combined = water + methane
|
83
|
+
|
84
|
+
# 运行识别函数
|
85
|
+
merged_molecules, molecules_flag, initial_info = identify_molecules(combined)
|
86
|
+
|
87
|
+
# 验证结果
|
88
|
+
water_mol = frozenset([("O", 1), ("H", 2)])
|
89
|
+
methane_mol = frozenset([("C", 1), ("H", 4)])
|
90
|
+
|
91
|
+
assert len(merged_molecules) == 2
|
92
|
+
assert water_mol in merged_molecules
|
93
|
+
assert methane_mol in merged_molecules
|
94
|
+
assert merged_molecules[water_mol] == 1
|
95
|
+
assert merged_molecules[methane_mol] == 1
|
96
|
+
assert molecules_flag is True
|
97
|
+
assert sorted(initial_info, key=lambda x: sorted(x.items())) == sorted(
|
98
|
+
[{"O": 1, "H": 2}, {"C": 1, "H": 4}], key=lambda x: sorted(x.items())
|
99
|
+
)
|
100
|
+
|
101
|
+
|
102
|
+
def test_mismatched_molecules(setup_test_environment):
|
103
|
+
"""测试分子不匹配的情况"""
|
104
|
+
# 只创建一个初始GJF文件
|
105
|
+
water = Atoms("OH2", positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
106
|
+
create_gjf_file(setup_test_environment, "water.gjf", water)
|
107
|
+
|
108
|
+
# 创建两个独立的水分子
|
109
|
+
water1 = Atoms("OH", positions=[[0, 0, 0], [1, 0, 0]])
|
110
|
+
water2 = Atoms("OH3", positions=[[10, 0, 0], [11, 0, 0], [10, 1, 0], [10, 1, 1]])
|
111
|
+
combined = water1 + water2
|
112
|
+
|
113
|
+
# 运行识别函数
|
114
|
+
merged_molecules, molecules_flag, initial_info = identify_molecules(combined)
|
115
|
+
|
116
|
+
# 验证结果
|
117
|
+
expected_molecule = frozenset([("O", 1), ("H", 2)])
|
118
|
+
assert len(merged_molecules) == 2
|
119
|
+
assert expected_molecule not in merged_molecules
|
120
|
+
assert merged_molecules[expected_molecule] == 0
|
121
|
+
assert molecules_flag is False
|
122
|
+
assert initial_info == [{"O": 1, "H": 2}]
|
123
|
+
|
124
|
+
|
125
|
+
def test_no_gjf_files(setup_test_environment):
|
126
|
+
"""测试没有GJF文件的情况"""
|
127
|
+
water = Atoms("OH2", positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
128
|
+
|
129
|
+
# 运行识别函数
|
130
|
+
merged_molecules, molecules_flag, initial_info = identify_molecules(water)
|
131
|
+
|
132
|
+
# 验证结果
|
133
|
+
expected_molecule = frozenset([("O", 1), ("H", 2)])
|
134
|
+
assert len(merged_molecules) == 1
|
135
|
+
assert expected_molecule in merged_molecules
|
136
|
+
assert merged_molecules[expected_molecule] == 1
|
137
|
+
assert molecules_flag is False # 没有初始GJF文件
|
138
|
+
assert initial_info == []
|
139
|
+
|
140
|
+
|
141
|
+
def test_molecules_information_logging(caplog):
|
142
|
+
"""测试分子信息日志输出"""
|
143
|
+
# 准备测试数据
|
144
|
+
molecules = {frozenset([("C", 1), ("H", 4)]): 1, frozenset([("O", 1), ("H", 2)]): 2}
|
145
|
+
molecules_flag = True
|
146
|
+
initial_info = [{"C": 1, "H": 4}, {"O": 1, "H": 2}]
|
147
|
+
|
148
|
+
# 运行信息输出函数
|
149
|
+
with caplog.at_level(logging.INFO):
|
150
|
+
molecules_information(molecules, molecules_flag, initial_info)
|
151
|
+
|
152
|
+
# 验证日志输出
|
153
|
+
assert "Initial molecules: [{'C': 1, 'H': 4}, {'O': 1, 'H': 2}]" in caplog.text
|
154
|
+
assert "Molecule 1 (Total Atoms: 5, Count: 1): C1H4" in caplog.text
|
155
|
+
assert "Molecule 2 (Total Atoms: 3, Count: 2): O1H2" in caplog.text
|
156
|
+
assert "Molecular Comparison Successful" in caplog.text
|
157
|
+
|
158
|
+
|
159
|
+
def test_molecules_information_failed_logging(caplog):
|
160
|
+
"""测试分子比较失败的日志输出"""
|
161
|
+
# 准备测试数据
|
162
|
+
molecules = {frozenset([("C", 1), ("H", 4)]): 1, frozenset([("O", 1), ("H", 2)]): 2}
|
163
|
+
molecules_flag = False
|
164
|
+
initial_info = [{"C": 1, "H": 4}, {"O": 1, "H": 2}]
|
165
|
+
|
166
|
+
# 运行信息输出函数
|
167
|
+
with caplog.at_level(logging.INFO):
|
168
|
+
molecules_information(molecules, molecules_flag, initial_info)
|
169
|
+
|
170
|
+
# 验证日志输出
|
171
|
+
assert "Molecular Comparison Failed" in caplog.text
|
@@ -1,121 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import time
|
3
|
-
import pytest
|
4
|
-
from pathlib import Path
|
5
|
-
from unittest.mock import patch, MagicMock, Mock
|
6
|
-
from ion_CSP.task_manager import TaskManager
|
7
|
-
|
8
|
-
|
9
|
-
@pytest.fixture(scope="session", autouse=True)
|
10
|
-
def set_working_directory():
|
11
|
-
"""设置工作目录为项目根目录"""
|
12
|
-
project_root = Path(__file__).resolve().parent # 假设测试文件在 tests 目录下
|
13
|
-
os.chdir(project_root)
|
14
|
-
yield
|
15
|
-
|
16
|
-
@pytest.fixture
|
17
|
-
def task_manager(tmp_path):
|
18
|
-
# 使用临时目录替代工作目录,避免污染真实文件系统
|
19
|
-
tm = TaskManager()
|
20
|
-
tm.workspace = tmp_path
|
21
|
-
tm.log_dir = tmp_path / tm.log_base
|
22
|
-
tm.log_dir.mkdir(parents=True, exist_ok=True)
|
23
|
-
return tm
|
24
|
-
|
25
|
-
|
26
|
-
def test_task_runner_creates_log_and_pid(monkeypatch, tmp_path, task_manager):
|
27
|
-
# 模拟 subprocess.Popen,避免真正启动子进程
|
28
|
-
mock_process = MagicMock()
|
29
|
-
mock_process.pid = 9999
|
30
|
-
|
31
|
-
def fake_popen(*args, **kwargs):
|
32
|
-
return mock_process
|
33
|
-
|
34
|
-
monkeypatch.setattr("subprocess.Popen", fake_popen)
|
35
|
-
|
36
|
-
# 调用 task_runner
|
37
|
-
work_dir = tmp_path / "work"
|
38
|
-
task_manager.task_runner("CSP", str(work_dir))
|
39
|
-
|
40
|
-
# 检查日志目录下是否生成了符号链接(symlink)
|
41
|
-
symlink_path = task_manager.log_dir / f"CSP_{mock_process.pid}.log"
|
42
|
-
assert symlink_path.exists()
|
43
|
-
assert symlink_path.is_symlink()
|
44
|
-
|
45
|
-
|
46
|
-
def test_get_related_tasks_basic(task_manager, monkeypatch):
|
47
|
-
"""测试标准日志文件处理"""
|
48
|
-
# 验证 log_dir 是 Path 对象
|
49
|
-
assert isinstance(task_manager.log_dir, Path), "log_dir 必须是 pathlib.Path 对象"
|
50
|
-
log_dir = task_manager.log_dir
|
51
|
-
log_dir.mkdir(parents=True, exist_ok=True)
|
52
|
-
print(log_dir)
|
53
|
-
# 模拟的时间戳
|
54
|
-
now = time.time()
|
55
|
-
|
56
|
-
# 创建模拟的 stat 对象
|
57
|
-
mock_stats = {
|
58
|
-
8: Mock(st_mtime=now - 8000),
|
59
|
-
5: Mock(st_mtime=now - 5000),
|
60
|
-
3: Mock(st_mtime=now - 3000),
|
61
|
-
1: Mock(st_mtime=now - 1000),
|
62
|
-
}
|
63
|
-
|
64
|
-
# 模拟 os.stat 行为
|
65
|
-
def mock_os_stat(path, **kwargs):
|
66
|
-
try:
|
67
|
-
base_name = path.name.split(".")[0]
|
68
|
-
pid = int(base_name.split("_")[-1])
|
69
|
-
return mock_stats.get(pid, Mock(st_mtime=0))
|
70
|
-
except (IndexError, ValueError):
|
71
|
-
return Mock(st_mtime=0)
|
72
|
-
|
73
|
-
monkeypatch.setattr("os.stat", mock_os_stat)
|
74
|
-
monkeypatch.setattr(task_manager, "_is_pid_running", lambda _: True)
|
75
|
-
|
76
|
-
# 创建日志文件
|
77
|
-
for pid in [8, 5, 3, 1]:
|
78
|
-
log_file = log_dir / f"CSP_{pid:04d}.log"
|
79
|
-
log_file.touch()
|
80
|
-
|
81
|
-
# 调用方法
|
82
|
-
tasks = task_manager.get_related_tasks()
|
83
|
-
print(tasks)
|
84
|
-
# 断言
|
85
|
-
assert len(tasks) == 4
|
86
|
-
for task in tasks:
|
87
|
-
assert task["pid"] in [8, 5, 3, 1]
|
88
|
-
assert task["status"] == "Running"
|
89
|
-
|
90
|
-
|
91
|
-
def test_main_menu_navigation(monkeypatch, task_manager):
|
92
|
-
"""测试主菜单导航"""
|
93
|
-
monkeypatch.setattr("builtins.input", MagicMock(side_effect=["3", "q"]))
|
94
|
-
with patch("sys.stdout") as mock_stdout:
|
95
|
-
task_manager.main_menu()
|
96
|
-
output = mock_stdout.getvalue()
|
97
|
-
assert "View Logs" in output
|
98
|
-
assert "Terminate Tasks" in output
|
99
|
-
|
100
|
-
|
101
|
-
# def test_get_related_tasks_process_status(task_manager, monkeypatch):
|
102
|
-
# """测试进程状态判断"""
|
103
|
-
# test_file = task_manager.log_dir / "CSP_0001.log"
|
104
|
-
# test_file.touch()
|
105
|
-
|
106
|
-
# # 模拟 os.stat
|
107
|
-
# mock_stat = Mock(st_mtime=time.time())
|
108
|
-
# monkeypatch.setattr("os.stat", lambda _: mock_stat)
|
109
|
-
|
110
|
-
# # 模拟进程状态
|
111
|
-
# monkeypatch.setattr(task_manager, "_is_pid_running", lambda pid: pid % 2 == 0)
|
112
|
-
|
113
|
-
# # 执行测试
|
114
|
-
# tasks = task_manager.get_related_tasks()
|
115
|
-
|
116
|
-
# # 验证结果
|
117
|
-
# assert len(tasks) == 1
|
118
|
-
# assert tasks[0]["status"] == "Terminated" # PID=1 为奇数,应标记为终止
|
119
|
-
|
120
|
-
|
121
|
-
# 你可以继续添加针对 _safe_kill、_is_pid_running 等方法的单元测试
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|