moleditpy 1.9.0__tar.gz → 1.9.2__tar.gz
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-1.9.0 → moleditpy-1.9.2}/PKG-INFO +1 -1
- {moleditpy-1.9.0 → moleditpy-1.9.2}/pyproject.toml +1 -1
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy/main.py +419 -128
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy.egg-info/PKG-INFO +1 -1
- {moleditpy-1.9.0 → moleditpy-1.9.2}/README.md +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/setup.cfg +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy/__init__.py +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy.egg-info/SOURCES.txt +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy.egg-info/dependency_links.txt +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy.egg-info/entry_points.txt +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy.egg-info/requires.txt +0 -0
- {moleditpy-1.9.0 → moleditpy-1.9.2}/src/moleditpy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moleditpy
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.2
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
|
|
@@ -11,7 +11,7 @@ DOI 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
#Version
|
|
14
|
-
VERSION = '1.9.
|
|
14
|
+
VERSION = '1.9.2'
|
|
15
15
|
|
|
16
16
|
print("-----------------------------------------------------")
|
|
17
17
|
print("MoleditPy — A Python-based molecular editing software")
|
|
@@ -37,7 +37,7 @@ from PyQt6.QtWidgets import (
|
|
|
37
37
|
QPushButton, QSplitter, QGraphicsView, QGraphicsScene, QGraphicsItem,
|
|
38
38
|
QToolBar, QStatusBar, QGraphicsTextItem, QGraphicsLineItem, QDialog, QGridLayout,
|
|
39
39
|
QFileDialog, QSizePolicy, QLabel, QLineEdit, QToolButton, QMenu, QMessageBox, QInputDialog,
|
|
40
|
-
QColorDialog, QCheckBox, QSlider, QFormLayout, QRadioButton, QComboBox, QListWidget, QListWidgetItem, QButtonGroup, QTabWidget, QScrollArea
|
|
40
|
+
QColorDialog, QCheckBox, QSlider, QFormLayout, QRadioButton, QComboBox, QListWidget, QListWidgetItem, QButtonGroup, QTabWidget, QScrollArea, QFrame
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
from PyQt6.QtGui import (
|
|
@@ -58,7 +58,14 @@ from rdkit.Chem import rdMolDescriptors
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
# Open Babel Python binding (optional; required for fallback)
|
|
61
|
-
|
|
61
|
+
try:
|
|
62
|
+
from openbabel import pybel
|
|
63
|
+
OBABEL_AVAILABLE = True
|
|
64
|
+
except Exception:
|
|
65
|
+
# pybel (Open Babel Python bindings) is optional. If not present, disable OBabel features.
|
|
66
|
+
pybel = None
|
|
67
|
+
OBABEL_AVAILABLE = False
|
|
68
|
+
print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
|
|
62
69
|
|
|
63
70
|
# PyVista
|
|
64
71
|
import pyvista as pv
|
|
@@ -1025,9 +1032,6 @@ class TranslationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
1025
1032
|
# 移動ベクトルを計算
|
|
1026
1033
|
translation_vector = target_pos - current_centroid
|
|
1027
1034
|
|
|
1028
|
-
# Undo状態を保存
|
|
1029
|
-
self.main_window.push_undo_state()
|
|
1030
|
-
|
|
1031
1035
|
# 全原子を平行移動
|
|
1032
1036
|
conf = self.mol.GetConformer()
|
|
1033
1037
|
for i in range(self.mol.GetNumAtoms()):
|
|
@@ -1036,6 +1040,7 @@ class TranslationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
1036
1040
|
conf.SetAtomPosition(i, new_pos.tolist())
|
|
1037
1041
|
self.main_window.atom_positions_3d[i] = new_pos
|
|
1038
1042
|
|
|
1043
|
+
|
|
1039
1044
|
# 3D表示を更新
|
|
1040
1045
|
self.main_window.draw_molecule_3d(self.mol)
|
|
1041
1046
|
|
|
@@ -1045,6 +1050,9 @@ class TranslationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
1045
1050
|
# Apply後に選択解除
|
|
1046
1051
|
self.clear_selection()
|
|
1047
1052
|
|
|
1053
|
+
# Undo状態を保存
|
|
1054
|
+
self.main_window.push_undo_state()
|
|
1055
|
+
|
|
1048
1056
|
except Exception as e:
|
|
1049
1057
|
QMessageBox.critical(self, "Error", f"Failed to apply translation: {str(e)}")
|
|
1050
1058
|
|
|
@@ -1522,8 +1530,6 @@ class SymmetrizeDialog(QDialog):
|
|
|
1522
1530
|
key, point_group = self.get_selected_point_group()
|
|
1523
1531
|
|
|
1524
1532
|
try:
|
|
1525
|
-
# Undo状態を保存(操作前の状態のみ)
|
|
1526
|
-
self.main_window.push_undo_state()
|
|
1527
1533
|
|
|
1528
1534
|
# 現在の分子の座標を取得
|
|
1529
1535
|
conf = self.mol.GetConformer()
|
|
@@ -1544,6 +1550,9 @@ class SymmetrizeDialog(QDialog):
|
|
|
1544
1550
|
|
|
1545
1551
|
# 3D表示のみを更新
|
|
1546
1552
|
self.main_window.draw_molecule_3d(self.mol)
|
|
1553
|
+
|
|
1554
|
+
# Undo状態を保存(操作前の状態のみ)
|
|
1555
|
+
self.main_window.push_undo_state()
|
|
1547
1556
|
|
|
1548
1557
|
# 成功メッセージ(ダイアログを閉じる前に表示)
|
|
1549
1558
|
QMessageBox.information(
|
|
@@ -2659,7 +2668,8 @@ class MirrorDialog(QDialog):
|
|
|
2659
2668
|
|
|
2660
2669
|
plane_names = ["XY", "XZ", "YZ"]
|
|
2661
2670
|
self.main_window.statusBar().showMessage(f"Molecule mirrored across {plane_names[plane_id]} plane.")
|
|
2662
|
-
|
|
2671
|
+
|
|
2672
|
+
|
|
2663
2673
|
except Exception as e:
|
|
2664
2674
|
QMessageBox.critical(self, "Error", f"Failed to apply mirror transformation: {str(e)}")
|
|
2665
2675
|
|
|
@@ -2816,26 +2826,26 @@ class PlanarizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
2816
2826
|
if len(self.selected_atoms) < 3:
|
|
2817
2827
|
QMessageBox.warning(self, "Warning", "Please select at least 3 atoms for planarization.")
|
|
2818
2828
|
return
|
|
2819
|
-
|
|
2820
2829
|
try:
|
|
2830
|
+
|
|
2821
2831
|
# 選択された原子の位置を取得
|
|
2822
2832
|
selected_indices = list(self.selected_atoms)
|
|
2823
2833
|
selected_positions = self.main_window.atom_positions_3d[selected_indices].copy()
|
|
2824
|
-
|
|
2834
|
+
|
|
2825
2835
|
# 重心を計算
|
|
2826
2836
|
centroid = np.mean(selected_positions, axis=0)
|
|
2827
|
-
|
|
2837
|
+
|
|
2828
2838
|
# 重心を原点に移動
|
|
2829
2839
|
centered_positions = selected_positions - centroid
|
|
2830
|
-
|
|
2840
|
+
|
|
2831
2841
|
# 主成分分析で最適な平面を見つける
|
|
2832
2842
|
# 選択された原子の座標の共分散行列を計算
|
|
2833
2843
|
cov_matrix = np.cov(centered_positions.T)
|
|
2834
2844
|
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
|
|
2835
|
-
|
|
2845
|
+
|
|
2836
2846
|
# 固有値が最も小さい固有ベクトルが平面の法線方向
|
|
2837
2847
|
normal_vector = eigenvectors[:, 0] # 最小固有値に対応する固有ベクトル
|
|
2838
|
-
|
|
2848
|
+
|
|
2839
2849
|
# 目標の平面の法線ベクトルを定義
|
|
2840
2850
|
if self.plane == 'xy':
|
|
2841
2851
|
target_normal = np.array([0, 0, 1]) # Z軸方向
|
|
@@ -2843,27 +2853,27 @@ class PlanarizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
2843
2853
|
target_normal = np.array([0, 1, 0]) # Y軸方向
|
|
2844
2854
|
elif self.plane == 'yz':
|
|
2845
2855
|
target_normal = np.array([1, 0, 0]) # X軸方向
|
|
2846
|
-
|
|
2856
|
+
|
|
2847
2857
|
# 法線ベクトルの向きを調整(内積が正になるように)
|
|
2848
2858
|
if np.dot(normal_vector, target_normal) < 0:
|
|
2849
2859
|
normal_vector = -normal_vector
|
|
2850
|
-
|
|
2860
|
+
|
|
2851
2861
|
# 回転軸と回転角度を計算
|
|
2852
2862
|
rotation_axis = np.cross(normal_vector, target_normal)
|
|
2853
2863
|
rotation_axis_norm = np.linalg.norm(rotation_axis)
|
|
2854
|
-
|
|
2864
|
+
|
|
2855
2865
|
if rotation_axis_norm > 1e-10: # 回転が必要な場合
|
|
2856
2866
|
rotation_axis = rotation_axis / rotation_axis_norm
|
|
2857
2867
|
cos_angle = np.dot(normal_vector, target_normal)
|
|
2858
2868
|
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
2859
2869
|
rotation_angle = np.arccos(cos_angle)
|
|
2860
|
-
|
|
2870
|
+
|
|
2861
2871
|
# Rodrigues回転公式を使用して全分子を回転
|
|
2862
2872
|
def rodrigues_rotation(v, axis, angle):
|
|
2863
2873
|
cos_a = np.cos(angle)
|
|
2864
2874
|
sin_a = np.sin(angle)
|
|
2865
2875
|
return v * cos_a + np.cross(axis, v) * sin_a + axis * np.dot(axis, v) * (1 - cos_a)
|
|
2866
|
-
|
|
2876
|
+
|
|
2867
2877
|
# 分子全体を回転させる
|
|
2868
2878
|
conf = self.mol.GetConformer()
|
|
2869
2879
|
for i in range(self.mol.GetNumAtoms()):
|
|
@@ -2874,13 +2884,16 @@ class PlanarizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
2874
2884
|
new_pos = rotated_pos + centroid
|
|
2875
2885
|
conf.SetAtomPosition(i, new_pos.tolist())
|
|
2876
2886
|
self.main_window.atom_positions_3d[i] = new_pos
|
|
2877
|
-
|
|
2887
|
+
|
|
2878
2888
|
# 3D表示を更新
|
|
2879
2889
|
self.main_window.draw_molecule_3d(self.mol)
|
|
2880
|
-
|
|
2890
|
+
|
|
2881
2891
|
# キラルラベルを更新
|
|
2882
2892
|
self.main_window.update_chiral_labels()
|
|
2883
|
-
|
|
2893
|
+
|
|
2894
|
+
# Undo状態を保存
|
|
2895
|
+
self.main_window.push_undo_state()
|
|
2896
|
+
|
|
2884
2897
|
except Exception as e:
|
|
2885
2898
|
QMessageBox.critical(self, "Error", f"Failed to apply planarization: {str(e)}")
|
|
2886
2899
|
|
|
@@ -3056,27 +3069,27 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
3056
3069
|
if len(self.selected_atoms) != 2:
|
|
3057
3070
|
QMessageBox.warning(self, "Warning", "Please select exactly 2 atoms for alignment.")
|
|
3058
3071
|
return
|
|
3059
|
-
|
|
3060
3072
|
try:
|
|
3073
|
+
|
|
3061
3074
|
selected_list = sorted(list(self.selected_atoms))
|
|
3062
3075
|
atom1_idx, atom2_idx = selected_list[0], selected_list[1]
|
|
3063
|
-
|
|
3076
|
+
|
|
3064
3077
|
conf = self.mol.GetConformer()
|
|
3065
|
-
|
|
3078
|
+
|
|
3066
3079
|
# 原子の現在位置を取得
|
|
3067
3080
|
pos1 = np.array(conf.GetAtomPosition(atom1_idx))
|
|
3068
3081
|
pos2 = np.array(conf.GetAtomPosition(atom2_idx))
|
|
3069
|
-
|
|
3082
|
+
|
|
3070
3083
|
# 最初に全分子を移動して、atom1を原点に配置
|
|
3071
3084
|
translation = -pos1
|
|
3072
3085
|
for i in range(self.mol.GetNumAtoms()):
|
|
3073
3086
|
current_pos = np.array(conf.GetAtomPosition(i))
|
|
3074
3087
|
new_pos = current_pos + translation
|
|
3075
3088
|
conf.SetAtomPosition(i, new_pos.tolist())
|
|
3076
|
-
|
|
3089
|
+
|
|
3077
3090
|
# atom2の新しい位置を取得(移動後)
|
|
3078
3091
|
pos2_translated = pos2 + translation
|
|
3079
|
-
|
|
3092
|
+
|
|
3080
3093
|
# atom2を選択した軸上に配置するための回転を計算
|
|
3081
3094
|
axis_vectors = {
|
|
3082
3095
|
'x': np.array([1.0, 0.0, 0.0]),
|
|
@@ -3126,6 +3139,9 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
3126
3139
|
|
|
3127
3140
|
# キラルラベルを更新
|
|
3128
3141
|
self.main_window.update_chiral_labels()
|
|
3142
|
+
|
|
3143
|
+
# Undo状態を保存
|
|
3144
|
+
self.main_window.push_undo_state()
|
|
3129
3145
|
|
|
3130
3146
|
QMessageBox.information(self, "Success", f"Alignment to {self.axis.upper()}-axis completed.")
|
|
3131
3147
|
|
|
@@ -4903,7 +4919,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
4903
4919
|
return points
|
|
4904
4920
|
|
|
4905
4921
|
def delete_items(self, items_to_delete):
|
|
4906
|
-
"""
|
|
4922
|
+
"""指定されたアイテムセット(原子・結合)を安全な順序で削除する修正版"""
|
|
4907
4923
|
try:
|
|
4908
4924
|
if not items_to_delete:
|
|
4909
4925
|
return False
|
|
@@ -4916,7 +4932,7 @@ class MoleculeScene(QGraphicsScene):
|
|
|
4916
4932
|
if hasattr(atom, 'bonds'):
|
|
4917
4933
|
bonds_to_delete.update(atom.bonds)
|
|
4918
4934
|
|
|
4919
|
-
#
|
|
4935
|
+
# 削除はされないが、影響を受ける原子(結合リストの更新が必要)を特定する
|
|
4920
4936
|
atoms_to_update = set()
|
|
4921
4937
|
for bond in bonds_to_delete:
|
|
4922
4938
|
if bond.atom1 and bond.atom1 not in atoms_to_delete:
|
|
@@ -4924,62 +4940,49 @@ class MoleculeScene(QGraphicsScene):
|
|
|
4924
4940
|
if bond.atom2 and bond.atom2 not in atoms_to_delete:
|
|
4925
4941
|
atoms_to_update.add(bond.atom2)
|
|
4926
4942
|
|
|
4927
|
-
# ---
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
self.data.remove_bond(bond.atom1.atom_id, bond.atom2.atom_id)
|
|
4937
|
-
|
|
4938
|
-
# --- シーンからのグラフィックアイテム削除(必ず結合を先に)---
|
|
4939
|
-
# まずremoveItem
|
|
4943
|
+
# --- 安全な順序で削除処理を実行 ---
|
|
4944
|
+
|
|
4945
|
+
# 1. まず、生き残る原子の内部状態を更新する
|
|
4946
|
+
for atom in atoms_to_update:
|
|
4947
|
+
if hasattr(atom, 'bonds'):
|
|
4948
|
+
# これから削除される結合を自身の結合リストから除去する
|
|
4949
|
+
atom.bonds = [b for b in atom.bonds if b not in bonds_to_delete]
|
|
4950
|
+
|
|
4951
|
+
# 2. 次に、グラフィックシーンからアイテムを削除する(必ず結合を先に)
|
|
4940
4952
|
for bond in bonds_to_delete:
|
|
4941
4953
|
if bond.scene():
|
|
4942
4954
|
self.removeItem(bond)
|
|
4943
|
-
|
|
4944
4955
|
for atom in atoms_to_delete:
|
|
4945
4956
|
if atom.scene():
|
|
4946
4957
|
self.removeItem(atom)
|
|
4947
4958
|
|
|
4948
|
-
#
|
|
4959
|
+
# 3. データモデル(MolecularData)からデータを削除する
|
|
4960
|
+
# ここでは安全なIDを使って操作する
|
|
4949
4961
|
for bond in bonds_to_delete:
|
|
4950
|
-
#
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
bond.atom1.bonds = [b for b in bond.atom1.bonds if b is not bond]
|
|
4954
|
-
if bond.atom2 and hasattr(bond.atom2, 'bonds') and bond in bond.atom2.bonds:
|
|
4955
|
-
bond.atom2.bonds = [b for b in bond.atom2.bonds if b is not bond]
|
|
4956
|
-
# Clear bond references to prevent dangling pointers
|
|
4957
|
-
bond.atom1 = None
|
|
4958
|
-
bond.atom2 = None
|
|
4959
|
-
except Exception as bond_cleanup_error:
|
|
4960
|
-
print(f"Error cleaning up bond references: {bond_cleanup_error}")
|
|
4961
|
-
|
|
4962
|
+
# 参照が既にクリアされている可能性を考慮
|
|
4963
|
+
if bond.atom1 and bond.atom2:
|
|
4964
|
+
self.data.remove_bond(bond.atom1.atom_id, bond.atom2.atom_id)
|
|
4962
4965
|
for atom in atoms_to_delete:
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
if hasattr(atom, 'bonds'):
|
|
4966
|
-
atom.bonds.clear()
|
|
4967
|
-
except Exception as atom_cleanup_error:
|
|
4968
|
-
print(f"Error cleaning up atom references: {atom_cleanup_error}")
|
|
4966
|
+
if hasattr(atom, 'atom_id'):
|
|
4967
|
+
self.data.remove_atom(atom.atom_id)
|
|
4969
4968
|
|
|
4970
|
-
#
|
|
4969
|
+
# 4. 削除されたオブジェクト間の参照をクリアし、メモリ解放を助ける
|
|
4970
|
+
for bond in bonds_to_delete:
|
|
4971
|
+
bond.atom1 = None
|
|
4972
|
+
bond.atom2 = None
|
|
4973
|
+
for atom in atoms_to_delete:
|
|
4974
|
+
if hasattr(atom, 'bonds'):
|
|
4975
|
+
atom.bonds.clear()
|
|
4976
|
+
|
|
4977
|
+
# 5. 最後に、生き残った原子の表示スタイルを更新する
|
|
4971
4978
|
for atom in atoms_to_update:
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
atom.bonds = [b for b in atom.bonds if b not in bonds_to_delete]
|
|
4975
|
-
if hasattr(atom, 'update_style'):
|
|
4976
|
-
atom.update_style()
|
|
4977
|
-
except Exception as update_error:
|
|
4978
|
-
print(f"Error updating atom style: {update_error}")
|
|
4979
|
+
if hasattr(atom, 'update_style'):
|
|
4980
|
+
atom.update_style()
|
|
4979
4981
|
|
|
4980
|
-
return True
|
|
4982
|
+
return True # 変更が行われたことを示す
|
|
4981
4983
|
|
|
4982
4984
|
except Exception as e:
|
|
4985
|
+
# 予期せぬエラーが発生した場合でもクラッシュを防ぐ
|
|
4983
4986
|
print(f"Error during delete_items operation: {e}")
|
|
4984
4987
|
import traceback
|
|
4985
4988
|
traceback.print_exc()
|
|
@@ -5720,6 +5723,7 @@ class CalculationWorker(QObject):
|
|
|
5720
5723
|
raise ValueError("No atoms to convert.")
|
|
5721
5724
|
|
|
5722
5725
|
self.status_update.emit("Creating 3D structure...")
|
|
5726
|
+
|
|
5723
5727
|
mol = Chem.MolFromMolBlock(mol_block, removeHs=False)
|
|
5724
5728
|
if mol is None:
|
|
5725
5729
|
raise ValueError("Failed to create molecule from MOL block.")
|
|
@@ -5926,6 +5930,9 @@ class CalculationWorker(QObject):
|
|
|
5926
5930
|
self.status_update.emit("RDKit embedding failed. Attempting Open Babel fallback...")
|
|
5927
5931
|
|
|
5928
5932
|
try:
|
|
5933
|
+
# Check availability first
|
|
5934
|
+
if not OBABEL_AVAILABLE:
|
|
5935
|
+
raise RuntimeError("Open Babel (pybel) is not available in this Python environment.")
|
|
5929
5936
|
# pybel expects an input format; provide mol block
|
|
5930
5937
|
# pybel.readstring accepts format strings like "mol" or "smi"
|
|
5931
5938
|
ob_mol = pybel.readstring("mol", mol_block)
|
|
@@ -6246,6 +6253,11 @@ class SettingsDialog(QDialog):
|
|
|
6246
6253
|
'triple_bond_offset_factor': 2.0,
|
|
6247
6254
|
'double_bond_radius_factor': 0.8,
|
|
6248
6255
|
'triple_bond_radius_factor': 0.7,
|
|
6256
|
+
# If True, attempts to be permissive when RDKit raises chemical/sanitization errors
|
|
6257
|
+
# during file import (useful for viewing malformed XYZ/MOL files). When enabled,
|
|
6258
|
+
# element symbol recognition will be coerced where possible and Chem.SanitizeMol
|
|
6259
|
+
# failures will be ignored so the 3D viewer can still display the structure.
|
|
6260
|
+
'skip_chemistry_checks': True,
|
|
6249
6261
|
}
|
|
6250
6262
|
|
|
6251
6263
|
# --- 選択された色を管理する専用のインスタンス変数 ---
|
|
@@ -6258,9 +6270,6 @@ class SettingsDialog(QDialog):
|
|
|
6258
6270
|
self.tab_widget = QTabWidget()
|
|
6259
6271
|
layout.addWidget(self.tab_widget)
|
|
6260
6272
|
|
|
6261
|
-
# 基本設定タブ
|
|
6262
|
-
self.create_general_tab()
|
|
6263
|
-
|
|
6264
6273
|
# Common設定タブ
|
|
6265
6274
|
self.create_common_tab()
|
|
6266
6275
|
|
|
@@ -6276,6 +6285,9 @@ class SettingsDialog(QDialog):
|
|
|
6276
6285
|
# Stick設定タブ
|
|
6277
6286
|
self.create_stick_tab()
|
|
6278
6287
|
|
|
6288
|
+
# 基本設定タブ
|
|
6289
|
+
self.create_general_tab()
|
|
6290
|
+
|
|
6279
6291
|
# 渡された設定でUIと内部変数を初期化
|
|
6280
6292
|
self.update_ui_from_settings(current_settings)
|
|
6281
6293
|
|
|
@@ -6366,7 +6378,23 @@ class SettingsDialog(QDialog):
|
|
|
6366
6378
|
"""Common設定タブを作成"""
|
|
6367
6379
|
common_widget = QWidget()
|
|
6368
6380
|
form_layout = QFormLayout(common_widget)
|
|
6381
|
+
|
|
6382
|
+
# 化学チェックスキップオプション(Commonタブに移動)
|
|
6383
|
+
self.skip_chem_checks_checkbox = QCheckBox()
|
|
6384
|
+
self.skip_chem_checks_checkbox.setToolTip("When enabled, file import will try to ignore chemical/sanitization errors and allow viewing malformed files.")
|
|
6385
|
+
# Immediately persist change to settings when user toggles the checkbox
|
|
6386
|
+
try:
|
|
6387
|
+
self.skip_chem_checks_checkbox.stateChanged.connect(lambda s: self._on_skip_chem_checks_changed(s))
|
|
6388
|
+
except Exception:
|
|
6389
|
+
pass
|
|
6390
|
+
form_layout.addRow("Skip chemistry checks on import:", self.skip_chem_checks_checkbox)
|
|
6369
6391
|
|
|
6392
|
+
# --- 区切り線(水平ライン) ---
|
|
6393
|
+
line = QFrame()
|
|
6394
|
+
line.setFrameShape(QFrame.Shape.HLine)
|
|
6395
|
+
line.setFrameShadow(QFrame.Shadow.Sunken)
|
|
6396
|
+
form_layout.addRow(line)
|
|
6397
|
+
|
|
6370
6398
|
# 二重結合オフセット倍率
|
|
6371
6399
|
self.double_offset_slider = QSlider(Qt.Orientation.Horizontal)
|
|
6372
6400
|
self.double_offset_slider.setRange(100, 400) # 1.0 ~ 4.0
|
|
@@ -6406,6 +6434,8 @@ class SettingsDialog(QDialog):
|
|
|
6406
6434
|
triple_radius_layout.addWidget(self.triple_radius_slider)
|
|
6407
6435
|
triple_radius_layout.addWidget(self.triple_radius_label)
|
|
6408
6436
|
form_layout.addRow("Triple Bond Thickness:", triple_radius_layout)
|
|
6437
|
+
|
|
6438
|
+
|
|
6409
6439
|
|
|
6410
6440
|
self.tab_widget.addTab(common_widget, "Common")
|
|
6411
6441
|
|
|
@@ -6735,6 +6765,9 @@ class SettingsDialog(QDialog):
|
|
|
6735
6765
|
triple_radius = int(settings_dict.get('triple_bond_radius_factor', self.default_settings['triple_bond_radius_factor']) * 100)
|
|
6736
6766
|
self.triple_radius_slider.setValue(triple_radius)
|
|
6737
6767
|
self.triple_radius_label.setText(f"{triple_radius/100:.2f}")
|
|
6768
|
+
|
|
6769
|
+
# skip chemistry checks
|
|
6770
|
+
self.skip_chem_checks_checkbox.setChecked(settings_dict.get('skip_chemistry_checks', self.default_settings.get('skip_chemistry_checks', False)))
|
|
6738
6771
|
|
|
6739
6772
|
def select_color(self):
|
|
6740
6773
|
"""カラーピッカーを開き、選択された色を内部変数とUIに反映させる"""
|
|
@@ -6777,6 +6810,7 @@ class SettingsDialog(QDialog):
|
|
|
6777
6810
|
'triple_bond_offset_factor': self.triple_offset_slider.value() / 100.0,
|
|
6778
6811
|
'double_bond_radius_factor': self.double_radius_slider.value() / 100.0,
|
|
6779
6812
|
'triple_bond_radius_factor': self.triple_radius_slider.value() / 100.0,
|
|
6813
|
+
'skip_chemistry_checks': self.skip_chem_checks_checkbox.isChecked(),
|
|
6780
6814
|
}
|
|
6781
6815
|
|
|
6782
6816
|
def apply_settings(self):
|
|
@@ -6792,7 +6826,21 @@ class SettingsDialog(QDialog):
|
|
|
6792
6826
|
if hasattr(self.parent_window, 'current_mol') and self.parent_window.current_mol:
|
|
6793
6827
|
self.parent_window.draw_molecule_3d(self.parent_window.current_mol)
|
|
6794
6828
|
# ステータスバーに適用完了を表示
|
|
6795
|
-
self.parent_window.statusBar().showMessage("Settings applied successfully"
|
|
6829
|
+
self.parent_window.statusBar().showMessage("Settings applied successfully")
|
|
6830
|
+
|
|
6831
|
+
def _on_skip_chem_checks_changed(self, state):
|
|
6832
|
+
"""Handle user toggling of skip chemistry checks: persist and update UI.
|
|
6833
|
+
|
|
6834
|
+
state: Qt.Checked (2) or Qt.Unchecked (0)
|
|
6835
|
+
"""
|
|
6836
|
+
try:
|
|
6837
|
+
enabled = bool(state)
|
|
6838
|
+
self.settings['skip_chemistry_checks'] = enabled
|
|
6839
|
+
self.save_settings()
|
|
6840
|
+
# If skip is enabled, allow Optimize button; otherwise, respect chem_check flags
|
|
6841
|
+
|
|
6842
|
+
except Exception:
|
|
6843
|
+
pass
|
|
6796
6844
|
|
|
6797
6845
|
def accept(self):
|
|
6798
6846
|
"""ダイアログの設定を適用してから閉じる"""
|
|
@@ -7048,7 +7096,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
7048
7096
|
|
|
7049
7097
|
class MainWindow(QMainWindow):
|
|
7050
7098
|
|
|
7051
|
-
start_calculation
|
|
7099
|
+
# start_calculation carries the MOL block and a boolean skip_chemistry_checks flag
|
|
7100
|
+
start_calculation = pyqtSignal(str, bool)
|
|
7052
7101
|
def __init__(self, initial_file=None):
|
|
7053
7102
|
super().__init__()
|
|
7054
7103
|
self.setAcceptDrops(True)
|
|
@@ -7068,6 +7117,11 @@ class MainWindow(QMainWindow):
|
|
|
7068
7117
|
self.atom_actor = None
|
|
7069
7118
|
self.is_2d_editable = True
|
|
7070
7119
|
self.is_xyz_derived = False # XYZ由来の分子かどうかのフラグ
|
|
7120
|
+
# Chemical check flags: whether a chemical/sanitization check was attempted and whether it failed
|
|
7121
|
+
self.chem_check_tried = False
|
|
7122
|
+
self.chem_check_failed = False
|
|
7123
|
+
# 3D最適化のデフォルト手法
|
|
7124
|
+
self.optimization_method = self.settings.get('optimization_method', 'MMFF_RDKIT')
|
|
7071
7125
|
self.axes_actor = None
|
|
7072
7126
|
self.axes_widget = None
|
|
7073
7127
|
self._template_dialog = None # テンプレートダイアログの参照
|
|
@@ -7699,6 +7753,63 @@ class MainWindow(QMainWindow):
|
|
|
7699
7753
|
optimize_3d_action.triggered.connect(self.optimize_3d_structure)
|
|
7700
7754
|
edit_menu.addAction(optimize_3d_action)
|
|
7701
7755
|
|
|
7756
|
+
# --- 3D Optimization Settings submenu (below Optimize 3D) ---
|
|
7757
|
+
opt3d_settings_menu = edit_menu.addMenu("3D Optimization Settings")
|
|
7758
|
+
opt3d_group = QActionGroup(self)
|
|
7759
|
+
opt3d_group.setExclusive(True)
|
|
7760
|
+
|
|
7761
|
+
# Only RDKit-backed optimization methods are offered here.
|
|
7762
|
+
# Open Babel-based optimization options have been removed; Open Babel is
|
|
7763
|
+
# retained only as a conversion fallback when RDKit embedding fails.
|
|
7764
|
+
opt_methods = [
|
|
7765
|
+
("MMFF", "MMFF_RDKIT"),
|
|
7766
|
+
("UFF", "UFF_RDKIT"),
|
|
7767
|
+
]
|
|
7768
|
+
|
|
7769
|
+
# Map key -> human-readable label for status messages and later lookups
|
|
7770
|
+
try:
|
|
7771
|
+
self.opt3d_method_labels = {key.upper(): label for (label, key) in opt_methods}
|
|
7772
|
+
except Exception:
|
|
7773
|
+
self.opt3d_method_labels = {}
|
|
7774
|
+
|
|
7775
|
+
opt_actions = {}
|
|
7776
|
+
for label, key in opt_methods:
|
|
7777
|
+
action = QAction(label, self)
|
|
7778
|
+
action.setCheckable(True)
|
|
7779
|
+
# Ensure explicit association with the action group so exclusive checking works reliably
|
|
7780
|
+
try:
|
|
7781
|
+
action.setActionGroup(opt3d_group)
|
|
7782
|
+
except Exception:
|
|
7783
|
+
# Older PyQt variants may not have setActionGroup; adding to group below still helps
|
|
7784
|
+
pass
|
|
7785
|
+
action.triggered.connect(lambda checked, m=key: self.set_optimization_method(m))
|
|
7786
|
+
opt3d_settings_menu.addAction(action)
|
|
7787
|
+
opt3d_group.addAction(action)
|
|
7788
|
+
opt_actions[key] = action
|
|
7789
|
+
|
|
7790
|
+
# Persist the actions mapping so other methods can update the checked state
|
|
7791
|
+
self.opt3d_actions = opt_actions
|
|
7792
|
+
|
|
7793
|
+
# Determine the initial checked menu item from saved settings (fall back to MMFF_RDKIT)
|
|
7794
|
+
try:
|
|
7795
|
+
saved = (self.settings.get('optimization_method') or self.optimization_method or 'MMFF_RDKIT').upper()
|
|
7796
|
+
except Exception:
|
|
7797
|
+
saved = 'MMFF_RDKIT'
|
|
7798
|
+
|
|
7799
|
+
if saved in self.opt3d_actions and self.opt3d_actions[saved].isEnabled():
|
|
7800
|
+
self.opt3d_actions[saved].setChecked(True)
|
|
7801
|
+
# Ensure internal state matches
|
|
7802
|
+
self.optimization_method = saved
|
|
7803
|
+
else:
|
|
7804
|
+
# fallback
|
|
7805
|
+
if 'MMFF_RDKIT' in self.opt3d_actions:
|
|
7806
|
+
self.opt3d_actions['MMFF_RDKIT'].setChecked(True)
|
|
7807
|
+
self.optimization_method = 'MMFF_RDKIT'
|
|
7808
|
+
|
|
7809
|
+
# Note: Open Babel-based optimization menu entries were intentionally
|
|
7810
|
+
# removed above. Open Babel (pybel) is still available for conversion
|
|
7811
|
+
# fallback elsewhere in the code, so we don't disable menu items here.
|
|
7812
|
+
|
|
7702
7813
|
edit_menu.addSeparator()
|
|
7703
7814
|
|
|
7704
7815
|
# Templates
|
|
@@ -8058,6 +8169,53 @@ class MainWindow(QMainWindow):
|
|
|
8058
8169
|
if self.current_mol:
|
|
8059
8170
|
self.draw_molecule_3d(self.current_mol)
|
|
8060
8171
|
|
|
8172
|
+
def set_optimization_method(self, method_name):
|
|
8173
|
+
"""Set preferred 3D optimization method and persist to settings.
|
|
8174
|
+
|
|
8175
|
+
Supported values: 'GAFF', 'MMFF'
|
|
8176
|
+
"""
|
|
8177
|
+
# Normalize input and validate
|
|
8178
|
+
if not method_name:
|
|
8179
|
+
return
|
|
8180
|
+
method = str(method_name).strip().upper()
|
|
8181
|
+
valid_methods = (
|
|
8182
|
+
'MMFF_RDKIT', 'UFF_RDKIT',
|
|
8183
|
+
'UFF_OBABEL', 'GAFF_OBABEL', 'MMFF94_OBABEL', 'GHEMICAL_OBABEL'
|
|
8184
|
+
)
|
|
8185
|
+
if method not in valid_methods:
|
|
8186
|
+
# Unknown method: ignore but notify
|
|
8187
|
+
self.statusBar().showMessage(f"Unknown 3D optimization method: {method_name}")
|
|
8188
|
+
return
|
|
8189
|
+
|
|
8190
|
+
# Update internal state (store canonical uppercase key)
|
|
8191
|
+
self.optimization_method = method
|
|
8192
|
+
|
|
8193
|
+
# Persist to settings
|
|
8194
|
+
try:
|
|
8195
|
+
self.settings['optimization_method'] = self.optimization_method
|
|
8196
|
+
self.save_settings()
|
|
8197
|
+
except Exception:
|
|
8198
|
+
pass
|
|
8199
|
+
|
|
8200
|
+
# Update menu checked state if actions mapping exists
|
|
8201
|
+
try:
|
|
8202
|
+
if hasattr(self, 'opt3d_actions') and self.opt3d_actions:
|
|
8203
|
+
for k, act in self.opt3d_actions.items():
|
|
8204
|
+
try:
|
|
8205
|
+
# keys in opt3d_actions may be mixed-case; compare uppercased
|
|
8206
|
+
act.setChecked(k.upper() == method)
|
|
8207
|
+
except Exception:
|
|
8208
|
+
pass
|
|
8209
|
+
except Exception:
|
|
8210
|
+
pass
|
|
8211
|
+
|
|
8212
|
+
# Also show user-friendly label if available
|
|
8213
|
+
try:
|
|
8214
|
+
label = self.opt3d_method_labels.get(self.optimization_method, self.optimization_method)
|
|
8215
|
+
except Exception:
|
|
8216
|
+
label = self.optimization_method
|
|
8217
|
+
self.statusBar().showMessage(f"3D optimization method set to: {label}")
|
|
8218
|
+
|
|
8061
8219
|
def copy_selection(self):
|
|
8062
8220
|
"""選択された原子と結合をクリップボードにコピーする"""
|
|
8063
8221
|
try:
|
|
@@ -8109,13 +8267,13 @@ class MainWindow(QMainWindow):
|
|
|
8109
8267
|
mime_data = QMimeData()
|
|
8110
8268
|
mime_data.setData(CLIPBOARD_MIME_TYPE, byte_array)
|
|
8111
8269
|
QApplication.clipboard().setMimeData(mime_data)
|
|
8112
|
-
self.statusBar().showMessage(f"Copied {len(fragment_atoms)} atoms and {len(fragment_bonds)} bonds."
|
|
8270
|
+
self.statusBar().showMessage(f"Copied {len(fragment_atoms)} atoms and {len(fragment_bonds)} bonds.")
|
|
8113
8271
|
|
|
8114
8272
|
except Exception as e:
|
|
8115
8273
|
print(f"Error during copy operation: {e}")
|
|
8116
8274
|
import traceback
|
|
8117
8275
|
traceback.print_exc()
|
|
8118
|
-
self.statusBar().showMessage(f"Error during copy operation: {e}"
|
|
8276
|
+
self.statusBar().showMessage(f"Error during copy operation: {e}")
|
|
8119
8277
|
|
|
8120
8278
|
def cut_selection(self):
|
|
8121
8279
|
"""選択されたアイテムを切り取り(コピーしてから削除)"""
|
|
@@ -8135,7 +8293,7 @@ class MainWindow(QMainWindow):
|
|
|
8135
8293
|
print(f"Error during cut operation: {e}")
|
|
8136
8294
|
import traceback
|
|
8137
8295
|
traceback.print_exc()
|
|
8138
|
-
self.statusBar().showMessage(f"Error during cut operation: {e}"
|
|
8296
|
+
self.statusBar().showMessage(f"Error during cut operation: {e}")
|
|
8139
8297
|
|
|
8140
8298
|
def paste_from_clipboard(self):
|
|
8141
8299
|
"""クリップボードから分子フラグメントを貼り付け"""
|
|
@@ -8150,7 +8308,7 @@ class MainWindow(QMainWindow):
|
|
|
8150
8308
|
try:
|
|
8151
8309
|
fragment_data = pickle.load(buffer)
|
|
8152
8310
|
except pickle.UnpicklingError:
|
|
8153
|
-
self.statusBar().showMessage("Error: Invalid clipboard data format"
|
|
8311
|
+
self.statusBar().showMessage("Error: Invalid clipboard data format")
|
|
8154
8312
|
return
|
|
8155
8313
|
|
|
8156
8314
|
paste_center_pos = self.view_2d.mapToScene(self.view_2d.mapFromGlobal(QCursor.pos()))
|
|
@@ -8184,7 +8342,7 @@ class MainWindow(QMainWindow):
|
|
|
8184
8342
|
print(f"Error during paste operation: {e}")
|
|
8185
8343
|
import traceback
|
|
8186
8344
|
traceback.print_exc()
|
|
8187
|
-
self.statusBar().showMessage(f"Error during paste operation: {e}"
|
|
8345
|
+
self.statusBar().showMessage(f"Error during paste operation: {e}")
|
|
8188
8346
|
self.statusBar().showMessage(f"Pasted {len(new_atoms)} atoms.", 2000)
|
|
8189
8347
|
self.activate_select_mode()
|
|
8190
8348
|
|
|
@@ -8207,13 +8365,13 @@ class MainWindow(QMainWindow):
|
|
|
8207
8365
|
self.push_undo_state()
|
|
8208
8366
|
self.statusBar().showMessage(f"Removed {len(hydrogen_atoms)} hydrogen atoms.", 2000)
|
|
8209
8367
|
else:
|
|
8210
|
-
self.statusBar().showMessage("Failed to remove hydrogen atoms."
|
|
8368
|
+
self.statusBar().showMessage("Failed to remove hydrogen atoms.")
|
|
8211
8369
|
|
|
8212
8370
|
except Exception as e:
|
|
8213
8371
|
print(f"Error during hydrogen removal: {e}")
|
|
8214
8372
|
import traceback
|
|
8215
8373
|
traceback.print_exc()
|
|
8216
|
-
self.statusBar().showMessage(f"Error removing hydrogen atoms: {e}"
|
|
8374
|
+
self.statusBar().showMessage(f"Error removing hydrogen atoms: {e}")
|
|
8217
8375
|
|
|
8218
8376
|
def update_edit_menu_actions(self):
|
|
8219
8377
|
"""選択状態やクリップボードの状態に応じて編集メニューを更新"""
|
|
@@ -8245,7 +8403,7 @@ class MainWindow(QMainWindow):
|
|
|
8245
8403
|
return
|
|
8246
8404
|
|
|
8247
8405
|
mol = self.data.to_rdkit_mol(use_2d_stereo=False)
|
|
8248
|
-
|
|
8406
|
+
|
|
8249
8407
|
# 分子オブジェクトが作成できない場合でも化学的問題をチェック
|
|
8250
8408
|
if not mol or mol.GetNumAtoms() == 0:
|
|
8251
8409
|
# RDKitでの変換に失敗した場合は、独自の化学的問題チェックを実行
|
|
@@ -8269,7 +8427,7 @@ class MainWindow(QMainWindow):
|
|
|
8269
8427
|
self.statusBar().showMessage(f"Error: {len(problems)} chemistry problem(s) found.")
|
|
8270
8428
|
# 既存の選択状態をクリア
|
|
8271
8429
|
self.scene.clearSelection()
|
|
8272
|
-
|
|
8430
|
+
|
|
8273
8431
|
# 問題のある原子に赤枠フラグを立てる
|
|
8274
8432
|
for prob in problems:
|
|
8275
8433
|
atom_idx = prob.GetAtomIdx()
|
|
@@ -8281,7 +8439,7 @@ class MainWindow(QMainWindow):
|
|
|
8281
8439
|
item = self.data.atoms[original_id]['item']
|
|
8282
8440
|
item.has_problem = True
|
|
8283
8441
|
item.update()
|
|
8284
|
-
|
|
8442
|
+
|
|
8285
8443
|
self.view_2d.setFocus()
|
|
8286
8444
|
return
|
|
8287
8445
|
|
|
@@ -8360,7 +8518,12 @@ class MainWindow(QMainWindow):
|
|
|
8360
8518
|
)
|
|
8361
8519
|
text_actor.GetTextProperty().SetOpacity(1)
|
|
8362
8520
|
self.plotter.render()
|
|
8363
|
-
|
|
8521
|
+
# Emit skip flag so the worker can ignore sanitization errors if user requested
|
|
8522
|
+
self.start_calculation.emit(mol_block, False)
|
|
8523
|
+
|
|
8524
|
+
# 状態をUndo履歴に保存
|
|
8525
|
+
self.push_undo_state()
|
|
8526
|
+
self.update_chiral_labels()
|
|
8364
8527
|
|
|
8365
8528
|
self.view_2d.setFocus()
|
|
8366
8529
|
|
|
@@ -8427,33 +8590,91 @@ class MainWindow(QMainWindow):
|
|
|
8427
8590
|
self.statusBar().showMessage("No 3D molecule to optimize.")
|
|
8428
8591
|
return
|
|
8429
8592
|
|
|
8593
|
+
# If a prior chemical/sanitization check was attempted and failed, do not run optimization
|
|
8594
|
+
if getattr(self, 'chem_check_tried', False) and getattr(self, 'chem_check_failed', False):
|
|
8595
|
+
self.statusBar().showMessage("3D optimization disabled: molecule failed chemical sanitization.")
|
|
8596
|
+
# Ensure the Optimize 3D button is disabled to reflect this
|
|
8597
|
+
if hasattr(self, 'optimize_3d_button'):
|
|
8598
|
+
try:
|
|
8599
|
+
self.optimize_3d_button.setEnabled(False)
|
|
8600
|
+
except Exception:
|
|
8601
|
+
pass
|
|
8602
|
+
return
|
|
8603
|
+
|
|
8430
8604
|
self.statusBar().showMessage("Optimizing 3D structure...")
|
|
8431
8605
|
QApplication.processEvents() # UIの更新を確実に行う
|
|
8432
8606
|
|
|
8433
8607
|
try:
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
AllChem.UFFOptimizeMolecule(self.current_mol)
|
|
8440
|
-
except Exception as e:
|
|
8441
|
-
self.statusBar().showMessage(f"3D optimization failed: {e}")
|
|
8608
|
+
method = getattr(self, 'optimization_method', 'MMFF_RDKIT')
|
|
8609
|
+
method = method.upper() if method else 'MMFF_RDKIT'
|
|
8610
|
+
# 事前チェック:コンフォーマがあるか
|
|
8611
|
+
if self.current_mol.GetNumConformers() == 0:
|
|
8612
|
+
self.statusBar().showMessage("No conformer found: cannot optimize. Embed molecule first.")
|
|
8442
8613
|
return
|
|
8614
|
+
if method == 'MMFF_RDKIT':
|
|
8615
|
+
try:
|
|
8616
|
+
res = AllChem.MMFFOptimizeMolecule(self.current_mol, maxIters=2000)
|
|
8617
|
+
if res != 0:
|
|
8618
|
+
# 非収束や何らかの問題が起きた可能性 -> ForceField API で詳細に試す
|
|
8619
|
+
try:
|
|
8620
|
+
mmff_props = AllChem.MMFFGetMoleculeProperties(self.current_mol)
|
|
8621
|
+
ff = AllChem.MMFFGetMoleculeForceField(self.current_mol, mmff_props, confId=0)
|
|
8622
|
+
ff_ret = ff.Minimize(maxIts=2000)
|
|
8623
|
+
if ff_ret != 0:
|
|
8624
|
+
self.statusBar().showMessage(f"MMFF minimize returned non-zero status: {ff_ret}")
|
|
8625
|
+
return
|
|
8626
|
+
except Exception as e:
|
|
8627
|
+
self.statusBar().showMessage(f"MMFF parameterization/minimize failed: {e}")
|
|
8628
|
+
return
|
|
8629
|
+
except Exception as e:
|
|
8630
|
+
self.statusBar().showMessage(f"MMFF (RDKit) optimization error: {e}")
|
|
8631
|
+
return
|
|
8632
|
+
elif method == 'UFF_RDKIT':
|
|
8633
|
+
try:
|
|
8634
|
+
res = AllChem.UFFOptimizeMolecule(self.current_mol, maxIters=2000)
|
|
8635
|
+
if res != 0:
|
|
8636
|
+
try:
|
|
8637
|
+
ff = AllChem.UFFGetMoleculeForceField(self.current_mol, confId=0)
|
|
8638
|
+
ff_ret = ff.Minimize(maxIts=2000)
|
|
8639
|
+
if ff_ret != 0:
|
|
8640
|
+
self.statusBar().showMessage(f"UFF minimize returned non-zero status: {ff_ret}")
|
|
8641
|
+
return
|
|
8642
|
+
except Exception as e:
|
|
8643
|
+
self.statusBar().showMessage(f"UFF parameterization/minimize failed: {e}")
|
|
8644
|
+
return
|
|
8645
|
+
except Exception as e:
|
|
8646
|
+
self.statusBar().showMessage(f"UFF (RDKit) optimization error: {e}")
|
|
8647
|
+
return
|
|
8648
|
+
else:
|
|
8649
|
+
self.statusBar().showMessage("Selected optimization method is not available. Use MMFF (RDKit) or UFF (RDKit).")
|
|
8650
|
+
return
|
|
8651
|
+
except Exception as e:
|
|
8652
|
+
self.statusBar().showMessage(f"3D optimization error: {e}")
|
|
8443
8653
|
|
|
8444
8654
|
# 最適化後の構造で3Dビューを再描画
|
|
8445
8655
|
try:
|
|
8446
8656
|
# 3D最適化後は3D座標から立体化学を再計算(2回目以降は3D優先)
|
|
8447
8657
|
if self.current_mol.GetNumConformers() > 0:
|
|
8448
8658
|
Chem.AssignAtomChiralTagsFromStructure(self.current_mol, confId=0)
|
|
8449
|
-
|
|
8450
8659
|
self.update_chiral_labels() # キラル中心のラベルも更新
|
|
8451
8660
|
except Exception:
|
|
8452
8661
|
pass
|
|
8453
8662
|
|
|
8454
8663
|
self.draw_molecule_3d(self.current_mol)
|
|
8455
8664
|
|
|
8456
|
-
|
|
8665
|
+
# Show which method was used in the status bar (prefer human-readable label)
|
|
8666
|
+
try:
|
|
8667
|
+
used_method = getattr(self, 'optimization_method', None)
|
|
8668
|
+
used_label = None
|
|
8669
|
+
if used_method:
|
|
8670
|
+
used_label = (getattr(self, 'opt3d_method_labels', {}) or {}).get(used_method.upper(), used_method)
|
|
8671
|
+
except Exception:
|
|
8672
|
+
used_label = None
|
|
8673
|
+
|
|
8674
|
+
if used_label:
|
|
8675
|
+
self.statusBar().showMessage(f"3D structure optimization successful. Method: {used_label}")
|
|
8676
|
+
else:
|
|
8677
|
+
self.statusBar().showMessage("3D structure optimization successful.")
|
|
8457
8678
|
self.push_undo_state() # Undo履歴に保存
|
|
8458
8679
|
self.view_2d.setFocus()
|
|
8459
8680
|
|
|
@@ -9253,13 +9474,13 @@ class MainWindow(QMainWindow):
|
|
|
9253
9474
|
self.update_window_title()
|
|
9254
9475
|
|
|
9255
9476
|
except FileNotFoundError:
|
|
9256
|
-
self.statusBar().showMessage(f"File not found: {file_path}"
|
|
9477
|
+
self.statusBar().showMessage(f"File not found: {file_path}")
|
|
9257
9478
|
self.restore_ui_for_editing()
|
|
9258
9479
|
except ValueError as e:
|
|
9259
|
-
self.statusBar().showMessage(f"Invalid 3D MOL file: {e}"
|
|
9480
|
+
self.statusBar().showMessage(f"Invalid 3D MOL file: {e}")
|
|
9260
9481
|
self.restore_ui_for_editing()
|
|
9261
9482
|
except Exception as e:
|
|
9262
|
-
self.statusBar().showMessage(f"Error loading 3D file: {e}"
|
|
9483
|
+
self.statusBar().showMessage(f"Error loading 3D file: {e}")
|
|
9263
9484
|
self.restore_ui_for_editing()
|
|
9264
9485
|
import traceback
|
|
9265
9486
|
traceback.print_exc()
|
|
@@ -9310,13 +9531,13 @@ class MainWindow(QMainWindow):
|
|
|
9310
9531
|
self.update_window_title()
|
|
9311
9532
|
|
|
9312
9533
|
except FileNotFoundError:
|
|
9313
|
-
self.statusBar().showMessage(f"File not found: {file_path}"
|
|
9534
|
+
self.statusBar().showMessage(f"File not found: {file_path}")
|
|
9314
9535
|
self.restore_ui_for_editing()
|
|
9315
9536
|
except ValueError as e:
|
|
9316
|
-
self.statusBar().showMessage(f"Invalid XYZ file: {e}"
|
|
9537
|
+
self.statusBar().showMessage(f"Invalid XYZ file: {e}")
|
|
9317
9538
|
self.restore_ui_for_editing()
|
|
9318
9539
|
except Exception as e:
|
|
9319
|
-
self.statusBar().showMessage(f"Error loading XYZ file: {e}"
|
|
9540
|
+
self.statusBar().showMessage(f"Error loading XYZ file: {e}")
|
|
9320
9541
|
self.restore_ui_for_editing()
|
|
9321
9542
|
import traceback
|
|
9322
9543
|
traceback.print_exc()
|
|
@@ -9374,13 +9595,17 @@ class MainWindow(QMainWindow):
|
|
|
9374
9595
|
try:
|
|
9375
9596
|
# RDKitで認識される元素かどうかをチェック
|
|
9376
9597
|
test_atom = Chem.Atom(symbol)
|
|
9377
|
-
except:
|
|
9598
|
+
except Exception:
|
|
9378
9599
|
# 認識されない場合、最初の文字を大文字にして再試行
|
|
9379
9600
|
symbol = symbol.capitalize()
|
|
9380
9601
|
try:
|
|
9381
9602
|
test_atom = Chem.Atom(symbol)
|
|
9382
|
-
except:
|
|
9383
|
-
|
|
9603
|
+
except Exception:
|
|
9604
|
+
# If user requested to skip chemistry checks, coerce unknown symbols to C
|
|
9605
|
+
if self.settings.get('skip_chemistry_checks', False):
|
|
9606
|
+
symbol = 'C'
|
|
9607
|
+
else:
|
|
9608
|
+
raise ValueError(f"Unrecognized element symbol: {parts[0]} at line {i+3}")
|
|
9384
9609
|
|
|
9385
9610
|
try:
|
|
9386
9611
|
x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
|
|
@@ -9411,13 +9636,14 @@ class MainWindow(QMainWindow):
|
|
|
9411
9636
|
# 結合を推定(距離ベース)
|
|
9412
9637
|
self.estimate_bonds_from_distances(mol)
|
|
9413
9638
|
|
|
9414
|
-
|
|
9639
|
+
# 分子を最終化
|
|
9415
9640
|
try:
|
|
9416
9641
|
mol = mol.GetMol()
|
|
9417
9642
|
# 基本的な妥当性チェック
|
|
9418
9643
|
if mol is None:
|
|
9419
9644
|
raise ValueError("Failed to create valid molecule object")
|
|
9420
|
-
|
|
9645
|
+
# Centralized chemical/sanitization handling
|
|
9646
|
+
self._apply_chem_check_and_set_flags(mol, source_desc='XYZ')
|
|
9421
9647
|
except Exception as e:
|
|
9422
9648
|
# 化学的に不正な構造でも表示は可能にする
|
|
9423
9649
|
mol = mol.GetMol()
|
|
@@ -9506,9 +9732,10 @@ class MainWindow(QMainWindow):
|
|
|
9506
9732
|
if not self.data.atoms and not self.current_mol:
|
|
9507
9733
|
self.statusBar().showMessage("Error: Nothing to save.")
|
|
9508
9734
|
return
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9735
|
+
# 非ネイティブ形式(.mol, .sdf, .xyz など)は上書き保存せず、必ず「名前を付けて保存」にする
|
|
9736
|
+
native_exts = ['.pmeprj', '.pmeraw']
|
|
9737
|
+
if self.current_file_path and any(self.current_file_path.lower().endswith(ext) for ext in native_exts):
|
|
9738
|
+
# 既存のPMEPRJ/PMERAWファイルの場合は上書き保存
|
|
9512
9739
|
try:
|
|
9513
9740
|
if self.current_file_path.lower().endswith('.pmeraw'):
|
|
9514
9741
|
# 既存のPMERAWファイルの場合はPMERAW形式で保存
|
|
@@ -9536,7 +9763,7 @@ class MainWindow(QMainWindow):
|
|
|
9536
9763
|
import traceback
|
|
9537
9764
|
traceback.print_exc()
|
|
9538
9765
|
else:
|
|
9539
|
-
#
|
|
9766
|
+
# MOL/SDF/XYZなどは上書き保存せず、必ず「名前を付けて保存」にする
|
|
9540
9767
|
self.save_project_as()
|
|
9541
9768
|
|
|
9542
9769
|
def save_project_as(self):
|
|
@@ -10564,7 +10791,7 @@ class MainWindow(QMainWindow):
|
|
|
10564
10791
|
|
|
10565
10792
|
def export_2d_png(self):
|
|
10566
10793
|
if not self.data.atoms:
|
|
10567
|
-
self.statusBar().showMessage("Nothing to export."
|
|
10794
|
+
self.statusBar().showMessage("Nothing to export.")
|
|
10568
10795
|
return
|
|
10569
10796
|
|
|
10570
10797
|
options = QFileDialog.Option.DontUseNativeDialog
|
|
@@ -10605,7 +10832,7 @@ class MainWindow(QMainWindow):
|
|
|
10605
10832
|
molecule_bounds = molecule_bounds.united(item.sceneBoundingRect())
|
|
10606
10833
|
|
|
10607
10834
|
if molecule_bounds.isEmpty() or not molecule_bounds.isValid():
|
|
10608
|
-
self.statusBar().showMessage("Error: Could not determine molecule bounds for export."
|
|
10835
|
+
self.statusBar().showMessage("Error: Could not determine molecule bounds for export.")
|
|
10609
10836
|
return
|
|
10610
10837
|
|
|
10611
10838
|
if is_transparent:
|
|
@@ -10619,7 +10846,7 @@ class MainWindow(QMainWindow):
|
|
|
10619
10846
|
h = max(1, int(math.ceil(rect_to_render.height())))
|
|
10620
10847
|
|
|
10621
10848
|
if w <= 0 or h <= 0:
|
|
10622
|
-
self.statusBar().showMessage("Error: Invalid image size calculated."
|
|
10849
|
+
self.statusBar().showMessage("Error: Invalid image size calculated.")
|
|
10623
10850
|
return
|
|
10624
10851
|
|
|
10625
10852
|
image = QImage(w, h, QImage.Format.Format_ARGB32_Premultiplied)
|
|
@@ -10631,7 +10858,7 @@ class MainWindow(QMainWindow):
|
|
|
10631
10858
|
painter = QPainter()
|
|
10632
10859
|
ok = painter.begin(image)
|
|
10633
10860
|
if not ok or not painter.isActive():
|
|
10634
|
-
self.statusBar().showMessage("Failed to start QPainter for image rendering."
|
|
10861
|
+
self.statusBar().showMessage("Failed to start QPainter for image rendering.")
|
|
10635
10862
|
return
|
|
10636
10863
|
|
|
10637
10864
|
try:
|
|
@@ -10644,12 +10871,12 @@ class MainWindow(QMainWindow):
|
|
|
10644
10871
|
|
|
10645
10872
|
saved = image.save(filePath, "PNG")
|
|
10646
10873
|
if saved:
|
|
10647
|
-
self.statusBar().showMessage(f"2D view exported to {filePath}"
|
|
10874
|
+
self.statusBar().showMessage(f"2D view exported to {filePath}")
|
|
10648
10875
|
else:
|
|
10649
|
-
self.statusBar().showMessage(f"Failed to save image. Check file path or permissions."
|
|
10876
|
+
self.statusBar().showMessage(f"Failed to save image. Check file path or permissions.")
|
|
10650
10877
|
|
|
10651
10878
|
except Exception as e:
|
|
10652
|
-
self.statusBar().showMessage(f"An unexpected error occurred during 2D export: {e}"
|
|
10879
|
+
self.statusBar().showMessage(f"An unexpected error occurred during 2D export: {e}")
|
|
10653
10880
|
|
|
10654
10881
|
finally:
|
|
10655
10882
|
for item, was_visible in items_to_restore.items():
|
|
@@ -10686,7 +10913,7 @@ class MainWindow(QMainWindow):
|
|
|
10686
10913
|
self.plotter.screenshot(filePath, transparent_background=is_transparent)
|
|
10687
10914
|
self.statusBar().showMessage(f"3D view exported to {filePath}", 3000)
|
|
10688
10915
|
except Exception as e:
|
|
10689
|
-
self.statusBar().showMessage(f"Error exporting 3D PNG: {e}"
|
|
10916
|
+
self.statusBar().showMessage(f"Error exporting 3D PNG: {e}")
|
|
10690
10917
|
|
|
10691
10918
|
|
|
10692
10919
|
def open_periodic_table_dialog(self):
|
|
@@ -11071,6 +11298,8 @@ class MainWindow(QMainWindow):
|
|
|
11071
11298
|
begin_color_rgb = [int(c * 255) for c in begin_color]
|
|
11072
11299
|
end_color_rgb = [int(c * 255) for c in end_color]
|
|
11073
11300
|
|
|
11301
|
+
# UI応答性維持のためイベント処理
|
|
11302
|
+
QApplication.processEvents()
|
|
11074
11303
|
if bt == Chem.rdchem.BondType.SINGLE or bt == Chem.rdchem.BondType.AROMATIC:
|
|
11075
11304
|
if self.current_3d_style == 'ball_and_stick':
|
|
11076
11305
|
# Ball and stickは全結合をまとめて処理(高速化)
|
|
@@ -11726,6 +11955,46 @@ class MainWindow(QMainWindow):
|
|
|
11726
11955
|
# 調査の結果、'style' プロパティへの代入が正しい設定方法と判明
|
|
11727
11956
|
self.plotter.interactor.SetInteractorStyle(style)
|
|
11728
11957
|
self.plotter.interactor.Initialize()
|
|
11958
|
+
|
|
11959
|
+
def _apply_chem_check_and_set_flags(self, mol, source_desc=None):
|
|
11960
|
+
"""Central helper to apply chemical sanitization (or skip it) and set
|
|
11961
|
+
chem_check_tried / chem_check_failed flags consistently.
|
|
11962
|
+
|
|
11963
|
+
When sanitization fails, a warning is shown and the Optimize 3D button
|
|
11964
|
+
is disabled. If the user setting 'skip_chemistry_checks' is True, no
|
|
11965
|
+
sanitization is attempted and both flags remain False.
|
|
11966
|
+
"""
|
|
11967
|
+
try:
|
|
11968
|
+
self.chem_check_tried = False
|
|
11969
|
+
self.chem_check_failed = False
|
|
11970
|
+
except Exception:
|
|
11971
|
+
# Ensure attributes exist even if called very early
|
|
11972
|
+
self.chem_check_tried = False
|
|
11973
|
+
self.chem_check_failed = False
|
|
11974
|
+
|
|
11975
|
+
if self.settings.get('skip_chemistry_checks', False):
|
|
11976
|
+
# User asked to skip chemistry checks entirely
|
|
11977
|
+
return
|
|
11978
|
+
|
|
11979
|
+
try:
|
|
11980
|
+
Chem.SanitizeMol(mol)
|
|
11981
|
+
self.chem_check_tried = True
|
|
11982
|
+
self.chem_check_failed = False
|
|
11983
|
+
except Exception:
|
|
11984
|
+
# Mark that we tried sanitization and it failed
|
|
11985
|
+
self.chem_check_tried = True
|
|
11986
|
+
self.chem_check_failed = True
|
|
11987
|
+
try:
|
|
11988
|
+
desc = f" ({source_desc})" if source_desc else ''
|
|
11989
|
+
self.statusBar().showMessage(f"Molecule sanitization failed{desc}; file may be malformed.")
|
|
11990
|
+
except Exception:
|
|
11991
|
+
pass
|
|
11992
|
+
# Disable 3D optimization UI to prevent running on invalid molecules
|
|
11993
|
+
if hasattr(self, 'optimize_3d_button'):
|
|
11994
|
+
try:
|
|
11995
|
+
self.optimize_3d_button.setEnabled(False)
|
|
11996
|
+
except Exception:
|
|
11997
|
+
pass
|
|
11729
11998
|
|
|
11730
11999
|
def load_mol_file_for_3d_viewing(self, file_path=None):
|
|
11731
12000
|
"""MOL/SDFファイルを3Dビューアーで開く"""
|
|
@@ -11755,13 +12024,28 @@ class MainWindow(QMainWindow):
|
|
|
11755
12024
|
if mol.GetNumConformers() == 0:
|
|
11756
12025
|
self.statusBar().showMessage("No 3D coordinates found. Converting to 3D...")
|
|
11757
12026
|
try:
|
|
11758
|
-
|
|
11759
|
-
|
|
12027
|
+
try:
|
|
12028
|
+
AllChem.EmbedMolecule(mol)
|
|
12029
|
+
# 最適化は実行しない
|
|
12030
|
+
# 3D変換直後にUndoスタックに積む
|
|
12031
|
+
self.current_mol = mol
|
|
12032
|
+
self.push_undo_state()
|
|
12033
|
+
except Exception as e_embed:
|
|
12034
|
+
# If skipping chemistry checks, allow molecule to be displayed without 3D embedding
|
|
12035
|
+
if self.settings.get('skip_chemistry_checks', False):
|
|
12036
|
+
self.statusBar().showMessage("Warning: failed to generate 3D coordinates but skip_chemistry_checks is enabled; continuing.")
|
|
12037
|
+
# Keep mol as-is (may lack conformer); downstream code checks for conformers
|
|
12038
|
+
else:
|
|
12039
|
+
raise
|
|
11760
12040
|
except:
|
|
11761
12041
|
self.statusBar().showMessage("Failed to generate 3D coordinates")
|
|
11762
12042
|
return
|
|
11763
12043
|
|
|
11764
12044
|
# 3Dビューアーに表示
|
|
12045
|
+
# Centralized chemical/sanitization handling
|
|
12046
|
+
# Ensure the skip_chemistry_checks setting is respected and flags are set
|
|
12047
|
+
self._apply_chem_check_and_set_flags(mol, source_desc='MOL/SDF')
|
|
12048
|
+
|
|
11765
12049
|
self.current_mol = mol
|
|
11766
12050
|
self.draw_molecule_3d(mol)
|
|
11767
12051
|
|
|
@@ -11888,7 +12172,14 @@ class MainWindow(QMainWindow):
|
|
|
11888
12172
|
|
|
11889
12173
|
for action_name in basic_3d_actions:
|
|
11890
12174
|
if hasattr(self, action_name):
|
|
11891
|
-
|
|
12175
|
+
# If enabling globally but chemical sanitization failed earlier, keep Optimize 3D disabled
|
|
12176
|
+
if enabled and action_name == 'optimize_3d_button' and getattr(self, 'chem_check_tried', False) and getattr(self, 'chem_check_failed', False):
|
|
12177
|
+
try:
|
|
12178
|
+
getattr(self, action_name).setEnabled(False)
|
|
12179
|
+
except Exception:
|
|
12180
|
+
pass
|
|
12181
|
+
else:
|
|
12182
|
+
getattr(self, action_name).setEnabled(enabled)
|
|
11892
12183
|
|
|
11893
12184
|
# 3D Selectボタンは常に有効にする
|
|
11894
12185
|
if hasattr(self, 'measurement_action'):
|
|
@@ -13884,14 +14175,14 @@ class DihedralDialog(Dialog3DPickingMixin, QDialog):
|
|
|
13884
14175
|
QMessageBox.warning(self, "Invalid Input", "Please enter a valid number.")
|
|
13885
14176
|
return
|
|
13886
14177
|
|
|
13887
|
-
# Undo状態を保存
|
|
13888
|
-
self.main_window.push_undo_state()
|
|
13889
|
-
|
|
13890
14178
|
# Apply the dihedral angle change
|
|
13891
14179
|
self.adjust_dihedral(new_dihedral)
|
|
13892
14180
|
|
|
13893
14181
|
# キラルラベルを更新
|
|
13894
14182
|
self.main_window.update_chiral_labels()
|
|
14183
|
+
|
|
14184
|
+
# Undo状態を保存
|
|
14185
|
+
self.main_window.push_undo_state()
|
|
13895
14186
|
|
|
13896
14187
|
def adjust_dihedral(self, new_dihedral_deg):
|
|
13897
14188
|
"""二面角を調整(改善されたアルゴリズム)"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moleditpy
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.2
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|