moleditpy 2.8.0a3__tar.gz → 2.8.1__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-2.8.0a3 → moleditpy-2.8.1}/PKG-INFO +5 -4
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/README.md +2 -2
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/pyproject.toml +3 -2
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/PKG-INFO +5 -4
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/requires.txt +6 -1
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/angle_dialog.py +21 -8
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/bond_length_dialog.py +3 -6
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/calculation_worker.py +88 -48
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/constants.py +1 -1
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/dihedral_dialog.py +3 -6
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_compute.py +9 -2
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_main_init.py +4 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/LICENSE +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/setup.cfg +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/__main__.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/main.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/__init__.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/about_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/align_plane_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/alignment_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/analysis_window.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/atom_item.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/bond_item.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/color_settings_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/custom_interactor_style.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_app_state.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_export.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_project_io.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_string_importers.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_view_3d.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/mirror_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/mol_geometry.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/molecular_data.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/molecule_scene.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/move_group_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/planarize_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_interface.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_manager.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_manager_window.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/settings_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/sip_isdeleted_safe.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/template_preview_item.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/template_preview_view.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/translation_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/user_template_dialog.py +0 -0
- {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/zoomable_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.1
|
|
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
|
|
@@ -694,7 +694,8 @@ Requires-Python: <3.15,>=3.9
|
|
|
694
694
|
Description-Content-Type: text/markdown
|
|
695
695
|
License-File: LICENSE
|
|
696
696
|
Requires-Dist: numpy
|
|
697
|
-
Requires-Dist: pyqt6<6.
|
|
697
|
+
Requires-Dist: pyqt6<6.10; sys_platform == "darwin"
|
|
698
|
+
Requires-Dist: pyqt6<6.11; sys_platform != "darwin"
|
|
698
699
|
Requires-Dist: pyvista<0.48
|
|
699
700
|
Requires-Dist: pyvistaqt<0.12
|
|
700
701
|
Requires-Dist: rdkit<2025.10
|
|
@@ -709,10 +710,10 @@ Dynamic: license-file
|
|
|
709
710
|
[](https://pypi.org/project/MoleditPy/)
|
|
710
711
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
711
712
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
712
|
-

|
|
713
714
|

|
|
714
715
|

|
|
715
|
-

|
|
716
717
|
[](https://pepy.tech/projects/moleditpy)
|
|
717
718
|
|
|
718
719
|
[🇯🇵 日本語 (Japanese)](#japanese)
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
[](https://pypi.org/project/MoleditPy/)
|
|
7
7
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
8
8
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
9
|
-

|
|
10
10
|

|
|
11
11
|

|
|
12
|
-

|
|
13
13
|
[](https://pepy.tech/projects/moleditpy)
|
|
14
14
|
|
|
15
15
|
[🇯🇵 日本語 (Japanese)](#japanese)
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MoleditPy"
|
|
7
7
|
|
|
8
|
-
version = "2.8.
|
|
8
|
+
version = "2.8.1"
|
|
9
9
|
|
|
10
10
|
license = {file = "LICENSE"}
|
|
11
11
|
|
|
@@ -34,7 +34,8 @@ classifiers = [
|
|
|
34
34
|
|
|
35
35
|
dependencies = [
|
|
36
36
|
"numpy",
|
|
37
|
-
"pyqt6 < 6.
|
|
37
|
+
"pyqt6 < 6.10; sys_platform == 'darwin'",
|
|
38
|
+
"pyqt6 < 6.11; sys_platform != 'darwin'",
|
|
38
39
|
"pyvista < 0.48",
|
|
39
40
|
"pyvistaqt < 0.12",
|
|
40
41
|
"rdkit < 2025.10",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.1
|
|
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
|
|
@@ -694,7 +694,8 @@ Requires-Python: <3.15,>=3.9
|
|
|
694
694
|
Description-Content-Type: text/markdown
|
|
695
695
|
License-File: LICENSE
|
|
696
696
|
Requires-Dist: numpy
|
|
697
|
-
Requires-Dist: pyqt6<6.
|
|
697
|
+
Requires-Dist: pyqt6<6.10; sys_platform == "darwin"
|
|
698
|
+
Requires-Dist: pyqt6<6.11; sys_platform != "darwin"
|
|
698
699
|
Requires-Dist: pyvista<0.48
|
|
699
700
|
Requires-Dist: pyvistaqt<0.12
|
|
700
701
|
Requires-Dist: rdkit<2025.10
|
|
@@ -709,10 +710,10 @@ Dynamic: license-file
|
|
|
709
710
|
[](https://pypi.org/project/MoleditPy/)
|
|
710
711
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
711
712
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
712
|
-

|
|
713
714
|

|
|
714
715
|

|
|
715
|
-

|
|
716
717
|
[](https://pepy.tech/projects/moleditpy)
|
|
717
718
|
|
|
718
719
|
[🇯🇵 日本語 (Japanese)](#japanese)
|
|
@@ -152,11 +152,15 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
152
152
|
self.atom2_idx = atom_idx
|
|
153
153
|
elif self.atom3_idx is None:
|
|
154
154
|
self.atom3_idx = atom_idx
|
|
155
|
+
# Take a fresh snapshot immediately upon completing the triad selection
|
|
156
|
+
# This locks in the "original" initial geometry the user started modifying from
|
|
157
|
+
self._snapshot_positions = self.mol.GetConformer().GetPositions().copy()
|
|
155
158
|
else:
|
|
156
159
|
# Reset and start over
|
|
157
160
|
self.atom1_idx = atom_idx
|
|
158
161
|
self.atom2_idx = None
|
|
159
162
|
self.atom3_idx = None
|
|
163
|
+
self._snapshot_positions = None
|
|
160
164
|
|
|
161
165
|
# 原子ラベルを表示
|
|
162
166
|
self.show_atom_labels()
|
|
@@ -177,12 +181,6 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
177
181
|
self.disable_picking()
|
|
178
182
|
super().closeEvent(event)
|
|
179
183
|
|
|
180
|
-
def reject(self):
|
|
181
|
-
"""キャンセル時の処理"""
|
|
182
|
-
self.clear_atom_labels()
|
|
183
|
-
self.disable_picking()
|
|
184
|
-
super().reject()
|
|
185
|
-
|
|
186
184
|
def accept(self):
|
|
187
185
|
"""OK時の処理"""
|
|
188
186
|
self.clear_atom_labels()
|
|
@@ -194,6 +192,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
194
192
|
self.atom1_idx = None
|
|
195
193
|
self.atom2_idx = None # vertex atom
|
|
196
194
|
self.atom3_idx = None
|
|
195
|
+
self._snapshot_positions = None
|
|
197
196
|
self.clear_selection_labels()
|
|
198
197
|
self.update_display()
|
|
199
198
|
|
|
@@ -215,6 +214,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
215
214
|
self.selection_label.setText("No atoms selected")
|
|
216
215
|
self.angle_label.setText("")
|
|
217
216
|
self.apply_button.setEnabled(False)
|
|
217
|
+
self._snapshot_positions = None
|
|
218
218
|
# Clear angle input when no selection
|
|
219
219
|
try:
|
|
220
220
|
self.angle_input.blockSignals(True)
|
|
@@ -235,6 +235,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
235
235
|
)
|
|
236
236
|
self.angle_label.setText("")
|
|
237
237
|
self.apply_button.setEnabled(False)
|
|
238
|
+
self._snapshot_positions = None
|
|
238
239
|
# ラベル追加
|
|
239
240
|
self.add_selection_label(self.atom1_idx, "1")
|
|
240
241
|
# Clear angle input while selection is incomplete
|
|
@@ -258,6 +259,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
258
259
|
)
|
|
259
260
|
self.angle_label.setText("")
|
|
260
261
|
self.apply_button.setEnabled(False)
|
|
262
|
+
self._snapshot_positions = None
|
|
261
263
|
# ラベル追加
|
|
262
264
|
self.add_selection_label(self.atom1_idx, "1")
|
|
263
265
|
self.add_selection_label(self.atom2_idx, "2(vertex)")
|
|
@@ -340,7 +342,9 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
340
342
|
self._slider_dragging = True
|
|
341
343
|
self.main_window.push_undo_state()
|
|
342
344
|
# Snapshot positions so the rotation axis stays stable during drag
|
|
343
|
-
|
|
345
|
+
# Only take snapshot if one doesn't exist to preserve directional info
|
|
346
|
+
if getattr(self, '_snapshot_positions', None) is None:
|
|
347
|
+
self._snapshot_positions = self.mol.GetConformer().GetPositions().copy()
|
|
344
348
|
|
|
345
349
|
def on_slider_moved(self, value):
|
|
346
350
|
"""Update geometry in real-time while dragging."""
|
|
@@ -356,7 +360,8 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
356
360
|
def on_slider_released(self):
|
|
357
361
|
"""Finalize slider dragging."""
|
|
358
362
|
self._slider_dragging = False
|
|
359
|
-
|
|
363
|
+
# Do NOT clear snapshot here. Keep it to preserve the turning direction
|
|
364
|
+
# even if they approach ±180°. It gets cleared when selection is changed.
|
|
360
365
|
self.main_window.draw_molecule_3d(self.mol)
|
|
361
366
|
self.main_window.update_chiral_labels()
|
|
362
367
|
|
|
@@ -367,6 +372,11 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
367
372
|
if self.atom1_idx is None or self.atom2_idx is None or self.atom3_idx is None:
|
|
368
373
|
return
|
|
369
374
|
self.main_window.push_undo_state()
|
|
375
|
+
|
|
376
|
+
# Ensure we have a snapshot for click-to-position as well to maintain direction
|
|
377
|
+
if getattr(self, '_snapshot_positions', None) is None:
|
|
378
|
+
self._snapshot_positions = self.mol.GetConformer().GetPositions().copy()
|
|
379
|
+
|
|
370
380
|
self.angle_input.blockSignals(True)
|
|
371
381
|
self.angle_input.setText(f"{value}")
|
|
372
382
|
self.angle_input.blockSignals(False)
|
|
@@ -468,6 +478,9 @@ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
468
478
|
self.main_window.draw_molecule_3d(self.mol)
|
|
469
479
|
|
|
470
480
|
def reject(self):
|
|
481
|
+
"""キャンセル時の処理"""
|
|
482
|
+
self.clear_atom_labels()
|
|
483
|
+
self.disable_picking()
|
|
471
484
|
super().reject()
|
|
472
485
|
try:
|
|
473
486
|
if self.main_window.current_mol:
|
|
@@ -168,12 +168,6 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
168
168
|
self.disable_picking()
|
|
169
169
|
super().closeEvent(event)
|
|
170
170
|
|
|
171
|
-
def reject(self):
|
|
172
|
-
"""キャンセル時の処理"""
|
|
173
|
-
self.clear_atom_labels()
|
|
174
|
-
self.disable_picking()
|
|
175
|
-
super().reject()
|
|
176
|
-
|
|
177
171
|
def accept(self):
|
|
178
172
|
"""OK時の処理"""
|
|
179
173
|
self.clear_atom_labels()
|
|
@@ -420,6 +414,9 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
420
414
|
self.main_window.draw_molecule_3d(self.mol)
|
|
421
415
|
|
|
422
416
|
def reject(self):
|
|
417
|
+
"""キャンセル時の処理"""
|
|
418
|
+
self.clear_atom_labels()
|
|
419
|
+
self.disable_picking()
|
|
423
420
|
super().reject()
|
|
424
421
|
try:
|
|
425
422
|
if self.main_window.current_mol:
|
|
@@ -21,21 +21,40 @@ from rdkit import Chem
|
|
|
21
21
|
from rdkit.Chem import AllChem, rdGeometry
|
|
22
22
|
from rdkit.DistanceGeometry import DoTriangleSmoothing
|
|
23
23
|
|
|
24
|
-
try:
|
|
24
|
+
try: # pragma: no cover
|
|
25
25
|
from . import OBABEL_AVAILABLE
|
|
26
|
-
except Exception:
|
|
26
|
+
except Exception: # pragma: no cover
|
|
27
27
|
from modules import OBABEL_AVAILABLE
|
|
28
28
|
# Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
|
|
29
|
-
if OBABEL_AVAILABLE:
|
|
29
|
+
if OBABEL_AVAILABLE: # pragma: no cover
|
|
30
30
|
try:
|
|
31
31
|
import os
|
|
32
|
+
import glob
|
|
32
33
|
import openbabel
|
|
33
34
|
from openbabel import pybel
|
|
34
35
|
|
|
35
36
|
# The python wheel often misses setting up the data directory.
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
# Check multiple potential locations for BABEL_DATADIR.
|
|
38
|
+
ob_base = os.path.dirname(openbabel.__file__)
|
|
39
|
+
data_candidates = [
|
|
40
|
+
os.path.join(ob_base, "bin", "data"), # Typical for Windows wheel
|
|
41
|
+
os.path.join(ob_base, "share", "openbabel", "*") # Typical for macOS/Linux
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
found_datadir = None
|
|
45
|
+
for pattern in data_candidates:
|
|
46
|
+
matches = glob.glob(pattern)
|
|
47
|
+
for m in matches:
|
|
48
|
+
if os.path.isdir(m):
|
|
49
|
+
found_datadir = m
|
|
50
|
+
break
|
|
51
|
+
if found_datadir:
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
if found_datadir:
|
|
55
|
+
os.environ["BABEL_DATADIR"] = found_datadir
|
|
56
|
+
|
|
57
|
+
os.environ["BABEL_LIBDIR"] = ob_base
|
|
39
58
|
except Exception:
|
|
40
59
|
# If import fails here, disable OBABEL locally; avoid raising
|
|
41
60
|
pybel = None
|
|
@@ -43,7 +62,7 @@ if OBABEL_AVAILABLE:
|
|
|
43
62
|
print(
|
|
44
63
|
"Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled."
|
|
45
64
|
)
|
|
46
|
-
else:
|
|
65
|
+
else: # pragma: no cover
|
|
47
66
|
pybel = None
|
|
48
67
|
|
|
49
68
|
|
|
@@ -172,7 +191,7 @@ def _adjust_collision_avoidance(rd_mol, check_halted_cb, safe_status_cb):
|
|
|
172
191
|
safe_status_cb("Collision avoidance completed.")
|
|
173
192
|
except WorkerHaltError:
|
|
174
193
|
raise
|
|
175
|
-
except Exception as e:
|
|
194
|
+
except Exception as e: # pragma: no cover
|
|
176
195
|
import traceback
|
|
177
196
|
traceback.print_exc()
|
|
178
197
|
safe_status_cb(f"Collision avoidance warning: {e}")
|
|
@@ -185,15 +204,15 @@ def _iterative_optimize(mol, method, check_halted_cb, safe_status_cb, max_iters=
|
|
|
185
204
|
mmff_variant = "MMFF94" if method == "MMFF94" else "MMFF94s"
|
|
186
205
|
props = AllChem.MMFFGetMoleculeProperties(mol, mmffVariant=mmff_variant)
|
|
187
206
|
if props is None:
|
|
188
|
-
|
|
207
|
+
raise ValueError(f"Failed to generate MMFF properties for variant {mmff_variant}.")
|
|
189
208
|
ff = AllChem.MMFFGetMoleculeForceField(mol, props, confId=0)
|
|
190
209
|
elif method == "UFF":
|
|
191
210
|
ff = AllChem.UFFGetMoleculeForceField(mol, confId=0)
|
|
192
211
|
else:
|
|
193
|
-
|
|
212
|
+
raise ValueError(f"Unknown optimization method: {method}")
|
|
194
213
|
|
|
195
214
|
if ff is None:
|
|
196
|
-
|
|
215
|
+
raise ValueError(f"Failed to setup force field for method: {method}")
|
|
197
216
|
|
|
198
217
|
ff.Initialize()
|
|
199
218
|
iters_done = 0
|
|
@@ -219,8 +238,7 @@ def _iterative_optimize_obabel(mol, method, check_halted_cb, safe_status_cb, max
|
|
|
219
238
|
"""Perform force field optimization using OpenBabel in chunks to avoid UI freezing."""
|
|
220
239
|
try:
|
|
221
240
|
if not OBABEL_AVAILABLE or pybel is None:
|
|
222
|
-
|
|
223
|
-
return False
|
|
241
|
+
raise RuntimeError("OpenBabel is not available.")
|
|
224
242
|
|
|
225
243
|
ff_name = method.lower()
|
|
226
244
|
|
|
@@ -231,8 +249,7 @@ def _iterative_optimize_obabel(mol, method, check_halted_cb, safe_status_cb, max
|
|
|
231
249
|
# Set up forcefield
|
|
232
250
|
ff = pybel.ob.OBForceField.FindForceField(ff_name)
|
|
233
251
|
if not ff:
|
|
234
|
-
|
|
235
|
-
return False
|
|
252
|
+
raise ValueError(f"Forcefield '{ff_name}' not found in OpenBabel.")
|
|
236
253
|
|
|
237
254
|
if not ff.Setup(ob_mol.OBMol):
|
|
238
255
|
error_msg = f"Failed to load parameters for '{ff_name}'."
|
|
@@ -276,11 +293,11 @@ class CalculationWorker(QObject):
|
|
|
276
293
|
error = pyqtSignal(object)
|
|
277
294
|
start_work = pyqtSignal(str, object)
|
|
278
295
|
|
|
279
|
-
def __init__(self, parent=None):
|
|
296
|
+
def __init__(self, parent=None): # pragma: no cover
|
|
280
297
|
super().__init__(parent)
|
|
281
298
|
try:
|
|
282
299
|
self.start_work.connect(self.run_calculation)
|
|
283
|
-
except Exception:
|
|
300
|
+
except Exception:
|
|
284
301
|
import traceback
|
|
285
302
|
traceback.print_exc()
|
|
286
303
|
|
|
@@ -308,7 +325,7 @@ class CalculationWorker(QObject):
|
|
|
308
325
|
return False
|
|
309
326
|
|
|
310
327
|
# Safe-emission helpers: do nothing if this worker has been halted.
|
|
311
|
-
def _safe_status(msg):
|
|
328
|
+
def _safe_status(msg): # pragma: no cover
|
|
312
329
|
try:
|
|
313
330
|
if _check_halted():
|
|
314
331
|
raise WorkerHaltError("Halted")
|
|
@@ -374,13 +391,13 @@ class CalculationWorker(QObject):
|
|
|
374
391
|
worker_id = None
|
|
375
392
|
|
|
376
393
|
_warned_no_worker_id = False
|
|
377
|
-
if worker_id is None:
|
|
394
|
+
if worker_id is None: # pragma: no cover
|
|
378
395
|
try:
|
|
379
396
|
# best-effort, swallow any errors (signals may not be connected)
|
|
380
397
|
self.status_update.emit(
|
|
381
398
|
"Warning: worker started without 'worker_id'; will listen for global halt signals."
|
|
382
399
|
)
|
|
383
|
-
except Exception:
|
|
400
|
+
except Exception:
|
|
384
401
|
import traceback
|
|
385
402
|
traceback.print_exc()
|
|
386
403
|
_warned_no_worker_id = True
|
|
@@ -941,7 +958,11 @@ class CalculationWorker(QObject):
|
|
|
941
958
|
_safe_status(f"Applying force field optimization ({method_key} / OpenBabel)...")
|
|
942
959
|
opt_success = _iterative_optimize_obabel(mol, method_key, _check_halted, _safe_status)
|
|
943
960
|
if not opt_success:
|
|
944
|
-
|
|
961
|
+
_safe_status(f"Warning: Optimization with {opt_method} failed. Using unoptimized structure.")
|
|
962
|
+
try:
|
|
963
|
+
mol.ClearProp("_pme_optimization_method")
|
|
964
|
+
except Exception:
|
|
965
|
+
pass
|
|
945
966
|
else: # RDKit backend
|
|
946
967
|
try:
|
|
947
968
|
# Add property for UI feedback
|
|
@@ -951,7 +972,11 @@ class CalculationWorker(QObject):
|
|
|
951
972
|
_safe_status(f"Applying force field optimization ({method_key} / RDKit)...")
|
|
952
973
|
opt_success = _iterative_optimize(mol, method_key, _check_halted, _safe_status)
|
|
953
974
|
if not opt_success:
|
|
954
|
-
|
|
975
|
+
_safe_status(f"Warning: Optimization with {opt_method} failed. Using unoptimized structure.")
|
|
976
|
+
try:
|
|
977
|
+
mol.ClearProp("_pme_optimization_method")
|
|
978
|
+
except Exception:
|
|
979
|
+
pass
|
|
955
980
|
|
|
956
981
|
if _check_halted():
|
|
957
982
|
raise WorkerHaltError("Halted")
|
|
@@ -1089,7 +1114,7 @@ class CalculationWorker(QObject):
|
|
|
1089
1114
|
conf_id = AllChem.EmbedMolecule(mol, basic_params)
|
|
1090
1115
|
else:
|
|
1091
1116
|
conf_id = -1
|
|
1092
|
-
except Exception:
|
|
1117
|
+
except Exception: # pragma: no cover
|
|
1093
1118
|
import traceback
|
|
1094
1119
|
traceback.print_exc()
|
|
1095
1120
|
"""
|
|
@@ -1144,14 +1169,14 @@ class CalculationWorker(QObject):
|
|
|
1144
1169
|
if backend == "OBABEL":
|
|
1145
1170
|
try:
|
|
1146
1171
|
mol.SetProp("_pme_optimization_method", opt_method_raw)
|
|
1147
|
-
except Exception:
|
|
1172
|
+
except Exception: # pragma: no cover
|
|
1148
1173
|
pass
|
|
1149
1174
|
_safe_status(f"Optimizing with OpenBabel ({method_key})...")
|
|
1150
1175
|
opt_success = _iterative_optimize_obabel(mol, method_key, _check_halted, _safe_status)
|
|
1151
1176
|
else:
|
|
1152
1177
|
try:
|
|
1153
1178
|
mol.SetProp("_pme_optimization_method", opt_method_raw)
|
|
1154
|
-
except Exception:
|
|
1179
|
+
except Exception: # pragma: no cover
|
|
1155
1180
|
pass
|
|
1156
1181
|
_safe_status(f"Optimizing with RDKit ({method_key})...")
|
|
1157
1182
|
opt_success = _iterative_optimize(mol, method_key, _check_halted, _safe_status)
|
|
@@ -1161,31 +1186,39 @@ class CalculationWorker(QObject):
|
|
|
1161
1186
|
except WorkerHaltError:
|
|
1162
1187
|
raise
|
|
1163
1188
|
except Exception as opt_err:
|
|
1164
|
-
|
|
1165
|
-
|
|
1189
|
+
if conversion_mode == "rdkit":
|
|
1190
|
+
raise Exception(f"Optimization with {opt_method_raw} failed: {opt_err}")
|
|
1191
|
+
_safe_status(f"Optimization failed: {opt_err}. Falling back...") # pragma: no cover
|
|
1192
|
+
try: # pragma: no cover
|
|
1193
|
+
mol.ClearProp("_pme_optimization_method")
|
|
1194
|
+
except Exception: # pragma: no cover
|
|
1195
|
+
pass
|
|
1196
|
+
# Allow fallback to proceed instead of crashing
|
|
1197
|
+
conf_id = -1 # pragma: no cover
|
|
1166
1198
|
|
|
1167
1199
|
# CRITICAL: Restore stereochemistry again after optimization (explicit labels priority)
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1200
|
+
if conf_id != -1:
|
|
1201
|
+
for bond_idx, stereo, stereo_atoms in original_stereo_info:
|
|
1202
|
+
bond = mol.GetBondWithIdx(bond_idx)
|
|
1203
|
+
if len(stereo_atoms) == 2:
|
|
1204
|
+
bond.SetStereoAtoms(stereo_atoms[0], stereo_atoms[1])
|
|
1205
|
+
bond.SetStereo(stereo)
|
|
1173
1206
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1207
|
+
# CRITICAL: Check for halt *before* emitting finished signal
|
|
1208
|
+
if _check_halted():
|
|
1209
|
+
raise WorkerHaltError("Halted")
|
|
1177
1210
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1211
|
+
# Collision avoidance
|
|
1212
|
+
_adjust_collision_avoidance(mol, _check_halted, _safe_status)
|
|
1180
1213
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1214
|
+
try:
|
|
1215
|
+
_safe_finished((worker_id, mol))
|
|
1216
|
+
except WorkerHaltError:
|
|
1217
|
+
raise
|
|
1218
|
+
except Exception:
|
|
1219
|
+
_safe_finished(mol)
|
|
1220
|
+
_safe_status("RDKit 3D conversion succeeded.")
|
|
1221
|
+
return
|
|
1189
1222
|
|
|
1190
1223
|
# If RDKit did not produce a conf and OBabel is allowed, try Open Babel
|
|
1191
1224
|
if conf_id == -1 and conversion_mode in ("fallback", "obabel"):
|
|
@@ -1241,12 +1274,19 @@ class CalculationWorker(QObject):
|
|
|
1241
1274
|
opt_success = _iterative_optimize(rd_mol, method_key, _check_halted, _safe_status)
|
|
1242
1275
|
|
|
1243
1276
|
if not opt_success:
|
|
1244
|
-
|
|
1277
|
+
_safe_status(f"Warning: Optimization with {opt_method_raw} failed. Using unoptimized structure.")
|
|
1278
|
+
try:
|
|
1279
|
+
rd_mol.ClearProp("_pme_optimization_method")
|
|
1280
|
+
except Exception:
|
|
1281
|
+
pass
|
|
1245
1282
|
except WorkerHaltError:
|
|
1246
1283
|
raise
|
|
1247
1284
|
except Exception as opt_err:
|
|
1248
|
-
_safe_status(f"Optimization failed: {opt_err}")
|
|
1249
|
-
|
|
1285
|
+
_safe_status(f"Warning: Optimization failed: {opt_err}. Using unoptimized structure.")
|
|
1286
|
+
try:
|
|
1287
|
+
rd_mol.ClearProp("_pme_optimization_method")
|
|
1288
|
+
except Exception:
|
|
1289
|
+
pass
|
|
1250
1290
|
|
|
1251
1291
|
_safe_status(
|
|
1252
1292
|
"Open Babel embedding succeeded. Warning: Conformation accuracy may be limited."
|
|
@@ -182,12 +182,6 @@ class DihedralDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
182
182
|
self.disable_picking()
|
|
183
183
|
super().closeEvent(event)
|
|
184
184
|
|
|
185
|
-
def reject(self):
|
|
186
|
-
"""キャンセル時の処理"""
|
|
187
|
-
self.clear_atom_labels()
|
|
188
|
-
self.disable_picking()
|
|
189
|
-
super().reject()
|
|
190
|
-
|
|
191
185
|
def accept(self):
|
|
192
186
|
"""OK時の処理"""
|
|
193
187
|
self.clear_atom_labels()
|
|
@@ -511,6 +505,9 @@ class DihedralDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
511
505
|
self.main_window.draw_molecule_3d(self.mol)
|
|
512
506
|
|
|
513
507
|
def reject(self):
|
|
508
|
+
"""キャンセル時の処理"""
|
|
509
|
+
self.clear_atom_labels()
|
|
510
|
+
self.disable_picking()
|
|
514
511
|
super().reject()
|
|
515
512
|
try:
|
|
516
513
|
if self.main_window.current_mol:
|
|
@@ -51,6 +51,9 @@ except Exception:
|
|
|
51
51
|
# --- クラス定義 ---
|
|
52
52
|
class MainWindowCompute(object):
|
|
53
53
|
"""main_window.py から分離された機能クラス"""
|
|
54
|
+
|
|
55
|
+
# Default initial state
|
|
56
|
+
last_successful_optimization_method = None
|
|
54
57
|
|
|
55
58
|
def set_optimization_method(self, method_name): # pragma: no cover
|
|
56
59
|
"""Set preferred 3D optimization method and persist to settings.
|
|
@@ -67,6 +70,7 @@ class MainWindowCompute(object):
|
|
|
67
70
|
"UFF_RDKIT",
|
|
68
71
|
"UFF_OBABEL",
|
|
69
72
|
"GAFF_OBABEL",
|
|
73
|
+
"MMFF94s_OBABEL",
|
|
70
74
|
"MMFF94_OBABEL",
|
|
71
75
|
"GHEMICAL_OBABEL",
|
|
72
76
|
)
|
|
@@ -166,6 +170,7 @@ class MainWindowCompute(object):
|
|
|
166
170
|
("MMFF94s (RDKit)", "MMFF_RDKIT"),
|
|
167
171
|
("MMFF94 (RDKit)", "MMFF94_RDKIT"),
|
|
168
172
|
("UFF (RDKit)", "UFF_RDKIT"),
|
|
173
|
+
("MMFF94s (Open Babel)", "MMFF94s_OBABEL"),
|
|
169
174
|
("MMFF94 (Open Babel)", "MMFF94_OBABEL"),
|
|
170
175
|
("UFF (Open Babel)", "UFF_OBABEL"),
|
|
171
176
|
("GAFF (Open Babel)", "GAFF_OBABEL"),
|
|
@@ -1022,8 +1027,8 @@ class MainWindowCompute(object):
|
|
|
1022
1027
|
except Exception: # pragma: no cover
|
|
1023
1028
|
import traceback
|
|
1024
1029
|
traceback.print_exc()
|
|
1025
|
-
|
|
1026
|
-
|
|
1030
|
+
# If the property is not on the molecule, it means optimization failed or was bypassed.
|
|
1031
|
+
# We explicitly do not fall back to self.optimization_method.
|
|
1027
1032
|
# Store the optimization method using its user-friendly UI label if available.
|
|
1028
1033
|
if opt_method:
|
|
1029
1034
|
try:
|
|
@@ -1033,6 +1038,8 @@ class MainWindowCompute(object):
|
|
|
1033
1038
|
self.last_successful_optimization_method = label
|
|
1034
1039
|
except Exception:
|
|
1035
1040
|
self.last_successful_optimization_method = opt_method
|
|
1041
|
+
else:
|
|
1042
|
+
self.last_successful_optimization_method = None
|
|
1036
1043
|
except Exception:
|
|
1037
1044
|
# non-fatal
|
|
1038
1045
|
pass
|
|
@@ -1458,6 +1458,7 @@ class MainWindowMainInit(object):
|
|
|
1458
1458
|
("MMFF94s (RDKit)", "MMFF_RDKIT"),
|
|
1459
1459
|
("MMFF94 (RDKit)", "MMFF94_RDKIT"),
|
|
1460
1460
|
("UFF (RDKit)", "UFF_RDKIT"),
|
|
1461
|
+
("MMFF94s (Open Babel)", "MMFF94s_OBABEL"),
|
|
1461
1462
|
("MMFF94 (Open Babel)", "MMFF94_OBABEL"),
|
|
1462
1463
|
("UFF (Open Babel)", "UFF_OBABEL"),
|
|
1463
1464
|
("GAFF (Open Babel)", "GAFF_OBABEL"),
|
|
@@ -1484,6 +1485,9 @@ class MainWindowMainInit(object):
|
|
|
1484
1485
|
import traceback
|
|
1485
1486
|
traceback.print_exc()
|
|
1486
1487
|
|
|
1488
|
+
# If Open Babel is not available, disable Open Babel-based optimization methods
|
|
1489
|
+
if key.endswith("_OBABEL") and not OBABEL_AVAILABLE:
|
|
1490
|
+
action.setEnabled(False)
|
|
1487
1491
|
action.triggered.connect(
|
|
1488
1492
|
lambda checked, m=key: self.set_optimization_method(m)
|
|
1489
1493
|
)
|
|
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
|
|
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
|
|
File without changes
|
{moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/constrained_optimization_dialog.py
RENAMED
|
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
|
{moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_molecular_parsers.py
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|