MoleditPy-linux 2.6.2__tar.gz → 2.7.0__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.
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/PKG-INFO +11 -1
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/README.md +9 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/pyproject.toml +2 -1
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/PKG-INFO +11 -1
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/analysis_window.py +1 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/calculation_worker.py +296 -44
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/constants.py +1 -1
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/custom_interactor_style.py +2 -5
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window.py +0 -7
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_app_state.py +1 -6
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_compute.py +191 -175
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_dialog_manager.py +0 -9
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_edit_3d.py +0 -1
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_edit_actions.py +14 -24
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_export.py +0 -9
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_main_init.py +7 -16
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_molecular_parsers.py +2 -13
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_project_io.py +0 -22
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_string_importers.py +0 -17
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_ui_manager.py +0 -9
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_view_3d.py +0 -8
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_view_loaders.py +0 -10
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/molecule_scene.py +0 -1
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/LICENSE +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/setup.cfg +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/__init__.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/about_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/align_plane_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/alignment_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/angle_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/atom_item.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/bond_item.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/bond_length_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/color_settings_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/constrained_optimization_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/custom_qt_interactor.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/dialog3_d_picking_mixin.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/dihedral_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/mirror_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/mol_geometry.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/molecular_data.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/move_group_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/periodic_table_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/planarize_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/plugin_interface.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/plugin_manager.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/plugin_manager_window.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/settings_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/template_preview_item.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/template_preview_view.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/translation_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/user_template_dialog.py +0 -0
- {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/zoomable_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -689,6 +689,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
689
689
|
Classifier: Programming Language :: Python :: 3.11
|
|
690
690
|
Classifier: Programming Language :: Python :: 3.12
|
|
691
691
|
Classifier: Programming Language :: Python :: 3.13
|
|
692
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
692
693
|
Requires-Python: <3.15,>=3.9
|
|
693
694
|
Description-Content-Type: text/markdown
|
|
694
695
|
License-File: LICENSE
|
|
@@ -705,6 +706,15 @@ This is the Linux version of MoleditPy. The Open Babel fallback is disabled due
|
|
|
705
706
|
|
|
706
707
|
[](https://doi.org/10.5281/zenodo.17268532)
|
|
707
708
|
[](https://www.rdkit.org/)
|
|
709
|
+
[](https://badge.fury.io/py/MoleditPy)
|
|
710
|
+
[](https://pypi.org/project/MoleditPy/)
|
|
711
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
712
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
713
|
+

|
|
714
|
+

|
|
715
|
+

|
|
716
|
+

|
|
717
|
+
[](https://pepy.tech/projects/moleditpy)
|
|
708
718
|
|
|
709
719
|
[🇯🇵 日本語 (Japanese)](#japanese)
|
|
710
720
|
|
|
@@ -4,6 +4,15 @@ This is the Linux version of MoleditPy. The Open Babel fallback is disabled due
|
|
|
4
4
|
|
|
5
5
|
[](https://doi.org/10.5281/zenodo.17268532)
|
|
6
6
|
[](https://www.rdkit.org/)
|
|
7
|
+
[](https://badge.fury.io/py/MoleditPy)
|
|
8
|
+
[](https://pypi.org/project/MoleditPy/)
|
|
9
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
10
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+
[](https://pepy.tech/projects/moleditpy)
|
|
7
16
|
|
|
8
17
|
[🇯🇵 日本語 (Japanese)](#japanese)
|
|
9
18
|
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MoleditPy-linux"
|
|
7
7
|
|
|
8
|
-
version = "2.
|
|
8
|
+
version = "2.7.0"
|
|
9
9
|
|
|
10
10
|
license = {file = "LICENSE"}
|
|
11
11
|
|
|
@@ -29,6 +29,7 @@ classifiers = [
|
|
|
29
29
|
"Programming Language :: Python :: 3.11",
|
|
30
30
|
"Programming Language :: Python :: 3.12",
|
|
31
31
|
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
dependencies = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -689,6 +689,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
689
689
|
Classifier: Programming Language :: Python :: 3.11
|
|
690
690
|
Classifier: Programming Language :: Python :: 3.12
|
|
691
691
|
Classifier: Programming Language :: Python :: 3.13
|
|
692
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
692
693
|
Requires-Python: <3.15,>=3.9
|
|
693
694
|
Description-Content-Type: text/markdown
|
|
694
695
|
License-File: LICENSE
|
|
@@ -705,6 +706,15 @@ This is the Linux version of MoleditPy. The Open Babel fallback is disabled due
|
|
|
705
706
|
|
|
706
707
|
[](https://doi.org/10.5281/zenodo.17268532)
|
|
707
708
|
[](https://www.rdkit.org/)
|
|
709
|
+
[](https://badge.fury.io/py/MoleditPy)
|
|
710
|
+
[](https://pypi.org/project/MoleditPy/)
|
|
711
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
712
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
713
|
+

|
|
714
|
+

|
|
715
|
+

|
|
716
|
+

|
|
717
|
+
[](https://pepy.tech/projects/moleditpy)
|
|
708
718
|
|
|
709
719
|
[🇯🇵 日本語 (Japanese)](#japanese)
|
|
710
720
|
|
{moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/calculation_worker.py
RENAMED
|
@@ -12,6 +12,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
12
12
|
|
|
13
13
|
import math
|
|
14
14
|
import re
|
|
15
|
+
import numpy as np
|
|
15
16
|
|
|
16
17
|
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
|
|
17
18
|
|
|
@@ -39,6 +40,174 @@ else:
|
|
|
39
40
|
pybel = None
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
class WorkerHaltError(Exception):
|
|
44
|
+
"""Custom exception raised when a calculation worker is requested to halt."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _adjust_collision_avoidance(rd_mol, check_halted_cb, safe_status_cb):
|
|
49
|
+
"""
|
|
50
|
+
Optimized collision avoidance using spatial partitioning (grid-based).
|
|
51
|
+
This avoids the O(F^2 * N^2) complexity of the previous implementation.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
frags = Chem.GetMolFrags(rd_mol, asMols=False, sanitizeFrags=False)
|
|
55
|
+
if len(frags) <= 1:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
safe_status_cb(f"Resolving potential collisions among {len(frags)} fragments...")
|
|
59
|
+
|
|
60
|
+
conf = rd_mol.GetConformer()
|
|
61
|
+
pt = Chem.GetPeriodicTable()
|
|
62
|
+
|
|
63
|
+
# 1. Precalculate fragment data
|
|
64
|
+
frag_data = []
|
|
65
|
+
for f_indices in frags:
|
|
66
|
+
pos_list = []
|
|
67
|
+
radii_list = []
|
|
68
|
+
for idx in f_indices:
|
|
69
|
+
p = conf.GetAtomPosition(idx)
|
|
70
|
+
pos_list.append([p.x, p.y, p.z])
|
|
71
|
+
try:
|
|
72
|
+
radii_list.append(pt.GetRvdw(rd_mol.GetAtomWithIdx(idx).GetAtomicNum()))
|
|
73
|
+
except Exception:
|
|
74
|
+
radii_list.append(1.5)
|
|
75
|
+
|
|
76
|
+
pos_np = np.array(pos_list)
|
|
77
|
+
radii_np = np.array(radii_list)
|
|
78
|
+
frag_data.append({
|
|
79
|
+
"indices": f_indices,
|
|
80
|
+
"positions": pos_np,
|
|
81
|
+
"radii": radii_np,
|
|
82
|
+
"max_radius": np.max(radii_np) if len(radii_np) > 0 else 1.5
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
# Parameters
|
|
86
|
+
scale = 1.1 # Collision threshold scale
|
|
87
|
+
max_iters = 50
|
|
88
|
+
grid_size = 5.0
|
|
89
|
+
|
|
90
|
+
for iteration in range(max_iters):
|
|
91
|
+
if check_halted_cb():
|
|
92
|
+
raise WorkerHaltError("Halted")
|
|
93
|
+
|
|
94
|
+
moved = False
|
|
95
|
+
grid = {}
|
|
96
|
+
for i, fd in enumerate(frag_data):
|
|
97
|
+
fd_min = np.min(fd["positions"], axis=0)
|
|
98
|
+
fd_max = np.max(fd["positions"], axis=0)
|
|
99
|
+
margin = fd["max_radius"] * scale
|
|
100
|
+
fd["bbox_min"] = fd_min - margin
|
|
101
|
+
fd["bbox_max"] = fd_max + margin
|
|
102
|
+
g_min = (fd["bbox_min"] / grid_size).astype(int)
|
|
103
|
+
g_max = (fd["bbox_max"] / grid_size).astype(int)
|
|
104
|
+
for gx in range(g_min[0], g_max[0] + 1):
|
|
105
|
+
for gy in range(g_min[1], g_max[1] + 1):
|
|
106
|
+
for gz in range(g_min[2], g_max[2] + 1):
|
|
107
|
+
cell = (gx, gy, gz)
|
|
108
|
+
if cell not in grid:
|
|
109
|
+
grid[cell] = []
|
|
110
|
+
grid[cell].append(i)
|
|
111
|
+
|
|
112
|
+
all_push_vectors = [np.zeros(3) for _ in range(len(frag_data))]
|
|
113
|
+
processed_pairs = set()
|
|
114
|
+
|
|
115
|
+
for cell_indices in grid.values():
|
|
116
|
+
if len(cell_indices) < 2:
|
|
117
|
+
continue
|
|
118
|
+
for idx_idx_i, i in enumerate(cell_indices):
|
|
119
|
+
for j in cell_indices[idx_idx_i + 1:]:
|
|
120
|
+
pair = tuple(sorted((i, j)))
|
|
121
|
+
if pair in processed_pairs:
|
|
122
|
+
continue
|
|
123
|
+
processed_pairs.add(pair)
|
|
124
|
+
if np.any(frag_data[i]["bbox_min"] > frag_data[j]["bbox_max"]) or \
|
|
125
|
+
np.any(frag_data[j]["bbox_min"] > frag_data[i]["bbox_max"]):
|
|
126
|
+
continue
|
|
127
|
+
fd_i = frag_data[i]
|
|
128
|
+
fd_j = frag_data[j]
|
|
129
|
+
push_i = np.zeros(3)
|
|
130
|
+
push_j = np.zeros(3)
|
|
131
|
+
collision_count = 0
|
|
132
|
+
for p_idx_i, p_i in enumerate(fd_i["positions"]):
|
|
133
|
+
r_i = fd_i["radii"][p_idx_i]
|
|
134
|
+
for p_idx_j, p_j in enumerate(fd_j["positions"]):
|
|
135
|
+
r_j = fd_j["radii"][p_idx_j]
|
|
136
|
+
diff = p_i - p_j
|
|
137
|
+
dist_sq = np.dot(diff, diff)
|
|
138
|
+
min_dist = (r_i + r_j) * scale
|
|
139
|
+
if dist_sq < 0.0001:
|
|
140
|
+
diff = np.random.uniform(-0.1, 0.1, 3)
|
|
141
|
+
dist_sq = np.dot(diff, diff)
|
|
142
|
+
if dist_sq < min_dist * min_dist:
|
|
143
|
+
dist = np.sqrt(dist_sq)
|
|
144
|
+
if dist < 0.0001:
|
|
145
|
+
dist = 0.0001
|
|
146
|
+
push_mag = (min_dist - dist) / 2.0
|
|
147
|
+
vec = (diff / dist) * push_mag
|
|
148
|
+
push_i += vec
|
|
149
|
+
push_j -= vec
|
|
150
|
+
collision_count += 1
|
|
151
|
+
if collision_count > 0:
|
|
152
|
+
all_push_vectors[i] += push_i / collision_count
|
|
153
|
+
all_push_vectors[j] += push_j / collision_count
|
|
154
|
+
moved = True
|
|
155
|
+
|
|
156
|
+
if not moved:
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
for i, push in enumerate(all_push_vectors):
|
|
160
|
+
if np.any(push != 0):
|
|
161
|
+
frag_data[i]["positions"] += push
|
|
162
|
+
for local_idx, global_idx in enumerate(frag_data[i]["indices"]):
|
|
163
|
+
conf.SetAtomPosition(global_idx, frag_data[i]["positions"][local_idx].tolist())
|
|
164
|
+
|
|
165
|
+
safe_status_cb("Collision avoidance completed.")
|
|
166
|
+
except WorkerHaltError:
|
|
167
|
+
raise
|
|
168
|
+
except Exception as e:
|
|
169
|
+
import traceback
|
|
170
|
+
traceback.print_exc()
|
|
171
|
+
safe_status_cb(f"Collision avoidance warning: {e}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _iterative_optimize(mol, method, check_halted_cb, safe_status_cb, max_iters=4000, chunk_size=100):
|
|
175
|
+
"""Perform force field optimization in small chunks to avoid UI freezing and allow halts."""
|
|
176
|
+
try:
|
|
177
|
+
if method in ("MMFF", "MMFF94", "MMFF94S", "MMFF94s"):
|
|
178
|
+
mmff_variant = "MMFF94" if method == "MMFF94" else "MMFF94s"
|
|
179
|
+
props = AllChem.MMFFGetMoleculeProperties(mol, mmffVariant=mmff_variant)
|
|
180
|
+
if props is None:
|
|
181
|
+
return False
|
|
182
|
+
ff = AllChem.MMFFGetMoleculeForceField(mol, props, confId=0)
|
|
183
|
+
elif method == "UFF":
|
|
184
|
+
ff = AllChem.UFFGetMoleculeForceField(mol, confId=0)
|
|
185
|
+
else:
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
if ff is None:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
ff.Initialize()
|
|
192
|
+
iters_done = 0
|
|
193
|
+
while iters_done < max_iters:
|
|
194
|
+
if check_halted_cb():
|
|
195
|
+
raise WorkerHaltError("Halted")
|
|
196
|
+
res = ff.Minimize(maxIts=chunk_size)
|
|
197
|
+
iters_done += chunk_size
|
|
198
|
+
if res == 0:
|
|
199
|
+
break
|
|
200
|
+
import time
|
|
201
|
+
time.sleep(0.001)
|
|
202
|
+
|
|
203
|
+
return True
|
|
204
|
+
except WorkerHaltError:
|
|
205
|
+
raise
|
|
206
|
+
except Exception as e:
|
|
207
|
+
safe_status_cb(f"Iterative optimization ({method}) error: {e}")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
42
211
|
class CalculationWorker(QObject):
|
|
43
212
|
status_update = pyqtSignal(str)
|
|
44
213
|
finished = pyqtSignal(object)
|
|
@@ -80,44 +249,57 @@ class CalculationWorker(QObject):
|
|
|
80
249
|
def _safe_status(msg):
|
|
81
250
|
try:
|
|
82
251
|
if _check_halted():
|
|
83
|
-
|
|
252
|
+
raise WorkerHaltError("Halted")
|
|
84
253
|
self.status_update.emit(msg)
|
|
85
|
-
except
|
|
254
|
+
except WorkerHaltError:
|
|
255
|
+
raise
|
|
256
|
+
except Exception:
|
|
86
257
|
# Swallow any signal-emission errors to avoid crashing the worker
|
|
87
258
|
pass
|
|
88
259
|
|
|
89
260
|
def _safe_finished(payload): # pragma: no cover
|
|
90
261
|
try:
|
|
91
|
-
|
|
262
|
+
if _check_halted():
|
|
263
|
+
raise WorkerHaltError("Halted")
|
|
92
264
|
try:
|
|
93
265
|
self.finished.emit(payload)
|
|
266
|
+
except WorkerHaltError:
|
|
267
|
+
raise
|
|
94
268
|
except TypeError:
|
|
95
|
-
# Some slots/old code may expect a single-molecule arg; try that too
|
|
96
269
|
try:
|
|
97
|
-
# If payload was a tuple like (worker_id, mol), try sending the second element
|
|
98
270
|
if isinstance(payload, (list, tuple)) and len(payload) >= 2:
|
|
99
271
|
self.finished.emit(payload[1])
|
|
100
272
|
else:
|
|
101
273
|
self.finished.emit(payload)
|
|
274
|
+
except WorkerHaltError:
|
|
275
|
+
raise
|
|
102
276
|
except Exception: # pragma: no cover
|
|
103
277
|
import traceback
|
|
104
278
|
traceback.print_exc()
|
|
279
|
+
except WorkerHaltError:
|
|
280
|
+
raise
|
|
105
281
|
except Exception: # pragma: no cover
|
|
106
282
|
import traceback
|
|
107
283
|
traceback.print_exc()
|
|
108
284
|
|
|
109
285
|
def _safe_error(msg): # pragma: no cover
|
|
110
286
|
try:
|
|
111
|
-
|
|
287
|
+
if msg != "Halted" and _check_halted():
|
|
288
|
+
raise WorkerHaltError("Halted")
|
|
112
289
|
try:
|
|
113
290
|
self.error.emit((worker_id, msg))
|
|
291
|
+
except WorkerHaltError:
|
|
292
|
+
raise
|
|
114
293
|
except Exception:
|
|
115
|
-
# Fallback to emitting the raw message if tuple emission fails for any reason
|
|
116
294
|
try:
|
|
117
295
|
self.error.emit(msg)
|
|
296
|
+
except WorkerHaltError:
|
|
297
|
+
raise
|
|
118
298
|
except Exception: # pragma: no cover
|
|
119
299
|
import traceback
|
|
120
300
|
traceback.print_exc()
|
|
301
|
+
except WorkerHaltError:
|
|
302
|
+
raise
|
|
121
303
|
except Exception: # pragma: no cover
|
|
122
304
|
import traceback
|
|
123
305
|
traceback.print_exc()
|
|
@@ -157,7 +339,7 @@ class CalculationWorker(QObject):
|
|
|
157
339
|
|
|
158
340
|
# Check early whether this run has been requested to halt
|
|
159
341
|
if _check_halted():
|
|
160
|
-
raise
|
|
342
|
+
raise WorkerHaltError("Halted")
|
|
161
343
|
|
|
162
344
|
explicit_stereo = {}
|
|
163
345
|
mol_lines = mol_block.split("\n")
|
|
@@ -222,7 +404,30 @@ class CalculationWorker(QObject):
|
|
|
222
404
|
|
|
223
405
|
# Check after adding Hs (may be a long operation)
|
|
224
406
|
if _check_halted():
|
|
225
|
-
raise
|
|
407
|
+
raise WorkerHaltError("Halted")
|
|
408
|
+
|
|
409
|
+
# Support for optimize_only mode
|
|
410
|
+
if conversion_mode == "optimize_only":
|
|
411
|
+
_safe_status("Optimizing existing 3D structure...")
|
|
412
|
+
opt_method = str(options.get("optimization_method", "MMFF94s")).upper()
|
|
413
|
+
if "MMFF" in opt_method:
|
|
414
|
+
method_key = "MMFF94" if "MMFF94" in opt_method and "MMFF94S" not in opt_method else "MMFF94s"
|
|
415
|
+
if not _iterative_optimize(mol, method_key, _check_halted, _safe_status):
|
|
416
|
+
_safe_status(f"{method_key} failed, falling back to UFF...")
|
|
417
|
+
_iterative_optimize(mol, "UFF", _check_halted, _safe_status)
|
|
418
|
+
elif "UFF" in opt_method:
|
|
419
|
+
_iterative_optimize(mol, "UFF", _check_halted, _safe_status)
|
|
420
|
+
else:
|
|
421
|
+
if not _iterative_optimize(mol, "MMFF94s", _check_halted, _safe_status):
|
|
422
|
+
_iterative_optimize(mol, "UFF", _check_halted, _safe_status)
|
|
423
|
+
if _check_halted():
|
|
424
|
+
raise WorkerHaltError("Halted")
|
|
425
|
+
try:
|
|
426
|
+
_safe_finished((worker_id, mol))
|
|
427
|
+
except Exception:
|
|
428
|
+
_safe_finished(mol)
|
|
429
|
+
_safe_status("Optimization completed.")
|
|
430
|
+
return
|
|
226
431
|
|
|
227
432
|
# CRITICAL: Re-apply explicit stereo after AddHs which may renumber atoms
|
|
228
433
|
for bond_idx, stereo_type in explicit_stereo.items():
|
|
@@ -617,14 +822,35 @@ class CalculationWorker(QObject):
|
|
|
617
822
|
traceback.print_exc()
|
|
618
823
|
mol.AddConformer(conf, assignId=True)
|
|
619
824
|
|
|
825
|
+
# Optimization (respects do_optimize flag)
|
|
826
|
+
do_optimize = options.get("do_optimize", True) if options else True
|
|
827
|
+
if do_optimize:
|
|
828
|
+
_safe_status("Direct conversion: optimizing geometry...")
|
|
829
|
+
if _check_halted():
|
|
830
|
+
raise WorkerHaltError("Halted")
|
|
831
|
+
mmff_method = "MMFF94s"
|
|
832
|
+
if options and str(options.get("optimization_method", "")).upper() == "MMFF94_RDKIT":
|
|
833
|
+
mmff_method = "MMFF94"
|
|
834
|
+
if not _iterative_optimize(mol, mmff_method, _check_halted, _safe_status):
|
|
835
|
+
if _check_halted():
|
|
836
|
+
raise WorkerHaltError("Halted")
|
|
837
|
+
_iterative_optimize(mol, "UFF", _check_halted, _safe_status)
|
|
838
|
+
|
|
620
839
|
if _check_halted():
|
|
621
|
-
raise
|
|
840
|
+
raise WorkerHaltError("Halted")
|
|
841
|
+
if do_optimize:
|
|
842
|
+
_adjust_collision_avoidance(mol, _check_halted, _safe_status)
|
|
843
|
+
|
|
622
844
|
try:
|
|
623
845
|
_safe_finished((worker_id, mol))
|
|
846
|
+
except WorkerHaltError:
|
|
847
|
+
raise
|
|
624
848
|
except Exception:
|
|
625
849
|
_safe_finished(mol)
|
|
626
850
|
_safe_status("Direct conversion completed.")
|
|
627
851
|
return
|
|
852
|
+
except WorkerHaltError:
|
|
853
|
+
raise
|
|
628
854
|
except Exception as e:
|
|
629
855
|
_safe_status(f"Direct conversion failed: {e}")
|
|
630
856
|
|
|
@@ -666,7 +892,7 @@ class CalculationWorker(QObject):
|
|
|
666
892
|
# direct mode (or any other explicit non-RDKit mode)
|
|
667
893
|
pass
|
|
668
894
|
if _check_halted():
|
|
669
|
-
raise
|
|
895
|
+
raise WorkerHaltError("Halted")
|
|
670
896
|
|
|
671
897
|
# Try multiple times with different approaches if needed
|
|
672
898
|
conf_id = -1
|
|
@@ -680,7 +906,9 @@ class CalculationWorker(QObject):
|
|
|
680
906
|
conf_id = -1
|
|
681
907
|
# Final check before returning success
|
|
682
908
|
if _check_halted():
|
|
683
|
-
raise
|
|
909
|
+
raise WorkerHaltError("Halted")
|
|
910
|
+
except WorkerHaltError:
|
|
911
|
+
raise
|
|
684
912
|
except Exception as e:
|
|
685
913
|
# Standard embedding failed; report and continue to fallback attempts
|
|
686
914
|
_safe_status(f"Standard embedding failed: {e}")
|
|
@@ -783,21 +1011,17 @@ class CalculationWorker(QObject):
|
|
|
783
1011
|
bond.SetStereo(stereo)
|
|
784
1012
|
|
|
785
1013
|
try:
|
|
786
|
-
|
|
1014
|
+
mmff_method = "MMFF94s"
|
|
787
1015
|
if opt_method and str(opt_method).upper() == "MMFF94_RDKIT":
|
|
788
|
-
|
|
789
|
-
if _check_halted
|
|
790
|
-
raise RuntimeError("Halted")
|
|
791
|
-
AllChem.MMFFOptimizeMolecule(mol, mmffVariant=mmff_variant)
|
|
792
|
-
except Exception:
|
|
793
|
-
# fallback to UFF if MMFF fails
|
|
794
|
-
try:
|
|
1016
|
+
mmff_method = "MMFF94"
|
|
1017
|
+
if not _iterative_optimize(mol, mmff_method, _check_halted, _safe_status):
|
|
795
1018
|
if _check_halted():
|
|
796
|
-
raise
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1019
|
+
raise WorkerHaltError("Halted")
|
|
1020
|
+
_iterative_optimize(mol, "UFF", _check_halted, _safe_status)
|
|
1021
|
+
except WorkerHaltError:
|
|
1022
|
+
raise
|
|
1023
|
+
except Exception as opt_err:
|
|
1024
|
+
_safe_status(f"RDKit optimization failed (ignoring): {opt_err}")
|
|
801
1025
|
# CRITICAL: Restore stereochemistry again after optimization (explicit labels priority)
|
|
802
1026
|
for bond_idx, stereo, stereo_atoms in original_stereo_info:
|
|
803
1027
|
bond = mol.GetBondWithIdx(bond_idx)
|
|
@@ -805,15 +1029,18 @@ class CalculationWorker(QObject):
|
|
|
805
1029
|
bond.SetStereoAtoms(stereo_atoms[0], stereo_atoms[1])
|
|
806
1030
|
bond.SetStereo(stereo)
|
|
807
1031
|
|
|
808
|
-
# Do NOT call AssignStereochemistry here as it would override our explicit labels
|
|
809
|
-
# Include worker_id so the main thread can ignore stale results
|
|
810
1032
|
# CRITICAL: Check for halt *before* emitting finished signal
|
|
811
1033
|
if _check_halted():
|
|
812
|
-
raise
|
|
1034
|
+
raise WorkerHaltError("Halted")
|
|
1035
|
+
|
|
1036
|
+
# Collision avoidance
|
|
1037
|
+
_adjust_collision_avoidance(mol, _check_halted, _safe_status)
|
|
1038
|
+
|
|
813
1039
|
try:
|
|
814
1040
|
_safe_finished((worker_id, mol))
|
|
1041
|
+
except WorkerHaltError:
|
|
1042
|
+
raise
|
|
815
1043
|
except Exception:
|
|
816
|
-
# Fallback to legacy single-arg emit
|
|
817
1044
|
_safe_finished(mol)
|
|
818
1045
|
_safe_status("RDKit 3D conversion succeeded.")
|
|
819
1046
|
return
|
|
@@ -838,13 +1065,13 @@ class CalculationWorker(QObject):
|
|
|
838
1065
|
try:
|
|
839
1066
|
_safe_status("Optimizing with Open Babel (MMFF94)...")
|
|
840
1067
|
if _check_halted():
|
|
841
|
-
raise
|
|
1068
|
+
raise WorkerHaltError("Halted")
|
|
842
1069
|
ob_mol.localopt(forcefield="mmff94", steps=500)
|
|
843
1070
|
except Exception:
|
|
844
1071
|
try:
|
|
845
1072
|
_safe_status("MMFF94 failed, falling back to UFF...")
|
|
846
1073
|
if _check_halted():
|
|
847
|
-
raise
|
|
1074
|
+
raise WorkerHaltError("Halted")
|
|
848
1075
|
ob_mol.localopt(forcefield="uff", steps=500)
|
|
849
1076
|
except Exception:
|
|
850
1077
|
_safe_status("UFF optimization also failed.")
|
|
@@ -854,36 +1081,61 @@ class CalculationWorker(QObject):
|
|
|
854
1081
|
raise ValueError("Open Babel produced invalid MOL block.")
|
|
855
1082
|
rd_mol = Chem.AddHs(rd_mol)
|
|
856
1083
|
try:
|
|
857
|
-
|
|
1084
|
+
mmff_method = "MMFF94s"
|
|
858
1085
|
if opt_method and str(opt_method).upper() == "MMFF94_RDKIT":
|
|
859
|
-
|
|
1086
|
+
mmff_method = "MMFF94"
|
|
860
1087
|
if _check_halted():
|
|
861
|
-
raise
|
|
862
|
-
|
|
863
|
-
except Exception:
|
|
864
|
-
try:
|
|
1088
|
+
raise WorkerHaltError("Halted")
|
|
1089
|
+
if not _iterative_optimize(rd_mol, mmff_method, _check_halted, _safe_status):
|
|
865
1090
|
if _check_halted():
|
|
866
|
-
raise
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1091
|
+
raise WorkerHaltError("Halted")
|
|
1092
|
+
_iterative_optimize(rd_mol, "UFF", _check_halted, _safe_status)
|
|
1093
|
+
except WorkerHaltError:
|
|
1094
|
+
raise
|
|
1095
|
+
except Exception: # pragma: no cover
|
|
1096
|
+
import traceback
|
|
1097
|
+
traceback.print_exc()
|
|
871
1098
|
_safe_status(
|
|
872
1099
|
"Open Babel embedding succeeded. Warning: Conformation accuracy may be limited."
|
|
873
1100
|
)
|
|
874
1101
|
# CRITICAL: Check for halt *before* emitting finished signal
|
|
875
1102
|
if _check_halted():
|
|
876
|
-
raise
|
|
1103
|
+
raise WorkerHaltError("Halted")
|
|
1104
|
+
|
|
1105
|
+
# Collision avoidance
|
|
1106
|
+
_adjust_collision_avoidance(rd_mol, _check_halted, _safe_status)
|
|
877
1107
|
try:
|
|
878
1108
|
_safe_finished((worker_id, rd_mol))
|
|
879
1109
|
except Exception:
|
|
880
1110
|
_safe_finished(rd_mol)
|
|
881
1111
|
return
|
|
1112
|
+
except WorkerHaltError:
|
|
1113
|
+
raise
|
|
882
1114
|
except Exception as ob_err:
|
|
883
|
-
|
|
1115
|
+
if conversion_mode == "obabel":
|
|
1116
|
+
# obabel-only mode: no further fallback
|
|
1117
|
+
raise RuntimeError(f"Open Babel 3D conversion failed: {ob_err}")
|
|
1118
|
+
# fallback mode: continue to direct conversion below
|
|
1119
|
+
_safe_status(
|
|
1120
|
+
f"Open Babel unavailable or failed ({ob_err}). "
|
|
1121
|
+
"Falling back to direct conversion..."
|
|
1122
|
+
)
|
|
884
1123
|
|
|
885
1124
|
if conf_id == -1 and conversion_mode == "rdkit":
|
|
886
1125
|
raise RuntimeError("RDKit 3D conversion failed (rdkit-only mode)")
|
|
887
1126
|
|
|
1127
|
+
# --- Last-resort fallback: direct conversion ---
|
|
1128
|
+
if conf_id == -1 and conversion_mode == "fallback":
|
|
1129
|
+
_safe_status(
|
|
1130
|
+
"All embedding methods failed. Using direct conversion as last resort..."
|
|
1131
|
+
)
|
|
1132
|
+
direct_opts = dict(options) if options else {}
|
|
1133
|
+
direct_opts["conversion_mode"] = "direct"
|
|
1134
|
+
self.run_calculation(mol_block, direct_opts)
|
|
1135
|
+
return
|
|
1136
|
+
|
|
1137
|
+
except WorkerHaltError:
|
|
1138
|
+
_safe_error("Halted")
|
|
1139
|
+
return
|
|
888
1140
|
except Exception as e:
|
|
889
1141
|
_safe_error(str(e))
|
|
@@ -599,11 +599,11 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
599
599
|
try:
|
|
600
600
|
pos = mw.atom_positions_3d[i]
|
|
601
601
|
conf.SetAtomPosition(i, pos.tolist())
|
|
602
|
-
except Exception:
|
|
602
|
+
except Exception:
|
|
603
603
|
# Skip individual failures but continue applying
|
|
604
604
|
# other atom positions.
|
|
605
605
|
pass
|
|
606
|
-
except Exception:
|
|
606
|
+
except Exception:
|
|
607
607
|
# If applying positions fails, continue to redraw from
|
|
608
608
|
# whatever authoritative state is available.
|
|
609
609
|
pass
|
|
@@ -640,9 +640,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
640
640
|
except Exception: # pragma: no cover
|
|
641
641
|
import traceback
|
|
642
642
|
traceback.print_exc()
|
|
643
|
-
except Exception: # pragma: no cover
|
|
644
|
-
# Do not allow a failure here to interrupt release flow
|
|
645
|
-
pass
|
|
646
643
|
else:
|
|
647
644
|
# カメラ回転の後始末を親クラスに任せます
|
|
648
645
|
super().OnLeftButtonUp()
|
|
@@ -12,19 +12,12 @@ DOI: 10.5281/zenodo.17268532
|
|
|
12
12
|
|
|
13
13
|
import traceback
|
|
14
14
|
|
|
15
|
-
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
16
|
-
try:
|
|
17
|
-
pass
|
|
18
|
-
except Exception: # pragma: no cover
|
|
19
|
-
traceback.print_exc()
|
|
20
|
-
|
|
21
15
|
# PyQt6 Modules
|
|
22
16
|
from PyQt6.QtCore import pyqtSignal, pyqtSlot
|
|
23
17
|
from PyQt6.QtWidgets import QMainWindow
|
|
24
18
|
|
|
25
19
|
try:
|
|
26
20
|
from PyQt6 import sip as _sip # type: ignore
|
|
27
|
-
|
|
28
21
|
_sip_isdeleted = getattr(_sip, "isdeleted", None)
|
|
29
22
|
except Exception:
|
|
30
23
|
_sip = None
|