MoleditPy 3.0.0a3__tar.gz → 3.0.0a5__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-3.0.0a3 → moleditpy-3.0.0a5}/PKG-INFO +1 -1
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/pyproject.toml +1 -1
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/SOURCES.txt +4 -8
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/core/mol_geometry.py +90 -21
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/core/molecular_data.py +21 -13
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_interface.py +112 -21
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_manager.py +109 -31
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_manager_window.py +313 -313
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/about_dialog.py +2 -2
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/align_plane_dialog.py +20 -94
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/alignment_dialog.py +8 -7
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/angle_dialog.py +406 -516
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/app_state.py +172 -283
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/atom_item.py +39 -31
- moleditpy-3.0.0a5/src/moleditpy/ui/base_picking_dialog.py +119 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/bond_item.py +69 -89
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/bond_length_dialog.py +356 -456
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/calculation_worker.py +32 -18
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/color_settings_dialog.py +66 -35
- moleditpy-3.0.0a5/src/moleditpy/ui/compute_logic.py +625 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/constrained_optimization_dialog.py +26 -26
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/custom_interactor_style.py +83 -101
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/custom_qt_interactor.py +4 -4
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/dialog_3d_picking_mixin.py +39 -34
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/dialog_logic.py +121 -135
- moleditpy-3.0.0a5/src/moleditpy/ui/dihedral_dialog.py +393 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/edit_3d_logic.py +47 -48
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/edit_actions_logic.py +331 -170
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/export_logic.py +130 -165
- moleditpy-3.0.0a5/src/moleditpy/ui/geometry_base_dialog.py +111 -0
- moleditpy-3.0.0a5/src/moleditpy/ui/io_logic.py +835 -0
- moleditpy-3.0.0a5/src/moleditpy/ui/main_window.py +97 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/main_window_init.py +507 -521
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/mirror_dialog.py +3 -3
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/molecular_scene_handler.py +63 -42
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/molecule_scene.py +72 -33
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/move_group_dialog.py +123 -259
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/planarize_dialog.py +22 -65
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_dialog.py +33 -66
- {moleditpy-3.0.0a3/src/moleditpy/utils → moleditpy-3.0.0a5/src/moleditpy/ui/settings_tabs}/__init__.py +11 -11
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +6 -34
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +20 -75
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +18 -6
- moleditpy-3.0.0a5/src/moleditpy/ui/settings_tabs/settings_tab_base.py +63 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/string_importers.py +61 -49
- moleditpy-3.0.0a5/src/moleditpy/ui/translation_dialog.py +194 -0
- moleditpy-3.0.0a5/src/moleditpy/ui/ui_manager.py +567 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/user_template_dialog.py +30 -33
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/view_3d_logic.py +324 -229
- {moleditpy-3.0.0a3/src/moleditpy/plugins → moleditpy-3.0.0a5/src/moleditpy/utils}/__init__.py +11 -11
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/utils/constants.py +1 -1
- moleditpy-3.0.0a5/src/moleditpy/utils/default_settings.py +94 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/utils/sip_isdeleted_safe.py +41 -41
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/utils/system_utils.py +71 -71
- moleditpy-3.0.0a3/src/moleditpy/modules/__init__.py +0 -122
- moleditpy-3.0.0a3/src/moleditpy/ui/compute_engine.py +0 -880
- moleditpy-3.0.0a3/src/moleditpy/ui/compute_logic.py +0 -526
- moleditpy-3.0.0a3/src/moleditpy/ui/dialog_manager.py +0 -464
- moleditpy-3.0.0a3/src/moleditpy/ui/dihedral_dialog.py +0 -545
- moleditpy-3.0.0a3/src/moleditpy/ui/edit_3d.py +0 -444
- moleditpy-3.0.0a3/src/moleditpy/ui/edit_actions.py +0 -1432
- moleditpy-3.0.0a3/src/moleditpy/ui/main_window.py +0 -218
- moleditpy-3.0.0a3/src/moleditpy/ui/molecular_parsers.py +0 -587
- moleditpy-3.0.0a3/src/moleditpy/ui/project_io.py +0 -386
- moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -33
- moleditpy-3.0.0a3/src/moleditpy/ui/translation_dialog.py +0 -347
- moleditpy-3.0.0a3/src/moleditpy/ui/ui_manager.py +0 -511
- moleditpy-3.0.0a3/src/moleditpy/ui/view_loaders.py +0 -274
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/LICENSE +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/README.md +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/setup.cfg +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/__main__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/core/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/main.py +0 -0
- {moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs → moleditpy-3.0.0a5/src/moleditpy/plugins}/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/analysis_window.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/sip_isdeleted_safe.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/template_preview_item.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/template_preview_view.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/zoomable_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0a5
|
|
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
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0a5
|
|
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
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -23,7 +23,6 @@ src/moleditpy/assets/icon.png
|
|
|
23
23
|
src/moleditpy/core/__init__.py
|
|
24
24
|
src/moleditpy/core/mol_geometry.py
|
|
25
25
|
src/moleditpy/core/molecular_data.py
|
|
26
|
-
src/moleditpy/modules/__init__.py
|
|
27
26
|
src/moleditpy/plugins/__init__.py
|
|
28
27
|
src/moleditpy/plugins/plugin_interface.py
|
|
29
28
|
src/moleditpy/plugins/plugin_manager.py
|
|
@@ -36,34 +35,31 @@ src/moleditpy/ui/analysis_window.py
|
|
|
36
35
|
src/moleditpy/ui/angle_dialog.py
|
|
37
36
|
src/moleditpy/ui/app_state.py
|
|
38
37
|
src/moleditpy/ui/atom_item.py
|
|
38
|
+
src/moleditpy/ui/base_picking_dialog.py
|
|
39
39
|
src/moleditpy/ui/bond_item.py
|
|
40
40
|
src/moleditpy/ui/bond_length_dialog.py
|
|
41
41
|
src/moleditpy/ui/calculation_worker.py
|
|
42
42
|
src/moleditpy/ui/color_settings_dialog.py
|
|
43
|
-
src/moleditpy/ui/compute_engine.py
|
|
44
43
|
src/moleditpy/ui/compute_logic.py
|
|
45
44
|
src/moleditpy/ui/constrained_optimization_dialog.py
|
|
46
45
|
src/moleditpy/ui/custom_interactor_style.py
|
|
47
46
|
src/moleditpy/ui/custom_qt_interactor.py
|
|
48
47
|
src/moleditpy/ui/dialog_3d_picking_mixin.py
|
|
49
48
|
src/moleditpy/ui/dialog_logic.py
|
|
50
|
-
src/moleditpy/ui/dialog_manager.py
|
|
51
49
|
src/moleditpy/ui/dihedral_dialog.py
|
|
52
|
-
src/moleditpy/ui/edit_3d.py
|
|
53
50
|
src/moleditpy/ui/edit_3d_logic.py
|
|
54
|
-
src/moleditpy/ui/edit_actions.py
|
|
55
51
|
src/moleditpy/ui/edit_actions_logic.py
|
|
56
52
|
src/moleditpy/ui/export_logic.py
|
|
53
|
+
src/moleditpy/ui/geometry_base_dialog.py
|
|
54
|
+
src/moleditpy/ui/io_logic.py
|
|
57
55
|
src/moleditpy/ui/main_window.py
|
|
58
56
|
src/moleditpy/ui/main_window_init.py
|
|
59
57
|
src/moleditpy/ui/mirror_dialog.py
|
|
60
|
-
src/moleditpy/ui/molecular_parsers.py
|
|
61
58
|
src/moleditpy/ui/molecular_scene_handler.py
|
|
62
59
|
src/moleditpy/ui/molecule_scene.py
|
|
63
60
|
src/moleditpy/ui/move_group_dialog.py
|
|
64
61
|
src/moleditpy/ui/periodic_table_dialog.py
|
|
65
62
|
src/moleditpy/ui/planarize_dialog.py
|
|
66
|
-
src/moleditpy/ui/project_io.py
|
|
67
63
|
src/moleditpy/ui/settings_dialog.py
|
|
68
64
|
src/moleditpy/ui/sip_isdeleted_safe.py
|
|
69
65
|
src/moleditpy/ui/string_importers.py
|
|
@@ -73,7 +69,6 @@ src/moleditpy/ui/translation_dialog.py
|
|
|
73
69
|
src/moleditpy/ui/ui_manager.py
|
|
74
70
|
src/moleditpy/ui/user_template_dialog.py
|
|
75
71
|
src/moleditpy/ui/view_3d_logic.py
|
|
76
|
-
src/moleditpy/ui/view_loaders.py
|
|
77
72
|
src/moleditpy/ui/zoomable_view.py
|
|
78
73
|
src/moleditpy/ui/settings_tabs/__init__.py
|
|
79
74
|
src/moleditpy/ui/settings_tabs/settings_2d_tab.py
|
|
@@ -82,5 +77,6 @@ src/moleditpy/ui/settings_tabs/settings_other_tab.py
|
|
|
82
77
|
src/moleditpy/ui/settings_tabs/settings_tab_base.py
|
|
83
78
|
src/moleditpy/utils/__init__.py
|
|
84
79
|
src/moleditpy/utils/constants.py
|
|
80
|
+
src/moleditpy/utils/default_settings.py
|
|
85
81
|
src/moleditpy/utils/sip_isdeleted_safe.py
|
|
86
82
|
src/moleditpy/utils/system_utils.py
|
|
@@ -10,23 +10,19 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
This module is intentionally free of any GUI imports so that it can
|
|
16
|
-
be unit-tested in isolation and shared across dialogs and main-window
|
|
17
|
-
submodules without circular dependencies.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
import numpy as np
|
|
13
|
+
from __future__ import annotations
|
|
21
14
|
import math
|
|
15
|
+
import logging
|
|
22
16
|
from collections import deque
|
|
17
|
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
23
20
|
|
|
24
21
|
# ------------------------------------------------------------------
|
|
25
22
|
# Primitive geometry helpers
|
|
26
23
|
# ------------------------------------------------------------------
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
def calc_distance(pos1, pos2) -> float:
|
|
25
|
+
def calc_distance(pos1: Union[np.ndarray, Tuple[float, float, float], List[float]], pos2: Union[np.ndarray, Tuple[float, float, float], List[float]]) -> float:
|
|
30
26
|
"""Return the Euclidean distance between two 3-D positions.
|
|
31
27
|
|
|
32
28
|
Parameters
|
|
@@ -44,7 +40,11 @@ def calc_distance(pos1, pos2) -> float:
|
|
|
44
40
|
)
|
|
45
41
|
|
|
46
42
|
|
|
47
|
-
def calc_angle_deg(
|
|
43
|
+
def calc_angle_deg(
|
|
44
|
+
pos1: Union[np.ndarray, Tuple[float, float, float], List[float]],
|
|
45
|
+
pos2_vertex: Union[np.ndarray, Tuple[float, float, float], List[float]],
|
|
46
|
+
pos3: Union[np.ndarray, Tuple[float, float, float], List[float]],
|
|
47
|
+
) -> float:
|
|
48
48
|
"""Return the angle pos1–pos2_vertex–pos3 in degrees.
|
|
49
49
|
|
|
50
50
|
The angle is measured at *pos2_vertex* and is always in [0, 180].
|
|
@@ -78,7 +78,7 @@ def calc_angle_deg(pos1, pos2_vertex, pos3) -> float:
|
|
|
78
78
|
# ------------------------------------------------------------------
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
def get_connected_group(mol, start_atom, exclude=None):
|
|
81
|
+
def get_connected_group(mol: Any, start_atom: int, exclude: Optional[int] = None) -> Set[int]:
|
|
82
82
|
"""Return the set of atom indices reachable from *start_atom*
|
|
83
83
|
without passing through *exclude*.
|
|
84
84
|
|
|
@@ -124,7 +124,7 @@ def get_connected_group(mol, start_atom, exclude=None):
|
|
|
124
124
|
# ------------------------------------------------------------------
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def rodrigues_rotate(v, axis, angle):
|
|
127
|
+
def rodrigues_rotate(v: np.ndarray, axis: np.ndarray, angle: float) -> np.ndarray:
|
|
128
128
|
"""Rotate vector *v* around a unit *axis* by *angle* radians.
|
|
129
129
|
|
|
130
130
|
Implements Rodrigues' rotation formula:
|
|
@@ -151,8 +151,13 @@ def rodrigues_rotate(v, axis, angle):
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def adjust_bond_angle(
|
|
154
|
-
positions
|
|
155
|
-
|
|
154
|
+
positions: np.ndarray,
|
|
155
|
+
idx_a: int,
|
|
156
|
+
idx_b: int,
|
|
157
|
+
idx_c: int,
|
|
158
|
+
target_angle_deg: float,
|
|
159
|
+
atom_indices_to_move: Iterable[int],
|
|
160
|
+
) -> float:
|
|
156
161
|
"""Adjust the A–B–C bond angle to *target_angle_deg* using a
|
|
157
162
|
difference-based rotation.
|
|
158
163
|
|
|
@@ -244,7 +249,7 @@ def adjust_bond_angle(
|
|
|
244
249
|
# ------------------------------------------------------------------
|
|
245
250
|
|
|
246
251
|
|
|
247
|
-
def calculate_dihedral(positions, i1, i2, i3, i4):
|
|
252
|
+
def calculate_dihedral(positions: Any, i1: int, i2: int, i3: int, i4: int) -> float:
|
|
248
253
|
"""Compute the dihedral angle defined by four atom indices.
|
|
249
254
|
|
|
250
255
|
Parameters
|
|
@@ -295,6 +300,70 @@ def calculate_dihedral(positions, i1, i2, i3, i4):
|
|
|
295
300
|
return float(np.degrees(angle_rad))
|
|
296
301
|
|
|
297
302
|
|
|
303
|
+
def adjust_dihedral(
|
|
304
|
+
positions: np.ndarray,
|
|
305
|
+
i1: int,
|
|
306
|
+
i2: int,
|
|
307
|
+
i3: int,
|
|
308
|
+
i4: int,
|
|
309
|
+
target_dihedral_deg: float,
|
|
310
|
+
atom_indices_to_move: Iterable[int],
|
|
311
|
+
) -> float:
|
|
312
|
+
"""Adjust the dihedral angle defined by i1-i2-i3-i4 to target_dihedral_deg.
|
|
313
|
+
The rotation is performed around the i2-i3 bond axis.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
positions : ndarray, shape (N, 3)
|
|
318
|
+
Atom coordinates. **Modified in-place.**
|
|
319
|
+
i1, i2, i3, i4 : int
|
|
320
|
+
Atom indices defining the dihedral.
|
|
321
|
+
target_dihedral_deg : float
|
|
322
|
+
Target dihedral angle in degrees.
|
|
323
|
+
atom_indices_to_move : iterable of int
|
|
324
|
+
Indices of atoms to be rotated.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
float
|
|
329
|
+
Applied rotation in radians.
|
|
330
|
+
"""
|
|
331
|
+
# Current dihedral
|
|
332
|
+
current_dihedral = calculate_dihedral(positions, i1, i2, i3, i4)
|
|
333
|
+
|
|
334
|
+
# Rotation angle needed
|
|
335
|
+
delta_deg = target_dihedral_deg - current_dihedral
|
|
336
|
+
|
|
337
|
+
# Shortest rotation path
|
|
338
|
+
if delta_deg > 180:
|
|
339
|
+
delta_deg -= 360
|
|
340
|
+
elif delta_deg < -180:
|
|
341
|
+
delta_deg += 360
|
|
342
|
+
|
|
343
|
+
delta_rad = np.radians(delta_deg)
|
|
344
|
+
|
|
345
|
+
if abs(delta_rad) < 1e-9:
|
|
346
|
+
return 0.0
|
|
347
|
+
|
|
348
|
+
# Rotation axis (i2 -> i3)
|
|
349
|
+
pos2 = positions[i2]
|
|
350
|
+
pos3 = positions[i3]
|
|
351
|
+
axis = pos3 - pos2
|
|
352
|
+
axis_norm = np.linalg.norm(axis)
|
|
353
|
+
|
|
354
|
+
if axis_norm < 1e-12:
|
|
355
|
+
return 0.0
|
|
356
|
+
|
|
357
|
+
axis_unit = axis / axis_norm
|
|
358
|
+
|
|
359
|
+
# Rotate each movable atom
|
|
360
|
+
for idx in atom_indices_to_move:
|
|
361
|
+
rel = positions[idx] - pos2
|
|
362
|
+
positions[idx] = pos2 + rodrigues_rotate(rel, axis_unit, delta_rad)
|
|
363
|
+
|
|
364
|
+
return float(delta_rad)
|
|
365
|
+
|
|
366
|
+
|
|
298
367
|
# ------------------------------------------------------------------
|
|
299
368
|
# Valence sanity check
|
|
300
369
|
# ------------------------------------------------------------------
|
|
@@ -314,7 +383,7 @@ _VALENCE_LIMITS = {
|
|
|
314
383
|
}
|
|
315
384
|
|
|
316
385
|
|
|
317
|
-
def is_problematic_valence(symbol, bond_count, charge=0):
|
|
386
|
+
def is_problematic_valence(symbol: str, bond_count: Union[int, float], charge: int = 0) -> bool:
|
|
318
387
|
"""Return ``True`` if the atom's total bond order exceeds its
|
|
319
388
|
typical maximum valence.
|
|
320
389
|
|
|
@@ -406,7 +475,7 @@ def inject_ez_stereo_to_mol_block(mol_block, rdkit_mol, bonds_data):
|
|
|
406
475
|
return "\n".join(mol_lines)
|
|
407
476
|
|
|
408
477
|
|
|
409
|
-
def identify_valence_problems(atoms_data, bonds_data):
|
|
478
|
+
def identify_valence_problems(atoms_data: Dict[int, Any], bonds_data: Dict[Tuple[int, int], Any]) -> List[int]:
|
|
410
479
|
"""Identify atoms with problematic valence.
|
|
411
480
|
|
|
412
481
|
Parameters
|
|
@@ -441,7 +510,7 @@ def identify_valence_problems(atoms_data, bonds_data):
|
|
|
441
510
|
return problem_atom_ids
|
|
442
511
|
|
|
443
512
|
|
|
444
|
-
def optimize_2d_coords(mol):
|
|
513
|
+
def optimize_2d_coords(mol: Any) -> Dict[int, Tuple[float, float]]:
|
|
445
514
|
"""Generate 2D coordinates using RDKit and return a map of (x, y) tuples."""
|
|
446
515
|
from rdkit.Chem import AllChem
|
|
447
516
|
|
|
@@ -571,12 +640,12 @@ def resolve_2d_overlaps(
|
|
|
571
640
|
rep_id1, rep_id2 = i1, i2
|
|
572
641
|
break
|
|
573
642
|
|
|
574
|
-
if
|
|
643
|
+
if rep_id1 is None:
|
|
575
644
|
continue
|
|
576
645
|
|
|
577
646
|
frag1 = next((f for f in fragments if rep_id1 in f), None)
|
|
578
647
|
frag2 = next((f for f in fragments if rep_id2 in f), None)
|
|
579
|
-
if
|
|
648
|
+
if frag1 is None or frag2 is None or frag1 == frag2:
|
|
580
649
|
continue
|
|
581
650
|
|
|
582
651
|
ids_to_move = frag1 if rep_id1 > rep_id2 else frag2
|
|
@@ -10,7 +10,9 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
from __future__ import annotations
|
|
13
14
|
import logging
|
|
15
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
14
16
|
from rdkit import Chem
|
|
15
17
|
|
|
16
18
|
try:
|
|
@@ -22,21 +24,26 @@ except ImportError:
|
|
|
22
24
|
class PointTuple(tuple):
|
|
23
25
|
"""Backward-compatible tuple that allows .x() and .y() access like QPointF."""
|
|
24
26
|
|
|
25
|
-
def x(self):
|
|
27
|
+
def x(self) -> float:
|
|
26
28
|
return self[0]
|
|
27
29
|
|
|
28
|
-
def y(self):
|
|
30
|
+
def y(self) -> float:
|
|
29
31
|
return self[1]
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class MolecularData:
|
|
33
|
-
|
|
35
|
+
atoms: Dict[int, Dict[str, Any]]
|
|
36
|
+
bonds: Dict[Tuple[int, int], Dict[str, Any]]
|
|
37
|
+
adjacency_list: Dict[int, List[int]]
|
|
38
|
+
_next_atom_id: int
|
|
39
|
+
|
|
40
|
+
def __init__(self) -> None:
|
|
34
41
|
self.atoms = {}
|
|
35
42
|
self.bonds = {}
|
|
36
43
|
self._next_atom_id = 0
|
|
37
44
|
self.adjacency_list = {}
|
|
38
45
|
|
|
39
|
-
def add_atom(self, symbol, pos, charge=0, radical=0):
|
|
46
|
+
def add_atom(self, symbol: str, pos: Union[Any, Tuple[float, float]], charge: int = 0, radical: int = 0) -> int:
|
|
40
47
|
atom_id = self._next_atom_id
|
|
41
48
|
# Internalize position as raw floats to decouple from UI types (QPointF)
|
|
42
49
|
if hasattr(pos, "x") and hasattr(pos, "y"):
|
|
@@ -55,7 +62,7 @@ class MolecularData:
|
|
|
55
62
|
self._next_atom_id += 1
|
|
56
63
|
return atom_id
|
|
57
64
|
|
|
58
|
-
def set_atom_pos(self, atom_id, pos):
|
|
65
|
+
def set_atom_pos(self, atom_id: int, pos: Union[Any, Tuple[float, float]]) -> None:
|
|
59
66
|
"""Update atom position using raw floats or QPointF."""
|
|
60
67
|
if atom_id in self.atoms:
|
|
61
68
|
if hasattr(pos, "x") and hasattr(pos, "y"):
|
|
@@ -65,7 +72,7 @@ class MolecularData:
|
|
|
65
72
|
else:
|
|
66
73
|
self.atoms[atom_id]["pos"] = PointTuple((float(pos[0]), float(pos[1])))
|
|
67
74
|
|
|
68
|
-
def add_bond(self, id1, id2, order=1, stereo=0):
|
|
75
|
+
def add_bond(self, id1: int, id2: int, order: Union[int, float] = 1, stereo: int = 0) -> Tuple[Tuple[int, int], str]:
|
|
69
76
|
# For stereo bonds, do not sort because ID order determines direction.
|
|
70
77
|
# For non-stereo bonds, sort to normalize the key.
|
|
71
78
|
if stereo == 0:
|
|
@@ -88,7 +95,7 @@ class MolecularData:
|
|
|
88
95
|
self.bonds[(id1, id2)] = bond_data
|
|
89
96
|
return (id1, id2), "created"
|
|
90
97
|
|
|
91
|
-
def remove_atom(self, atom_id):
|
|
98
|
+
def remove_atom(self, atom_id: int) -> None:
|
|
92
99
|
if atom_id in self.atoms:
|
|
93
100
|
# Safely get neighbors before deleting the atom's own entry
|
|
94
101
|
neighbors = self.adjacency_list.get(atom_id, [])
|
|
@@ -111,7 +118,7 @@ class MolecularData:
|
|
|
111
118
|
for key in bonds_to_remove:
|
|
112
119
|
self.bonds.pop(key, None)
|
|
113
120
|
|
|
114
|
-
def remove_bond(self, id1, id2):
|
|
121
|
+
def remove_bond(self, id1: int, id2: int) -> None:
|
|
115
122
|
# Look for directional stereo bonds (forward/reverse) and normalized non-stereo bond keys.
|
|
116
123
|
key_to_remove = None
|
|
117
124
|
if (id1, id2) in self.bonds:
|
|
@@ -126,7 +133,7 @@ class MolecularData:
|
|
|
126
133
|
self.adjacency_list[id2].remove(id1)
|
|
127
134
|
self.bonds.pop(key_to_remove, None)
|
|
128
135
|
|
|
129
|
-
def to_rdkit_mol(self, use_2d_stereo=True):
|
|
136
|
+
def to_rdkit_mol(self, use_2d_stereo: bool = True) -> Optional[Chem.Mol]:
|
|
130
137
|
"""
|
|
131
138
|
use_2d_stereo: True estimates E/Z from 2D coordinates (as before). False prioritizes E/Z labels.
|
|
132
139
|
Call with use_2d_stereo=False for 3D conversion.
|
|
@@ -147,6 +154,7 @@ class MolecularData:
|
|
|
147
154
|
atom.SetFormalCharge(data.get("charge", 0))
|
|
148
155
|
atom.SetNumRadicalElectrons(data.get("radical", 0))
|
|
149
156
|
atom.SetIntProp("_original_atom_id", atom_id)
|
|
157
|
+
atom.SetNoImplicit(False) # Allow RDKit to perceive implicit valence
|
|
150
158
|
idx = mol.AddAtom(atom)
|
|
151
159
|
atom_id_to_idx_map[atom_id] = idx
|
|
152
160
|
|
|
@@ -227,7 +235,7 @@ class MolecularData:
|
|
|
227
235
|
)
|
|
228
236
|
|
|
229
237
|
# Helper: Pick neighbors prioritizing heavy atoms
|
|
230
|
-
def pick_preferred_neighbor(atom, exclude_idx):
|
|
238
|
+
def pick_preferred_neighbor(atom: Chem.Atom, exclude_idx: int) -> Optional[int]:
|
|
231
239
|
for nbr in atom.GetNeighbors():
|
|
232
240
|
if nbr.GetIdx() == exclude_idx:
|
|
233
241
|
continue
|
|
@@ -305,7 +313,7 @@ class MolecularData:
|
|
|
305
313
|
Chem.AssignStereochemistry(final_mol, cleanIt=False, force=False)
|
|
306
314
|
return final_mol
|
|
307
315
|
|
|
308
|
-
def update_ring_info_2d(self):
|
|
316
|
+
def update_ring_info_2d(self) -> None:
|
|
309
317
|
"""Update is_in_ring and ring_center for all BondItems based on 2D topology."""
|
|
310
318
|
if not self.atoms or not self.bonds:
|
|
311
319
|
return
|
|
@@ -386,7 +394,7 @@ class MolecularData:
|
|
|
386
394
|
# so this is usually correct for 2D drawing.
|
|
387
395
|
bond_item.ring_center = ring_center
|
|
388
396
|
|
|
389
|
-
def to_mol_block(self):
|
|
397
|
+
def to_mol_block(self) -> Optional[str]:
|
|
390
398
|
mol = self.to_rdkit_mol()
|
|
391
399
|
if mol:
|
|
392
400
|
try:
|
|
@@ -449,7 +457,7 @@ class MolecularData:
|
|
|
449
457
|
mol_block += "M END\n"
|
|
450
458
|
return mol_block
|
|
451
459
|
|
|
452
|
-
def to_template_dict(self, name, version="1.0", application_version=""):
|
|
460
|
+
def to_template_dict(self, name: str, version: str = "1.0", application_version: str = "") -> Dict[str, Any]:
|
|
453
461
|
"""Convert current structure to a dictionary for template storage."""
|
|
454
462
|
import datetime
|
|
455
463
|
|
|
@@ -10,7 +10,7 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from typing import Any, Callable, Optional
|
|
13
|
+
from typing import Any, Callable, List, Optional, Union
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class PluginContext:
|
|
@@ -75,6 +75,53 @@ class PluginContext:
|
|
|
75
75
|
"""
|
|
76
76
|
return Plugin3DController(self._manager.get_main_window())
|
|
77
77
|
|
|
78
|
+
def show_status_message(self, message: str, timeout: int = 3000) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Display a message in the application status bar.
|
|
81
|
+
"""
|
|
82
|
+
self._manager.show_status_message(message, timeout)
|
|
83
|
+
|
|
84
|
+
def push_undo_checkpoint(self) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Create an undo checkpoint for the current state.
|
|
87
|
+
Call this AFTER making modifications to the molecule to ensure the
|
|
88
|
+
new state is saved to the undo history.
|
|
89
|
+
"""
|
|
90
|
+
self._manager.push_undo_checkpoint()
|
|
91
|
+
|
|
92
|
+
def refresh_3d_view(self) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Force a refresh (re-render) of the 3D scene.
|
|
95
|
+
"""
|
|
96
|
+
self._manager.refresh_3d_view()
|
|
97
|
+
|
|
98
|
+
def reset_3d_camera(self) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Resets the 3D camera to fit the current molecule.
|
|
101
|
+
"""
|
|
102
|
+
self._manager.reset_3d_camera()
|
|
103
|
+
|
|
104
|
+
def get_selected_atom_indices(self) -> List[int]:
|
|
105
|
+
"""
|
|
106
|
+
Returns a list of RDKit atom indices currently selected in the 2D or 3D view.
|
|
107
|
+
Note: RDKit indices are returned, which map to the current_mol.
|
|
108
|
+
"""
|
|
109
|
+
return self._manager.get_selected_atom_indices()
|
|
110
|
+
|
|
111
|
+
def register_window(self, window_id: str, window: Any) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Register a custom plugin window/dialog with the application.
|
|
114
|
+
This allows the application to manage the window lifecycle.
|
|
115
|
+
Windows are namespaced by the plugin name automatically.
|
|
116
|
+
"""
|
|
117
|
+
self._manager.register_window(self._plugin_name, window_id, window)
|
|
118
|
+
|
|
119
|
+
def get_window(self, window_id: str) -> Optional[Any]:
|
|
120
|
+
"""
|
|
121
|
+
Retrieve a previously registered window by its ID.
|
|
122
|
+
"""
|
|
123
|
+
return self._manager.get_window(self._plugin_name, window_id)
|
|
124
|
+
|
|
78
125
|
def get_main_window(self) -> Any:
|
|
79
126
|
"""
|
|
80
127
|
Returns the raw MainWindow instance.
|
|
@@ -83,22 +130,45 @@ class PluginContext:
|
|
|
83
130
|
return self._manager.get_main_window()
|
|
84
131
|
|
|
85
132
|
@property
|
|
86
|
-
def
|
|
133
|
+
def current_mol(self) -> Any:
|
|
87
134
|
"""
|
|
88
|
-
Get or set the current molecule (RDKit Mol object).
|
|
135
|
+
Get or set the current molecule (RDKit Mol object). Shortcut for current_molecule.
|
|
89
136
|
"""
|
|
90
|
-
mw = self.
|
|
137
|
+
mw = self.get_main_window()
|
|
138
|
+
return mw.current_mol if mw else None
|
|
139
|
+
|
|
140
|
+
@current_mol.setter
|
|
141
|
+
def current_mol(self, mol: Any):
|
|
142
|
+
mw = self.get_main_window()
|
|
91
143
|
if mw:
|
|
92
|
-
|
|
93
|
-
|
|
144
|
+
mw.current_mol = mol
|
|
145
|
+
if hasattr(mw.view_3d_manager, "draw_molecule_3d"):
|
|
146
|
+
mw.view_3d_manager.draw_molecule_3d(mol)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def current_molecule(self) -> Any:
|
|
150
|
+
"""Alias for current_mol for backward compatibility."""
|
|
151
|
+
return self.current_mol
|
|
94
152
|
|
|
95
153
|
@current_molecule.setter
|
|
96
154
|
def current_molecule(self, mol: Any):
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
155
|
+
self.current_mol = mol
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def plotter(self) -> Any:
|
|
159
|
+
"""
|
|
160
|
+
Returns the PyVista plotter from the MainWindow.
|
|
161
|
+
"""
|
|
162
|
+
mw = self.get_main_window()
|
|
163
|
+
return mw.plotter if mw else None
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def scene(self) -> Any:
|
|
167
|
+
"""
|
|
168
|
+
Returns the 2D MoleculeScene from the MainWindow.
|
|
169
|
+
"""
|
|
170
|
+
mw = self.get_main_window()
|
|
171
|
+
return mw.scene if mw else None
|
|
102
172
|
|
|
103
173
|
def add_export_action(self, label: str, callback: Callable):
|
|
104
174
|
"""
|
|
@@ -202,6 +272,10 @@ class Plugin3DController:
|
|
|
202
272
|
def __init__(self, main_window):
|
|
203
273
|
self._mw = main_window
|
|
204
274
|
|
|
275
|
+
def _get_v3d(self):
|
|
276
|
+
"""Helper to get the 3D manager."""
|
|
277
|
+
return getattr(self._mw, "view_3d_manager", None)
|
|
278
|
+
|
|
205
279
|
def set_atom_color(self, atom_index: int, color_hex: str):
|
|
206
280
|
"""
|
|
207
281
|
Set the color of a specific atom in the 3D view.
|
|
@@ -209,11 +283,11 @@ class Plugin3DController:
|
|
|
209
283
|
atom_index: RDKit atom index.
|
|
210
284
|
color_hex: Hex string e.g., "#FF0000".
|
|
211
285
|
"""
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
|
|
286
|
+
v3d = self._get_v3d()
|
|
287
|
+
if v3d:
|
|
288
|
+
v3d.update_atom_color_override(atom_index, color_hex)
|
|
289
|
+
if hasattr(self._mw, "plotter") and self._mw.plotter:
|
|
290
|
+
self._mw.plotter.render()
|
|
217
291
|
|
|
218
292
|
def set_bond_color(self, bond_index: int, color_hex: str):
|
|
219
293
|
"""
|
|
@@ -223,8 +297,25 @@ class Plugin3DController:
|
|
|
223
297
|
bond_index: RDKit bond index.
|
|
224
298
|
color_hex: Hex string e.g., "#00FF00".
|
|
225
299
|
"""
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
300
|
+
v3d = self._get_v3d()
|
|
301
|
+
if v3d:
|
|
302
|
+
v3d.update_bond_color_override(bond_index, color_hex)
|
|
303
|
+
if hasattr(self._mw, "plotter") and self._mw.plotter:
|
|
304
|
+
self._mw.plotter.render()
|
|
305
|
+
|
|
306
|
+
def set_bond_color_by_atoms(self, atom_idx1: int, atom_idx2: int, color_hex: str):
|
|
307
|
+
"""
|
|
308
|
+
Set the color of the bond between two atoms.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
atom_idx1: First RDKit atom index.
|
|
312
|
+
atom_idx2: Second RDKit atom index.
|
|
313
|
+
color_hex: Hex string e.g., "#00FF00".
|
|
314
|
+
"""
|
|
315
|
+
mol = getattr(self._mw, "current_mol", None)
|
|
316
|
+
if not mol:
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
bond = mol.GetBondBetweenAtoms(atom_idx1, atom_idx2)
|
|
320
|
+
if bond:
|
|
321
|
+
self.set_bond_color(bond.GetIdx(), color_hex)
|