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