MoleditPy-linux 2.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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