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,272 @@
|
|
|
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
|
|
15
|
+
)
|
|
16
|
+
from PyQt6.QtCore import Qt
|
|
17
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
22
|
+
except Exception:
|
|
23
|
+
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
24
|
+
|
|
25
|
+
class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
26
|
+
def __init__(self, mol, main_window, axis, preselected_atoms=None, parent=None):
|
|
27
|
+
QDialog.__init__(self, parent)
|
|
28
|
+
Dialog3DPickingMixin.__init__(self)
|
|
29
|
+
self.mol = mol
|
|
30
|
+
self.main_window = main_window
|
|
31
|
+
self.axis = axis
|
|
32
|
+
self.selected_atoms = set()
|
|
33
|
+
|
|
34
|
+
# 事前選択された原子を追加(最大2個まで)
|
|
35
|
+
if preselected_atoms:
|
|
36
|
+
self.selected_atoms.update(preselected_atoms[:2])
|
|
37
|
+
|
|
38
|
+
self.init_ui()
|
|
39
|
+
|
|
40
|
+
# 事前選択された原子にラベルを追加
|
|
41
|
+
if self.selected_atoms:
|
|
42
|
+
for i, atom_idx in enumerate(sorted(self.selected_atoms), 1):
|
|
43
|
+
self.add_selection_label(atom_idx, f"Atom {i}")
|
|
44
|
+
self.update_display()
|
|
45
|
+
|
|
46
|
+
def init_ui(self):
|
|
47
|
+
axis_names = {'x': 'X-axis', 'y': 'Y-axis', 'z': 'Z-axis'}
|
|
48
|
+
self.setWindowTitle(f"Align to {axis_names[self.axis]}")
|
|
49
|
+
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
50
|
+
layout = QVBoxLayout(self)
|
|
51
|
+
|
|
52
|
+
# Instructions
|
|
53
|
+
instruction_label = QLabel(f"Click atoms in the 3D view to select them for alignment to the {axis_names[self.axis]}. Exactly 2 atoms are required. The first atom will be moved to the origin, and the second atom will be positioned on the {axis_names[self.axis]}.")
|
|
54
|
+
instruction_label.setWordWrap(True)
|
|
55
|
+
layout.addWidget(instruction_label)
|
|
56
|
+
|
|
57
|
+
# Selected atoms display
|
|
58
|
+
self.selection_label = QLabel("No atoms selected")
|
|
59
|
+
layout.addWidget(self.selection_label)
|
|
60
|
+
|
|
61
|
+
# Buttons
|
|
62
|
+
button_layout = QHBoxLayout()
|
|
63
|
+
self.clear_button = QPushButton("Clear Selection")
|
|
64
|
+
self.clear_button.clicked.connect(self.clear_selection)
|
|
65
|
+
button_layout.addWidget(self.clear_button)
|
|
66
|
+
|
|
67
|
+
button_layout.addStretch()
|
|
68
|
+
|
|
69
|
+
self.apply_button = QPushButton("Apply Alignment")
|
|
70
|
+
self.apply_button.clicked.connect(self.apply_alignment)
|
|
71
|
+
self.apply_button.setEnabled(False)
|
|
72
|
+
button_layout.addWidget(self.apply_button)
|
|
73
|
+
|
|
74
|
+
close_button = QPushButton("Close")
|
|
75
|
+
close_button.clicked.connect(self.reject)
|
|
76
|
+
button_layout.addWidget(close_button)
|
|
77
|
+
|
|
78
|
+
layout.addLayout(button_layout)
|
|
79
|
+
|
|
80
|
+
# Connect to main window's picker
|
|
81
|
+
self.picker_connection = None
|
|
82
|
+
self.enable_picking()
|
|
83
|
+
|
|
84
|
+
def enable_picking(self):
|
|
85
|
+
"""3Dビューでの原子選択を有効にする"""
|
|
86
|
+
# Dialog3DPickingMixinの機能を使用
|
|
87
|
+
super().enable_picking()
|
|
88
|
+
|
|
89
|
+
def disable_picking(self):
|
|
90
|
+
"""3Dビューでの原子選択を無効にする"""
|
|
91
|
+
# Dialog3DPickingMixinの機能を使用
|
|
92
|
+
super().disable_picking()
|
|
93
|
+
|
|
94
|
+
def on_atom_picked(self, atom_idx):
|
|
95
|
+
"""原子がクリックされた時の処理"""
|
|
96
|
+
if self.main_window.current_mol is None:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if atom_idx in self.selected_atoms:
|
|
100
|
+
# 既に選択されている場合は選択解除
|
|
101
|
+
self.selected_atoms.remove(atom_idx)
|
|
102
|
+
self.remove_atom_label(atom_idx)
|
|
103
|
+
else:
|
|
104
|
+
# 2つまでしか選択できない
|
|
105
|
+
if len(self.selected_atoms) < 2:
|
|
106
|
+
self.selected_atoms.add(atom_idx)
|
|
107
|
+
# ラベルの順番を示す
|
|
108
|
+
label_text = f"Atom {len(self.selected_atoms)}"
|
|
109
|
+
self.add_selection_label(atom_idx, label_text)
|
|
110
|
+
|
|
111
|
+
self.update_display()
|
|
112
|
+
|
|
113
|
+
def update_display(self):
|
|
114
|
+
"""選択状態の表示を更新"""
|
|
115
|
+
if len(self.selected_atoms) == 0:
|
|
116
|
+
self.selection_label.setText("Click atoms to select for alignment (exactly 2 required)")
|
|
117
|
+
self.apply_button.setEnabled(False)
|
|
118
|
+
elif len(self.selected_atoms) == 1:
|
|
119
|
+
selected_list = list(self.selected_atoms)
|
|
120
|
+
atom = self.mol.GetAtomWithIdx(selected_list[0])
|
|
121
|
+
self.selection_label.setText(f"Selected 1 atom: {atom.GetSymbol()}{selected_list[0]+1}")
|
|
122
|
+
self.apply_button.setEnabled(False)
|
|
123
|
+
elif len(self.selected_atoms) == 2:
|
|
124
|
+
selected_list = sorted(list(self.selected_atoms))
|
|
125
|
+
atom1 = self.mol.GetAtomWithIdx(selected_list[0])
|
|
126
|
+
atom2 = self.mol.GetAtomWithIdx(selected_list[1])
|
|
127
|
+
self.selection_label.setText(f"Selected 2 atoms: {atom1.GetSymbol()}{selected_list[0]+1}, {atom2.GetSymbol()}{selected_list[1]+1}")
|
|
128
|
+
self.apply_button.setEnabled(True)
|
|
129
|
+
|
|
130
|
+
def clear_selection(self):
|
|
131
|
+
"""選択をクリア"""
|
|
132
|
+
self.clear_selection_labels()
|
|
133
|
+
self.selected_atoms.clear()
|
|
134
|
+
self.update_display()
|
|
135
|
+
|
|
136
|
+
def add_selection_label(self, atom_idx, label_text):
|
|
137
|
+
"""選択された原子にラベルを追加"""
|
|
138
|
+
if not hasattr(self, 'selection_labels'):
|
|
139
|
+
self.selection_labels = []
|
|
140
|
+
|
|
141
|
+
# 原子の位置を取得
|
|
142
|
+
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
143
|
+
|
|
144
|
+
# ラベルを追加
|
|
145
|
+
label_actor = self.main_window.plotter.add_point_labels(
|
|
146
|
+
[pos], [label_text],
|
|
147
|
+
point_size=20,
|
|
148
|
+
font_size=12,
|
|
149
|
+
text_color='yellow',
|
|
150
|
+
always_visible=True
|
|
151
|
+
)
|
|
152
|
+
self.selection_labels.append(label_actor)
|
|
153
|
+
|
|
154
|
+
def remove_atom_label(self, atom_idx):
|
|
155
|
+
"""特定の原子のラベルを削除"""
|
|
156
|
+
# 簡単化のため、全ラベルをクリアして再描画
|
|
157
|
+
self.clear_selection_labels()
|
|
158
|
+
for i, idx in enumerate(sorted(self.selected_atoms), 1):
|
|
159
|
+
if idx != atom_idx:
|
|
160
|
+
self.add_selection_label(idx, f"Atom {i}")
|
|
161
|
+
|
|
162
|
+
def clear_selection_labels(self):
|
|
163
|
+
"""選択ラベルをクリア"""
|
|
164
|
+
if hasattr(self, 'selection_labels'):
|
|
165
|
+
for label_actor in self.selection_labels:
|
|
166
|
+
try:
|
|
167
|
+
self.main_window.plotter.remove_actor(label_actor)
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
self.selection_labels = []
|
|
171
|
+
|
|
172
|
+
def apply_alignment(self):
|
|
173
|
+
"""アライメントを適用"""
|
|
174
|
+
if len(self.selected_atoms) != 2:
|
|
175
|
+
QMessageBox.warning(self, "Warning", "Please select exactly 2 atoms for alignment.")
|
|
176
|
+
return
|
|
177
|
+
try:
|
|
178
|
+
|
|
179
|
+
selected_list = sorted(list(self.selected_atoms))
|
|
180
|
+
atom1_idx, atom2_idx = selected_list[0], selected_list[1]
|
|
181
|
+
|
|
182
|
+
conf = self.mol.GetConformer()
|
|
183
|
+
|
|
184
|
+
# 原子の現在位置を取得
|
|
185
|
+
pos1 = np.array(conf.GetAtomPosition(atom1_idx))
|
|
186
|
+
pos2 = np.array(conf.GetAtomPosition(atom2_idx))
|
|
187
|
+
|
|
188
|
+
# 最初に全分子を移動して、atom1を原点に配置
|
|
189
|
+
translation = -pos1
|
|
190
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
191
|
+
current_pos = np.array(conf.GetAtomPosition(i))
|
|
192
|
+
new_pos = current_pos + translation
|
|
193
|
+
conf.SetAtomPosition(i, new_pos.tolist())
|
|
194
|
+
|
|
195
|
+
# atom2の新しい位置を取得(移動後)
|
|
196
|
+
pos2_translated = pos2 + translation
|
|
197
|
+
|
|
198
|
+
# atom2を選択した軸上に配置するための回転を計算
|
|
199
|
+
axis_vectors = {
|
|
200
|
+
'x': np.array([1.0, 0.0, 0.0]),
|
|
201
|
+
'y': np.array([0.0, 1.0, 0.0]),
|
|
202
|
+
'z': np.array([0.0, 0.0, 1.0])
|
|
203
|
+
}
|
|
204
|
+
target_axis = axis_vectors[self.axis]
|
|
205
|
+
|
|
206
|
+
# atom2から原点への方向ベクトル
|
|
207
|
+
current_vector = pos2_translated
|
|
208
|
+
current_length = np.linalg.norm(current_vector)
|
|
209
|
+
|
|
210
|
+
if current_length > 1e-10: # ゼロベクトルでない場合
|
|
211
|
+
current_vector_normalized = current_vector / current_length
|
|
212
|
+
|
|
213
|
+
# 回転軸と角度を計算
|
|
214
|
+
rotation_axis = np.cross(current_vector_normalized, target_axis)
|
|
215
|
+
rotation_axis_length = np.linalg.norm(rotation_axis)
|
|
216
|
+
|
|
217
|
+
if rotation_axis_length > 1e-10: # 回転が必要
|
|
218
|
+
rotation_axis = rotation_axis / rotation_axis_length
|
|
219
|
+
cos_angle = np.dot(current_vector_normalized, target_axis)
|
|
220
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
221
|
+
rotation_angle = np.arccos(cos_angle)
|
|
222
|
+
|
|
223
|
+
# ロドリゲスの回転公式を使用
|
|
224
|
+
def rodrigues_rotation(v, k, theta):
|
|
225
|
+
cos_theta = np.cos(theta)
|
|
226
|
+
sin_theta = np.sin(theta)
|
|
227
|
+
return (v * cos_theta +
|
|
228
|
+
np.cross(k, v) * sin_theta +
|
|
229
|
+
k * np.dot(k, v) * (1 - cos_theta))
|
|
230
|
+
|
|
231
|
+
# 全ての原子に回転を適用
|
|
232
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
233
|
+
current_pos = np.array(conf.GetAtomPosition(i))
|
|
234
|
+
rotated_pos = rodrigues_rotation(current_pos, rotation_axis, rotation_angle)
|
|
235
|
+
conf.SetAtomPosition(i, rotated_pos.tolist())
|
|
236
|
+
|
|
237
|
+
# 3D座標を更新
|
|
238
|
+
self.main_window.atom_positions_3d = np.array([
|
|
239
|
+
list(conf.GetAtomPosition(i)) for i in range(self.mol.GetNumAtoms())
|
|
240
|
+
])
|
|
241
|
+
|
|
242
|
+
# 3Dビューを更新
|
|
243
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
244
|
+
|
|
245
|
+
# キラルラベルを更新
|
|
246
|
+
self.main_window.update_chiral_labels()
|
|
247
|
+
|
|
248
|
+
# Undo状態を保存
|
|
249
|
+
self.main_window.push_undo_state()
|
|
250
|
+
|
|
251
|
+
QMessageBox.information(self, "Success", f"Alignment to {self.axis.upper()}-axis completed.")
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
QMessageBox.critical(self, "Error", f"Failed to apply alignment: {str(e)}")
|
|
255
|
+
|
|
256
|
+
def closeEvent(self, event):
|
|
257
|
+
"""ダイアログが閉じられる時の処理"""
|
|
258
|
+
self.clear_selection_labels()
|
|
259
|
+
self.disable_picking()
|
|
260
|
+
super().closeEvent(event)
|
|
261
|
+
|
|
262
|
+
def reject(self):
|
|
263
|
+
"""キャンセル時の処理"""
|
|
264
|
+
self.clear_selection_labels()
|
|
265
|
+
self.disable_picking()
|
|
266
|
+
super().reject()
|
|
267
|
+
|
|
268
|
+
def accept(self):
|
|
269
|
+
"""OK時の処理"""
|
|
270
|
+
self.clear_selection_labels()
|
|
271
|
+
self.disable_picking()
|
|
272
|
+
super().accept()
|
|
@@ -0,0 +1,209 @@
|
|
|
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, QGridLayout, QLabel, QLineEdit, QPushButton, QApplication
|
|
15
|
+
)
|
|
16
|
+
from PyQt6.QtCore import Qt
|
|
17
|
+
from rdkit import Chem
|
|
18
|
+
from rdkit.Chem import Descriptors, rdMolDescriptors
|
|
19
|
+
from rdkit.Chem import inchi as rd_inchi
|
|
20
|
+
|
|
21
|
+
class AnalysisWindow(QDialog):
|
|
22
|
+
def __init__(self, mol, parent=None, is_xyz_derived=False):
|
|
23
|
+
super().__init__(parent)
|
|
24
|
+
self.mol = mol
|
|
25
|
+
self.is_xyz_derived = is_xyz_derived # XYZ由来かどうかのフラグ
|
|
26
|
+
self.setWindowTitle("Molecule Analysis")
|
|
27
|
+
self.setMinimumWidth(400)
|
|
28
|
+
self.init_ui()
|
|
29
|
+
|
|
30
|
+
def init_ui(self):
|
|
31
|
+
main_layout = QVBoxLayout(self)
|
|
32
|
+
grid_layout = QGridLayout()
|
|
33
|
+
|
|
34
|
+
# --- 分子特性を計算 ---
|
|
35
|
+
try:
|
|
36
|
+
# RDKitのモジュールをインポート
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if self.is_xyz_derived:
|
|
40
|
+
# XYZ由来の場合:元のXYZファイルの原子情報から直接計算
|
|
41
|
+
# (結合推定の影響を受けない)
|
|
42
|
+
|
|
43
|
+
# XYZファイルから読み込んだ元の原子情報を取得
|
|
44
|
+
if hasattr(self.mol, '_xyz_atom_data'):
|
|
45
|
+
xyz_atoms = self.mol._xyz_atom_data
|
|
46
|
+
else:
|
|
47
|
+
# フォールバック: RDKitオブジェクトから取得
|
|
48
|
+
xyz_atoms = [(atom.GetSymbol(), 0, 0, 0) for atom in self.mol.GetAtoms()]
|
|
49
|
+
|
|
50
|
+
# 原子数と元素種を集計
|
|
51
|
+
atom_counts = {}
|
|
52
|
+
total_atoms = len(xyz_atoms)
|
|
53
|
+
num_heavy_atoms = 0
|
|
54
|
+
|
|
55
|
+
for symbol, x, y, z in xyz_atoms:
|
|
56
|
+
atom_counts[symbol] = atom_counts.get(symbol, 0) + 1
|
|
57
|
+
if symbol != 'H': # 水素以外
|
|
58
|
+
num_heavy_atoms += 1
|
|
59
|
+
|
|
60
|
+
# 化学式を手動で構築(元素順序を考慮)
|
|
61
|
+
element_order = ['C', 'H', 'N', 'O', 'P', 'S', 'F', 'Cl', 'Br', 'I']
|
|
62
|
+
formula_parts = []
|
|
63
|
+
|
|
64
|
+
# 定義された順序で元素を追加
|
|
65
|
+
remaining_counts = atom_counts.copy()
|
|
66
|
+
for element in element_order:
|
|
67
|
+
if element in remaining_counts:
|
|
68
|
+
count = remaining_counts[element]
|
|
69
|
+
if count == 1:
|
|
70
|
+
formula_parts.append(element)
|
|
71
|
+
else:
|
|
72
|
+
formula_parts.append(f"{element}{count}")
|
|
73
|
+
del remaining_counts[element]
|
|
74
|
+
|
|
75
|
+
# 残りの元素をアルファベット順で追加
|
|
76
|
+
for element in sorted(remaining_counts.keys()):
|
|
77
|
+
count = remaining_counts[element]
|
|
78
|
+
if count == 1:
|
|
79
|
+
formula_parts.append(element)
|
|
80
|
+
else:
|
|
81
|
+
formula_parts.append(f"{element}{count}")
|
|
82
|
+
|
|
83
|
+
mol_formula = ''.join(formula_parts)
|
|
84
|
+
|
|
85
|
+
# 分子量と精密質量をRDKitから取得
|
|
86
|
+
|
|
87
|
+
mol_wt = 0.0
|
|
88
|
+
exact_mw = 0.0
|
|
89
|
+
pt = Chem.GetPeriodicTable()
|
|
90
|
+
|
|
91
|
+
for symbol, count in atom_counts.items():
|
|
92
|
+
try:
|
|
93
|
+
# RDKitの周期表から原子量と精密質量を取得
|
|
94
|
+
atomic_num = pt.GetAtomicNumber(symbol)
|
|
95
|
+
atomic_weight = pt.GetAtomicWeight(atomic_num)
|
|
96
|
+
exact_mass = pt.GetMostCommonIsotopeMass(atomic_num)
|
|
97
|
+
|
|
98
|
+
mol_wt += atomic_weight * count
|
|
99
|
+
exact_mw += exact_mass * count
|
|
100
|
+
except (ValueError, RuntimeError):
|
|
101
|
+
# 認識されない元素の場合はスキップ
|
|
102
|
+
print(f"Warning: Unknown element {symbol}, skipping in mass calculation")
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# 表示するプロパティを辞書にまとめる(XYZ元データから計算)
|
|
106
|
+
properties = {
|
|
107
|
+
"Molecular Formula:": mol_formula,
|
|
108
|
+
"Molecular Weight:": f"{mol_wt:.4f}",
|
|
109
|
+
"Exact Mass:": f"{exact_mw:.4f}",
|
|
110
|
+
"Heavy Atoms:": str(num_heavy_atoms),
|
|
111
|
+
"Total Atoms:": str(total_atoms),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# 注意メッセージを追加
|
|
115
|
+
note_label = QLabel("<i>Note: SMILES and structure-dependent properties are not available for XYZ-derived structures due to potential bond estimation inaccuracies.</i>")
|
|
116
|
+
note_label.setWordWrap(True)
|
|
117
|
+
main_layout.addWidget(note_label)
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
# 通常の分子(MOLファイルや2Dエディタ由来)の場合:全てのプロパティを計算
|
|
121
|
+
|
|
122
|
+
# SMILES生成用に、一時的に水素原子を取り除いた分子オブジェクトを作成
|
|
123
|
+
mol_for_smiles = Chem.RemoveHs(self.mol)
|
|
124
|
+
# 水素を取り除いた分子からSMILESを生成(常に簡潔な表記になる)
|
|
125
|
+
smiles = Chem.MolToSmiles(mol_for_smiles, isomericSmiles=True)
|
|
126
|
+
|
|
127
|
+
# 各種プロパティを計算
|
|
128
|
+
mol_formula = rdMolDescriptors.CalcMolFormula(self.mol)
|
|
129
|
+
mol_wt = Descriptors.MolWt(self.mol)
|
|
130
|
+
exact_mw = Descriptors.ExactMolWt(self.mol)
|
|
131
|
+
num_heavy_atoms = self.mol.GetNumHeavyAtoms()
|
|
132
|
+
num_rings = rdMolDescriptors.CalcNumRings(self.mol)
|
|
133
|
+
log_p = Descriptors.MolLogP(self.mol)
|
|
134
|
+
tpsa = Descriptors.TPSA(self.mol)
|
|
135
|
+
num_h_donors = rdMolDescriptors.CalcNumHBD(self.mol)
|
|
136
|
+
num_h_acceptors = rdMolDescriptors.CalcNumHBA(self.mol)
|
|
137
|
+
|
|
138
|
+
# InChIを生成
|
|
139
|
+
try:
|
|
140
|
+
inchi = Chem.MolToInchi(self.mol)
|
|
141
|
+
except Exception:
|
|
142
|
+
inchi = "N/A"
|
|
143
|
+
|
|
144
|
+
# InChIKeyを生成(RDKitのinchi APIが無い場合に備えてフォールバック)
|
|
145
|
+
try:
|
|
146
|
+
# Prefer Chem.MolToInchiKey when available
|
|
147
|
+
inchi_key = None
|
|
148
|
+
try:
|
|
149
|
+
inchi_key = Chem.MolToInchiKey(self.mol)
|
|
150
|
+
except Exception:
|
|
151
|
+
# Fallback to rdkit.Chem.inchi if present
|
|
152
|
+
try:
|
|
153
|
+
inchi_key = rd_inchi.MolToInchiKey(self.mol)
|
|
154
|
+
except Exception:
|
|
155
|
+
inchi_key = None
|
|
156
|
+
|
|
157
|
+
if not inchi_key:
|
|
158
|
+
inchi_key = "N/A"
|
|
159
|
+
except Exception:
|
|
160
|
+
inchi_key = "N/A"
|
|
161
|
+
|
|
162
|
+
# 表示するプロパティを辞書にまとめる
|
|
163
|
+
properties = {
|
|
164
|
+
"SMILES:": smiles,
|
|
165
|
+
"InChI:": inchi,
|
|
166
|
+
"InChIKey:": inchi_key,
|
|
167
|
+
"Molecular Formula:": mol_formula,
|
|
168
|
+
"Molecular Weight:": f"{mol_wt:.4f}",
|
|
169
|
+
"Exact Mass:": f"{exact_mw:.4f}",
|
|
170
|
+
"Heavy Atoms:": str(num_heavy_atoms),
|
|
171
|
+
"Ring Count:": str(num_rings),
|
|
172
|
+
"LogP (o/w):": f"{log_p:.3f}",
|
|
173
|
+
"TPSA (Ų):": f"{tpsa:.2f}",
|
|
174
|
+
"H-Bond Donors:": str(num_h_donors),
|
|
175
|
+
"H-Bond Acceptors:": str(num_h_acceptors),
|
|
176
|
+
}
|
|
177
|
+
except Exception as e:
|
|
178
|
+
main_layout.addWidget(QLabel(f"Error calculating properties: {e}"))
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# --- 計算結果をUIに表示 ---
|
|
182
|
+
row = 0
|
|
183
|
+
for label_text, value_text in properties.items():
|
|
184
|
+
label = QLabel(f"<b>{label_text}</b>")
|
|
185
|
+
value = QLineEdit(value_text)
|
|
186
|
+
value.setReadOnly(True)
|
|
187
|
+
|
|
188
|
+
copy_btn = QPushButton("Copy")
|
|
189
|
+
copy_btn.clicked.connect(lambda _, v=value: self.copy_to_clipboard(v.text()))
|
|
190
|
+
|
|
191
|
+
grid_layout.addWidget(label, row, 0)
|
|
192
|
+
grid_layout.addWidget(value, row, 1)
|
|
193
|
+
grid_layout.addWidget(copy_btn, row, 2)
|
|
194
|
+
row += 1
|
|
195
|
+
|
|
196
|
+
main_layout.addLayout(grid_layout)
|
|
197
|
+
|
|
198
|
+
# --- OKボタン ---
|
|
199
|
+
ok_button = QPushButton("OK")
|
|
200
|
+
ok_button.clicked.connect(self.accept)
|
|
201
|
+
main_layout.addWidget(ok_button, 0, Qt.AlignmentFlag.AlignCenter)
|
|
202
|
+
|
|
203
|
+
self.setLayout(main_layout)
|
|
204
|
+
|
|
205
|
+
def copy_to_clipboard(self, text):
|
|
206
|
+
clipboard = QApplication.clipboard()
|
|
207
|
+
clipboard.setText(text)
|
|
208
|
+
if self.parent() and hasattr(self.parent(), 'statusBar'):
|
|
209
|
+
self.parent().statusBar().showMessage(f"Copied '{text}' to clipboard.", 2000)
|