MoleditPy 2.2.0a2__py3-none-any.whl → 2.2.1__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.
Files changed (27) hide show
  1. moleditpy/modules/constants.py +1 -1
  2. moleditpy/modules/main_window_main_init.py +32 -14
  3. moleditpy/modules/plugin_interface.py +1 -10
  4. moleditpy/modules/plugin_manager.py +0 -3
  5. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/METADATA +1 -1
  6. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/RECORD +10 -27
  7. moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -919
  8. moleditpy/plugins/File/animated_xyz_giffer.py +0 -583
  9. moleditpy/plugins/File/cube_viewer.py +0 -689
  10. moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -1148
  11. moleditpy/plugins/File/mapped_cube_viewer.py +0 -552
  12. moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -1226
  13. moleditpy/plugins/File/paste_xyz.py +0 -336
  14. moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -930
  15. moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -1028
  16. moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +0 -286
  17. moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -65
  18. moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -268
  19. moleditpy/plugins/Optimization/conf_search.py +0 -224
  20. moleditpy/plugins/Utility/atom_colorizer.py +0 -262
  21. moleditpy/plugins/Utility/console.py +0 -163
  22. moleditpy/plugins/Utility/pubchem_ressolver.py +0 -244
  23. moleditpy/plugins/Utility/vdw_radii_overlay.py +0 -432
  24. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/WHEEL +0 -0
  25. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/entry_points.txt +0 -0
  26. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/licenses/LICENSE +0 -0
  27. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.1.dist-info}/top_level.txt +0 -0
@@ -1,286 +0,0 @@
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
@@ -1,65 +0,0 @@
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
@@ -1,268 +0,0 @@
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