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