MoleditPy-linux 2.4.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.
- moleditpy_linux/__init__.py +17 -0
- moleditpy_linux/__main__.py +29 -0
- moleditpy_linux/main.py +37 -0
- moleditpy_linux/modules/__init__.py +41 -0
- moleditpy_linux/modules/about_dialog.py +104 -0
- moleditpy_linux/modules/align_plane_dialog.py +292 -0
- moleditpy_linux/modules/alignment_dialog.py +272 -0
- moleditpy_linux/modules/analysis_window.py +209 -0
- moleditpy_linux/modules/angle_dialog.py +440 -0
- moleditpy_linux/modules/assets/file_icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.icns +0 -0
- moleditpy_linux/modules/assets/icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.png +0 -0
- moleditpy_linux/modules/atom_item.py +395 -0
- moleditpy_linux/modules/bond_item.py +464 -0
- moleditpy_linux/modules/bond_length_dialog.py +380 -0
- moleditpy_linux/modules/calculation_worker.py +766 -0
- moleditpy_linux/modules/color_settings_dialog.py +321 -0
- moleditpy_linux/modules/constants.py +88 -0
- moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
- moleditpy_linux/modules/custom_interactor_style.py +749 -0
- moleditpy_linux/modules/custom_qt_interactor.py +102 -0
- moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
- moleditpy_linux/modules/dihedral_dialog.py +443 -0
- moleditpy_linux/modules/main_window.py +850 -0
- moleditpy_linux/modules/main_window_app_state.py +787 -0
- moleditpy_linux/modules/main_window_compute.py +1242 -0
- moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
- moleditpy_linux/modules/main_window_edit_3d.py +536 -0
- moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
- moleditpy_linux/modules/main_window_export.py +917 -0
- moleditpy_linux/modules/main_window_main_init.py +2100 -0
- moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
- moleditpy_linux/modules/main_window_project_io.py +434 -0
- moleditpy_linux/modules/main_window_string_importers.py +275 -0
- moleditpy_linux/modules/main_window_ui_manager.py +602 -0
- moleditpy_linux/modules/main_window_view_3d.py +1539 -0
- moleditpy_linux/modules/main_window_view_loaders.py +355 -0
- moleditpy_linux/modules/mirror_dialog.py +122 -0
- moleditpy_linux/modules/molecular_data.py +302 -0
- moleditpy_linux/modules/molecule_scene.py +2000 -0
- moleditpy_linux/modules/move_group_dialog.py +600 -0
- moleditpy_linux/modules/periodic_table_dialog.py +84 -0
- moleditpy_linux/modules/planarize_dialog.py +220 -0
- moleditpy_linux/modules/plugin_interface.py +215 -0
- moleditpy_linux/modules/plugin_manager.py +473 -0
- moleditpy_linux/modules/plugin_manager_window.py +274 -0
- moleditpy_linux/modules/settings_dialog.py +1503 -0
- moleditpy_linux/modules/template_preview_item.py +157 -0
- moleditpy_linux/modules/template_preview_view.py +74 -0
- moleditpy_linux/modules/translation_dialog.py +364 -0
- moleditpy_linux/modules/user_template_dialog.py +692 -0
- moleditpy_linux/modules/zoomable_view.py +129 -0
- moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
- moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
- moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
- moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
- moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
- moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from PyQt6.QtWidgets import (
|
|
14
|
+
QDialog, QVBoxLayout, QLabel, QFormLayout, QComboBox, QLineEdit, QTableWidget, QTableWidgetItem, QHBoxLayout,
|
|
15
|
+
QPushButton, QMessageBox, QAbstractItemView
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
19
|
+
|
|
20
|
+
from PyQt6.QtCore import Qt
|
|
21
|
+
from rdkit.Chem import AllChem, rdMolTransforms
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
25
|
+
"""制約付き最適化ダイアログ"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, mol, main_window, parent=None):
|
|
28
|
+
QDialog.__init__(self, parent)
|
|
29
|
+
Dialog3DPickingMixin.__init__(self)
|
|
30
|
+
self.mol = mol
|
|
31
|
+
self.main_window = main_window
|
|
32
|
+
self.selected_atoms = [] # 順序が重要なのでリストを使用
|
|
33
|
+
self.constraints = [] # (type, atoms_indices, value)
|
|
34
|
+
self.constraint_labels = [] # 3Dラベルアクター
|
|
35
|
+
self.init_ui()
|
|
36
|
+
self.enable_picking()
|
|
37
|
+
|
|
38
|
+
# MainWindowから既存の制約を読み込む
|
|
39
|
+
if self.main_window.constraints_3d:
|
|
40
|
+
self.constraint_table.blockSignals(True) # 読み込み中のシグナルをブロック
|
|
41
|
+
try:
|
|
42
|
+
# self.constraints には (Type, (Idx...), Value, Force) のタプル形式で読み込む
|
|
43
|
+
for const_data in self.main_window.constraints_3d:
|
|
44
|
+
# 後方互換性のため、3要素または4要素の制約に対応
|
|
45
|
+
if len(const_data) == 4:
|
|
46
|
+
const_type, atom_indices, value, force_const = const_data
|
|
47
|
+
else:
|
|
48
|
+
const_type, atom_indices, value = const_data
|
|
49
|
+
force_const = 1.0e5 # デフォルト値
|
|
50
|
+
|
|
51
|
+
# タプル化して内部リストに追加
|
|
52
|
+
self.constraints.append((const_type, tuple(atom_indices), value, force_const))
|
|
53
|
+
|
|
54
|
+
row_count = self.constraint_table.rowCount()
|
|
55
|
+
self.constraint_table.insertRow(row_count)
|
|
56
|
+
|
|
57
|
+
value_str = ""
|
|
58
|
+
if const_type == "Distance":
|
|
59
|
+
value_str = f"{value:.3f}"
|
|
60
|
+
else:
|
|
61
|
+
value_str = f"{value:.2f}"
|
|
62
|
+
|
|
63
|
+
# カラム 0 (Type)
|
|
64
|
+
item_type = QTableWidgetItem(const_type)
|
|
65
|
+
item_type.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
66
|
+
item_type.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
67
|
+
self.constraint_table.setItem(row_count, 0, item_type)
|
|
68
|
+
|
|
69
|
+
# カラム 1 (Atom Indices)
|
|
70
|
+
item_indices = QTableWidgetItem(str(atom_indices))
|
|
71
|
+
item_indices.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
72
|
+
item_indices.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
73
|
+
self.constraint_table.setItem(row_count, 1, item_indices)
|
|
74
|
+
|
|
75
|
+
# カラム 2 (Value)
|
|
76
|
+
item_value = QTableWidgetItem(value_str)
|
|
77
|
+
item_value.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
78
|
+
self.constraint_table.setItem(row_count, 2, item_value)
|
|
79
|
+
|
|
80
|
+
# カラム 3 (Force)
|
|
81
|
+
item_force = QTableWidgetItem(f"{force_const:.2e}")
|
|
82
|
+
item_force.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
83
|
+
self.constraint_table.setItem(row_count, 3, item_force)
|
|
84
|
+
finally:
|
|
85
|
+
self.constraint_table.blockSignals(False)
|
|
86
|
+
|
|
87
|
+
# <<< MainWindowの現在の最適化設定を読み込み、デフォルトにする >>>
|
|
88
|
+
try:
|
|
89
|
+
# (修正) None の場合に備えてフォールバックを追加
|
|
90
|
+
current_method_str = self.main_window.optimization_method or "MMFF_RDKIT"
|
|
91
|
+
current_method = current_method_str.upper()
|
|
92
|
+
|
|
93
|
+
# (修正) 比較順序を厳密化
|
|
94
|
+
|
|
95
|
+
# 1. UFF_RDKIT
|
|
96
|
+
if current_method == "UFF_RDKIT":
|
|
97
|
+
self.ff_combo.setCurrentText("UFF")
|
|
98
|
+
|
|
99
|
+
# 2. MMFF94_RDKIT (MMFF94)
|
|
100
|
+
elif current_method == "MMFF94_RDKIT":
|
|
101
|
+
self.ff_combo.setCurrentText("MMFF94")
|
|
102
|
+
|
|
103
|
+
# 3. MMFF_RDKIT (MMFF94s) - これがデフォルトでもある
|
|
104
|
+
elif current_method == "MMFF_RDKIT":
|
|
105
|
+
self.ff_combo.setCurrentText("MMFF94s")
|
|
106
|
+
|
|
107
|
+
# 4. (古い設定ファイルなどからのフォールバック)
|
|
108
|
+
elif "UFF" in current_method:
|
|
109
|
+
self.ff_combo.setCurrentText("UFF")
|
|
110
|
+
elif "MMFF94S" in current_method:
|
|
111
|
+
self.ff_combo.setCurrentText("MMFF94s")
|
|
112
|
+
elif "MMFF94" in current_method: # MMFF94_RDKITも含むが、先で処理済み
|
|
113
|
+
self.ff_combo.setCurrentText("MMFF94")
|
|
114
|
+
|
|
115
|
+
# 5. デフォルト
|
|
116
|
+
else:
|
|
117
|
+
self.ff_combo.setCurrentText("MMFF94s")
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"Could not set default force field: {e}")
|
|
121
|
+
|
|
122
|
+
def init_ui(self):
|
|
123
|
+
self.setWindowTitle("Constrained Optimization")
|
|
124
|
+
self.setModal(False)
|
|
125
|
+
self.resize(450, 500)
|
|
126
|
+
layout = QVBoxLayout(self)
|
|
127
|
+
|
|
128
|
+
# 1. 説明
|
|
129
|
+
instruction_label = QLabel("Select 2-4 atoms to add a constraint. Select constraints in the table to remove them.")
|
|
130
|
+
instruction_label.setWordWrap(True)
|
|
131
|
+
layout.addWidget(instruction_label)
|
|
132
|
+
|
|
133
|
+
# 2. 最適化方法とForce Constant
|
|
134
|
+
form_layout = QFormLayout()
|
|
135
|
+
self.ff_combo = QComboBox()
|
|
136
|
+
self.ff_combo.addItems(["MMFF94s", "MMFF94", "UFF"])
|
|
137
|
+
form_layout.addRow("Force Field:", self.ff_combo)
|
|
138
|
+
|
|
139
|
+
# Force Constant設定
|
|
140
|
+
self.force_const_input = QLineEdit("1.0e5")
|
|
141
|
+
self.force_const_input.setToolTip("Force constant for constraints (default: 1.0e5)")
|
|
142
|
+
form_layout.addRow("Force Constant:", self.force_const_input)
|
|
143
|
+
|
|
144
|
+
layout.addLayout(form_layout)
|
|
145
|
+
|
|
146
|
+
# 3. 選択中の原子
|
|
147
|
+
self.selection_label = QLabel("Selected atoms: None")
|
|
148
|
+
layout.addWidget(self.selection_label)
|
|
149
|
+
|
|
150
|
+
# 4. 制約の表
|
|
151
|
+
self.constraint_table = QTableWidget()
|
|
152
|
+
self.constraint_table.setColumnCount(4)
|
|
153
|
+
self.constraint_table.setHorizontalHeaderLabels(["Type", "Atom Indices", "Value (Å or °)", "Force"])
|
|
154
|
+
self.constraint_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
155
|
+
# 編集トリガーをダブルクリックなどに変更
|
|
156
|
+
self.constraint_table.setEditTriggers(QTableWidget.EditTrigger.DoubleClicked | QTableWidget.EditTrigger.SelectedClicked | QTableWidget.EditTrigger.EditKeyPressed)
|
|
157
|
+
self.constraint_table.itemSelectionChanged.connect(self.show_constraint_labels)
|
|
158
|
+
self.constraint_table.cellChanged.connect(self.on_cell_changed)
|
|
159
|
+
|
|
160
|
+
self.constraint_table.setStyleSheet("""
|
|
161
|
+
QTableWidget QLineEdit {
|
|
162
|
+
background-color: white;
|
|
163
|
+
color: black;
|
|
164
|
+
border: none;
|
|
165
|
+
}
|
|
166
|
+
""")
|
|
167
|
+
|
|
168
|
+
layout.addWidget(self.constraint_table)
|
|
169
|
+
|
|
170
|
+
# 5. ボタン (Add / Remove)
|
|
171
|
+
button_layout = QHBoxLayout()
|
|
172
|
+
self.add_button = QPushButton("Add Constraint")
|
|
173
|
+
self.add_button.clicked.connect(self.add_constraint)
|
|
174
|
+
self.add_button.setEnabled(False)
|
|
175
|
+
button_layout.addWidget(self.add_button)
|
|
176
|
+
|
|
177
|
+
self.remove_button = QPushButton("Remove Selected")
|
|
178
|
+
self.remove_button.clicked.connect(self.remove_constraint)
|
|
179
|
+
self.remove_button.setEnabled(False)
|
|
180
|
+
button_layout.addWidget(self.remove_button)
|
|
181
|
+
layout.addLayout(button_layout)
|
|
182
|
+
|
|
183
|
+
self.remove_all_button = QPushButton("Remove All")
|
|
184
|
+
self.remove_all_button.clicked.connect(self.remove_all_constraints)
|
|
185
|
+
button_layout.addWidget(self.remove_all_button)
|
|
186
|
+
|
|
187
|
+
# 6. メインボタン (Optimize / Close)
|
|
188
|
+
main_buttons = QHBoxLayout()
|
|
189
|
+
main_buttons.addStretch()
|
|
190
|
+
self.optimize_button = QPushButton("Optimize")
|
|
191
|
+
self.optimize_button.clicked.connect(self.apply_optimization)
|
|
192
|
+
main_buttons.addWidget(self.optimize_button)
|
|
193
|
+
|
|
194
|
+
close_button = QPushButton("Close")
|
|
195
|
+
close_button.clicked.connect(self.reject)
|
|
196
|
+
main_buttons.addWidget(close_button)
|
|
197
|
+
layout.addLayout(main_buttons)
|
|
198
|
+
|
|
199
|
+
def on_atom_picked(self, atom_idx):
|
|
200
|
+
if atom_idx in self.selected_atoms:
|
|
201
|
+
self.selected_atoms.remove(atom_idx)
|
|
202
|
+
else:
|
|
203
|
+
if len(self.selected_atoms) >= 4:
|
|
204
|
+
self.selected_atoms.pop(0) # 4つまで
|
|
205
|
+
self.selected_atoms.append(atom_idx)
|
|
206
|
+
|
|
207
|
+
self.show_selection_labels()
|
|
208
|
+
self.update_selection_display()
|
|
209
|
+
|
|
210
|
+
def update_selection_display(self):
|
|
211
|
+
self.show_selection_labels()
|
|
212
|
+
n = len(self.selected_atoms)
|
|
213
|
+
|
|
214
|
+
atom_str = ", ".join(map(str, self.selected_atoms))
|
|
215
|
+
prefix = ""
|
|
216
|
+
can_add = False
|
|
217
|
+
|
|
218
|
+
if n == 0:
|
|
219
|
+
prefix = "Selected atoms: None"
|
|
220
|
+
atom_str = "" # atom_str を空にする
|
|
221
|
+
elif n == 1:
|
|
222
|
+
prefix = "Selected atoms: "
|
|
223
|
+
elif n == 2:
|
|
224
|
+
prefix = "Selected atoms: <b>Distance</b> "
|
|
225
|
+
can_add = True
|
|
226
|
+
elif n == 3:
|
|
227
|
+
prefix = "Selected atoms: <b>Angle</b> "
|
|
228
|
+
can_add = True
|
|
229
|
+
elif n == 4:
|
|
230
|
+
prefix = "Selected atoms: <b>Torsion</b> "
|
|
231
|
+
can_add = True
|
|
232
|
+
else: # n > 4
|
|
233
|
+
prefix = "Selected atoms (max 4): "
|
|
234
|
+
|
|
235
|
+
# ラベルテキストを設定
|
|
236
|
+
if n == 0:
|
|
237
|
+
self.selection_label.setText(prefix)
|
|
238
|
+
else:
|
|
239
|
+
self.selection_label.setText(f"{prefix}[{atom_str}]")
|
|
240
|
+
|
|
241
|
+
# ボタンのテキストは常に固定
|
|
242
|
+
self.add_button.setText("Add Constraint")
|
|
243
|
+
# ボタンの有効状態を設定
|
|
244
|
+
self.add_button.setEnabled(can_add)
|
|
245
|
+
|
|
246
|
+
def add_constraint(self):
|
|
247
|
+
n = len(self.selected_atoms)
|
|
248
|
+
conf = self.mol.GetConformer()
|
|
249
|
+
|
|
250
|
+
# Force Constantを取得
|
|
251
|
+
try:
|
|
252
|
+
force_const = float(self.force_const_input.text())
|
|
253
|
+
except ValueError:
|
|
254
|
+
QMessageBox.warning(self, "Warning", "Invalid Force Constant. Using default 1.0e5.")
|
|
255
|
+
force_const = 1.0e5
|
|
256
|
+
|
|
257
|
+
if n == 2:
|
|
258
|
+
constraint_type = "Distance"
|
|
259
|
+
value = conf.GetAtomPosition(self.selected_atoms[0]).Distance(conf.GetAtomPosition(self.selected_atoms[1]))
|
|
260
|
+
value_str = f"{value:.3f}"
|
|
261
|
+
elif n == 3:
|
|
262
|
+
constraint_type = "Angle"
|
|
263
|
+
value = rdMolTransforms.GetAngleDeg(conf, *self.selected_atoms)
|
|
264
|
+
value_str = f"{value:.2f}"
|
|
265
|
+
elif n == 4:
|
|
266
|
+
constraint_type = "Torsion"
|
|
267
|
+
value = rdMolTransforms.GetDihedralDeg(conf, *self.selected_atoms)
|
|
268
|
+
value_str = f"{value:.2f}"
|
|
269
|
+
else:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
atom_indices = tuple(self.selected_atoms)
|
|
273
|
+
|
|
274
|
+
# 既存の制約と重複チェック (原子インデックスが同じもの)
|
|
275
|
+
for const in self.constraints:
|
|
276
|
+
if const[0] == constraint_type and const[1] == atom_indices:
|
|
277
|
+
QMessageBox.warning(self, "Warning", "This exact constraint already exists.")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
self.constraints.append((constraint_type, atom_indices, value, force_const))
|
|
281
|
+
|
|
282
|
+
# 表を更新
|
|
283
|
+
# 表を更新
|
|
284
|
+
row_count = self.constraint_table.rowCount()
|
|
285
|
+
self.constraint_table.insertRow(row_count)
|
|
286
|
+
|
|
287
|
+
# --- カラム 0 (Type) ---
|
|
288
|
+
item_type = QTableWidgetItem(constraint_type)
|
|
289
|
+
# 編集不可フラグを設定 (ItemIsEnabled | ItemIsSelectable)
|
|
290
|
+
item_type.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
291
|
+
item_type.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
292
|
+
self.constraint_table.setItem(row_count, 0, item_type)
|
|
293
|
+
|
|
294
|
+
# --- カラム 1 (Atom Indices) ---
|
|
295
|
+
item_indices = QTableWidgetItem(str(atom_indices))
|
|
296
|
+
# 編集不可フラグを設定
|
|
297
|
+
item_indices.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
298
|
+
item_indices.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
299
|
+
self.constraint_table.setItem(row_count, 1, item_indices)
|
|
300
|
+
|
|
301
|
+
# --- カラム 2 (Value) ---
|
|
302
|
+
item_value = QTableWidgetItem(value_str)
|
|
303
|
+
item_value.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
304
|
+
# 編集可能フラグはデフォルトで有効 (ItemIsEnabled | ItemIsSelectable | ItemIsEditable)
|
|
305
|
+
self.constraint_table.setItem(row_count, 2, item_value)
|
|
306
|
+
|
|
307
|
+
# --- カラム 3 (Force) ---
|
|
308
|
+
item_force = QTableWidgetItem(f"{force_const:.2e}")
|
|
309
|
+
item_force.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
310
|
+
# 編集可能
|
|
311
|
+
self.constraint_table.setItem(row_count, 3, item_force)
|
|
312
|
+
|
|
313
|
+
# 選択をクリア
|
|
314
|
+
self.selected_atoms.clear()
|
|
315
|
+
self.update_selection_display()
|
|
316
|
+
|
|
317
|
+
def remove_constraint(self):
|
|
318
|
+
selected_rows = sorted(list(set(index.row() for index in self.constraint_table.selectedIndexes())), reverse=True)
|
|
319
|
+
if not selected_rows:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
self.constraint_table.blockSignals(True)
|
|
323
|
+
|
|
324
|
+
for row in selected_rows:
|
|
325
|
+
self.constraints.pop(row)
|
|
326
|
+
self.constraint_table.removeRow(row)
|
|
327
|
+
|
|
328
|
+
self.constraint_table.blockSignals(False)
|
|
329
|
+
|
|
330
|
+
self.clear_constraint_labels()
|
|
331
|
+
|
|
332
|
+
def remove_all_constraints(self):
|
|
333
|
+
"""全ての制約をクリアする"""
|
|
334
|
+
if not self.constraints:
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# 内部リストをクリア
|
|
338
|
+
self.constraints.clear()
|
|
339
|
+
|
|
340
|
+
# テーブルの行を全て削除
|
|
341
|
+
self.constraint_table.blockSignals(True)
|
|
342
|
+
self.constraint_table.setRowCount(0)
|
|
343
|
+
self.constraint_table.blockSignals(False)
|
|
344
|
+
|
|
345
|
+
# 3Dラベルをクリア
|
|
346
|
+
self.clear_constraint_labels()
|
|
347
|
+
|
|
348
|
+
# 選択ボタンを無効化
|
|
349
|
+
self.remove_button.setEnabled(False)
|
|
350
|
+
|
|
351
|
+
def show_constraint_labels(self):
|
|
352
|
+
self.clear_constraint_labels()
|
|
353
|
+
selected_items = self.constraint_table.selectedItems()
|
|
354
|
+
if not selected_items:
|
|
355
|
+
self.remove_button.setEnabled(False)
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
self.remove_button.setEnabled(True)
|
|
359
|
+
|
|
360
|
+
# 選択された行の制約を取得 (最初の選択行のみ)
|
|
361
|
+
try:
|
|
362
|
+
row = selected_items[0].row()
|
|
363
|
+
constraint_type, atom_indices, value, force_const = self.constraints[row]
|
|
364
|
+
except (IndexError, TypeError, ValueError):
|
|
365
|
+
# 古い形式の制約の場合は3要素でunpack
|
|
366
|
+
try:
|
|
367
|
+
constraint_type, atom_indices, value = self.constraints[row]
|
|
368
|
+
except (IndexError, TypeError):
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
labels = []
|
|
372
|
+
if constraint_type == "Distance":
|
|
373
|
+
labels = ["A1", "A2"]
|
|
374
|
+
elif constraint_type == "Angle":
|
|
375
|
+
labels = ["A1", "A2 (V)", "A3"]
|
|
376
|
+
elif constraint_type == "Torsion":
|
|
377
|
+
labels = ["A1", "A2", "A3", "A4"]
|
|
378
|
+
|
|
379
|
+
positions = []
|
|
380
|
+
texts = []
|
|
381
|
+
for i, atom_idx in enumerate(atom_indices):
|
|
382
|
+
positions.append(self.main_window.atom_positions_3d[atom_idx])
|
|
383
|
+
texts.append(labels[i])
|
|
384
|
+
|
|
385
|
+
if positions:
|
|
386
|
+
label_actor = self.main_window.plotter.add_point_labels(
|
|
387
|
+
positions, texts,
|
|
388
|
+
point_size=20, font_size=12, text_color='cyan', always_visible=True
|
|
389
|
+
)
|
|
390
|
+
self.constraint_labels.append(label_actor)
|
|
391
|
+
|
|
392
|
+
def clear_constraint_labels(self):
|
|
393
|
+
for label_actor in self.constraint_labels:
|
|
394
|
+
try:
|
|
395
|
+
self.main_window.plotter.remove_actor(label_actor)
|
|
396
|
+
except Exception:
|
|
397
|
+
pass
|
|
398
|
+
self.constraint_labels = []
|
|
399
|
+
|
|
400
|
+
def apply_optimization(self):
|
|
401
|
+
if not self.mol or self.mol.GetNumConformers() == 0:
|
|
402
|
+
QMessageBox.warning(self, "Error", "No valid 3D molecule found.")
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
ff_name = self.ff_combo.currentText()
|
|
406
|
+
conf = self.mol.GetConformer()
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
if ff_name.startswith("MMFF"):
|
|
410
|
+
props = AllChem.MMFFGetMoleculeProperties(self.mol, mmffVariant=ff_name)
|
|
411
|
+
ff = AllChem.MMFFGetMoleculeForceField(self.mol, props, confId=0)
|
|
412
|
+
add_dist_constraint = ff.MMFFAddDistanceConstraint
|
|
413
|
+
add_angle_constraint = ff.MMFFAddAngleConstraint
|
|
414
|
+
add_torsion_constraint = ff.MMFFAddTorsionConstraint
|
|
415
|
+
else: # UFF
|
|
416
|
+
ff = AllChem.UFFGetMoleculeForceField(self.mol, confId=0)
|
|
417
|
+
add_dist_constraint = ff.UFFAddDistanceConstraint
|
|
418
|
+
add_angle_constraint = ff.UFFAddAngleConstraint
|
|
419
|
+
add_torsion_constraint = ff.UFFAddTorsionConstraint
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
QMessageBox.critical(self, "Error", f"Failed to initialize force field {ff_name}: {e}")
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
# 制約を追加
|
|
426
|
+
try:
|
|
427
|
+
for constraint in self.constraints:
|
|
428
|
+
# 後方互換性のため、4要素または3要素の制約に対応
|
|
429
|
+
if len(constraint) == 4:
|
|
430
|
+
const_type, atoms, value, force_const = constraint
|
|
431
|
+
else:
|
|
432
|
+
const_type, atoms, value = constraint
|
|
433
|
+
force_const = 1.0e5 # デフォルト値
|
|
434
|
+
|
|
435
|
+
if const_type == "Distance":
|
|
436
|
+
# C++ signature: (self, idx1, idx2, bool relative, minLen, maxLen, forceConst)
|
|
437
|
+
add_dist_constraint(
|
|
438
|
+
int(atoms[0]),
|
|
439
|
+
int(atoms[1]),
|
|
440
|
+
False,
|
|
441
|
+
float(value),
|
|
442
|
+
float(value),
|
|
443
|
+
float(force_const)
|
|
444
|
+
)
|
|
445
|
+
elif const_type == "Angle":
|
|
446
|
+
# C++ signature: (self, idx1, idx2, idx3, bool relative, minDeg, maxDeg, forceConst)
|
|
447
|
+
add_angle_constraint(
|
|
448
|
+
int(atoms[0]),
|
|
449
|
+
int(atoms[1]),
|
|
450
|
+
int(atoms[2]),
|
|
451
|
+
False,
|
|
452
|
+
float(value),
|
|
453
|
+
float(value),
|
|
454
|
+
float(force_const)
|
|
455
|
+
)
|
|
456
|
+
elif const_type == "Torsion":
|
|
457
|
+
# C++ signature: (self, idx1, idx2, idx3, idx4, bool relative, minDeg, maxDeg, forceConst)
|
|
458
|
+
add_torsion_constraint(
|
|
459
|
+
int(atoms[0]),
|
|
460
|
+
int(atoms[1]),
|
|
461
|
+
int(atoms[2]),
|
|
462
|
+
int(atoms[3]),
|
|
463
|
+
False,
|
|
464
|
+
float(value),
|
|
465
|
+
float(value),
|
|
466
|
+
float(force_const)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
except Exception as e:
|
|
470
|
+
QMessageBox.critical(self, "Error", f"Failed to add constraints: {e}")
|
|
471
|
+
print(e)
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# 最適化の実行
|
|
475
|
+
try:
|
|
476
|
+
self.main_window.statusBar().showMessage(f"Running constrained {ff_name} optimization...")
|
|
477
|
+
ff.Minimize(maxIts=20000)
|
|
478
|
+
|
|
479
|
+
# 最適化後の座標をメインウィンドウの numpy 配列に反映
|
|
480
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
481
|
+
pos = conf.GetAtomPosition(i)
|
|
482
|
+
self.main_window.atom_positions_3d[i] = [pos.x, pos.y, pos.z]
|
|
483
|
+
|
|
484
|
+
# 3Dビューを更新
|
|
485
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
486
|
+
self.main_window.update_chiral_labels()
|
|
487
|
+
self.main_window.push_undo_state()
|
|
488
|
+
self.main_window.statusBar().showMessage("Constrained optimization finished.")
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
constrained_method_name = f"Constrained_{ff_name}"
|
|
492
|
+
self.main_window.last_successful_optimization_method = constrained_method_name
|
|
493
|
+
except Exception as e:
|
|
494
|
+
print(f"Failed to set last_successful_optimization_method: {e}")
|
|
495
|
+
|
|
496
|
+
# (修正) 最適化成功時にも制約リストをMainWindowに保存 (reject と同じロジック)
|
|
497
|
+
try:
|
|
498
|
+
# JSON互換のため、タプルをリストに変換して保存
|
|
499
|
+
json_safe_constraints = []
|
|
500
|
+
for const in self.constraints:
|
|
501
|
+
# 4要素の制約(Type, Indices, Value, Force)
|
|
502
|
+
if len(const) == 4:
|
|
503
|
+
json_safe_constraints.append([const[0], list(const[1]), const[2], const[3]])
|
|
504
|
+
else:
|
|
505
|
+
# 古い形式の場合は3要素にデフォルトのForceを追加
|
|
506
|
+
json_safe_constraints.append([const[0], list(const[1]), const[2], 1.0e5])
|
|
507
|
+
|
|
508
|
+
# 変更があった場合のみ MainWindow を更新
|
|
509
|
+
if self.main_window.constraints_3d != json_safe_constraints:
|
|
510
|
+
self.main_window.constraints_3d = json_safe_constraints
|
|
511
|
+
self.main_window.has_unsaved_changes = True # 制約の変更も「未保存」扱い
|
|
512
|
+
self.main_window.update_window_title()
|
|
513
|
+
|
|
514
|
+
except Exception as e:
|
|
515
|
+
print(f"Failed to save constraints post-optimization: {e}")
|
|
516
|
+
|
|
517
|
+
except Exception as e:
|
|
518
|
+
QMessageBox.critical(self, "Error", f"Optimization failed: {e}")
|
|
519
|
+
|
|
520
|
+
def closeEvent(self, event):
|
|
521
|
+
self.reject()
|
|
522
|
+
event.accept()
|
|
523
|
+
|
|
524
|
+
def reject(self):
|
|
525
|
+
self.clear_constraint_labels()
|
|
526
|
+
self.clear_selection_labels()
|
|
527
|
+
self.disable_picking()
|
|
528
|
+
|
|
529
|
+
# ダイアログを閉じる際に現在の制約リストをMainWindowに保存
|
|
530
|
+
try:
|
|
531
|
+
# JSON互換のため、タプルをリストに変換して保存
|
|
532
|
+
json_safe_constraints = []
|
|
533
|
+
for const in self.constraints:
|
|
534
|
+
# (Type, (Idx...), Value, Force) -> [Type, [Idx...], Value, Force]
|
|
535
|
+
if len(const) == 4:
|
|
536
|
+
json_safe_constraints.append([const[0], list(const[1]), const[2], const[3]])
|
|
537
|
+
else:
|
|
538
|
+
# 古い形式の場合は3要素にデフォルトのForceを追加
|
|
539
|
+
json_safe_constraints.append([const[0], list(const[1]), const[2], 1.0e5])
|
|
540
|
+
|
|
541
|
+
# 変更があった場合のみ MainWindow を更新
|
|
542
|
+
if self.main_window.constraints_3d != json_safe_constraints:
|
|
543
|
+
self.main_window.constraints_3d = json_safe_constraints
|
|
544
|
+
self.main_window.has_unsaved_changes = True # 制約の変更も「未保存」扱い
|
|
545
|
+
self.main_window.update_window_title()
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
print(f"Failed to save constraints to main window: {e}")
|
|
549
|
+
|
|
550
|
+
super().reject()
|
|
551
|
+
|
|
552
|
+
def clear_selection(self):
|
|
553
|
+
"""選択をクリア (原子以外をクリックした時にMixinから呼ばれる)"""
|
|
554
|
+
self.selected_atoms.clear()
|
|
555
|
+
self.clear_selection_labels()
|
|
556
|
+
self.update_selection_display()
|
|
557
|
+
|
|
558
|
+
def show_selection_labels(self):
|
|
559
|
+
"""選択された原子にラベルを表示"""
|
|
560
|
+
self.clear_selection_labels()
|
|
561
|
+
|
|
562
|
+
if not hasattr(self, 'selection_labels'):
|
|
563
|
+
self.selection_labels = []
|
|
564
|
+
|
|
565
|
+
if not hasattr(self.main_window, 'atom_positions_3d') or self.main_window.atom_positions_3d is None:
|
|
566
|
+
return # 3D座標データがない場合は何もしない
|
|
567
|
+
|
|
568
|
+
max_idx = len(self.main_window.atom_positions_3d) - 1
|
|
569
|
+
positions = []
|
|
570
|
+
texts = []
|
|
571
|
+
|
|
572
|
+
for i, atom_idx in enumerate(self.selected_atoms):
|
|
573
|
+
if atom_idx is not None and 0 <= atom_idx <= max_idx:
|
|
574
|
+
positions.append(self.main_window.atom_positions_3d[atom_idx])
|
|
575
|
+
texts.append(f"A{i+1}")
|
|
576
|
+
elif atom_idx is not None:
|
|
577
|
+
# インデックスが無効な場合はログ(デバッグ用)
|
|
578
|
+
print(f"Warning: Invalid atom index {atom_idx} in show_selection_labels")
|
|
579
|
+
|
|
580
|
+
if positions:
|
|
581
|
+
label_actor = self.main_window.plotter.add_point_labels(
|
|
582
|
+
positions, texts,
|
|
583
|
+
point_size=20, font_size=12, text_color='yellow', always_visible=True
|
|
584
|
+
)
|
|
585
|
+
# add_point_labelsがリストを返す場合も考慮
|
|
586
|
+
if isinstance(label_actor, list):
|
|
587
|
+
self.selection_labels.extend(label_actor)
|
|
588
|
+
else:
|
|
589
|
+
self.selection_labels.append(label_actor)
|
|
590
|
+
|
|
591
|
+
def clear_selection_labels(self):
|
|
592
|
+
"""選択ラベル(A1, A2...)をクリア"""
|
|
593
|
+
if hasattr(self, 'selection_labels'):
|
|
594
|
+
for label_actor in self.selection_labels:
|
|
595
|
+
try:
|
|
596
|
+
self.main_window.plotter.remove_actor(label_actor)
|
|
597
|
+
except Exception:
|
|
598
|
+
pass
|
|
599
|
+
self.selection_labels = []
|
|
600
|
+
|
|
601
|
+
def on_cell_changed(self, row, column):
|
|
602
|
+
"""テーブルのセルが編集されたときに内部データを更新する"""
|
|
603
|
+
|
|
604
|
+
# "Value" 列 (カラムインデックス 2) と "Force" 列 (カラムインデックス 3) のみ対応
|
|
605
|
+
if column not in [2, 3]:
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
# 変更されたアイテムからテキストを取得
|
|
610
|
+
item = self.constraint_table.item(row, column)
|
|
611
|
+
if not item:
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
new_value_str = item.text()
|
|
615
|
+
new_value = float(new_value_str)
|
|
616
|
+
|
|
617
|
+
# 内部の constraints リストを更新
|
|
618
|
+
old_constraint = self.constraints[row]
|
|
619
|
+
|
|
620
|
+
# 後方互換性のため、3要素または4要素の制約に対応
|
|
621
|
+
if len(old_constraint) == 4:
|
|
622
|
+
if column == 2: # Value列
|
|
623
|
+
self.constraints[row] = (old_constraint[0], old_constraint[1], new_value, old_constraint[3])
|
|
624
|
+
elif column == 3: # Force列
|
|
625
|
+
self.constraints[row] = (old_constraint[0], old_constraint[1], old_constraint[2], new_value)
|
|
626
|
+
else:
|
|
627
|
+
# 古い3要素形式の場合
|
|
628
|
+
if column == 2: # Value列
|
|
629
|
+
self.constraints[row] = (old_constraint[0], old_constraint[1], new_value, 1.0e5)
|
|
630
|
+
elif column == 3: # Force列(新規追加)
|
|
631
|
+
self.constraints[row] = (old_constraint[0], old_constraint[1], old_constraint[2], new_value)
|
|
632
|
+
|
|
633
|
+
except (ValueError, TypeError):
|
|
634
|
+
# 不正な値(数値以外)が入力された場合
|
|
635
|
+
# 元の値をテーブルに戻す
|
|
636
|
+
self.constraint_table.blockSignals(True)
|
|
637
|
+
|
|
638
|
+
if column == 2: # Value列
|
|
639
|
+
old_value = self.constraints[row][2]
|
|
640
|
+
if self.constraints[row][0] == "Distance":
|
|
641
|
+
item.setText(f"{old_value:.3f}")
|
|
642
|
+
else:
|
|
643
|
+
item.setText(f"{old_value:.2f}")
|
|
644
|
+
elif column == 3: # Force列
|
|
645
|
+
old_force = self.constraints[row][3] if len(self.constraints[row]) == 4 else 1.0e5
|
|
646
|
+
item.setText(f"{old_force:.2e}")
|
|
647
|
+
|
|
648
|
+
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
649
|
+
self.constraint_table.blockSignals(False)
|
|
650
|
+
|
|
651
|
+
QMessageBox.warning(self, "Invalid Value", "Please enter a valid floating-point number.")
|
|
652
|
+
except IndexError:
|
|
653
|
+
# constraints リストとテーブルが同期していない場合(通常発生しない)
|
|
654
|
+
pass
|
|
655
|
+
|
|
656
|
+
def keyPressEvent(self, event):
|
|
657
|
+
"""キーボードイベントを処理 (Delete/Backspaceで制約を削除, Enterで最適化)"""
|
|
658
|
+
key = event.key()
|
|
659
|
+
|
|
660
|
+
# DeleteキーまたはBackspaceキーが押されたかチェック
|
|
661
|
+
if key == Qt.Key.Key_Delete or key == Qt.Key.Key_Backspace:
|
|
662
|
+
# テーブルがフォーカスを持っているか、またはアイテムが選択されているか確認
|
|
663
|
+
if self.constraint_table.hasFocus() or len(self.constraint_table.selectedIndexes()) > 0:
|
|
664
|
+
self.remove_constraint()
|
|
665
|
+
event.accept()
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
# Enter/Returnキーが押されたかチェック (最適化を実行)
|
|
669
|
+
if key == Qt.Key.Key_Return or key == Qt.Key.Key_Enter:
|
|
670
|
+
# テーブルが編集中でないことを確認(セルの編集中にEnterを押した場合)
|
|
671
|
+
if self.constraint_table.state() != QAbstractItemView.State.EditingState:
|
|
672
|
+
if self.optimize_button.isEnabled():
|
|
673
|
+
self.apply_optimization()
|
|
674
|
+
event.accept()
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
# それ以外のキーはデフォルトの処理
|
|
678
|
+
super().keyPressEvent(event)
|