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,302 @@
|
|
|
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 rdkit import Chem
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from .constants import ANGSTROM_PER_PIXEL
|
|
18
|
+
except Exception:
|
|
19
|
+
from modules.constants import ANGSTROM_PER_PIXEL
|
|
20
|
+
|
|
21
|
+
class MolecularData:
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.atoms = {}
|
|
24
|
+
self.bonds = {}
|
|
25
|
+
self._next_atom_id = 0
|
|
26
|
+
self.adjacency_list = {}
|
|
27
|
+
|
|
28
|
+
def add_atom(self, symbol, pos, charge=0, radical=0):
|
|
29
|
+
atom_id = self._next_atom_id
|
|
30
|
+
self.atoms[atom_id] = {'symbol': symbol, 'pos': pos, 'item': None, 'charge': charge, 'radical': radical}
|
|
31
|
+
self.adjacency_list[atom_id] = []
|
|
32
|
+
self._next_atom_id += 1
|
|
33
|
+
return atom_id
|
|
34
|
+
|
|
35
|
+
def add_bond(self, id1, id2, order=1, stereo=0):
|
|
36
|
+
# 立体結合の場合、IDの順序は方向性を意味するため、ソートしない。
|
|
37
|
+
# 非立体結合の場合は、キーを正規化するためにソートする。
|
|
38
|
+
if stereo == 0:
|
|
39
|
+
if id1 > id2: id1, id2 = id2, id1
|
|
40
|
+
|
|
41
|
+
bond_data = {'order': order, 'stereo': stereo, 'item': None}
|
|
42
|
+
|
|
43
|
+
# 逆方向のキーも考慮して、新規結合かどうかをチェック
|
|
44
|
+
is_new_bond = (id1, id2) not in self.bonds and (id2, id1) not in self.bonds
|
|
45
|
+
if is_new_bond:
|
|
46
|
+
if id1 in self.adjacency_list and id2 in self.adjacency_list:
|
|
47
|
+
self.adjacency_list[id1].append(id2)
|
|
48
|
+
self.adjacency_list[id2].append(id1)
|
|
49
|
+
|
|
50
|
+
if (id1, id2) in self.bonds:
|
|
51
|
+
self.bonds[(id1, id2)].update(bond_data)
|
|
52
|
+
return (id1, id2), 'updated'
|
|
53
|
+
else:
|
|
54
|
+
self.bonds[(id1, id2)] = bond_data
|
|
55
|
+
return (id1, id2), 'created'
|
|
56
|
+
|
|
57
|
+
def remove_atom(self, atom_id):
|
|
58
|
+
if atom_id in self.atoms:
|
|
59
|
+
try:
|
|
60
|
+
# Safely get neighbors before deleting the atom's own entry
|
|
61
|
+
neighbors = self.adjacency_list.get(atom_id, [])
|
|
62
|
+
for neighbor_id in neighbors:
|
|
63
|
+
if neighbor_id in self.adjacency_list and atom_id in self.adjacency_list[neighbor_id]:
|
|
64
|
+
self.adjacency_list[neighbor_id].remove(atom_id)
|
|
65
|
+
|
|
66
|
+
# Now, safely delete the atom's own entry from the adjacency list
|
|
67
|
+
if atom_id in self.adjacency_list:
|
|
68
|
+
del self.adjacency_list[atom_id]
|
|
69
|
+
|
|
70
|
+
del self.atoms[atom_id]
|
|
71
|
+
|
|
72
|
+
# Remove bonds involving this atom
|
|
73
|
+
bonds_to_remove = [key for key in self.bonds if atom_id in key]
|
|
74
|
+
for key in bonds_to_remove:
|
|
75
|
+
del self.bonds[key]
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Error removing atom {atom_id}: {e}")
|
|
79
|
+
|
|
80
|
+
traceback.print_exc()
|
|
81
|
+
|
|
82
|
+
def remove_bond(self, id1, id2):
|
|
83
|
+
try:
|
|
84
|
+
# 方向性のある立体結合(順方向/逆方向)と、正規化された非立体結合のキーを探す
|
|
85
|
+
key_to_remove = None
|
|
86
|
+
if (id1, id2) in self.bonds:
|
|
87
|
+
key_to_remove = (id1, id2)
|
|
88
|
+
elif (id2, id1) in self.bonds:
|
|
89
|
+
key_to_remove = (id2, id1)
|
|
90
|
+
|
|
91
|
+
if key_to_remove:
|
|
92
|
+
if id1 in self.adjacency_list and id2 in self.adjacency_list[id1]:
|
|
93
|
+
self.adjacency_list[id1].remove(id2)
|
|
94
|
+
if id2 in self.adjacency_list and id1 in self.adjacency_list[id2]:
|
|
95
|
+
self.adjacency_list[id2].remove(id1)
|
|
96
|
+
del self.bonds[key_to_remove]
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"Error removing bond {id1}-{id2}: {e}")
|
|
100
|
+
|
|
101
|
+
traceback.print_exc()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def to_rdkit_mol(self, use_2d_stereo=True):
|
|
105
|
+
"""
|
|
106
|
+
use_2d_stereo: Trueなら2D座標からE/Zを推定(従来通り)。FalseならE/Zラベル優先、ラベルがない場合のみ2D座標推定。
|
|
107
|
+
3D変換時はuse_2d_stereo=Falseで呼び出すこと。
|
|
108
|
+
"""
|
|
109
|
+
if not self.atoms:
|
|
110
|
+
return None
|
|
111
|
+
mol = Chem.RWMol()
|
|
112
|
+
|
|
113
|
+
# --- Step 1: atoms ---
|
|
114
|
+
atom_id_to_idx_map = {}
|
|
115
|
+
for atom_id, data in self.atoms.items():
|
|
116
|
+
atom = Chem.Atom(data['symbol'])
|
|
117
|
+
atom.SetFormalCharge(data.get('charge', 0))
|
|
118
|
+
atom.SetNumRadicalElectrons(data.get('radical', 0))
|
|
119
|
+
atom.SetIntProp("_original_atom_id", atom_id)
|
|
120
|
+
idx = mol.AddAtom(atom)
|
|
121
|
+
atom_id_to_idx_map[atom_id] = idx
|
|
122
|
+
|
|
123
|
+
# --- Step 2: bonds & stereo info保存(ラベル情報はここで保持) ---
|
|
124
|
+
bond_stereo_info = {} # bond_idx -> {'type': int, 'atom_ids': (id1,id2), 'bond_data': bond_data}
|
|
125
|
+
for (id1, id2), bond_data in self.bonds.items():
|
|
126
|
+
if id1 not in atom_id_to_idx_map or id2 not in atom_id_to_idx_map:
|
|
127
|
+
continue
|
|
128
|
+
idx1, idx2 = atom_id_to_idx_map[id1], atom_id_to_idx_map[id2]
|
|
129
|
+
|
|
130
|
+
order_val = float(bond_data['order'])
|
|
131
|
+
order = {1.0: Chem.BondType.SINGLE, 1.5: Chem.BondType.AROMATIC,
|
|
132
|
+
2.0: Chem.BondType.DOUBLE, 3.0: Chem.BondType.TRIPLE}.get(order_val, Chem.BondType.SINGLE)
|
|
133
|
+
|
|
134
|
+
bond_idx = mol.AddBond(idx1, idx2, order) - 1
|
|
135
|
+
|
|
136
|
+
# stereoラベルがあれば、bond_idxに対して詳細を保持(あとで使う)
|
|
137
|
+
if 'stereo' in bond_data and bond_data['stereo'] in [1, 2, 3, 4]:
|
|
138
|
+
bond_stereo_info[bond_idx] = {
|
|
139
|
+
'type': int(bond_data['stereo']),
|
|
140
|
+
'atom_ids': (id1, id2),
|
|
141
|
+
'bond_data': bond_data
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# --- Step 3: sanitize ---
|
|
145
|
+
final_mol = mol.GetMol()
|
|
146
|
+
try:
|
|
147
|
+
Chem.SanitizeMol(final_mol)
|
|
148
|
+
except Exception:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# --- Step 4: add 2D conformer ---
|
|
152
|
+
# Convert from scene pixels to angstroms when creating RDKit conformer.
|
|
153
|
+
conf = Chem.Conformer(final_mol.GetNumAtoms())
|
|
154
|
+
conf.Set3D(False)
|
|
155
|
+
for atom_id, data in self.atoms.items():
|
|
156
|
+
if atom_id in atom_id_to_idx_map:
|
|
157
|
+
idx = atom_id_to_idx_map[atom_id]
|
|
158
|
+
pos = data.get('pos')
|
|
159
|
+
if pos:
|
|
160
|
+
ax = pos.x() * ANGSTROM_PER_PIXEL
|
|
161
|
+
ay = -pos.y() * ANGSTROM_PER_PIXEL # Y座標を反転(画面座標系→化学座標系)
|
|
162
|
+
conf.SetAtomPosition(idx, (ax, ay, 0.0))
|
|
163
|
+
final_mol.AddConformer(conf)
|
|
164
|
+
|
|
165
|
+
# --- Step 5: E/Zラベル優先の立体設定 ---
|
|
166
|
+
# まず、E/Zラベルがあるbondを記録
|
|
167
|
+
ez_labeled_bonds = set()
|
|
168
|
+
for bond_idx, info in bond_stereo_info.items():
|
|
169
|
+
if info['type'] in [3, 4]:
|
|
170
|
+
ez_labeled_bonds.add(bond_idx)
|
|
171
|
+
|
|
172
|
+
# 2D座標からE/Zを推定するのは、use_2d_stereo=True かつE/Zラベルがないbondのみ
|
|
173
|
+
if use_2d_stereo:
|
|
174
|
+
Chem.SetDoubleBondNeighborDirections(final_mol, final_mol.GetConformer(0))
|
|
175
|
+
else:
|
|
176
|
+
# 3D変換時: E/Zラベルがある場合は座標ベースの推定を完全に無効化
|
|
177
|
+
if ez_labeled_bonds:
|
|
178
|
+
# E/Zラベルがある場合は、すべての結合のBondDirをクリアして座標ベースの推定を無効化
|
|
179
|
+
for b in final_mol.GetBonds():
|
|
180
|
+
b.SetBondDir(Chem.BondDir.NONE)
|
|
181
|
+
else:
|
|
182
|
+
# E/Zラベルがない場合のみ座標ベースの推定を実行
|
|
183
|
+
Chem.SetDoubleBondNeighborDirections(final_mol, final_mol.GetConformer(0))
|
|
184
|
+
|
|
185
|
+
# ヘルパー: 重原子優先で近傍を選ぶ
|
|
186
|
+
def pick_preferred_neighbor(atom, exclude_idx):
|
|
187
|
+
for nbr in atom.GetNeighbors():
|
|
188
|
+
if nbr.GetIdx() == exclude_idx:
|
|
189
|
+
continue
|
|
190
|
+
if nbr.GetAtomicNum() > 1:
|
|
191
|
+
return nbr.GetIdx()
|
|
192
|
+
for nbr in atom.GetNeighbors():
|
|
193
|
+
if nbr.GetIdx() != exclude_idx:
|
|
194
|
+
return nbr.GetIdx()
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
# --- Step 6: ラベルベースで上書き(E/Z を最優先) ---
|
|
198
|
+
for bond_idx, info in bond_stereo_info.items():
|
|
199
|
+
stereo_type = info['type']
|
|
200
|
+
bond = final_mol.GetBondWithIdx(bond_idx)
|
|
201
|
+
|
|
202
|
+
# 単結合の wedge/dash ラベル(1/2)がある場合
|
|
203
|
+
if stereo_type in [1, 2]:
|
|
204
|
+
if stereo_type == 1:
|
|
205
|
+
bond.SetBondDir(Chem.BondDir.BEGINWEDGE)
|
|
206
|
+
elif stereo_type == 2:
|
|
207
|
+
bond.SetBondDir(Chem.BondDir.BEGINDASH)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# 二重結合の E/Z ラベル(3/4)
|
|
211
|
+
if stereo_type in [3, 4]:
|
|
212
|
+
if bond.GetBondType() != Chem.BondType.DOUBLE:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
begin_atom_idx = bond.GetBeginAtomIdx()
|
|
216
|
+
end_atom_idx = bond.GetEndAtomIdx()
|
|
217
|
+
|
|
218
|
+
bond_data = info.get('bond_data', {}) or {}
|
|
219
|
+
stereo_atoms_specified = bond_data.get('stereo_atoms')
|
|
220
|
+
|
|
221
|
+
if stereo_atoms_specified:
|
|
222
|
+
try:
|
|
223
|
+
a1_id, a2_id = stereo_atoms_specified
|
|
224
|
+
neigh1_idx = atom_id_to_idx_map.get(a1_id)
|
|
225
|
+
neigh2_idx = atom_id_to_idx_map.get(a2_id)
|
|
226
|
+
except Exception:
|
|
227
|
+
neigh1_idx = None
|
|
228
|
+
neigh2_idx = None
|
|
229
|
+
else:
|
|
230
|
+
neigh1_idx = pick_preferred_neighbor(final_mol.GetAtomWithIdx(begin_atom_idx), end_atom_idx)
|
|
231
|
+
neigh2_idx = pick_preferred_neighbor(final_mol.GetAtomWithIdx(end_atom_idx), begin_atom_idx)
|
|
232
|
+
|
|
233
|
+
if neigh1_idx is None or neigh2_idx is None:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
bond.SetStereoAtoms(neigh1_idx, neigh2_idx)
|
|
237
|
+
if stereo_type == 3:
|
|
238
|
+
bond.SetStereo(Chem.BondStereo.STEREOZ)
|
|
239
|
+
elif stereo_type == 4:
|
|
240
|
+
bond.SetStereo(Chem.BondStereo.STEREOE)
|
|
241
|
+
|
|
242
|
+
# 座標ベースでつけられた隣接単結合の BondDir(wedge/dash)がラベルと矛盾する可能性があるので消す
|
|
243
|
+
b1 = final_mol.GetBondBetweenAtoms(begin_atom_idx, neigh1_idx)
|
|
244
|
+
b2 = final_mol.GetBondBetweenAtoms(end_atom_idx, neigh2_idx)
|
|
245
|
+
if b1 is not None:
|
|
246
|
+
b1.SetBondDir(Chem.BondDir.NONE)
|
|
247
|
+
if b2 is not None:
|
|
248
|
+
b2.SetBondDir(Chem.BondDir.NONE)
|
|
249
|
+
|
|
250
|
+
# Step 7: 最終化(キャッシュ更新 + 立体割当の再実行)
|
|
251
|
+
final_mol.UpdatePropertyCache(strict=False)
|
|
252
|
+
|
|
253
|
+
# 3D変換時(use_2d_stereo=False)でE/Zラベルがある場合は、force=Trueで強制適用
|
|
254
|
+
if not use_2d_stereo and ez_labeled_bonds:
|
|
255
|
+
Chem.AssignStereochemistry(final_mol, cleanIt=False, force=True)
|
|
256
|
+
else:
|
|
257
|
+
Chem.AssignStereochemistry(final_mol, cleanIt=False, force=False)
|
|
258
|
+
return final_mol
|
|
259
|
+
|
|
260
|
+
def to_mol_block(self):
|
|
261
|
+
try:
|
|
262
|
+
mol = self.to_rdkit_mol()
|
|
263
|
+
if mol:
|
|
264
|
+
return Chem.MolToMolBlock(mol, includeStereo=True)
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
if not self.atoms: return None
|
|
268
|
+
atom_map = {old_id: new_id for new_id, old_id in enumerate(self.atoms.keys())}
|
|
269
|
+
num_atoms, num_bonds = len(self.atoms), len(self.bonds)
|
|
270
|
+
mol_block = "\n MoleditPy\n\n"
|
|
271
|
+
mol_block += f"{num_atoms:3d}{num_bonds:3d} 0 0 0 0 0 0 0 0999 V2000\n"
|
|
272
|
+
for old_id, atom in self.atoms.items():
|
|
273
|
+
# Convert scene pixel coordinates to angstroms when emitting MOL block
|
|
274
|
+
x_px = atom['item'].pos().x()
|
|
275
|
+
y_px = -atom['item'].pos().y()
|
|
276
|
+
x, y = x_px * ANGSTROM_PER_PIXEL, y_px * ANGSTROM_PER_PIXEL
|
|
277
|
+
z, symbol = 0.0, atom['symbol']
|
|
278
|
+
charge = atom.get('charge', 0)
|
|
279
|
+
|
|
280
|
+
chg_code = 0
|
|
281
|
+
if charge == 3: chg_code = 1
|
|
282
|
+
elif charge == 2: chg_code = 2
|
|
283
|
+
elif charge == 1: chg_code = 3
|
|
284
|
+
elif charge == -1: chg_code = 5
|
|
285
|
+
elif charge == -2: chg_code = 6
|
|
286
|
+
elif charge == -3: chg_code = 7
|
|
287
|
+
|
|
288
|
+
mol_block += f"{x:10.4f}{y:10.4f}{z:10.4f} {symbol:<3} 0 0 0{chg_code:3d} 0 0 0 0 0 0 0\n"
|
|
289
|
+
|
|
290
|
+
for (id1, id2), bond in self.bonds.items():
|
|
291
|
+
idx1, idx2, order = atom_map[id1] + 1, atom_map[id2] + 1, bond['order']
|
|
292
|
+
stereo_code = 0
|
|
293
|
+
bond_stereo = bond.get('stereo', 0)
|
|
294
|
+
if bond_stereo == 1:
|
|
295
|
+
stereo_code = 1
|
|
296
|
+
elif bond_stereo == 2:
|
|
297
|
+
stereo_code = 6
|
|
298
|
+
|
|
299
|
+
mol_block += f"{idx1:3d}{idx2:3d}{order:3d}{stereo_code:3d} 0 0 0\n"
|
|
300
|
+
|
|
301
|
+
mol_block += "M END\n"
|
|
302
|
+
return mol_block
|