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.
Files changed (66) hide show
  1. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/PKG-INFO +5 -4
  2. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/README.md +2 -2
  3. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/pyproject.toml +3 -2
  4. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/PKG-INFO +5 -4
  5. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/requires.txt +6 -1
  6. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/angle_dialog.py +21 -8
  7. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/bond_length_dialog.py +3 -6
  8. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/calculation_worker.py +88 -48
  9. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/constants.py +1 -1
  10. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/dihedral_dialog.py +3 -6
  11. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_compute.py +9 -2
  12. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_main_init.py +4 -0
  13. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/LICENSE +0 -0
  14. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/setup.cfg +0 -0
  15. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  16. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  17. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  18. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
  19. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/__init__.py +0 -0
  20. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/__main__.py +0 -0
  21. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/assets/file_icon.ico +0 -0
  22. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/assets/icon.icns +0 -0
  23. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/assets/icon.ico +0 -0
  24. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/assets/icon.png +0 -0
  25. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/main.py +0 -0
  26. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/__init__.py +0 -0
  27. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/about_dialog.py +0 -0
  28. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/align_plane_dialog.py +0 -0
  29. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/alignment_dialog.py +0 -0
  30. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/analysis_window.py +0 -0
  31. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/atom_item.py +0 -0
  32. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/bond_item.py +0 -0
  33. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/color_settings_dialog.py +0 -0
  34. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
  35. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/custom_interactor_style.py +0 -0
  36. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
  37. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
  38. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window.py +0 -0
  39. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_app_state.py +0 -0
  40. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
  41. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
  42. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
  43. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_export.py +0 -0
  44. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
  45. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_project_io.py +0 -0
  46. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_string_importers.py +0 -0
  47. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
  48. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_view_3d.py +0 -0
  49. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
  50. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/mirror_dialog.py +0 -0
  51. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/mol_geometry.py +0 -0
  52. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/molecular_data.py +0 -0
  53. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/molecule_scene.py +0 -0
  54. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/move_group_dialog.py +0 -0
  55. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
  56. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/planarize_dialog.py +0 -0
  57. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_interface.py +0 -0
  58. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_manager.py +0 -0
  59. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_manager_window.py +0 -0
  60. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/settings_dialog.py +0 -0
  61. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/sip_isdeleted_safe.py +0 -0
  62. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/template_preview_item.py +0 -0
  63. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/template_preview_view.py +0 -0
  64. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/translation_dialog.py +0 -0
  65. {moleditpy-2.8.0a3 → moleditpy-2.8.1}/src/moleditpy/modules/user_template_dialog.py +0 -0
  66. {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.0a3
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.11
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
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
710
711
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
711
712
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
712
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-72%25-green)
713
714
  ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
714
715
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
715
- ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.64%2F10-brightgreen)
716
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)
717
718
 
718
719
  [🇯🇵 日本語 (Japanese)](#japanese)
@@ -6,10 +6,10 @@
6
6
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
7
7
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
8
8
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
9
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
9
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-72%25-green)
10
10
  ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
11
11
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
12
- ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
12
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.64%2F10-brightgreen)
13
13
  [![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)
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.0a3"
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.11",
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.0a3
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.11
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
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
710
711
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
711
712
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
712
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-72%25-green)
713
714
  ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
714
715
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
715
- ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.64%2F10-brightgreen)
716
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)
717
718
 
718
719
  [🇯🇵 日本語 (Japanese)](#japanese)
@@ -1,6 +1,11 @@
1
1
  numpy
2
- pyqt6<6.11
3
2
  pyvista<0.48
4
3
  pyvistaqt<0.12
5
4
  rdkit<2025.10
6
5
  openbabel-wheel<3.2
6
+
7
+ [:sys_platform != "darwin"]
8
+ pyqt6<6.11
9
+
10
+ [:sys_platform == "darwin"]
11
+ pyqt6<6.10
@@ -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
- self._snapshot_positions = self.mol.GetConformer().GetPositions().copy()
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
- self._snapshot_positions = None
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
- # It typically places parameter files in openbabel/bin/data.
37
- os.environ["BABEL_DATADIR"] = os.path.join(os.path.dirname(openbabel.__file__), "bin", "data")
38
- os.environ["BABEL_LIBDIR"] = os.path.dirname(openbabel.__file__)
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
- return False
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
- return False
212
+ raise ValueError(f"Unknown optimization method: {method}")
194
213
 
195
214
  if ff is None:
196
- return False
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
- safe_status_cb("OpenBabel is not available.")
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
- safe_status_cb(f"Forcefield '{ff_name}' not found in OpenBabel.")
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: # pragma: no cover
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: # pragma: no cover
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
- raise Exception(f"Optimization with {opt_method} failed. You can change the method in the settings.")
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
- raise Exception(f"Optimization with {opt_method} failed. You can change the method in the settings.")
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: # pragma: no cover
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
- _safe_status(f"Optimization failed: {opt_err}")
1165
- raise
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
- for bond_idx, stereo, stereo_atoms in original_stereo_info:
1169
- bond = mol.GetBondWithIdx(bond_idx)
1170
- if len(stereo_atoms) == 2:
1171
- bond.SetStereoAtoms(stereo_atoms[0], stereo_atoms[1])
1172
- bond.SetStereo(stereo)
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
- # CRITICAL: Check for halt *before* emitting finished signal
1175
- if _check_halted():
1176
- raise WorkerHaltError("Halted")
1207
+ # CRITICAL: Check for halt *before* emitting finished signal
1208
+ if _check_halted():
1209
+ raise WorkerHaltError("Halted")
1177
1210
 
1178
- # Collision avoidance
1179
- _adjust_collision_avoidance(mol, _check_halted, _safe_status)
1211
+ # Collision avoidance
1212
+ _adjust_collision_avoidance(mol, _check_halted, _safe_status)
1180
1213
 
1181
- try:
1182
- _safe_finished((worker_id, mol))
1183
- except WorkerHaltError:
1184
- raise
1185
- except Exception:
1186
- _safe_finished(mol)
1187
- _safe_status("RDKit 3D conversion succeeded.")
1188
- return
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
- raise Exception(f"Optimization with {opt_method_raw} failed. You can change the method in the settings.")
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
- raise
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."
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QColor, QFont
16
16
  from rdkit import Chem
17
17
 
18
18
  # Version
19
- VERSION = "2.8.0a3"
19
+ VERSION = "2.8.1"
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -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
- if not opt_method:
1026
- opt_method = getattr(self, "optimization_method", None)
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