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,380 @@
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, QHBoxLayout, QPushButton, QLineEdit, QWidget, QRadioButton, QMessageBox
15
+ )
16
+
17
+ from .dialog3_d_picking_mixin import Dialog3DPickingMixin
18
+
19
+ from PyQt6.QtCore import Qt
20
+ import numpy as np
21
+
22
+ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
23
+ def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
24
+ QDialog.__init__(self, parent)
25
+ Dialog3DPickingMixin.__init__(self)
26
+ self.mol = mol
27
+ self.main_window = main_window
28
+ self.atom1_idx = None
29
+ self.atom2_idx = None
30
+
31
+ # 事前選択された原子を設定
32
+ if preselected_atoms and len(preselected_atoms) >= 2:
33
+ self.atom1_idx = preselected_atoms[0]
34
+ self.atom2_idx = preselected_atoms[1]
35
+
36
+ self.init_ui()
37
+
38
+ def init_ui(self):
39
+ self.setWindowTitle("Adjust Bond Length")
40
+ self.setModal(False) # モードレスにしてクリックを阻害しない
41
+ # 常に前面表示
42
+ layout = QVBoxLayout(self)
43
+
44
+ # Instructions
45
+ instruction_label = QLabel("Click two atoms in the 3D view to select a bond, then specify the new length.")
46
+ instruction_label.setWordWrap(True)
47
+ layout.addWidget(instruction_label)
48
+
49
+ # Selected atoms display
50
+ self.selection_label = QLabel("No atoms selected")
51
+ layout.addWidget(self.selection_label)
52
+
53
+ # Current distance display
54
+ self.distance_label = QLabel("")
55
+ layout.addWidget(self.distance_label)
56
+
57
+ # New distance input
58
+ distance_layout = QHBoxLayout()
59
+ distance_layout.addWidget(QLabel("New distance (Å):"))
60
+ self.distance_input = QLineEdit()
61
+ self.distance_input.setPlaceholderText("1.54")
62
+ distance_layout.addWidget(self.distance_input)
63
+ layout.addLayout(distance_layout)
64
+
65
+ # Movement options
66
+ group_box = QWidget()
67
+ group_layout = QVBoxLayout(group_box)
68
+ group_layout.addWidget(QLabel("Movement Options:"))
69
+
70
+ self.atom1_fix_group_radio = QRadioButton("Atom 1: Fixed, Atom 2: Move connected group")
71
+ self.atom1_fix_group_radio.setChecked(True)
72
+ group_layout.addWidget(self.atom1_fix_group_radio)
73
+
74
+ self.atom1_fix_radio = QRadioButton("Atom 1: Fixed, Atom 2: Move atom only")
75
+ group_layout.addWidget(self.atom1_fix_radio)
76
+
77
+ self.both_groups_radio = QRadioButton("Both groups: Move towards center equally")
78
+ group_layout.addWidget(self.both_groups_radio)
79
+
80
+ layout.addWidget(group_box)
81
+
82
+ # Buttons
83
+ button_layout = QHBoxLayout()
84
+ self.clear_button = QPushButton("Clear Selection")
85
+ self.clear_button.clicked.connect(self.clear_selection)
86
+ button_layout.addWidget(self.clear_button)
87
+
88
+ button_layout.addStretch()
89
+
90
+ self.apply_button = QPushButton("Apply")
91
+ self.apply_button.clicked.connect(self.apply_changes)
92
+ self.apply_button.setEnabled(False)
93
+ button_layout.addWidget(self.apply_button)
94
+
95
+ close_button = QPushButton("Close")
96
+ close_button.clicked.connect(self.reject)
97
+ button_layout.addWidget(close_button)
98
+
99
+ layout.addLayout(button_layout)
100
+
101
+ # Connect to main window's picker
102
+ self.picker_connection = None
103
+ self.enable_picking()
104
+
105
+ # 事前選択された原子がある場合は初期表示を更新
106
+ if self.atom1_idx is not None:
107
+ self.show_atom_labels()
108
+ self.update_display()
109
+
110
+ def on_atom_picked(self, atom_idx):
111
+ """原子がピックされたときの処理"""
112
+ if self.atom1_idx is None:
113
+ self.atom1_idx = atom_idx
114
+ elif self.atom2_idx is None:
115
+ self.atom2_idx = atom_idx
116
+ else:
117
+ # Reset and start over
118
+ self.atom1_idx = atom_idx
119
+ self.atom2_idx = None
120
+
121
+ # 原子ラベルを表示
122
+ self.show_atom_labels()
123
+ self.update_display()
124
+
125
+ def keyPressEvent(self, event):
126
+ """キーボードイベントを処理"""
127
+ if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
128
+ if self.apply_button.isEnabled():
129
+ self.apply_changes()
130
+ event.accept()
131
+ else:
132
+ super().keyPressEvent(event)
133
+
134
+ def closeEvent(self, event):
135
+ """ダイアログが閉じられる時の処理"""
136
+ self.clear_atom_labels()
137
+ self.disable_picking()
138
+ super().closeEvent(event)
139
+
140
+ def reject(self):
141
+ """キャンセル時の処理"""
142
+ self.clear_atom_labels()
143
+ self.disable_picking()
144
+ super().reject()
145
+
146
+ def accept(self):
147
+ """OK時の処理"""
148
+ self.clear_atom_labels()
149
+ self.disable_picking()
150
+ super().accept()
151
+
152
+ def clear_selection(self):
153
+ """選択をクリア"""
154
+ self.atom1_idx = None
155
+ self.atom2_idx = None
156
+ self.clear_selection_labels()
157
+ self.update_display()
158
+
159
+ def show_atom_labels(self):
160
+ """選択された原子にラベルを表示"""
161
+ # 既存のラベルをクリア
162
+ self.clear_atom_labels()
163
+
164
+ # 新しいラベルを表示
165
+ if not hasattr(self, 'selection_labels'):
166
+ self.selection_labels = []
167
+
168
+ selected_atoms = [self.atom1_idx, self.atom2_idx]
169
+ labels = ["1st", "2nd"]
170
+ colors = ["yellow", "yellow"]
171
+
172
+ for i, atom_idx in enumerate(selected_atoms):
173
+ if atom_idx is not None:
174
+ pos = self.main_window.atom_positions_3d[atom_idx]
175
+ label_text = f"{labels[i]}"
176
+
177
+ # ラベルを追加
178
+ label_actor = self.main_window.plotter.add_point_labels(
179
+ [pos], [label_text],
180
+ point_size=20,
181
+ font_size=12,
182
+ text_color=colors[i],
183
+ always_visible=True
184
+ )
185
+ self.selection_labels.append(label_actor)
186
+
187
+ def clear_atom_labels(self):
188
+ """原子ラベルをクリア"""
189
+ if hasattr(self, 'selection_labels'):
190
+ for label_actor in self.selection_labels:
191
+ try:
192
+ self.main_window.plotter.remove_actor(label_actor)
193
+ except Exception:
194
+ pass
195
+ self.selection_labels = []
196
+
197
+ def clear_selection_labels(self):
198
+ """選択ラベルをクリア"""
199
+ if hasattr(self, 'selection_labels'):
200
+ for label_actor in self.selection_labels:
201
+ try:
202
+ self.main_window.plotter.remove_actor(label_actor)
203
+ except Exception:
204
+ pass
205
+ self.selection_labels = []
206
+
207
+ def add_selection_label(self, atom_idx, label_text):
208
+ """選択された原子にラベルを追加"""
209
+ if not hasattr(self, 'selection_labels'):
210
+ self.selection_labels = []
211
+
212
+ # 原子の位置を取得
213
+ pos = self.main_window.atom_positions_3d[atom_idx]
214
+
215
+ # ラベルを追加
216
+ label_actor = self.main_window.plotter.add_point_labels(
217
+ [pos], [label_text],
218
+ point_size=20,
219
+ font_size=12,
220
+ text_color='yellow',
221
+ always_visible=True
222
+ )
223
+ self.selection_labels.append(label_actor)
224
+
225
+ def update_display(self):
226
+ """表示を更新"""
227
+ # 既存のラベルをクリア
228
+ self.clear_selection_labels()
229
+
230
+ if self.atom1_idx is None:
231
+ self.selection_label.setText("No atoms selected")
232
+ self.distance_label.setText("")
233
+ self.apply_button.setEnabled(False)
234
+ # Clear distance input when no selection
235
+ try:
236
+ self.distance_input.clear()
237
+ except Exception:
238
+ pass
239
+ elif self.atom2_idx is None:
240
+ symbol1 = self.mol.GetAtomWithIdx(self.atom1_idx).GetSymbol()
241
+ self.selection_label.setText(f"First atom: {symbol1} (index {self.atom1_idx})")
242
+ self.distance_label.setText("")
243
+ self.apply_button.setEnabled(False)
244
+ # ラベル追加
245
+ self.add_selection_label(self.atom1_idx, "1")
246
+ # Clear distance input while selection is incomplete
247
+ try:
248
+ self.distance_input.clear()
249
+ except Exception:
250
+ pass
251
+ else:
252
+ symbol1 = self.mol.GetAtomWithIdx(self.atom1_idx).GetSymbol()
253
+ symbol2 = self.mol.GetAtomWithIdx(self.atom2_idx).GetSymbol()
254
+ self.selection_label.setText(f"Bond: {symbol1}({self.atom1_idx}) - {symbol2}({self.atom2_idx})")
255
+
256
+ # Calculate current distance
257
+ conf = self.mol.GetConformer()
258
+ pos1 = np.array(conf.GetAtomPosition(self.atom1_idx))
259
+ pos2 = np.array(conf.GetAtomPosition(self.atom2_idx))
260
+ current_distance = np.linalg.norm(pos2 - pos1)
261
+ self.distance_label.setText(f"Current distance: {current_distance:.3f} Å")
262
+ self.apply_button.setEnabled(True)
263
+ # Update the distance input box to show current distance
264
+ try:
265
+ self.distance_input.setText(f"{current_distance:.3f}")
266
+ except Exception:
267
+ pass
268
+ # ラベル追加
269
+ self.add_selection_label(self.atom1_idx, "1")
270
+ self.add_selection_label(self.atom2_idx, "2")
271
+
272
+ def apply_changes(self):
273
+ """変更を適用"""
274
+ if self.atom1_idx is None or self.atom2_idx is None:
275
+ return
276
+
277
+ try:
278
+ new_distance = float(self.distance_input.text())
279
+ if new_distance <= 0:
280
+ QMessageBox.warning(self, "Invalid Input", "Distance must be positive.")
281
+ return
282
+ except ValueError:
283
+ QMessageBox.warning(self, "Invalid Input", "Please enter a valid number.")
284
+ return
285
+
286
+ # Undo状態を保存
287
+ self.main_window.push_undo_state()
288
+
289
+ # Apply the bond length change
290
+ self.adjust_bond_length(new_distance)
291
+
292
+ # キラルラベルを更新
293
+ self.main_window.update_chiral_labels()
294
+
295
+ def adjust_bond_length(self, new_distance):
296
+ """結合長を調整"""
297
+ conf = self.mol.GetConformer()
298
+ pos1 = np.array(conf.GetAtomPosition(self.atom1_idx))
299
+ pos2 = np.array(conf.GetAtomPosition(self.atom2_idx))
300
+
301
+ # Direction vector from atom1 to atom2
302
+ direction = pos2 - pos1
303
+ current_distance = np.linalg.norm(direction)
304
+
305
+ if current_distance == 0:
306
+ return
307
+
308
+ direction = direction / current_distance
309
+
310
+ if self.both_groups_radio.isChecked():
311
+ # Both groups move towards center equally
312
+ bond_center = (pos1 + pos2) / 2
313
+ half_distance = new_distance / 2
314
+
315
+ # New positions for both atoms
316
+ new_pos1 = bond_center - direction * half_distance
317
+ new_pos2 = bond_center + direction * half_distance
318
+
319
+ # Get both connected groups
320
+ group1_atoms = self.get_connected_group(self.atom1_idx, exclude=self.atom2_idx)
321
+ group2_atoms = self.get_connected_group(self.atom2_idx, exclude=self.atom1_idx)
322
+
323
+ # Calculate displacements
324
+ displacement1 = new_pos1 - pos1
325
+ displacement2 = new_pos2 - pos2
326
+
327
+ # Move group 1
328
+ for atom_idx in group1_atoms:
329
+ current_pos = np.array(conf.GetAtomPosition(atom_idx))
330
+ new_pos = current_pos + displacement1
331
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
332
+ self.main_window.atom_positions_3d[atom_idx] = new_pos
333
+
334
+ # Move group 2
335
+ for atom_idx in group2_atoms:
336
+ current_pos = np.array(conf.GetAtomPosition(atom_idx))
337
+ new_pos = current_pos + displacement2
338
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
339
+ self.main_window.atom_positions_3d[atom_idx] = new_pos
340
+
341
+ elif self.atom1_fix_radio.isChecked():
342
+ # Move only the second atom
343
+ new_pos2 = pos1 + direction * new_distance
344
+ conf.SetAtomPosition(self.atom2_idx, new_pos2.tolist())
345
+ self.main_window.atom_positions_3d[self.atom2_idx] = new_pos2
346
+ else:
347
+ # Move the connected group (default behavior)
348
+ new_pos2 = pos1 + direction * new_distance
349
+ atoms_to_move = self.get_connected_group(self.atom2_idx, exclude=self.atom1_idx)
350
+ displacement = new_pos2 - pos2
351
+
352
+ for atom_idx in atoms_to_move:
353
+ current_pos = np.array(conf.GetAtomPosition(atom_idx))
354
+ new_pos = current_pos + displacement
355
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
356
+ self.main_window.atom_positions_3d[atom_idx] = new_pos
357
+
358
+ # Update the 3D view
359
+ self.main_window.draw_molecule_3d(self.mol)
360
+
361
+ def get_connected_group(self, start_atom, exclude=None):
362
+ """指定された原子から連結されているグループを取得"""
363
+ visited = set()
364
+ to_visit = [start_atom]
365
+
366
+ while to_visit:
367
+ current = to_visit.pop()
368
+ if current in visited or current == exclude:
369
+ continue
370
+
371
+ visited.add(current)
372
+
373
+ # Get neighboring atoms
374
+ atom = self.mol.GetAtomWithIdx(current)
375
+ for bond in atom.GetBonds():
376
+ other_idx = bond.GetOtherAtomIdx(current)
377
+ if other_idx not in visited and other_idx != exclude:
378
+ to_visit.append(other_idx)
379
+
380
+ return visited