MoleditPy 2.2.0a0__py3-none-any.whl → 2.2.0a2__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.
- moleditpy/modules/constants.py +1 -1
- moleditpy/modules/main_window_main_init.py +73 -103
- moleditpy/modules/main_window_ui_manager.py +21 -2
- moleditpy/modules/plugin_manager.py +10 -0
- moleditpy/plugins/Analysis/ms_spectrum_neo.py +919 -0
- moleditpy/plugins/File/animated_xyz_giffer.py +583 -0
- moleditpy/plugins/File/cube_viewer.py +689 -0
- moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +1148 -0
- moleditpy/plugins/File/mapped_cube_viewer.py +552 -0
- moleditpy/plugins/File/orca_out_freq_analyzer.py +1226 -0
- moleditpy/plugins/File/paste_xyz.py +336 -0
- moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +930 -0
- moleditpy/plugins/Input Generator/orca_input_generator_neo.py +1028 -0
- moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +286 -0
- moleditpy/plugins/Optimization/all-trans_optimizer.py +65 -0
- moleditpy/plugins/Optimization/complex_molecule_untangler.py +268 -0
- moleditpy/plugins/Optimization/conf_search.py +224 -0
- moleditpy/plugins/Utility/atom_colorizer.py +262 -0
- moleditpy/plugins/Utility/console.py +163 -0
- moleditpy/plugins/Utility/pubchem_ressolver.py +244 -0
- moleditpy/plugins/Utility/vdw_radii_overlay.py +432 -0
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/METADATA +1 -1
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/RECORD +27 -10
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/WHEEL +0 -0
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/entry_points.txt +0 -0
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit,
|
|
4
|
+
QPushButton, QHBoxLayout, QSpinBox, QMessageBox, QComboBox, QFileDialog)
|
|
5
|
+
from rdkit import Chem
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
__version__="2025.12.25"
|
|
10
|
+
__author__="HiroYokoyama"
|
|
11
|
+
PLUGIN_NAME = "ORCA xyz2inp GUI"
|
|
12
|
+
SETTINGS_JSON = os.path.join(os.path.dirname(__file__), "orca_xyz2inp_gui.json")
|
|
13
|
+
|
|
14
|
+
class OrcaInputDialog(QDialog):
|
|
15
|
+
def __init__(self, main_window):
|
|
16
|
+
super().__init__(main_window)
|
|
17
|
+
self.setWindowTitle(PLUGIN_NAME)
|
|
18
|
+
self.main_window = main_window
|
|
19
|
+
self.resize(400, 350)
|
|
20
|
+
self.setup_ui()
|
|
21
|
+
self.load_defaults()
|
|
22
|
+
|
|
23
|
+
def setup_ui(self):
|
|
24
|
+
layout = QVBoxLayout()
|
|
25
|
+
form_layout = QFormLayout()
|
|
26
|
+
|
|
27
|
+
# 1. Template File Selection
|
|
28
|
+
# Template Directory: same name as script file (without extension)
|
|
29
|
+
self.template_dir = os.path.splitext(__file__)[0]
|
|
30
|
+
if not os.path.exists(self.template_dir):
|
|
31
|
+
try:
|
|
32
|
+
os.makedirs(self.template_dir)
|
|
33
|
+
except OSError:
|
|
34
|
+
pass # Ignore if cannot create
|
|
35
|
+
|
|
36
|
+
self.combo_template = QComboBox()
|
|
37
|
+
self.combo_template.currentIndexChanged.connect(self.on_template_combo_changed)
|
|
38
|
+
|
|
39
|
+
self.btn_open_dir = QPushButton("Open Dir")
|
|
40
|
+
self.btn_open_dir.clicked.connect(self.open_template_dir)
|
|
41
|
+
|
|
42
|
+
self.le_template = QLineEdit()
|
|
43
|
+
self.btn_template = QPushButton("Browse...")
|
|
44
|
+
self.btn_template.clicked.connect(self.browse_template)
|
|
45
|
+
|
|
46
|
+
# Initial populate
|
|
47
|
+
self.populate_templates()
|
|
48
|
+
|
|
49
|
+
h_template_top = QHBoxLayout()
|
|
50
|
+
h_template_top.addWidget(self.combo_template, 1)
|
|
51
|
+
h_template_top.addWidget(self.btn_open_dir)
|
|
52
|
+
|
|
53
|
+
h_template_btm = QHBoxLayout()
|
|
54
|
+
h_template_btm.addWidget(self.le_template)
|
|
55
|
+
h_template_btm.addWidget(self.btn_template)
|
|
56
|
+
|
|
57
|
+
form_layout.addRow("Template Preset:", h_template_top)
|
|
58
|
+
form_layout.addRow("Template Path:", h_template_btm)
|
|
59
|
+
|
|
60
|
+
# 1.5. Filename Suffix
|
|
61
|
+
self.le_suffix = QLineEdit()
|
|
62
|
+
form_layout.addRow("Filename Suffix:", self.le_suffix)
|
|
63
|
+
|
|
64
|
+
# 2. Parameters (Charge, Multiplicity)
|
|
65
|
+
self.sb_charge = QSpinBox()
|
|
66
|
+
self.sb_charge.setRange(-10, 10)
|
|
67
|
+
self.sb_charge.setValue(0)
|
|
68
|
+
|
|
69
|
+
self.sb_mult = QSpinBox()
|
|
70
|
+
self.sb_mult.setRange(1, 10)
|
|
71
|
+
self.sb_mult.setValue(1)
|
|
72
|
+
|
|
73
|
+
form_layout.addRow("Charge:", self.sb_charge)
|
|
74
|
+
form_layout.addRow("Multiplicity:", self.sb_mult)
|
|
75
|
+
|
|
76
|
+
# 3. ORCA Resources (NProcs, MaxCore)
|
|
77
|
+
self.sb_nprocs = QSpinBox()
|
|
78
|
+
self.sb_nprocs.setRange(1, 128)
|
|
79
|
+
self.sb_nprocs.setValue(1)
|
|
80
|
+
|
|
81
|
+
self.sb_maxcore = QSpinBox()
|
|
82
|
+
self.sb_maxcore.setRange(100, 100000)
|
|
83
|
+
self.sb_maxcore.setSingleStep(100)
|
|
84
|
+
self.sb_maxcore.setValue(1000)
|
|
85
|
+
self.sb_maxcore.setSuffix(" MB")
|
|
86
|
+
|
|
87
|
+
form_layout.addRow("NProcs:", self.sb_nprocs)
|
|
88
|
+
form_layout.addRow("MaxCore:", self.sb_maxcore)
|
|
89
|
+
|
|
90
|
+
layout.addLayout(form_layout)
|
|
91
|
+
|
|
92
|
+
# --- Action Buttons ---
|
|
93
|
+
btn_layout = QHBoxLayout()
|
|
94
|
+
btn_layout.addStretch()
|
|
95
|
+
|
|
96
|
+
self.btn_cancel = QPushButton("Cancel")
|
|
97
|
+
self.btn_cancel.clicked.connect(self.reject)
|
|
98
|
+
|
|
99
|
+
self.btn_generate = QPushButton("Generate Input")
|
|
100
|
+
self.btn_generate.setDefault(True)
|
|
101
|
+
self.btn_generate.clicked.connect(self.generate_file) # ここで保存ダイアログを開く
|
|
102
|
+
|
|
103
|
+
btn_layout.addWidget(self.btn_cancel)
|
|
104
|
+
btn_layout.addWidget(self.btn_generate)
|
|
105
|
+
|
|
106
|
+
layout.addLayout(btn_layout)
|
|
107
|
+
self.setLayout(layout)
|
|
108
|
+
|
|
109
|
+
def load_defaults(self):
|
|
110
|
+
# 1. Load from JSON
|
|
111
|
+
defaults = {"nprocs": 1, "maxcore": 1000}
|
|
112
|
+
if os.path.exists(SETTINGS_JSON):
|
|
113
|
+
try:
|
|
114
|
+
with open(SETTINGS_JSON, 'r') as f:
|
|
115
|
+
saved = json.load(f)
|
|
116
|
+
defaults.update(saved)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f"Error loading settings: {e}")
|
|
119
|
+
|
|
120
|
+
# 2. Override with Environment Variables
|
|
121
|
+
if "orca_xyz2inp_nprocs" in os.environ:
|
|
122
|
+
try:
|
|
123
|
+
defaults["nprocs"] = int(os.environ["orca_xyz2inp_nprocs"])
|
|
124
|
+
except ValueError: pass
|
|
125
|
+
|
|
126
|
+
if "orca_xyz2inp_maxcore" in os.environ:
|
|
127
|
+
try:
|
|
128
|
+
defaults["maxcore"] = int(os.environ["orca_xyz2inp_maxcore"])
|
|
129
|
+
except ValueError: pass
|
|
130
|
+
|
|
131
|
+
# 3. Set UI
|
|
132
|
+
self.sb_nprocs.setValue(int(defaults.get("nprocs", 1)))
|
|
133
|
+
self.sb_maxcore.setValue(int(defaults.get("maxcore", 1000)))
|
|
134
|
+
|
|
135
|
+
# 電荷と多重度の自動推測
|
|
136
|
+
if hasattr(self.main_window, 'current_mol') and self.main_window.current_mol:
|
|
137
|
+
mol = self.main_window.current_mol
|
|
138
|
+
try:
|
|
139
|
+
# Charge
|
|
140
|
+
charge = Chem.GetFormalCharge(mol)
|
|
141
|
+
self.sb_charge.setValue(charge)
|
|
142
|
+
|
|
143
|
+
# Multiplicity
|
|
144
|
+
# RDKit keeps track of radical electrons on atoms
|
|
145
|
+
num_radical_electrons = sum(atom.GetNumRadicalElectrons() for atom in mol.GetAtoms())
|
|
146
|
+
multiplicity = num_radical_electrons + 1
|
|
147
|
+
self.sb_mult.setValue(multiplicity)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"Error estimating charge/multiplicity: {e}")
|
|
150
|
+
|
|
151
|
+
def populate_templates(self):
|
|
152
|
+
self.combo_template.blockSignals(True)
|
|
153
|
+
self.combo_template.clear()
|
|
154
|
+
|
|
155
|
+
found_templates = []
|
|
156
|
+
if os.path.exists(self.template_dir):
|
|
157
|
+
for f in os.listdir(self.template_dir):
|
|
158
|
+
if f.lower().endswith(".tmplt"):
|
|
159
|
+
found_templates.append(f)
|
|
160
|
+
|
|
161
|
+
found_templates.sort()
|
|
162
|
+
self.combo_template.addItems(found_templates)
|
|
163
|
+
self.combo_template.addItem("External File...")
|
|
164
|
+
|
|
165
|
+
self.combo_template.blockSignals(False)
|
|
166
|
+
# Trigger update of visibility/path based on initial selection
|
|
167
|
+
self.on_template_combo_changed()
|
|
168
|
+
|
|
169
|
+
def on_template_combo_changed(self):
|
|
170
|
+
text = self.combo_template.currentText()
|
|
171
|
+
if text == "External File...":
|
|
172
|
+
self.le_template.setEnabled(True)
|
|
173
|
+
self.btn_template.setEnabled(True)
|
|
174
|
+
# Retain previous text if generic, or clear? Better keep it.
|
|
175
|
+
else:
|
|
176
|
+
self.le_template.setEnabled(False)
|
|
177
|
+
self.btn_template.setEnabled(False)
|
|
178
|
+
full_path = os.path.join(self.template_dir, text)
|
|
179
|
+
self.le_template.setText(full_path)
|
|
180
|
+
|
|
181
|
+
def open_template_dir(self):
|
|
182
|
+
if not os.path.exists(self.template_dir):
|
|
183
|
+
try:
|
|
184
|
+
os.makedirs(self.template_dir)
|
|
185
|
+
except:
|
|
186
|
+
QMessageBox.warning(self, "Error", f"Could not create directory:\n{self.template_dir}")
|
|
187
|
+
return
|
|
188
|
+
try:
|
|
189
|
+
os.startfile(self.template_dir)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
QMessageBox.warning(self, "Error", f"Could not open directory:\n{e}")
|
|
192
|
+
|
|
193
|
+
def browse_template(self):
|
|
194
|
+
path, _ = QFileDialog.getOpenFileName(
|
|
195
|
+
self, "Select Template", "", "ORCA Template (*.tmplt);;Text Files (*.txt *.inp);;All Files (*)"
|
|
196
|
+
)
|
|
197
|
+
if path:
|
|
198
|
+
self.le_template.setText(path)
|
|
199
|
+
# Ensure External File is selected
|
|
200
|
+
idx = self.combo_template.findText("External File...")
|
|
201
|
+
if idx >= 0:
|
|
202
|
+
self.combo_template.setCurrentIndex(idx)
|
|
203
|
+
|
|
204
|
+
def generate_file(self):
|
|
205
|
+
# 1. バリデーション
|
|
206
|
+
template_path = self.le_template.text()
|
|
207
|
+
if not template_path or not os.path.exists(template_path):
|
|
208
|
+
QMessageBox.warning(self, "Error", "Template file not found.")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
mol = self.main_window.current_mol
|
|
212
|
+
if not mol:
|
|
213
|
+
QMessageBox.warning(self, "Error", "No molecule loaded.")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# 2. 保存先ダイアログを表示 (Generateボタン押下時)
|
|
217
|
+
suffix = self.le_suffix.text().strip()
|
|
218
|
+
|
|
219
|
+
default_name = "orca_input.inp"
|
|
220
|
+
if hasattr(self.main_window, 'current_file_path') and self.main_window.current_file_path:
|
|
221
|
+
base_name = os.path.splitext(os.path.basename(self.main_window.current_file_path))[0]
|
|
222
|
+
default_name = f"{base_name}{suffix}.inp"
|
|
223
|
+
else:
|
|
224
|
+
default_name = f"orca_input{suffix}.inp"
|
|
225
|
+
|
|
226
|
+
output_path, _ = QFileDialog.getSaveFileName(
|
|
227
|
+
self, "Save ORCA Input File", default_name, "ORCA Input (*.inp)"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if not output_path:
|
|
231
|
+
return # キャンセル時は何もしない
|
|
232
|
+
|
|
233
|
+
# 3. Saving Settings
|
|
234
|
+
try:
|
|
235
|
+
settings_to_save = {
|
|
236
|
+
"nprocs": self.sb_nprocs.value(),
|
|
237
|
+
"maxcore": self.sb_maxcore.value()
|
|
238
|
+
}
|
|
239
|
+
with open(SETTINGS_JSON, 'w') as f:
|
|
240
|
+
json.dump(settings_to_save, f)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print(f"Warning: Could not save settings: {e}")
|
|
243
|
+
|
|
244
|
+
# 4. ファイル生成処理
|
|
245
|
+
try:
|
|
246
|
+
# パラメータ取得
|
|
247
|
+
charge = self.sb_charge.value()
|
|
248
|
+
mult = self.sb_mult.value()
|
|
249
|
+
nprocs = self.sb_nprocs.value()
|
|
250
|
+
maxcore = self.sb_maxcore.value()
|
|
251
|
+
|
|
252
|
+
# テンプレート読み込み
|
|
253
|
+
with open(template_path, 'r', encoding='utf-8') as f:
|
|
254
|
+
template_content = f.read().strip()
|
|
255
|
+
|
|
256
|
+
# 座標データの作成
|
|
257
|
+
xyz_block = Chem.MolToXYZBlock(mol)
|
|
258
|
+
xyz_lines = xyz_block.strip().split('\n')
|
|
259
|
+
# 1,2行目(原子数/コメント)をスキップ
|
|
260
|
+
coords_data = "\n".join(xyz_lines[2:]) if len(xyz_lines) > 2 else ""
|
|
261
|
+
|
|
262
|
+
# 書き込み
|
|
263
|
+
with open(output_path, 'w', encoding='utf-8') as out:
|
|
264
|
+
out.write(f"# Generated by MoleditPy ({PLUGIN_NAME})\n")
|
|
265
|
+
out.write(f"%pal nprocs {nprocs} end\n")
|
|
266
|
+
out.write(f"%MaxCore {maxcore}\n\n")
|
|
267
|
+
out.write(f"{template_content}\n\n")
|
|
268
|
+
out.write(f"* xyz {charge} {mult}\n")
|
|
269
|
+
out.write(f"{coords_data}\n")
|
|
270
|
+
out.write("*\n")
|
|
271
|
+
|
|
272
|
+
QMessageBox.information(self, "Success", f"Input file generated:\n{output_path}")
|
|
273
|
+
self.accept() # ダイアログを閉じる
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
QMessageBox.critical(self, "Error", f"Failed to generate file:\n{str(e)}")
|
|
277
|
+
|
|
278
|
+
def run(mw):
|
|
279
|
+
if not hasattr(mw, 'current_mol') or not mw.current_mol:
|
|
280
|
+
QMessageBox.warning(mw, PLUGIN_NAME, "No molecule loaded.")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
dialog = OrcaInputDialog(mw)
|
|
284
|
+
dialog.exec()
|
|
285
|
+
|
|
286
|
+
# initialize removed as it only registered the menu action
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
2
|
+
from rdkit import Chem
|
|
3
|
+
from rdkit.Chem import rdMolTransforms
|
|
4
|
+
|
|
5
|
+
__version__="2025.12.25"
|
|
6
|
+
__author__="HiroYokoyama"
|
|
7
|
+
PLUGIN_NAME = "All-Trans Optimizer"
|
|
8
|
+
|
|
9
|
+
def run(mw):
|
|
10
|
+
"""
|
|
11
|
+
現在の分子のアルキル鎖(非環状C-C結合)をAll-Trans配座に整形する
|
|
12
|
+
"""
|
|
13
|
+
# Access the current molecule via mw.current_mol
|
|
14
|
+
mol = getattr(mw, "current_mol", None)
|
|
15
|
+
|
|
16
|
+
if not mol:
|
|
17
|
+
QMessageBox.warning(mw, PLUGIN_NAME, "No molecule loaded.")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
# 3Dコンフォマーの取得 (存在しない場合は作成しない)
|
|
22
|
+
if mol.GetNumConformers() == 0:
|
|
23
|
+
QMessageBox.warning(mw, PLUGIN_NAME, "Molecule has no 3D coordinates.")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
conf = mol.GetConformer()
|
|
27
|
+
|
|
28
|
+
# SMARTSパターン: 炭素-炭素(非環状)-炭素-炭素
|
|
29
|
+
# 中央の結合(!@)が環に含まれていない4連続の炭素を検索
|
|
30
|
+
# [#6]は炭素原子を表します
|
|
31
|
+
patt = Chem.MolFromSmarts("[#6]-[#6]!@[#6]-[#6]")
|
|
32
|
+
matches = mol.GetSubstructMatches(patt)
|
|
33
|
+
|
|
34
|
+
count = 0
|
|
35
|
+
if matches:
|
|
36
|
+
# マッチしたすべてのねじれ角を180(Trans)に設定
|
|
37
|
+
# 順番によっては後続の変更が前の変更に影響を与える可能性がありますが、
|
|
38
|
+
# 単純な適用でも直鎖構造には効果的です。
|
|
39
|
+
for match in matches:
|
|
40
|
+
idx1, idx2, idx3, idx4 = match
|
|
41
|
+
|
|
42
|
+
# 二面角を180度(Trans)に設定
|
|
43
|
+
rdMolTransforms.SetDihedralDeg(conf, idx1, idx2, idx3, idx4, 180.0)
|
|
44
|
+
count += 1
|
|
45
|
+
|
|
46
|
+
# ビューの更新
|
|
47
|
+
if hasattr(mw, "draw_molecule_3d"):
|
|
48
|
+
mw.draw_molecule_3d(mol)
|
|
49
|
+
elif hasattr(mw, "update_view"):
|
|
50
|
+
mw.update_view()
|
|
51
|
+
elif hasattr(mw, "gl_widget"):
|
|
52
|
+
getattr(mw.gl_widget, "update", lambda: None)()
|
|
53
|
+
|
|
54
|
+
# Push undo state after modification
|
|
55
|
+
if hasattr(mw, "push_undo_state"):
|
|
56
|
+
mw.push_undo_state()
|
|
57
|
+
|
|
58
|
+
QMessageBox.information(mw, PLUGIN_NAME, f"Applied All-Trans to {count} torsions.")
|
|
59
|
+
else:
|
|
60
|
+
QMessageBox.information(mw, PLUGIN_NAME, "No alkyl chains found.")
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
QMessageBox.critical(mw, PLUGIN_NAME, f"Error: {str(e)}")
|
|
64
|
+
|
|
65
|
+
# initialize removed as it only registered the menu action
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import math
|
|
3
|
+
from PyQt6.QtWidgets import (
|
|
4
|
+
QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
|
|
5
|
+
QMessageBox, QLabel, QProgressBar, QSpinBox,
|
|
6
|
+
QGroupBox, QFormLayout, QComboBox
|
|
7
|
+
)
|
|
8
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
9
|
+
from PyQt6.QtGui import QFont
|
|
10
|
+
from rdkit import Chem
|
|
11
|
+
from rdkit.Chem import AllChem, rdMolTransforms
|
|
12
|
+
|
|
13
|
+
PLUGIN_NAME = "Complex Molecule Untangler"
|
|
14
|
+
__version__="2025.12.25"
|
|
15
|
+
__author__="HiroYokoyama"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UntangleWorker(QThread):
|
|
19
|
+
"""
|
|
20
|
+
回転可能な結合をランダムに回して、衝突(エネルギー)が減る配置を探すスレッド
|
|
21
|
+
"""
|
|
22
|
+
progress = pyqtSignal(int)
|
|
23
|
+
finished = pyqtSignal(object, str)
|
|
24
|
+
|
|
25
|
+
def __init__(self, mol, max_iter=500, force_field="MMFF94"):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.mol = mol
|
|
28
|
+
self.max_iter = max_iter
|
|
29
|
+
self.force_field = force_field
|
|
30
|
+
|
|
31
|
+
def run(self):
|
|
32
|
+
try:
|
|
33
|
+
# 元の分子をコピーして操作
|
|
34
|
+
work_mol = Chem.Mol(self.mol)
|
|
35
|
+
|
|
36
|
+
# フォースフィールドのセットアップ
|
|
37
|
+
ff = None
|
|
38
|
+
if self.force_field == "MMFF94":
|
|
39
|
+
try:
|
|
40
|
+
props = AllChem.MMFFGetMoleculeProperties(work_mol)
|
|
41
|
+
if props:
|
|
42
|
+
ff = AllChem.MMFFGetMoleculeForceField(work_mol, props)
|
|
43
|
+
except:
|
|
44
|
+
pass
|
|
45
|
+
elif self.force_field == "UFF":
|
|
46
|
+
try:
|
|
47
|
+
ff = AllChem.UFFGetMoleculeForceField(work_mol)
|
|
48
|
+
except:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# フォースフィールド構築失敗時のフォールバックなどは今回は厳密にしない(エラー通知)
|
|
52
|
+
if not ff:
|
|
53
|
+
msg = f"Could not setup Force Field ({self.force_field})."
|
|
54
|
+
if self.force_field == "MMFF94":
|
|
55
|
+
msg += "\nTry using UFF if MMFF94 parameters are missing."
|
|
56
|
+
self.finished.emit(None, msg)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# 初期エネルギー(衝突具合)
|
|
60
|
+
current_energy = ff.CalcEnergy()
|
|
61
|
+
|
|
62
|
+
# 回転可能な結合(二面角)を探索
|
|
63
|
+
# SMARTSパターン: アミド結合などを除外した、厳密な回転可能結合
|
|
64
|
+
rotatable_smarts = Chem.MolFromSmarts('[!$(*#*)&!D1]-&!@[!$(*#*)&!D1]')
|
|
65
|
+
matches = work_mol.GetSubstructMatches(rotatable_smarts)
|
|
66
|
+
|
|
67
|
+
if not matches:
|
|
68
|
+
self.finished.emit(None, "No rotatable bonds found.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# 結合ごとの4原子インデックス(i, j, k, l)のリストを作成
|
|
72
|
+
dihedrals = []
|
|
73
|
+
for (j, k) in matches:
|
|
74
|
+
# j-k が回転軸。それぞれの隣接原子 i, l を探す
|
|
75
|
+
atom_j = work_mol.GetAtomWithIdx(j)
|
|
76
|
+
atom_k = work_mol.GetAtomWithIdx(k)
|
|
77
|
+
|
|
78
|
+
# jの隣接原子でk以外
|
|
79
|
+
nbrs_j = [n.GetIdx() for n in atom_j.GetNeighbors() if n.GetIdx() != k]
|
|
80
|
+
# kの隣接原子でj以外
|
|
81
|
+
nbrs_k = [n.GetIdx() for n in atom_k.GetNeighbors() if n.GetIdx() != j]
|
|
82
|
+
|
|
83
|
+
if nbrs_j and nbrs_k:
|
|
84
|
+
dihedrals.append((nbrs_j[0], j, k, nbrs_k[0]))
|
|
85
|
+
|
|
86
|
+
if not dihedrals:
|
|
87
|
+
self.finished.emit(None, "Could not define dihedrals.")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# --- メインループ: ランダム回転による衝突回避 ---
|
|
91
|
+
conf = work_mol.GetConformer()
|
|
92
|
+
|
|
93
|
+
for i in range(self.max_iter):
|
|
94
|
+
# ランダムに1つ選択
|
|
95
|
+
i_idx, j_idx, k_idx, l_idx = random.choice(dihedrals)
|
|
96
|
+
|
|
97
|
+
# 現在の角度を保存
|
|
98
|
+
old_angle = rdMolTransforms.GetDihedralDeg(conf, i_idx, j_idx, k_idx, l_idx)
|
|
99
|
+
|
|
100
|
+
# ランダムに回転 (-180度 〜 +180度 の範囲で新しい角度を決定)
|
|
101
|
+
new_angle = random.uniform(-180, 180)
|
|
102
|
+
|
|
103
|
+
# 回転適用
|
|
104
|
+
rdMolTransforms.SetDihedralDeg(conf, i_idx, j_idx, k_idx, l_idx, new_angle)
|
|
105
|
+
|
|
106
|
+
# 判定
|
|
107
|
+
# 座標が変わったのでFFを更新する必要があるか? -> RDKitのFFは座標更新を追跡しない場合があるが、
|
|
108
|
+
# CalcEnergyは現在のCoordsを使うはず。ただしInitializeが必要な場合も。
|
|
109
|
+
# RDKit通常の使用法では座標を変えたらそのままCalcEnergyで反映される。
|
|
110
|
+
new_energy = ff.CalcEnergy()
|
|
111
|
+
|
|
112
|
+
if new_energy < current_energy:
|
|
113
|
+
# 改善した(衝突が減った) -> 採用
|
|
114
|
+
current_energy = new_energy
|
|
115
|
+
else:
|
|
116
|
+
# 悪化した(ぶつかった) -> 元に戻す
|
|
117
|
+
rdMolTransforms.SetDihedralDeg(conf, i_idx, j_idx, k_idx, l_idx, old_angle)
|
|
118
|
+
|
|
119
|
+
# 進捗通知
|
|
120
|
+
self.progress.emit(i + 1)
|
|
121
|
+
|
|
122
|
+
# 最後に軽く整列(微調整)して仕上げ
|
|
123
|
+
# 選択されたFFで最適化
|
|
124
|
+
if self.force_field == "MMFF94":
|
|
125
|
+
try:
|
|
126
|
+
AllChem.MMFFOptimizeMolecule(work_mol, maxIters=50)
|
|
127
|
+
except:
|
|
128
|
+
pass
|
|
129
|
+
elif self.force_field == "UFF":
|
|
130
|
+
try:
|
|
131
|
+
AllChem.UFFOptimizeMolecule(work_mol, maxIters=50)
|
|
132
|
+
except:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
self.finished.emit(work_mol, f"Processed {len(matches)} bonds.\nFinal Score ({self.force_field}): {current_energy:.2f}")
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self.finished.emit(None, str(e))
|
|
139
|
+
|
|
140
|
+
class UntanglerDialog(QDialog):
|
|
141
|
+
def __init__(self, main_window, parent=None):
|
|
142
|
+
super().__init__(parent)
|
|
143
|
+
self.main_window = main_window
|
|
144
|
+
self.setWindowTitle("Complex Molecule Untangler")
|
|
145
|
+
self.resize(320, 350)
|
|
146
|
+
|
|
147
|
+
self.worker = None
|
|
148
|
+
self.init_ui()
|
|
149
|
+
|
|
150
|
+
def init_ui(self):
|
|
151
|
+
layout = QVBoxLayout(self)
|
|
152
|
+
|
|
153
|
+
# --- Header Section ---
|
|
154
|
+
title_label = QLabel("Complex Molecule Untangler")
|
|
155
|
+
title_font = QFont()
|
|
156
|
+
title_font.setBold(True)
|
|
157
|
+
title_font.setPointSize(11)
|
|
158
|
+
title_label.setFont(title_font)
|
|
159
|
+
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
160
|
+
layout.addWidget(title_label)
|
|
161
|
+
|
|
162
|
+
sub_label = QLabel("Resolves steric clashes by randomly rotating single bonds (Monte Carlo).")
|
|
163
|
+
sub_label.setWordWrap(True)
|
|
164
|
+
sub_label.setStyleSheet("color: #555; margin-bottom: 10px;")
|
|
165
|
+
layout.addWidget(sub_label)
|
|
166
|
+
|
|
167
|
+
# --- Configuration Group ---
|
|
168
|
+
config_group = QGroupBox("Configuration")
|
|
169
|
+
form_layout = QFormLayout()
|
|
170
|
+
|
|
171
|
+
# Force Field Selection
|
|
172
|
+
self.combo_ff = QComboBox()
|
|
173
|
+
self.combo_ff.addItems(["MMFF94", "UFF"])
|
|
174
|
+
self.combo_ff.setToolTip("Select the Force Field for energy calculation.")
|
|
175
|
+
form_layout.addRow("Force Field:", self.combo_ff)
|
|
176
|
+
|
|
177
|
+
# Set default based on main window setting
|
|
178
|
+
default_method = getattr(self.main_window, "optimization_method", "MMFF_RDKIT")
|
|
179
|
+
if default_method:
|
|
180
|
+
default_method = default_method.upper()
|
|
181
|
+
if "UFF" in default_method:
|
|
182
|
+
self.combo_ff.setCurrentText("UFF")
|
|
183
|
+
else:
|
|
184
|
+
self.combo_ff.setCurrentText("MMFF94")
|
|
185
|
+
|
|
186
|
+
# Max Iterations
|
|
187
|
+
self.spin_iter = QSpinBox()
|
|
188
|
+
self.spin_iter.setRange(100, 10000)
|
|
189
|
+
self.spin_iter.setValue(500)
|
|
190
|
+
self.spin_iter.setSingleStep(100)
|
|
191
|
+
self.spin_iter.setToolTip("Number of random rotation attempts.")
|
|
192
|
+
form_layout.addRow("Max Iterations:", self.spin_iter)
|
|
193
|
+
|
|
194
|
+
config_group.setLayout(form_layout)
|
|
195
|
+
layout.addWidget(config_group)
|
|
196
|
+
|
|
197
|
+
# --- Progress Area ---
|
|
198
|
+
self.pbar = QProgressBar()
|
|
199
|
+
self.pbar.setValue(0)
|
|
200
|
+
self.pbar.setTextVisible(False)
|
|
201
|
+
layout.addWidget(self.pbar)
|
|
202
|
+
|
|
203
|
+
# --- Action Area ---
|
|
204
|
+
self.btn_run = QPushButton("Untangle Molecule")
|
|
205
|
+
self.btn_run.setMinimumHeight(40)
|
|
206
|
+
font_btn = QFont()
|
|
207
|
+
font_btn.setBold(True)
|
|
208
|
+
self.btn_run.setFont(font_btn)
|
|
209
|
+
self.btn_run.clicked.connect(self.run_untangle)
|
|
210
|
+
layout.addWidget(self.btn_run)
|
|
211
|
+
|
|
212
|
+
def run_untangle(self):
|
|
213
|
+
mol = getattr(self.main_window, "current_mol", None)
|
|
214
|
+
if not mol:
|
|
215
|
+
QMessageBox.warning(self, PLUGIN_NAME, "No molecule loaded.\nPlease load a molecule first.")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
self.btn_run.setEnabled(False)
|
|
219
|
+
self.btn_run.setText("Processing...")
|
|
220
|
+
|
|
221
|
+
max_iter = self.spin_iter.value()
|
|
222
|
+
ff_choice = self.combo_ff.currentText()
|
|
223
|
+
|
|
224
|
+
self.pbar.setRange(0, max_iter)
|
|
225
|
+
self.pbar.setValue(0)
|
|
226
|
+
self.pbar.setTextVisible(True)
|
|
227
|
+
|
|
228
|
+
self.worker = UntangleWorker(mol, max_iter=max_iter, force_field=ff_choice)
|
|
229
|
+
self.worker.progress.connect(self.pbar.setValue)
|
|
230
|
+
self.worker.finished.connect(self.on_finished)
|
|
231
|
+
self.worker.start()
|
|
232
|
+
|
|
233
|
+
def on_finished(self, new_mol, msg):
|
|
234
|
+
self.btn_run.setEnabled(True)
|
|
235
|
+
self.btn_run.setText("Untangle Molecule")
|
|
236
|
+
self.pbar.setTextVisible(False)
|
|
237
|
+
self.pbar.setValue(0)
|
|
238
|
+
|
|
239
|
+
if new_mol:
|
|
240
|
+
self.main_window.current_mol = new_mol
|
|
241
|
+
|
|
242
|
+
# ビュー更新
|
|
243
|
+
if hasattr(self.main_window, "draw_molecule_3d"):
|
|
244
|
+
self.main_window.draw_molecule_3d(new_mol)
|
|
245
|
+
elif hasattr(self.main_window, "update_view"):
|
|
246
|
+
self.main_window.update_view()
|
|
247
|
+
elif hasattr(self.main_window, "gl_widget"):
|
|
248
|
+
getattr(self.main_window.gl_widget, "update", lambda: None)()
|
|
249
|
+
|
|
250
|
+
# Push undo state using the newly applied molecule
|
|
251
|
+
if hasattr(self.main_window, "push_undo_state"):
|
|
252
|
+
self.main_window.push_undo_state()
|
|
253
|
+
|
|
254
|
+
QMessageBox.information(self, PLUGIN_NAME, f"Untangling Complete!\n{msg}")
|
|
255
|
+
else:
|
|
256
|
+
QMessageBox.warning(self, PLUGIN_NAME, f"Error: {msg}")
|
|
257
|
+
|
|
258
|
+
def run(mw):
|
|
259
|
+
if hasattr(mw, "_untangler_dialog") and mw._untangler_dialog.isVisible():
|
|
260
|
+
mw._untangler_dialog.raise_()
|
|
261
|
+
mw._untangler_dialog.activateWindow()
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
dialog = UntanglerDialog(mw, parent=mw)
|
|
265
|
+
mw._untangler_dialog = dialog
|
|
266
|
+
dialog.show()
|
|
267
|
+
|
|
268
|
+
# initialize removed as it only registered the menu action
|