moleditpy 2.8.0a4__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.0a4 → moleditpy-2.8.1}/PKG-INFO +3 -3
  2. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/README.md +2 -2
  3. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/pyproject.toml +1 -1
  4. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/MoleditPy.egg-info/PKG-INFO +3 -3
  5. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/angle_dialog.py +21 -8
  6. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/bond_length_dialog.py +3 -6
  7. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/calculation_worker.py +55 -34
  8. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/constants.py +1 -1
  9. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/dihedral_dialog.py +3 -6
  10. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_compute.py +9 -2
  11. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_main_init.py +4 -0
  12. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/LICENSE +0 -0
  13. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/setup.cfg +0 -0
  14. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  15. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  16. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  17. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/MoleditPy.egg-info/requires.txt +0 -0
  18. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
  19. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/__init__.py +0 -0
  20. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/__main__.py +0 -0
  21. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/assets/file_icon.ico +0 -0
  22. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/assets/icon.icns +0 -0
  23. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/assets/icon.ico +0 -0
  24. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/assets/icon.png +0 -0
  25. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/main.py +0 -0
  26. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/__init__.py +0 -0
  27. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/about_dialog.py +0 -0
  28. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/align_plane_dialog.py +0 -0
  29. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/alignment_dialog.py +0 -0
  30. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/analysis_window.py +0 -0
  31. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/atom_item.py +0 -0
  32. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/bond_item.py +0 -0
  33. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/color_settings_dialog.py +0 -0
  34. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
  35. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/custom_interactor_style.py +0 -0
  36. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
  37. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
  38. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window.py +0 -0
  39. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_app_state.py +0 -0
  40. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
  41. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
  42. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
  43. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_export.py +0 -0
  44. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
  45. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_project_io.py +0 -0
  46. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_string_importers.py +0 -0
  47. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
  48. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_view_3d.py +0 -0
  49. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
  50. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/mirror_dialog.py +0 -0
  51. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/mol_geometry.py +0 -0
  52. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/molecular_data.py +0 -0
  53. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/molecule_scene.py +0 -0
  54. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/move_group_dialog.py +0 -0
  55. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
  56. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/planarize_dialog.py +0 -0
  57. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_interface.py +0 -0
  58. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_manager.py +0 -0
  59. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/plugin_manager_window.py +0 -0
  60. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/settings_dialog.py +0 -0
  61. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/sip_isdeleted_safe.py +0 -0
  62. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/template_preview_item.py +0 -0
  63. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/template_preview_view.py +0 -0
  64. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/translation_dialog.py +0 -0
  65. {moleditpy-2.8.0a4 → moleditpy-2.8.1}/src/moleditpy/modules/user_template_dialog.py +0 -0
  66. {moleditpy-2.8.0a4 → 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.0a4
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
@@ -710,10 +710,10 @@ Dynamic: license-file
710
710
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
711
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
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)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-72%25-green)
714
714
  ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
715
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)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.64%2F10-brightgreen)
717
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)
718
718
 
719
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.0a4"
8
+ version = "2.8.1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.8.0a4
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
@@ -710,10 +710,10 @@ Dynamic: license-file
710
710
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
711
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
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)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-72%25-green)
714
714
  ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
715
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)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.64%2F10-brightgreen)
717
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)
718
718
 
719
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
- 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:
@@ -204,15 +204,15 @@ def _iterative_optimize(mol, method, check_halted_cb, safe_status_cb, max_iters=
204
204
  mmff_variant = "MMFF94" if method == "MMFF94" else "MMFF94s"
205
205
  props = AllChem.MMFFGetMoleculeProperties(mol, mmffVariant=mmff_variant)
206
206
  if props is None:
207
- return False
207
+ raise ValueError(f"Failed to generate MMFF properties for variant {mmff_variant}.")
208
208
  ff = AllChem.MMFFGetMoleculeForceField(mol, props, confId=0)
209
209
  elif method == "UFF":
210
210
  ff = AllChem.UFFGetMoleculeForceField(mol, confId=0)
211
211
  else:
212
- return False
212
+ raise ValueError(f"Unknown optimization method: {method}")
213
213
 
214
214
  if ff is None:
215
- return False
215
+ raise ValueError(f"Failed to setup force field for method: {method}")
216
216
 
217
217
  ff.Initialize()
218
218
  iters_done = 0
@@ -238,8 +238,7 @@ def _iterative_optimize_obabel(mol, method, check_halted_cb, safe_status_cb, max
238
238
  """Perform force field optimization using OpenBabel in chunks to avoid UI freezing."""
239
239
  try:
240
240
  if not OBABEL_AVAILABLE or pybel is None:
241
- safe_status_cb("OpenBabel is not available.")
242
- return False
241
+ raise RuntimeError("OpenBabel is not available.")
243
242
 
244
243
  ff_name = method.lower()
245
244
 
@@ -250,8 +249,7 @@ def _iterative_optimize_obabel(mol, method, check_halted_cb, safe_status_cb, max
250
249
  # Set up forcefield
251
250
  ff = pybel.ob.OBForceField.FindForceField(ff_name)
252
251
  if not ff:
253
- safe_status_cb(f"Forcefield '{ff_name}' not found in OpenBabel.")
254
- return False
252
+ raise ValueError(f"Forcefield '{ff_name}' not found in OpenBabel.")
255
253
 
256
254
  if not ff.Setup(ob_mol.OBMol):
257
255
  error_msg = f"Failed to load parameters for '{ff_name}'."
@@ -960,7 +958,11 @@ class CalculationWorker(QObject):
960
958
  _safe_status(f"Applying force field optimization ({method_key} / OpenBabel)...")
961
959
  opt_success = _iterative_optimize_obabel(mol, method_key, _check_halted, _safe_status)
962
960
  if not opt_success:
963
- 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
964
966
  else: # RDKit backend
965
967
  try:
966
968
  # Add property for UI feedback
@@ -970,7 +972,11 @@ class CalculationWorker(QObject):
970
972
  _safe_status(f"Applying force field optimization ({method_key} / RDKit)...")
971
973
  opt_success = _iterative_optimize(mol, method_key, _check_halted, _safe_status)
972
974
  if not opt_success:
973
- 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
974
980
 
975
981
  if _check_halted():
976
982
  raise WorkerHaltError("Halted")
@@ -1163,14 +1169,14 @@ class CalculationWorker(QObject):
1163
1169
  if backend == "OBABEL":
1164
1170
  try:
1165
1171
  mol.SetProp("_pme_optimization_method", opt_method_raw)
1166
- except Exception:
1172
+ except Exception: # pragma: no cover
1167
1173
  pass
1168
1174
  _safe_status(f"Optimizing with OpenBabel ({method_key})...")
1169
1175
  opt_success = _iterative_optimize_obabel(mol, method_key, _check_halted, _safe_status)
1170
1176
  else:
1171
1177
  try:
1172
1178
  mol.SetProp("_pme_optimization_method", opt_method_raw)
1173
- except Exception:
1179
+ except Exception: # pragma: no cover
1174
1180
  pass
1175
1181
  _safe_status(f"Optimizing with RDKit ({method_key})...")
1176
1182
  opt_success = _iterative_optimize(mol, method_key, _check_halted, _safe_status)
@@ -1180,31 +1186,39 @@ class CalculationWorker(QObject):
1180
1186
  except WorkerHaltError:
1181
1187
  raise
1182
1188
  except Exception as opt_err:
1183
- _safe_status(f"Optimization failed: {opt_err}")
1184
- 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
1185
1198
 
1186
1199
  # CRITICAL: Restore stereochemistry again after optimization (explicit labels priority)
1187
- for bond_idx, stereo, stereo_atoms in original_stereo_info:
1188
- bond = mol.GetBondWithIdx(bond_idx)
1189
- if len(stereo_atoms) == 2:
1190
- bond.SetStereoAtoms(stereo_atoms[0], stereo_atoms[1])
1191
- 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)
1192
1206
 
1193
- # CRITICAL: Check for halt *before* emitting finished signal
1194
- if _check_halted():
1195
- raise WorkerHaltError("Halted")
1207
+ # CRITICAL: Check for halt *before* emitting finished signal
1208
+ if _check_halted():
1209
+ raise WorkerHaltError("Halted")
1196
1210
 
1197
- # Collision avoidance
1198
- _adjust_collision_avoidance(mol, _check_halted, _safe_status)
1211
+ # Collision avoidance
1212
+ _adjust_collision_avoidance(mol, _check_halted, _safe_status)
1199
1213
 
1200
- try:
1201
- _safe_finished((worker_id, mol))
1202
- except WorkerHaltError:
1203
- raise
1204
- except Exception:
1205
- _safe_finished(mol)
1206
- _safe_status("RDKit 3D conversion succeeded.")
1207
- 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
1208
1222
 
1209
1223
  # If RDKit did not produce a conf and OBabel is allowed, try Open Babel
1210
1224
  if conf_id == -1 and conversion_mode in ("fallback", "obabel"):
@@ -1260,12 +1274,19 @@ class CalculationWorker(QObject):
1260
1274
  opt_success = _iterative_optimize(rd_mol, method_key, _check_halted, _safe_status)
1261
1275
 
1262
1276
  if not opt_success:
1263
- 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
1264
1282
  except WorkerHaltError:
1265
1283
  raise
1266
1284
  except Exception as opt_err:
1267
- _safe_status(f"Optimization failed: {opt_err}")
1268
- 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
1269
1290
 
1270
1291
  _safe_status(
1271
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.0a4"
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