MoleditPy-linux 2.6.2__tar.gz → 2.7.0__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.
Files changed (66) hide show
  1. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/PKG-INFO +11 -1
  2. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/README.md +9 -0
  3. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/pyproject.toml +2 -1
  4. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/PKG-INFO +11 -1
  5. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/analysis_window.py +1 -0
  6. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/calculation_worker.py +296 -44
  7. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/constants.py +1 -1
  8. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/custom_interactor_style.py +2 -5
  9. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window.py +0 -7
  10. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_app_state.py +1 -6
  11. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_compute.py +191 -175
  12. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_dialog_manager.py +0 -9
  13. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_edit_3d.py +0 -1
  14. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_edit_actions.py +14 -24
  15. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_export.py +0 -9
  16. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_main_init.py +7 -16
  17. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_molecular_parsers.py +2 -13
  18. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_project_io.py +0 -22
  19. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_string_importers.py +0 -17
  20. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_ui_manager.py +0 -9
  21. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_view_3d.py +0 -8
  22. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/main_window_view_loaders.py +0 -10
  23. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/molecule_scene.py +0 -1
  24. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/LICENSE +0 -0
  25. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/setup.cfg +0 -0
  26. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  27. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  28. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  29. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  30. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  31. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/__init__.py +0 -0
  32. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/__main__.py +0 -0
  33. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  34. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/assets/icon.icns +0 -0
  35. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/assets/icon.ico +0 -0
  36. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/assets/icon.png +0 -0
  37. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/main.py +0 -0
  38. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/__init__.py +0 -0
  39. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/about_dialog.py +0 -0
  40. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/align_plane_dialog.py +0 -0
  41. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/alignment_dialog.py +0 -0
  42. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/angle_dialog.py +0 -0
  43. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/atom_item.py +0 -0
  44. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/bond_item.py +0 -0
  45. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/bond_length_dialog.py +0 -0
  46. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/color_settings_dialog.py +0 -0
  47. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/constrained_optimization_dialog.py +0 -0
  48. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/custom_qt_interactor.py +0 -0
  49. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/dialog3_d_picking_mixin.py +0 -0
  50. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/dihedral_dialog.py +0 -0
  51. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/mirror_dialog.py +0 -0
  52. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/mol_geometry.py +0 -0
  53. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/molecular_data.py +0 -0
  54. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/move_group_dialog.py +0 -0
  55. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/periodic_table_dialog.py +0 -0
  56. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/planarize_dialog.py +0 -0
  57. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/plugin_interface.py +0 -0
  58. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/plugin_manager.py +0 -0
  59. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/plugin_manager_window.py +0 -0
  60. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/settings_dialog.py +0 -0
  61. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/sip_isdeleted_safe.py +0 -0
  62. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/template_preview_item.py +0 -0
  63. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/template_preview_view.py +0 -0
  64. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/translation_dialog.py +0 -0
  65. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/user_template_dialog.py +0 -0
  66. {moleditpy_linux-2.6.2 → moleditpy_linux-2.7.0}/src/moleditpy_linux/modules/zoomable_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 2.6.2
3
+ Version: 2.7.0
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
@@ -689,6 +689,7 @@ Classifier: Programming Language :: Python :: 3.10
689
689
  Classifier: Programming Language :: Python :: 3.11
690
690
  Classifier: Programming Language :: Python :: 3.12
691
691
  Classifier: Programming Language :: Python :: 3.13
692
+ Classifier: Programming Language :: Python :: 3.14
692
693
  Requires-Python: <3.15,>=3.9
693
694
  Description-Content-Type: text/markdown
694
695
  License-File: LICENSE
@@ -705,6 +706,15 @@ This is the Linux version of MoleditPy. The Open Babel fallback is disabled due
705
706
 
706
707
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17268532.svg)](https://doi.org/10.5281/zenodo.17268532)
707
708
  [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/)
709
+ [![PyPI version](https://badge.fury.io/py/MoleditPy.svg)](https://badge.fury.io/py/MoleditPy)
710
+ [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
+ [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
714
+ ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
715
+ ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
717
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
708
718
 
709
719
  [🇯🇵 日本語 (Japanese)](#japanese)
710
720
 
@@ -4,6 +4,15 @@ This is the Linux version of MoleditPy. The Open Babel fallback is disabled due
4
4
 
5
5
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17268532.svg)](https://doi.org/10.5281/zenodo.17268532)
6
6
  [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/)
7
+ [![PyPI version](https://badge.fury.io/py/MoleditPy.svg)](https://badge.fury.io/py/MoleditPy)
8
+ [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
9
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
10
+ [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
11
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
12
+ ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
13
+ ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
14
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
15
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
7
16
 
8
17
  [🇯🇵 日本語 (Japanese)](#japanese)
9
18
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "2.6.2"
8
+ version = "2.7.0"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -29,6 +29,7 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.11",
30
30
  "Programming Language :: Python :: 3.12",
31
31
  "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
32
33
  ]
33
34
 
34
35
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 2.6.2
3
+ Version: 2.7.0
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
@@ -689,6 +689,7 @@ Classifier: Programming Language :: Python :: 3.10
689
689
  Classifier: Programming Language :: Python :: 3.11
690
690
  Classifier: Programming Language :: Python :: 3.12
691
691
  Classifier: Programming Language :: Python :: 3.13
692
+ Classifier: Programming Language :: Python :: 3.14
692
693
  Requires-Python: <3.15,>=3.9
693
694
  Description-Content-Type: text/markdown
694
695
  License-File: LICENSE
@@ -705,6 +706,15 @@ This is the Linux version of MoleditPy. The Open Babel fallback is disabled due
705
706
 
706
707
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17268532.svg)](https://doi.org/10.5281/zenodo.17268532)
707
708
  [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/)
709
+ [![PyPI version](https://badge.fury.io/py/MoleditPy.svg)](https://badge.fury.io/py/MoleditPy)
710
+ [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
+ [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
714
+ ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
715
+ ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
717
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
708
718
 
709
719
  [🇯🇵 日本語 (Japanese)](#japanese)
710
720
 
@@ -20,6 +20,7 @@ from PyQt6.QtWidgets import (
20
20
  QPushButton,
21
21
  QVBoxLayout,
22
22
  )
23
+
23
24
  from rdkit import Chem
24
25
  from rdkit.Chem import Descriptors, rdMolDescriptors
25
26
  from rdkit.Chem import inchi as rd_inchi
@@ -12,6 +12,7 @@ DOI: 10.5281/zenodo.17268532
12
12
 
13
13
  import math
14
14
  import re
15
+ import numpy as np
15
16
 
16
17
  from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
17
18
 
@@ -39,6 +40,174 @@ else:
39
40
  pybel = None
40
41
 
41
42
 
43
+ class WorkerHaltError(Exception):
44
+ """Custom exception raised when a calculation worker is requested to halt."""
45
+ pass
46
+
47
+
48
+ def _adjust_collision_avoidance(rd_mol, check_halted_cb, safe_status_cb):
49
+ """
50
+ Optimized collision avoidance using spatial partitioning (grid-based).
51
+ This avoids the O(F^2 * N^2) complexity of the previous implementation.
52
+ """
53
+ try:
54
+ frags = Chem.GetMolFrags(rd_mol, asMols=False, sanitizeFrags=False)
55
+ if len(frags) <= 1:
56
+ return
57
+
58
+ safe_status_cb(f"Resolving potential collisions among {len(frags)} fragments...")
59
+
60
+ conf = rd_mol.GetConformer()
61
+ pt = Chem.GetPeriodicTable()
62
+
63
+ # 1. Precalculate fragment data
64
+ frag_data = []
65
+ for f_indices in frags:
66
+ pos_list = []
67
+ radii_list = []
68
+ for idx in f_indices:
69
+ p = conf.GetAtomPosition(idx)
70
+ pos_list.append([p.x, p.y, p.z])
71
+ try:
72
+ radii_list.append(pt.GetRvdw(rd_mol.GetAtomWithIdx(idx).GetAtomicNum()))
73
+ except Exception:
74
+ radii_list.append(1.5)
75
+
76
+ pos_np = np.array(pos_list)
77
+ radii_np = np.array(radii_list)
78
+ frag_data.append({
79
+ "indices": f_indices,
80
+ "positions": pos_np,
81
+ "radii": radii_np,
82
+ "max_radius": np.max(radii_np) if len(radii_np) > 0 else 1.5
83
+ })
84
+
85
+ # Parameters
86
+ scale = 1.1 # Collision threshold scale
87
+ max_iters = 50
88
+ grid_size = 5.0
89
+
90
+ for iteration in range(max_iters):
91
+ if check_halted_cb():
92
+ raise WorkerHaltError("Halted")
93
+
94
+ moved = False
95
+ grid = {}
96
+ for i, fd in enumerate(frag_data):
97
+ fd_min = np.min(fd["positions"], axis=0)
98
+ fd_max = np.max(fd["positions"], axis=0)
99
+ margin = fd["max_radius"] * scale
100
+ fd["bbox_min"] = fd_min - margin
101
+ fd["bbox_max"] = fd_max + margin
102
+ g_min = (fd["bbox_min"] / grid_size).astype(int)
103
+ g_max = (fd["bbox_max"] / grid_size).astype(int)
104
+ for gx in range(g_min[0], g_max[0] + 1):
105
+ for gy in range(g_min[1], g_max[1] + 1):
106
+ for gz in range(g_min[2], g_max[2] + 1):
107
+ cell = (gx, gy, gz)
108
+ if cell not in grid:
109
+ grid[cell] = []
110
+ grid[cell].append(i)
111
+
112
+ all_push_vectors = [np.zeros(3) for _ in range(len(frag_data))]
113
+ processed_pairs = set()
114
+
115
+ for cell_indices in grid.values():
116
+ if len(cell_indices) < 2:
117
+ continue
118
+ for idx_idx_i, i in enumerate(cell_indices):
119
+ for j in cell_indices[idx_idx_i + 1:]:
120
+ pair = tuple(sorted((i, j)))
121
+ if pair in processed_pairs:
122
+ continue
123
+ processed_pairs.add(pair)
124
+ if np.any(frag_data[i]["bbox_min"] > frag_data[j]["bbox_max"]) or \
125
+ np.any(frag_data[j]["bbox_min"] > frag_data[i]["bbox_max"]):
126
+ continue
127
+ fd_i = frag_data[i]
128
+ fd_j = frag_data[j]
129
+ push_i = np.zeros(3)
130
+ push_j = np.zeros(3)
131
+ collision_count = 0
132
+ for p_idx_i, p_i in enumerate(fd_i["positions"]):
133
+ r_i = fd_i["radii"][p_idx_i]
134
+ for p_idx_j, p_j in enumerate(fd_j["positions"]):
135
+ r_j = fd_j["radii"][p_idx_j]
136
+ diff = p_i - p_j
137
+ dist_sq = np.dot(diff, diff)
138
+ min_dist = (r_i + r_j) * scale
139
+ if dist_sq < 0.0001:
140
+ diff = np.random.uniform(-0.1, 0.1, 3)
141
+ dist_sq = np.dot(diff, diff)
142
+ if dist_sq < min_dist * min_dist:
143
+ dist = np.sqrt(dist_sq)
144
+ if dist < 0.0001:
145
+ dist = 0.0001
146
+ push_mag = (min_dist - dist) / 2.0
147
+ vec = (diff / dist) * push_mag
148
+ push_i += vec
149
+ push_j -= vec
150
+ collision_count += 1
151
+ if collision_count > 0:
152
+ all_push_vectors[i] += push_i / collision_count
153
+ all_push_vectors[j] += push_j / collision_count
154
+ moved = True
155
+
156
+ if not moved:
157
+ break
158
+
159
+ for i, push in enumerate(all_push_vectors):
160
+ if np.any(push != 0):
161
+ frag_data[i]["positions"] += push
162
+ for local_idx, global_idx in enumerate(frag_data[i]["indices"]):
163
+ conf.SetAtomPosition(global_idx, frag_data[i]["positions"][local_idx].tolist())
164
+
165
+ safe_status_cb("Collision avoidance completed.")
166
+ except WorkerHaltError:
167
+ raise
168
+ except Exception as e:
169
+ import traceback
170
+ traceback.print_exc()
171
+ safe_status_cb(f"Collision avoidance warning: {e}")
172
+
173
+
174
+ def _iterative_optimize(mol, method, check_halted_cb, safe_status_cb, max_iters=4000, chunk_size=100):
175
+ """Perform force field optimization in small chunks to avoid UI freezing and allow halts."""
176
+ try:
177
+ if method in ("MMFF", "MMFF94", "MMFF94S", "MMFF94s"):
178
+ mmff_variant = "MMFF94" if method == "MMFF94" else "MMFF94s"
179
+ props = AllChem.MMFFGetMoleculeProperties(mol, mmffVariant=mmff_variant)
180
+ if props is None:
181
+ return False
182
+ ff = AllChem.MMFFGetMoleculeForceField(mol, props, confId=0)
183
+ elif method == "UFF":
184
+ ff = AllChem.UFFGetMoleculeForceField(mol, confId=0)
185
+ else:
186
+ return False
187
+
188
+ if ff is None:
189
+ return False
190
+
191
+ ff.Initialize()
192
+ iters_done = 0
193
+ while iters_done < max_iters:
194
+ if check_halted_cb():
195
+ raise WorkerHaltError("Halted")
196
+ res = ff.Minimize(maxIts=chunk_size)
197
+ iters_done += chunk_size
198
+ if res == 0:
199
+ break
200
+ import time
201
+ time.sleep(0.001)
202
+
203
+ return True
204
+ except WorkerHaltError:
205
+ raise
206
+ except Exception as e:
207
+ safe_status_cb(f"Iterative optimization ({method}) error: {e}")
208
+ return False
209
+
210
+
42
211
  class CalculationWorker(QObject):
43
212
  status_update = pyqtSignal(str)
44
213
  finished = pyqtSignal(object)
@@ -80,44 +249,57 @@ class CalculationWorker(QObject):
80
249
  def _safe_status(msg):
81
250
  try:
82
251
  if _check_halted():
83
- return
252
+ raise WorkerHaltError("Halted")
84
253
  self.status_update.emit(msg)
85
- except Exception: # pragma: no cover
254
+ except WorkerHaltError:
255
+ raise
256
+ except Exception:
86
257
  # Swallow any signal-emission errors to avoid crashing the worker
87
258
  pass
88
259
 
89
260
  def _safe_finished(payload): # pragma: no cover
90
261
  try:
91
- # Attempt to emit the payload; preserve existing fallback behavior
262
+ if _check_halted():
263
+ raise WorkerHaltError("Halted")
92
264
  try:
93
265
  self.finished.emit(payload)
266
+ except WorkerHaltError:
267
+ raise
94
268
  except TypeError:
95
- # Some slots/old code may expect a single-molecule arg; try that too
96
269
  try:
97
- # If payload was a tuple like (worker_id, mol), try sending the second element
98
270
  if isinstance(payload, (list, tuple)) and len(payload) >= 2:
99
271
  self.finished.emit(payload[1])
100
272
  else:
101
273
  self.finished.emit(payload)
274
+ except WorkerHaltError:
275
+ raise
102
276
  except Exception: # pragma: no cover
103
277
  import traceback
104
278
  traceback.print_exc()
279
+ except WorkerHaltError:
280
+ raise
105
281
  except Exception: # pragma: no cover
106
282
  import traceback
107
283
  traceback.print_exc()
108
284
 
109
285
  def _safe_error(msg): # pragma: no cover
110
286
  try:
111
- # Emit a tuple containing the worker_id (may be None) and the message
287
+ if msg != "Halted" and _check_halted():
288
+ raise WorkerHaltError("Halted")
112
289
  try:
113
290
  self.error.emit((worker_id, msg))
291
+ except WorkerHaltError:
292
+ raise
114
293
  except Exception:
115
- # Fallback to emitting the raw message if tuple emission fails for any reason
116
294
  try:
117
295
  self.error.emit(msg)
296
+ except WorkerHaltError:
297
+ raise
118
298
  except Exception: # pragma: no cover
119
299
  import traceback
120
300
  traceback.print_exc()
301
+ except WorkerHaltError:
302
+ raise
121
303
  except Exception: # pragma: no cover
122
304
  import traceback
123
305
  traceback.print_exc()
@@ -157,7 +339,7 @@ class CalculationWorker(QObject):
157
339
 
158
340
  # Check early whether this run has been requested to halt
159
341
  if _check_halted():
160
- raise RuntimeError("Halted")
342
+ raise WorkerHaltError("Halted")
161
343
 
162
344
  explicit_stereo = {}
163
345
  mol_lines = mol_block.split("\n")
@@ -222,7 +404,30 @@ class CalculationWorker(QObject):
222
404
 
223
405
  # Check after adding Hs (may be a long operation)
224
406
  if _check_halted():
225
- raise RuntimeError("Halted")
407
+ raise WorkerHaltError("Halted")
408
+
409
+ # Support for optimize_only mode
410
+ if conversion_mode == "optimize_only":
411
+ _safe_status("Optimizing existing 3D structure...")
412
+ opt_method = str(options.get("optimization_method", "MMFF94s")).upper()
413
+ if "MMFF" in opt_method:
414
+ method_key = "MMFF94" if "MMFF94" in opt_method and "MMFF94S" not in opt_method else "MMFF94s"
415
+ if not _iterative_optimize(mol, method_key, _check_halted, _safe_status):
416
+ _safe_status(f"{method_key} failed, falling back to UFF...")
417
+ _iterative_optimize(mol, "UFF", _check_halted, _safe_status)
418
+ elif "UFF" in opt_method:
419
+ _iterative_optimize(mol, "UFF", _check_halted, _safe_status)
420
+ else:
421
+ if not _iterative_optimize(mol, "MMFF94s", _check_halted, _safe_status):
422
+ _iterative_optimize(mol, "UFF", _check_halted, _safe_status)
423
+ if _check_halted():
424
+ raise WorkerHaltError("Halted")
425
+ try:
426
+ _safe_finished((worker_id, mol))
427
+ except Exception:
428
+ _safe_finished(mol)
429
+ _safe_status("Optimization completed.")
430
+ return
226
431
 
227
432
  # CRITICAL: Re-apply explicit stereo after AddHs which may renumber atoms
228
433
  for bond_idx, stereo_type in explicit_stereo.items():
@@ -617,14 +822,35 @@ class CalculationWorker(QObject):
617
822
  traceback.print_exc()
618
823
  mol.AddConformer(conf, assignId=True)
619
824
 
825
+ # Optimization (respects do_optimize flag)
826
+ do_optimize = options.get("do_optimize", True) if options else True
827
+ if do_optimize:
828
+ _safe_status("Direct conversion: optimizing geometry...")
829
+ if _check_halted():
830
+ raise WorkerHaltError("Halted")
831
+ mmff_method = "MMFF94s"
832
+ if options and str(options.get("optimization_method", "")).upper() == "MMFF94_RDKIT":
833
+ mmff_method = "MMFF94"
834
+ if not _iterative_optimize(mol, mmff_method, _check_halted, _safe_status):
835
+ if _check_halted():
836
+ raise WorkerHaltError("Halted")
837
+ _iterative_optimize(mol, "UFF", _check_halted, _safe_status)
838
+
620
839
  if _check_halted():
621
- raise RuntimeError("Halted (after optimization)")
840
+ raise WorkerHaltError("Halted")
841
+ if do_optimize:
842
+ _adjust_collision_avoidance(mol, _check_halted, _safe_status)
843
+
622
844
  try:
623
845
  _safe_finished((worker_id, mol))
846
+ except WorkerHaltError:
847
+ raise
624
848
  except Exception:
625
849
  _safe_finished(mol)
626
850
  _safe_status("Direct conversion completed.")
627
851
  return
852
+ except WorkerHaltError:
853
+ raise
628
854
  except Exception as e:
629
855
  _safe_status(f"Direct conversion failed: {e}")
630
856
 
@@ -666,7 +892,7 @@ class CalculationWorker(QObject):
666
892
  # direct mode (or any other explicit non-RDKit mode)
667
893
  pass
668
894
  if _check_halted():
669
- raise RuntimeError("Halted")
895
+ raise WorkerHaltError("Halted")
670
896
 
671
897
  # Try multiple times with different approaches if needed
672
898
  conf_id = -1
@@ -680,7 +906,9 @@ class CalculationWorker(QObject):
680
906
  conf_id = -1
681
907
  # Final check before returning success
682
908
  if _check_halted():
683
- raise RuntimeError("Halted")
909
+ raise WorkerHaltError("Halted")
910
+ except WorkerHaltError:
911
+ raise
684
912
  except Exception as e:
685
913
  # Standard embedding failed; report and continue to fallback attempts
686
914
  _safe_status(f"Standard embedding failed: {e}")
@@ -783,21 +1011,17 @@ class CalculationWorker(QObject):
783
1011
  bond.SetStereo(stereo)
784
1012
 
785
1013
  try:
786
- mmff_variant = "MMFF94s"
1014
+ mmff_method = "MMFF94s"
787
1015
  if opt_method and str(opt_method).upper() == "MMFF94_RDKIT":
788
- mmff_variant = "MMFF94"
789
- if _check_halted():
790
- raise RuntimeError("Halted")
791
- AllChem.MMFFOptimizeMolecule(mol, mmffVariant=mmff_variant)
792
- except Exception:
793
- # fallback to UFF if MMFF fails
794
- try:
1016
+ mmff_method = "MMFF94"
1017
+ if not _iterative_optimize(mol, mmff_method, _check_halted, _safe_status):
795
1018
  if _check_halted():
796
- raise RuntimeError("Halted")
797
- AllChem.UFFOptimizeMolecule(mol)
798
- except Exception: # pragma: no cover
799
- import traceback
800
- traceback.print_exc()
1019
+ raise WorkerHaltError("Halted")
1020
+ _iterative_optimize(mol, "UFF", _check_halted, _safe_status)
1021
+ except WorkerHaltError:
1022
+ raise
1023
+ except Exception as opt_err:
1024
+ _safe_status(f"RDKit optimization failed (ignoring): {opt_err}")
801
1025
  # CRITICAL: Restore stereochemistry again after optimization (explicit labels priority)
802
1026
  for bond_idx, stereo, stereo_atoms in original_stereo_info:
803
1027
  bond = mol.GetBondWithIdx(bond_idx)
@@ -805,15 +1029,18 @@ class CalculationWorker(QObject):
805
1029
  bond.SetStereoAtoms(stereo_atoms[0], stereo_atoms[1])
806
1030
  bond.SetStereo(stereo)
807
1031
 
808
- # Do NOT call AssignStereochemistry here as it would override our explicit labels
809
- # Include worker_id so the main thread can ignore stale results
810
1032
  # CRITICAL: Check for halt *before* emitting finished signal
811
1033
  if _check_halted():
812
- raise RuntimeError("Halted (after optimization)")
1034
+ raise WorkerHaltError("Halted")
1035
+
1036
+ # Collision avoidance
1037
+ _adjust_collision_avoidance(mol, _check_halted, _safe_status)
1038
+
813
1039
  try:
814
1040
  _safe_finished((worker_id, mol))
1041
+ except WorkerHaltError:
1042
+ raise
815
1043
  except Exception:
816
- # Fallback to legacy single-arg emit
817
1044
  _safe_finished(mol)
818
1045
  _safe_status("RDKit 3D conversion succeeded.")
819
1046
  return
@@ -838,13 +1065,13 @@ class CalculationWorker(QObject):
838
1065
  try:
839
1066
  _safe_status("Optimizing with Open Babel (MMFF94)...")
840
1067
  if _check_halted():
841
- raise RuntimeError("Halted")
1068
+ raise WorkerHaltError("Halted")
842
1069
  ob_mol.localopt(forcefield="mmff94", steps=500)
843
1070
  except Exception:
844
1071
  try:
845
1072
  _safe_status("MMFF94 failed, falling back to UFF...")
846
1073
  if _check_halted():
847
- raise RuntimeError("Halted")
1074
+ raise WorkerHaltError("Halted")
848
1075
  ob_mol.localopt(forcefield="uff", steps=500)
849
1076
  except Exception:
850
1077
  _safe_status("UFF optimization also failed.")
@@ -854,36 +1081,61 @@ class CalculationWorker(QObject):
854
1081
  raise ValueError("Open Babel produced invalid MOL block.")
855
1082
  rd_mol = Chem.AddHs(rd_mol)
856
1083
  try:
857
- mmff_variant = "MMFF94s"
1084
+ mmff_method = "MMFF94s"
858
1085
  if opt_method and str(opt_method).upper() == "MMFF94_RDKIT":
859
- mmff_variant = "MMFF94"
1086
+ mmff_method = "MMFF94"
860
1087
  if _check_halted():
861
- raise RuntimeError("Halted")
862
- AllChem.MMFFOptimizeMolecule(rd_mol, mmffVariant=mmff_variant)
863
- except Exception:
864
- try:
1088
+ raise WorkerHaltError("Halted")
1089
+ if not _iterative_optimize(rd_mol, mmff_method, _check_halted, _safe_status):
865
1090
  if _check_halted():
866
- raise RuntimeError("Halted")
867
- AllChem.UFFOptimizeMolecule(rd_mol)
868
- except Exception: # pragma: no cover
869
- import traceback
870
- traceback.print_exc()
1091
+ raise WorkerHaltError("Halted")
1092
+ _iterative_optimize(rd_mol, "UFF", _check_halted, _safe_status)
1093
+ except WorkerHaltError:
1094
+ raise
1095
+ except Exception: # pragma: no cover
1096
+ import traceback
1097
+ traceback.print_exc()
871
1098
  _safe_status(
872
1099
  "Open Babel embedding succeeded. Warning: Conformation accuracy may be limited."
873
1100
  )
874
1101
  # CRITICAL: Check for halt *before* emitting finished signal
875
1102
  if _check_halted():
876
- raise RuntimeError("Halted (after optimization)")
1103
+ raise WorkerHaltError("Halted")
1104
+
1105
+ # Collision avoidance
1106
+ _adjust_collision_avoidance(rd_mol, _check_halted, _safe_status)
877
1107
  try:
878
1108
  _safe_finished((worker_id, rd_mol))
879
1109
  except Exception:
880
1110
  _safe_finished(rd_mol)
881
1111
  return
1112
+ except WorkerHaltError:
1113
+ raise
882
1114
  except Exception as ob_err:
883
- raise RuntimeError(f"Open Babel 3D conversion failed: {ob_err}")
1115
+ if conversion_mode == "obabel":
1116
+ # obabel-only mode: no further fallback
1117
+ raise RuntimeError(f"Open Babel 3D conversion failed: {ob_err}")
1118
+ # fallback mode: continue to direct conversion below
1119
+ _safe_status(
1120
+ f"Open Babel unavailable or failed ({ob_err}). "
1121
+ "Falling back to direct conversion..."
1122
+ )
884
1123
 
885
1124
  if conf_id == -1 and conversion_mode == "rdkit":
886
1125
  raise RuntimeError("RDKit 3D conversion failed (rdkit-only mode)")
887
1126
 
1127
+ # --- Last-resort fallback: direct conversion ---
1128
+ if conf_id == -1 and conversion_mode == "fallback":
1129
+ _safe_status(
1130
+ "All embedding methods failed. Using direct conversion as last resort..."
1131
+ )
1132
+ direct_opts = dict(options) if options else {}
1133
+ direct_opts["conversion_mode"] = "direct"
1134
+ self.run_calculation(mol_block, direct_opts)
1135
+ return
1136
+
1137
+ except WorkerHaltError:
1138
+ _safe_error("Halted")
1139
+ return
888
1140
  except Exception as e:
889
1141
  _safe_error(str(e))
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QColor, QFont
16
16
  from rdkit import Chem
17
17
 
18
18
  # Version
19
- VERSION = "2.6.2"
19
+ VERSION = "2.7.0"
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -599,11 +599,11 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
599
599
  try:
600
600
  pos = mw.atom_positions_3d[i]
601
601
  conf.SetAtomPosition(i, pos.tolist())
602
- except Exception: # pragma: no cover
602
+ except Exception:
603
603
  # Skip individual failures but continue applying
604
604
  # other atom positions.
605
605
  pass
606
- except Exception: # pragma: no cover
606
+ except Exception:
607
607
  # If applying positions fails, continue to redraw from
608
608
  # whatever authoritative state is available.
609
609
  pass
@@ -640,9 +640,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
640
640
  except Exception: # pragma: no cover
641
641
  import traceback
642
642
  traceback.print_exc()
643
- except Exception: # pragma: no cover
644
- # Do not allow a failure here to interrupt release flow
645
- pass
646
643
  else:
647
644
  # カメラ回転の後始末を親クラスに任せます
648
645
  super().OnLeftButtonUp()
@@ -12,19 +12,12 @@ DOI: 10.5281/zenodo.17268532
12
12
 
13
13
  import traceback
14
14
 
15
- # RDKit imports (explicit to satisfy flake8 and used features)
16
- try:
17
- pass
18
- except Exception: # pragma: no cover
19
- traceback.print_exc()
20
-
21
15
  # PyQt6 Modules
22
16
  from PyQt6.QtCore import pyqtSignal, pyqtSlot
23
17
  from PyQt6.QtWidgets import QMainWindow
24
18
 
25
19
  try:
26
20
  from PyQt6 import sip as _sip # type: ignore
27
-
28
21
  _sip_isdeleted = getattr(_sip, "isdeleted", None)
29
22
  except Exception:
30
23
  _sip = None