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.
Files changed (59) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. 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)