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,355 @@
|
|
|
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
|
+
"""
|
|
14
|
+
main_window_view_loaders.py
|
|
15
|
+
MainWindow (main_window.py) から分離されたモジュール
|
|
16
|
+
機能クラス: MainWindowViewLoaders
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import traceback
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
25
|
+
from rdkit import Chem
|
|
26
|
+
from rdkit.Chem import AllChem
|
|
27
|
+
try:
|
|
28
|
+
pass
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
# PyQt6 Modules
|
|
33
|
+
from PyQt6.QtWidgets import (
|
|
34
|
+
QFileDialog
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Use centralized Open Babel availability from package-level __init__
|
|
42
|
+
# Use per-package modules availability (local __init__).
|
|
43
|
+
try:
|
|
44
|
+
from . import OBABEL_AVAILABLE
|
|
45
|
+
except Exception:
|
|
46
|
+
from modules import OBABEL_AVAILABLE
|
|
47
|
+
# Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
|
|
48
|
+
if OBABEL_AVAILABLE:
|
|
49
|
+
try:
|
|
50
|
+
from openbabel import pybel
|
|
51
|
+
except Exception:
|
|
52
|
+
# If import fails here, disable OBABEL locally; avoid raising
|
|
53
|
+
pybel = None
|
|
54
|
+
OBABEL_AVAILABLE = False
|
|
55
|
+
print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
|
|
56
|
+
else:
|
|
57
|
+
pybel = None
|
|
58
|
+
|
|
59
|
+
# Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
|
|
60
|
+
# allows safely detecting C++ wrapper objects that have been deleted. Import
|
|
61
|
+
# it once at module import time and expose a small, robust wrapper so callers
|
|
62
|
+
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
63
|
+
# handling (this reduces crash risk during teardown and deletion operations).
|
|
64
|
+
try:
|
|
65
|
+
import sip as _sip # type: ignore
|
|
66
|
+
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
67
|
+
except Exception:
|
|
68
|
+
_sip = None
|
|
69
|
+
_sip_isdeleted = None
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# package relative imports (preferred when running as `python -m moleditpy`)
|
|
73
|
+
from .constants import VERSION
|
|
74
|
+
except Exception:
|
|
75
|
+
# Fallback to absolute imports for script-style execution
|
|
76
|
+
from modules.constants import VERSION
|
|
77
|
+
|
|
78
|
+
# --- クラス定義 ---
|
|
79
|
+
class MainWindowViewLoaders(object):
|
|
80
|
+
""" main_window.py から分離された機能クラス """
|
|
81
|
+
|
|
82
|
+
def load_xyz_for_3d_viewing(self, file_path=None):
|
|
83
|
+
"""XYZファイルを読み込んで3Dビューアで表示する"""
|
|
84
|
+
if not file_path:
|
|
85
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Load 3D XYZ (View Only)", "", "XYZ Files (*.xyz);;All Files (*)")
|
|
86
|
+
if not file_path:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
mol = self.load_xyz_file(file_path)
|
|
91
|
+
if mol is None:
|
|
92
|
+
raise ValueError("Failed to create molecule from XYZ file.")
|
|
93
|
+
if mol.GetNumConformers() == 0:
|
|
94
|
+
raise ValueError("XYZ file has no 3D coordinates.")
|
|
95
|
+
|
|
96
|
+
# 2Dエディタをクリア
|
|
97
|
+
self.clear_2d_editor(push_to_undo=False)
|
|
98
|
+
|
|
99
|
+
# 3D構造をセットして描画
|
|
100
|
+
# Set the molecule. If bonds were determined (mol has bonds),
|
|
101
|
+
# treat this the same as loading a MOL file: clear the XYZ-derived
|
|
102
|
+
# flag and enable 3D optimization. Only mark as XYZ-derived and
|
|
103
|
+
# disable 3D optimization when the molecule has no bond information.
|
|
104
|
+
self.current_mol = mol
|
|
105
|
+
|
|
106
|
+
# XYZファイル読み込み時はマッピングをクリア(2D構造がないため)
|
|
107
|
+
self.atom_id_to_rdkit_idx_map = {}
|
|
108
|
+
|
|
109
|
+
# If the loader marked the molecule as produced under skip_chemistry_checks,
|
|
110
|
+
# always treat it as XYZ-derived and disable optimization. Otherwise
|
|
111
|
+
# fall back to the existing behavior based on bond presence.
|
|
112
|
+
skip_flag = False
|
|
113
|
+
try:
|
|
114
|
+
# Prefer RDKit int prop
|
|
115
|
+
skip_flag = bool(self.current_mol.GetIntProp("_xyz_skip_checks"))
|
|
116
|
+
except Exception:
|
|
117
|
+
try:
|
|
118
|
+
skip_flag = bool(getattr(self.current_mol, '_xyz_skip_checks', False))
|
|
119
|
+
except Exception:
|
|
120
|
+
skip_flag = False
|
|
121
|
+
|
|
122
|
+
if skip_flag:
|
|
123
|
+
self.is_xyz_derived = True
|
|
124
|
+
if hasattr(self, 'optimize_3d_button'):
|
|
125
|
+
try:
|
|
126
|
+
self.optimize_3d_button.setEnabled(False)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
else:
|
|
130
|
+
try:
|
|
131
|
+
has_bonds = (self.current_mol.GetNumBonds() > 0)
|
|
132
|
+
except Exception:
|
|
133
|
+
has_bonds = False
|
|
134
|
+
|
|
135
|
+
if has_bonds:
|
|
136
|
+
self.is_xyz_derived = False
|
|
137
|
+
if hasattr(self, 'optimize_3d_button'):
|
|
138
|
+
try:
|
|
139
|
+
# Only enable optimize if the molecule is not considered XYZ-derived
|
|
140
|
+
if not getattr(self, 'is_xyz_derived', False):
|
|
141
|
+
self.optimize_3d_button.setEnabled(True)
|
|
142
|
+
else:
|
|
143
|
+
self.optimize_3d_button.setEnabled(False)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
else:
|
|
147
|
+
self.is_xyz_derived = True
|
|
148
|
+
if hasattr(self, 'optimize_3d_button'):
|
|
149
|
+
try:
|
|
150
|
+
self.optimize_3d_button.setEnabled(False)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
self.draw_molecule_3d(self.current_mol)
|
|
155
|
+
self.plotter.reset_camera()
|
|
156
|
+
|
|
157
|
+
# UIを3Dビューアモードに設定
|
|
158
|
+
self._enter_3d_viewer_ui_mode()
|
|
159
|
+
|
|
160
|
+
# 3D関連機能を統一的に有効化
|
|
161
|
+
self._enable_3d_features(True)
|
|
162
|
+
|
|
163
|
+
# メニューテキストと状態を更新
|
|
164
|
+
self.update_atom_id_menu_text()
|
|
165
|
+
self.update_atom_id_menu_state()
|
|
166
|
+
|
|
167
|
+
self.statusBar().showMessage(f"3D Viewer Mode: Loaded {os.path.basename(file_path)}")
|
|
168
|
+
self.reset_undo_stack()
|
|
169
|
+
# XYZファイル名をcurrent_file_pathにセットし、未保存状態はFalse
|
|
170
|
+
self.current_file_path = file_path
|
|
171
|
+
self.has_unsaved_changes = False
|
|
172
|
+
self.update_window_title()
|
|
173
|
+
|
|
174
|
+
except FileNotFoundError:
|
|
175
|
+
self.statusBar().showMessage(f"File not found: {file_path}")
|
|
176
|
+
self.restore_ui_for_editing()
|
|
177
|
+
except ValueError as e:
|
|
178
|
+
self.statusBar().showMessage(f"Invalid XYZ file: {e}")
|
|
179
|
+
self.restore_ui_for_editing()
|
|
180
|
+
except Exception as e:
|
|
181
|
+
self.statusBar().showMessage(f"Error loading XYZ file: {e}")
|
|
182
|
+
self.restore_ui_for_editing()
|
|
183
|
+
|
|
184
|
+
traceback.print_exc()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def save_3d_as_mol(self):
|
|
189
|
+
if not self.current_mol:
|
|
190
|
+
self.statusBar().showMessage("Error: Please generate a 3D structure first.")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
# default filename based on current file
|
|
195
|
+
default_name = "untitled"
|
|
196
|
+
try:
|
|
197
|
+
if self.current_file_path:
|
|
198
|
+
base = os.path.basename(self.current_file_path)
|
|
199
|
+
name = os.path.splitext(base)[0]
|
|
200
|
+
default_name = f"{name}"
|
|
201
|
+
except Exception:
|
|
202
|
+
default_name = "untitled"
|
|
203
|
+
|
|
204
|
+
# prefer same directory as current file when available
|
|
205
|
+
default_path = default_name
|
|
206
|
+
try:
|
|
207
|
+
if self.current_file_path:
|
|
208
|
+
default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
|
|
209
|
+
except Exception:
|
|
210
|
+
default_path = default_name
|
|
211
|
+
|
|
212
|
+
file_path, _ = QFileDialog.getSaveFileName(self, "Save 3D MOL File", default_path, "MOL Files (*.mol);;All Files (*)")
|
|
213
|
+
if not file_path:
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
if not file_path.lower().endswith('.mol'):
|
|
217
|
+
file_path += '.mol'
|
|
218
|
+
|
|
219
|
+
mol_to_save = Chem.Mol(self.current_mol)
|
|
220
|
+
|
|
221
|
+
if mol_to_save.HasProp("_2D"):
|
|
222
|
+
mol_to_save.ClearProp("_2D")
|
|
223
|
+
|
|
224
|
+
mol_block = Chem.MolToMolBlock(mol_to_save, includeStereo=True)
|
|
225
|
+
lines = mol_block.split('\n')
|
|
226
|
+
if len(lines) > 1 and 'RDKit' in lines[1]:
|
|
227
|
+
lines[1] = ' MoleditPy Ver. ' + VERSION + ' 3D'
|
|
228
|
+
modified_mol_block = '\n'.join(lines)
|
|
229
|
+
|
|
230
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
231
|
+
f.write(modified_mol_block)
|
|
232
|
+
self.statusBar().showMessage(f"3D data saved to {file_path}")
|
|
233
|
+
|
|
234
|
+
except (OSError, IOError) as e:
|
|
235
|
+
self.statusBar().showMessage(f"File I/O error: {e}")
|
|
236
|
+
except UnicodeEncodeError as e:
|
|
237
|
+
self.statusBar().showMessage(f"Text encoding error: {e}")
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.statusBar().showMessage(f"Error saving 3D MOL file: {e}")
|
|
240
|
+
|
|
241
|
+
traceback.print_exc()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def load_mol_file_for_3d_viewing(self, file_path=None):
|
|
246
|
+
"""MOL/SDFファイルを3Dビューアーで開く"""
|
|
247
|
+
if not self.check_unsaved_changes():
|
|
248
|
+
return # ユーザーがキャンセルした場合は何もしない
|
|
249
|
+
if not file_path:
|
|
250
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
251
|
+
self, "Open MOL/SDF File", "",
|
|
252
|
+
"MOL/SDF Files (*.mol *.sdf);;All Files (*)"
|
|
253
|
+
)
|
|
254
|
+
if not file_path:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
|
|
259
|
+
# Determine extension early and handle .mol specially by reading the
|
|
260
|
+
# raw block and running it through fix_mol_block before parsing.
|
|
261
|
+
_, ext = os.path.splitext(file_path)
|
|
262
|
+
ext = ext.lower() if ext else ''
|
|
263
|
+
|
|
264
|
+
if ext == '.sdf':
|
|
265
|
+
suppl = Chem.SDMolSupplier(file_path, removeHs=False)
|
|
266
|
+
mol = next(suppl, None)
|
|
267
|
+
|
|
268
|
+
elif ext == '.mol':
|
|
269
|
+
# Read the file contents and attempt to fix malformed counts lines
|
|
270
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as fh:
|
|
271
|
+
raw = fh.read()
|
|
272
|
+
fixed_block = self.fix_mol_block(raw)
|
|
273
|
+
mol = Chem.MolFromMolBlock(fixed_block, sanitize=True, removeHs=False)
|
|
274
|
+
|
|
275
|
+
# If parsing the fixed block fails, fall back to RDKit's file reader
|
|
276
|
+
# as a last resort (keeps behavior conservative).
|
|
277
|
+
if mol is None:
|
|
278
|
+
try:
|
|
279
|
+
mol = Chem.MolFromMolFile(file_path, removeHs=False)
|
|
280
|
+
except Exception:
|
|
281
|
+
mol = None
|
|
282
|
+
|
|
283
|
+
if mol is None:
|
|
284
|
+
self.statusBar().showMessage(f"Failed to load molecule from {file_path}")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
else:
|
|
288
|
+
# Default: let RDKit try to read the file (most common case)
|
|
289
|
+
if file_path.lower().endswith('.sdf'):
|
|
290
|
+
suppl = Chem.SDMolSupplier(file_path, removeHs=False)
|
|
291
|
+
mol = next(suppl, None)
|
|
292
|
+
else:
|
|
293
|
+
mol = Chem.MolFromMolFile(file_path, removeHs=False)
|
|
294
|
+
|
|
295
|
+
if mol is None:
|
|
296
|
+
self.statusBar().showMessage(f"Failed to load molecule from {file_path}")
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# 3D座標がない場合は2Dから3D変換(最適化なし)
|
|
300
|
+
if mol.GetNumConformers() == 0:
|
|
301
|
+
self.statusBar().showMessage("No 3D coordinates found. Converting to 3D...")
|
|
302
|
+
try:
|
|
303
|
+
try:
|
|
304
|
+
AllChem.EmbedMolecule(mol)
|
|
305
|
+
# 最適化は実行しない
|
|
306
|
+
# 3D変換直後にUndoスタックに積む
|
|
307
|
+
self.current_mol = mol
|
|
308
|
+
self.push_undo_state()
|
|
309
|
+
except Exception:
|
|
310
|
+
# If skipping chemistry checks, allow molecule to be displayed without 3D embedding
|
|
311
|
+
if self.settings.get('skip_chemistry_checks', False):
|
|
312
|
+
self.statusBar().showMessage("Warning: failed to generate 3D coordinates but skip_chemistry_checks is enabled; continuing.")
|
|
313
|
+
# Keep mol as-is (may lack conformer); downstream code checks for conformers
|
|
314
|
+
else:
|
|
315
|
+
raise
|
|
316
|
+
except Exception:
|
|
317
|
+
self.statusBar().showMessage("Failed to generate 3D coordinates")
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
# Clear XYZ markers on the newly loaded MOL/SDF so Optimize 3D is
|
|
321
|
+
# correctly enabled when appropriate.
|
|
322
|
+
try:
|
|
323
|
+
self._clear_xyz_flags(mol)
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
# 3Dビューアーに表示
|
|
328
|
+
# Centralized chemical/sanitization handling
|
|
329
|
+
# Ensure the skip_chemistry_checks setting is respected and flags are set
|
|
330
|
+
self._apply_chem_check_and_set_flags(mol, source_desc='MOL/SDF')
|
|
331
|
+
|
|
332
|
+
self.current_mol = mol
|
|
333
|
+
self.draw_molecule_3d(mol)
|
|
334
|
+
|
|
335
|
+
# カメラをリセット
|
|
336
|
+
self.plotter.reset_camera()
|
|
337
|
+
|
|
338
|
+
# UIを3Dビューアーモードに設定
|
|
339
|
+
self._enter_3d_viewer_ui_mode()
|
|
340
|
+
|
|
341
|
+
# メニューテキストと状態を更新
|
|
342
|
+
self.update_atom_id_menu_text()
|
|
343
|
+
self.update_atom_id_menu_state()
|
|
344
|
+
|
|
345
|
+
self.statusBar().showMessage(f"Loaded {file_path} in 3D viewer")
|
|
346
|
+
|
|
347
|
+
self.reset_undo_stack()
|
|
348
|
+
self.has_unsaved_changes = False # ファイル読込直後は未変更扱い
|
|
349
|
+
self.current_file_path = file_path
|
|
350
|
+
self.update_window_title()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
self.statusBar().showMessage(f"Error loading MOL/SDF file: {e}")
|
|
355
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
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, QButtonGroup, QRadioButton,
|
|
15
|
+
QHBoxLayout, QPushButton, QMessageBox
|
|
16
|
+
)
|
|
17
|
+
from rdkit import Chem
|
|
18
|
+
|
|
19
|
+
class MirrorDialog(QDialog):
|
|
20
|
+
"""分子の鏡像を作成するダイアログ"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, mol, main_window, parent=None):
|
|
23
|
+
super().__init__(parent)
|
|
24
|
+
self.mol = mol
|
|
25
|
+
self.main_window = main_window
|
|
26
|
+
self.init_ui()
|
|
27
|
+
|
|
28
|
+
def init_ui(self):
|
|
29
|
+
self.setWindowTitle("Mirror Molecule")
|
|
30
|
+
self.setMinimumSize(300, 200)
|
|
31
|
+
|
|
32
|
+
layout = QVBoxLayout(self)
|
|
33
|
+
|
|
34
|
+
# 説明テキスト
|
|
35
|
+
info_label = QLabel("Select the mirror plane to create molecular mirror image:")
|
|
36
|
+
layout.addWidget(info_label)
|
|
37
|
+
|
|
38
|
+
# ミラー平面選択のラジオボタン
|
|
39
|
+
self.plane_group = QButtonGroup(self)
|
|
40
|
+
|
|
41
|
+
self.xy_radio = QRadioButton("XY plane (Z = 0)")
|
|
42
|
+
self.xz_radio = QRadioButton("XZ plane (Y = 0)")
|
|
43
|
+
self.yz_radio = QRadioButton("YZ plane (X = 0)")
|
|
44
|
+
|
|
45
|
+
self.xy_radio.setChecked(True) # デフォルト選択
|
|
46
|
+
|
|
47
|
+
self.plane_group.addButton(self.xy_radio, 0)
|
|
48
|
+
self.plane_group.addButton(self.xz_radio, 1)
|
|
49
|
+
self.plane_group.addButton(self.yz_radio, 2)
|
|
50
|
+
|
|
51
|
+
layout.addWidget(self.xy_radio)
|
|
52
|
+
layout.addWidget(self.xz_radio)
|
|
53
|
+
layout.addWidget(self.yz_radio)
|
|
54
|
+
|
|
55
|
+
layout.addSpacing(20)
|
|
56
|
+
|
|
57
|
+
# ボタン
|
|
58
|
+
button_layout = QHBoxLayout()
|
|
59
|
+
|
|
60
|
+
apply_button = QPushButton("Apply Mirror")
|
|
61
|
+
apply_button.clicked.connect(self.apply_mirror)
|
|
62
|
+
|
|
63
|
+
close_button = QPushButton("Close")
|
|
64
|
+
close_button.clicked.connect(self.reject)
|
|
65
|
+
|
|
66
|
+
button_layout.addWidget(apply_button)
|
|
67
|
+
button_layout.addWidget(close_button)
|
|
68
|
+
|
|
69
|
+
layout.addLayout(button_layout)
|
|
70
|
+
|
|
71
|
+
def apply_mirror(self):
|
|
72
|
+
"""選択された平面に対してミラー変換を適用"""
|
|
73
|
+
if not self.mol or self.mol.GetNumConformers() == 0:
|
|
74
|
+
QMessageBox.warning(self, "Error", "No 3D coordinates available.")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# 選択された平面を取得
|
|
78
|
+
plane_id = self.plane_group.checkedId()
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
conf = self.mol.GetConformer()
|
|
82
|
+
|
|
83
|
+
# 各原子の座標を変換
|
|
84
|
+
for atom_idx in range(self.mol.GetNumAtoms()):
|
|
85
|
+
pos = conf.GetAtomPosition(atom_idx)
|
|
86
|
+
|
|
87
|
+
if plane_id == 0: # XY平面(Z軸に対してミラー)
|
|
88
|
+
new_pos = [pos.x, pos.y, -pos.z]
|
|
89
|
+
elif plane_id == 1: # XZ平面(Y軸に対してミラー)
|
|
90
|
+
new_pos = [pos.x, -pos.y, pos.z]
|
|
91
|
+
elif plane_id == 2: # YZ平面(X軸に対してミラー)
|
|
92
|
+
new_pos = [-pos.x, pos.y, pos.z]
|
|
93
|
+
|
|
94
|
+
# 新しい座標を設定
|
|
95
|
+
from rdkit.Geometry import Point3D
|
|
96
|
+
conf.SetAtomPosition(atom_idx, Point3D(new_pos[0], new_pos[1], new_pos[2]))
|
|
97
|
+
|
|
98
|
+
# 3Dビューを更新
|
|
99
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
100
|
+
|
|
101
|
+
# ミラー変換後にキラルタグを強制的に再計算
|
|
102
|
+
try:
|
|
103
|
+
if self.mol.GetNumConformers() > 0:
|
|
104
|
+
# 既存のキラルタグをクリア
|
|
105
|
+
for atom in self.mol.GetAtoms():
|
|
106
|
+
atom.SetChiralTag(Chem.rdchem.ChiralType.CHI_UNSPECIFIED)
|
|
107
|
+
# 3D座標から新しいキラルタグを計算
|
|
108
|
+
Chem.AssignAtomChiralTagsFromStructure(self.mol, confId=0)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(f"Error updating chiral tags: {e}")
|
|
111
|
+
|
|
112
|
+
# キラルラベルを更新(鏡像変換でキラリティが変わる可能性があるため)
|
|
113
|
+
self.main_window.update_chiral_labels()
|
|
114
|
+
|
|
115
|
+
self.main_window.push_undo_state()
|
|
116
|
+
|
|
117
|
+
plane_names = ["XY", "XZ", "YZ"]
|
|
118
|
+
self.main_window.statusBar().showMessage(f"Molecule mirrored across {plane_names[plane_id]} plane.")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
QMessageBox.critical(self, "Error", f"Failed to apply mirror transformation: {str(e)}")
|