MoleditPy-linux 4.1.0__tar.gz → 4.1.2__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 (80) hide show
  1. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/PKG-INFO +5 -5
  2. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/README.md +4 -4
  3. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/pyproject.toml +1 -1
  4. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/MoleditPy_linux.egg-info/PKG-INFO +5 -5
  5. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/plugins/plugin_interface.py +11 -0
  6. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/calculation_worker.py +60 -12
  7. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/compute_logic.py +3 -3
  8. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/io_logic.py +184 -111
  9. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/utils/constants.py +5 -0
  10. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/LICENSE +0 -0
  11. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/setup.cfg +0 -0
  12. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  13. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  14. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  15. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  16. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  17. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/__init__.py +0 -0
  18. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/__main__.py +0 -0
  19. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  20. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/assets/icon.icns +0 -0
  21. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/assets/icon.ico +0 -0
  22. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/assets/icon.png +0 -0
  23. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/core/__init__.py +0 -0
  24. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  25. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/core/molecular_data.py +0 -0
  26. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/main.py +0 -0
  27. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/plugins/__init__.py +0 -0
  28. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
  29. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
  30. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/__init__.py +0 -0
  31. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  32. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
  33. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  34. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  35. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
  36. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/app_state.py +0 -0
  37. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/atom_item.py +0 -0
  38. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/atom_picking.py +0 -0
  39. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  40. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/bond_item.py +0 -0
  41. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
  42. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  43. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
  44. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
  45. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  46. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
  47. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
  48. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
  49. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  50. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
  51. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/export_logic.py +0 -0
  52. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  53. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/main_window.py +0 -0
  54. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/main_window_init.py +0 -0
  55. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  56. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
  57. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
  58. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
  59. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
  60. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  61. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
  62. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
  63. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  64. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  65. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
  66. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  67. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
  68. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  69. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/string_importers.py +0 -0
  70. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  71. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  72. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
  73. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/ui_manager.py +0 -0
  74. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
  75. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
  76. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  77. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/utils/__init__.py +0 -0
  78. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/utils/default_settings.py +0 -0
  79. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  80. {moleditpy_linux-4.1.0 → moleditpy_linux-4.1.2}/src/moleditpy_linux/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.1.0
3
+ Version: 4.1.2
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
@@ -837,9 +837,9 @@ moleditpy
837
837
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
838
838
  * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
839
839
 
840
- ## License
840
+ ## License & Disclaimer
841
841
 
842
- This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
842
+ This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the [LICENSE](LICENSE) file for details. As open-source software, it is provided 'as is' without warranty of any kind, and the author assumes no responsibility or liability for the results. Although outputs have been carefully verified, users are strongly encouraged to independently check and validate them for critical applications (such as publications). If you encounter any bugs, please open an issue.
843
843
 
844
844
  ## Citation
845
845
 
@@ -971,9 +971,9 @@ moleditpy
971
971
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
972
972
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
973
973
 
974
- ## ライセンス
974
+ ## ライセンス & 免責事項
975
975
 
976
- このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
976
+ このプロジェクトは GNU General Public License v3.0 (GPLv3) のもとでライセンスされています。詳細は [LICENSE](LICENSE) ファイルを参照してください。オープンソースソフトウェアとして、本ソフトウェアは「現状のまま」提供され、いかなる明示または黙示の保証も行いません。また、本ソフトウェアを使用した結果について、作者は一切の責任や義務を負いません。出力結果は慎重に検証されていますが、学術論文の作成など重要な用途においては、ユーザーご自身で結果を独立して確認および検証することを強くお勧めします。バグに遭遇した場合は、Issueを作成してください。
977
977
 
978
978
  ## 引用
979
979
 
@@ -134,9 +134,9 @@ moleditpy
134
134
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
135
135
  * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
136
136
 
137
- ## License
137
+ ## License & Disclaimer
138
138
 
139
- This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
139
+ This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the [LICENSE](LICENSE) file for details. As open-source software, it is provided 'as is' without warranty of any kind, and the author assumes no responsibility or liability for the results. Although outputs have been carefully verified, users are strongly encouraged to independently check and validate them for critical applications (such as publications). If you encounter any bugs, please open an issue.
140
140
 
141
141
  ## Citation
142
142
 
@@ -268,9 +268,9 @@ moleditpy
268
268
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
269
269
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
270
270
 
271
- ## ライセンス
271
+ ## ライセンス & 免責事項
272
272
 
273
- このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
273
+ このプロジェクトは GNU General Public License v3.0 (GPLv3) のもとでライセンスされています。詳細は [LICENSE](LICENSE) ファイルを参照してください。オープンソースソフトウェアとして、本ソフトウェアは「現状のまま」提供され、いかなる明示または黙示の保証も行いません。また、本ソフトウェアを使用した結果について、作者は一切の責任や義務を負いません。出力結果は慎重に検証されていますが、学術論文の作成など重要な用途においては、ユーザーご自身で結果を独立して確認および検証することを強くお勧めします。バグに遭遇した場合は、Issueを作成してください。
274
274
 
275
275
  ## 引用
276
276
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "4.1.0"
8
+ version = "4.1.2"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.1.0
3
+ Version: 4.1.2
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
@@ -837,9 +837,9 @@ moleditpy
837
837
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
838
838
  * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
839
839
 
840
- ## License
840
+ ## License & Disclaimer
841
841
 
842
- This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
842
+ This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the [LICENSE](LICENSE) file for details. As open-source software, it is provided 'as is' without warranty of any kind, and the author assumes no responsibility or liability for the results. Although outputs have been carefully verified, users are strongly encouraged to independently check and validate them for critical applications (such as publications). If you encounter any bugs, please open an issue.
843
843
 
844
844
  ## Citation
845
845
 
@@ -971,9 +971,9 @@ moleditpy
971
971
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
972
972
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
973
973
 
974
- ## ライセンス
974
+ ## ライセンス & 免責事項
975
975
 
976
- このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
976
+ このプロジェクトは GNU General Public License v3.0 (GPLv3) のもとでライセンスされています。詳細は [LICENSE](LICENSE) ファイルを参照してください。オープンソースソフトウェアとして、本ソフトウェアは「現状のまま」提供され、いかなる明示または黙示の保証も行いません。また、本ソフトウェアを使用した結果について、作者は一切の責任や義務を負いません。出力結果は慎重に検証されていますが、学術論文の作成など重要な用途においては、ユーザーご自身で結果を独立して確認および検証することを強くお勧めします。バグに遭遇した場合は、Issueを作成してください。
977
977
 
978
978
  ## 引用
979
979
 
@@ -499,6 +499,17 @@ class PluginContext:
499
499
  if mw and hasattr(mw, "string_importer_manager"):
500
500
  mw.string_importer_manager.load_from_smiles(smiles)
501
501
 
502
+ def show_xyz_data(
503
+ self, xyz_text: str, source_name: str = "XYZ data"
504
+ ) -> Optional[Any]:
505
+ """Display XYZ text in the 3D viewer and return the loaded RDKit Mol."""
506
+ mw = self.get_main_window()
507
+ if mw and hasattr(mw, "io_manager"):
508
+ show = getattr(mw.io_manager, "show_xyz_data", None)
509
+ if show is not None:
510
+ return show(xyz_text, source_name=source_name)
511
+ return None
512
+
502
513
  def to_xyz_block(self) -> Optional[str]:
503
514
  """Return the current 3D structure as an XYZ block (only element x y z lines)."""
504
515
  mol = self.current_mol
@@ -548,7 +548,22 @@ def _perform_direct_conversion(
548
548
  _safe_status,
549
549
  options=options if backend == "RDKIT" else None,
550
550
  ):
551
- raise RuntimeError(f"Optimization with {opt_method} failed.")
551
+ fallback_success = False
552
+ if backend == "RDKIT" and "MMFF" in method_key:
553
+ _safe_status("MMFF optimization failed. Auto-falling back to UFF...")
554
+ fallback_success = opt_func(
555
+ mol, "UFF", _check_halted, _safe_status, options=options
556
+ )
557
+ if fallback_success:
558
+ with contextlib.suppress(Exception):
559
+ mol.SetProp("_pme_optimization_method", "UFF_RDKIT")
560
+
561
+ if not fallback_success:
562
+ _safe_status(
563
+ "Warning: Optimization failed. Using unoptimized structure."
564
+ )
565
+ with contextlib.suppress(Exception):
566
+ mol.ClearProp("_pme_optimization_method")
552
567
 
553
568
  if _check_halted():
554
569
  raise WorkerHaltError("Halted")
@@ -664,15 +679,30 @@ print(ob_mol.write("mol"))
664
679
  _safe_status,
665
680
  options=options if backend == "RDKIT" else None,
666
681
  ):
667
- _safe_status("Warning: Optimization failed. Using unoptimized structure.")
668
- # Best-effort property cleanup if optimization was skipped
669
- with contextlib.suppress(AttributeError, RuntimeError, TypeError):
670
- rd_mol.ClearProp("_pme_optimization_method")
682
+ fallback_success = False
683
+ if backend == "RDKIT" and "MMFF" in method_key:
684
+ _safe_status("MMFF optimization failed. Auto-falling back to UFF...")
685
+ fallback_success = opt_func(
686
+ rd_mol, "UFF", _check_halted, _safe_status, options=options
687
+ )
688
+ if fallback_success:
689
+ with contextlib.suppress(Exception):
690
+ rd_mol.SetProp("_pme_optimization_method", "UFF_RDKIT")
691
+
692
+ if not fallback_success:
693
+ _safe_status(
694
+ "Warning: Optimization failed. Using unoptimized structure."
695
+ )
696
+ with contextlib.suppress(Exception):
697
+ rd_mol.ClearProp("_pme_optimization_method")
671
698
 
672
699
  if _check_halted():
673
700
  raise WorkerHaltError("Halted")
674
701
  # Final status message before finishing (to ensure it doesn't overwrite error/halt messages)
675
- opt_label = _OPT_METHOD_LABELS.get(opt_method, opt_method)
702
+ final_opt = "Unoptimized"
703
+ if rd_mol.HasProp("_pme_optimization_method"):
704
+ final_opt = rd_mol.GetProp("_pme_optimization_method")
705
+ opt_label = _OPT_METHOD_LABELS.get(final_opt, final_opt)
676
706
  _safe_status(f"Process completed (Open Babel Conversion / {opt_label}).")
677
707
  _safe_finished((worker_id, rd_mol))
678
708
  return True
@@ -925,9 +955,22 @@ class CalculationWorker(QObject):
925
955
  _safe_status,
926
956
  options=options if backend == "RDKIT" else None,
927
957
  ):
928
- _safe_status("Warning: Optimization failed. Using unoptimized structure.")
929
- with contextlib.suppress(Exception):
930
- mol.ClearProp("_pme_optimization_method")
958
+ fallback_success = False
959
+ if backend == "RDKIT" and "MMFF" in method_key:
960
+ _safe_status("MMFF optimization failed. Auto-falling back to UFF...")
961
+ fallback_success = opt_func(
962
+ mol, "UFF", _check_halted, _safe_status, options=options
963
+ )
964
+ if fallback_success:
965
+ with contextlib.suppress(Exception):
966
+ mol.SetProp("_pme_optimization_method", "UFF_RDKIT")
967
+
968
+ if not fallback_success:
969
+ _safe_status(
970
+ "Warning: Optimization failed. Using unoptimized structure."
971
+ )
972
+ with contextlib.suppress(Exception):
973
+ mol.ClearProp("_pme_optimization_method")
931
974
 
932
975
  # Final stereo restoration check
933
976
  for b_idx, s, satoms in orig_stereo:
@@ -939,7 +982,10 @@ class CalculationWorker(QObject):
939
982
  if _check_halted():
940
983
  raise WorkerHaltError("Halted")
941
984
  # Final status message before finishing (to ensure it doesn't overwrite error/halt messages)
942
- opt_label = _OPT_METHOD_LABELS.get(opt_method, opt_method)
985
+ final_opt = "Unoptimized"
986
+ if mol.HasProp("_pme_optimization_method"):
987
+ final_opt = mol.GetProp("_pme_optimization_method")
988
+ opt_label = _OPT_METHOD_LABELS.get(final_opt, final_opt)
943
989
  _safe_status(f"Process completed (RDKit Conversion / {opt_label}).")
944
990
  _safe_finished((w_id, mol))
945
991
  return True
@@ -972,9 +1018,11 @@ class CalculationWorker(QObject):
972
1018
  )
973
1019
  # Final status message before finishing (to ensure it doesn't overwrite error/halt messages)
974
1020
  _safe_status = helpers["status"]
975
- opt_method = (options or {}).get("optimization_method") or "MMFF94s_RDKIT"
976
1021
  if (options or {}).get("do_optimize", True):
977
- opt_label = _OPT_METHOD_LABELS.get(opt_method, opt_method)
1022
+ final_opt = "Unoptimized"
1023
+ if mol.HasProp("_pme_optimization_method"):
1024
+ final_opt = mol.GetProp("_pme_optimization_method")
1025
+ opt_label = _OPT_METHOD_LABELS.get(final_opt, final_opt)
978
1026
  _safe_status(f"Process completed (Direct 2D->3D Conversion / {opt_label}).")
979
1027
  else:
980
1028
  _safe_status("Process completed (Direct 2D->3D Conversion).")
@@ -484,13 +484,13 @@ class ComputeManager:
484
484
  method_key = None
485
485
  if mol and mol.HasProp("_pme_optimization_method"):
486
486
  method_key = mol.GetProp("_pme_optimization_method")
487
- if not method_key:
488
- method_key = self.host.init_manager.optimization_method
489
487
  if method_key:
490
488
  labels = self.host.init_manager.opt3d_method_labels or {}
491
489
  self.last_successful_optimization_method = labels.get(
492
490
  method_key, method_key
493
491
  )
492
+ else:
493
+ self.last_successful_optimization_method = "Unoptimized"
494
494
  except (AttributeError, TypeError):
495
495
  # Safe defensive fallback catching AttributeError, TypeError
496
496
  pass
@@ -539,7 +539,7 @@ class ComputeManager:
539
539
  "Retry with UFF?",
540
540
  f"{msg}\n\nWould you like to retry using UFF (RDKit) instead?",
541
541
  QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
542
- QMessageBox.StandardButton.No,
542
+ QMessageBox.StandardButton.Yes,
543
543
  )
544
544
  if reply == QMessageBox.StandardButton.Yes:
545
545
  self.optimize_3d_structure("UFF_RDKIT")
@@ -34,7 +34,7 @@ from PyQt6.QtWidgets import (
34
34
  from rdkit import Chem
35
35
  from rdkit.Chem import AllChem, rdGeometry, rdMolTransforms, Descriptors
36
36
 
37
- from ..utils.constants import COVALENT_RADII, VERSION
37
+ from ..utils.constants import COVALENT_RADII, DUMMY_XYZ_SYMBOLS, VERSION
38
38
 
39
39
 
40
40
  class IOManager:
@@ -84,139 +84,210 @@ class IOManager:
84
84
  lines[3] = self.fix_mol_counts_line(lines[3])
85
85
  return "\n".join(lines)
86
86
 
87
- def load_xyz_file(self, file_path: str) -> Optional[Any]:
88
- """Load XYZ file and create RDKit Mol with charge prompt and bond determination."""
89
- if not self.host.state_manager.check_unsaved_changes():
90
- return None
91
-
87
+ def _normalize_xyz_symbol(self, raw_symbol: str) -> Tuple[str, bool]:
88
+ """Return the RDKit symbol for an XYZ atom and whether it is a dummy."""
89
+ stripped = raw_symbol.strip()
90
+ if ":" in stripped:
91
+ return "*", True
92
+ if stripped.upper() in DUMMY_XYZ_SYMBOLS:
93
+ return "*", True
94
+ symbol = stripped.capitalize()
95
+ try:
96
+ atomic_num = Chem.GetPeriodicTable().GetAtomicNumber(symbol)
97
+ except (RuntimeError, ValueError, TypeError):
98
+ return "*", True
99
+ if atomic_num <= 0:
100
+ return "*", True
101
+ return symbol, False
102
+
103
+ def _mol_from_xyz_lines(self, raw_lines: list[str]) -> Any:
104
+ """Create an RDKit molecule from XYZ text lines."""
105
+ lines = [ln.strip() for ln in raw_lines if not ln.strip().startswith("#")]
106
+ while lines and not lines[0]:
107
+ lines.pop(0)
108
+
109
+ if not lines:
110
+ raise ValueError("XYZ file format error: too few lines")
111
+
112
+ atom_start = 2
92
113
  try:
93
- with open(file_path, "r", encoding="utf-8") as f:
94
- raw_lines = f.readlines()
95
-
96
- lines = [ln.strip() for ln in raw_lines if not ln.strip().startswith("#")]
97
- while lines and not lines[0]:
98
- lines.pop(0)
99
-
100
114
  if len(lines) < 2:
101
115
  raise ValueError("XYZ file format error: too few lines")
102
-
103
116
  num_atoms = int(lines[0])
104
117
  if num_atoms == 0:
105
118
  raise ValueError("XYZ file has zero atoms")
119
+ except ValueError as exc:
120
+ # Not a standard headed XYZ — treat all lines as atom rows
121
+ if "zero atoms" in str(exc) or "too few" in str(exc):
122
+ raise
123
+ num_atoms = len(lines)
124
+ atom_start = 0
125
+
126
+ atoms_data = []
127
+ has_dummy_atoms = False
128
+ atom_lines = lines[atom_start : atom_start + num_atoms]
129
+ if len(atom_lines) < num_atoms:
130
+ raise ValueError("XYZ file format error: fewer atom rows than expected")
131
+
132
+ for i, line in enumerate(atom_lines):
133
+ parts = line.split()
134
+ if len(parts) < 4:
135
+ raise ValueError(f"Invalid atom data at line {atom_start + i + 1}")
136
+ raw_symbol = parts[0]
137
+ symbol, is_dummy = self._normalize_xyz_symbol(raw_symbol)
138
+ has_dummy_atoms = has_dummy_atoms or is_dummy
139
+ atoms_data.append(
140
+ (symbol, float(parts[1]), float(parts[2]), float(parts[3]))
141
+ )
106
142
 
107
- atoms_data = []
108
- for i, line in enumerate(lines[2 : 2 + num_atoms]):
109
- parts = line.split()
110
- if len(parts) < 4:
111
- raise ValueError(f"Invalid atom data at line {i + 3}")
112
- symbol = parts[0].capitalize()
113
- try:
114
- Chem.Atom(symbol)
115
- except (RuntimeError, ValueError):
116
- settings = self.host.init_manager.settings
117
- if settings.get("skip_chemistry_checks", False):
118
- symbol = "C"
119
- else:
120
- raise ValueError(f"Unrecognized element symbol: {parts[0]}")
121
- atoms_data.append(
122
- (symbol, float(parts[1]), float(parts[2]), float(parts[3]))
123
- )
124
-
125
- if not atoms_data:
126
- raise ValueError("No valid atoms found in XYZ file")
143
+ if not atoms_data:
144
+ raise ValueError("No valid atoms found in XYZ file")
127
145
 
128
- mol = Chem.RWMol()
129
- conf = Chem.Conformer(len(atoms_data))
130
- for i, (symbol, x, y, z) in enumerate(atoms_data):
131
- atom = Chem.Atom(symbol)
132
- atom.SetIntProp("xyz_unique_id", i)
133
- mol.AddAtom(atom)
134
- conf.SetAtomPosition(i, rdGeometry.Point3D(x, y, z))
135
- mol.AddConformer(conf)
146
+ mol = Chem.RWMol()
147
+ conf = Chem.Conformer(len(atoms_data))
148
+ for i, (symbol, x, y, z) in enumerate(atoms_data):
149
+ atom = Chem.Atom(symbol)
150
+ atom.SetIntProp("xyz_unique_id", i)
151
+ if atom.GetAtomicNum() == 0:
152
+ atom.SetProp("xyz_original_symbol", atom_lines[i].split()[0])
153
+ mol.AddAtom(atom)
154
+ conf.SetAtomPosition(i, rdGeometry.Point3D(x, y, z))
155
+ mol.AddConformer(conf)
136
156
 
137
- settings = self.host.init_manager.settings
138
- skip_checks = bool(settings.get("skip_chemistry_checks", False))
157
+ settings = self.host.init_manager.settings
158
+ skip_checks = bool(settings.get("skip_chemistry_checks", False))
139
159
 
140
- def _set_prop(m: Chem.Mol, key: str, val: Any) -> None:
160
+ def _set_prop(m: Chem.Mol, key: str, val: Any) -> None:
161
+ try:
162
+ if isinstance(val, int):
163
+ m.SetIntProp(key, val)
164
+ elif isinstance(val, float):
165
+ m.SetDoubleProp(key, val)
166
+ except (RuntimeError, TypeError, ValueError):
167
+ # Safe defensive fallback catching RuntimeError, TypeError, ValueError
168
+ pass
169
+
170
+ def _process(charge_val: int, use_rd_determine: bool = True) -> Any:
171
+ if use_rd_determine:
141
172
  try:
142
- if isinstance(val, int):
143
- m.SetIntProp(key, val)
144
- elif isinstance(val, float):
145
- m.SetDoubleProp(key, val)
146
- except (RuntimeError, TypeError, ValueError):
147
- # Safe defensive fallback catching RuntimeError, TypeError, ValueError
148
- pass
149
-
150
- def _process(charge_val: int, use_rd_determine: bool = True) -> Any:
151
- if use_rd_determine:
152
- try:
153
- from rdkit.Chem import rdDetermineBonds
173
+ from rdkit.Chem import rdDetermineBonds
154
174
 
155
- mol_copy = Chem.RWMol(mol)
156
- rdDetermineBonds.DetermineBonds(mol_copy, charge=charge_val)
157
- candidate = mol_copy.GetMol()
158
- _set_prop(candidate, "_xyz_charge", charge_val)
159
- return candidate
160
- except (RuntimeError, ValueError, TypeError) as e:
161
- raise e
162
- else:
163
- self.estimate_bonds_from_distances(mol)
164
- candidate = mol.GetMol()
175
+ mol_copy = Chem.RWMol(mol)
176
+ rdDetermineBonds.DetermineBonds(mol_copy, charge=charge_val)
177
+ candidate = mol_copy.GetMol()
165
178
  _set_prop(candidate, "_xyz_charge", charge_val)
166
179
  return candidate
167
-
168
- if skip_checks:
169
- final_mol = _process(0, use_rd_determine=False)
170
- _set_prop(final_mol, "_xyz_skip_checks", 1)
180
+ except (RuntimeError, ValueError, TypeError):
181
+ raise
171
182
  else:
172
- final_mol = None
173
- settings = self.host.init_manager.settings
174
- # First try with charge 0 (per user's 'first try with 0 then ask' requirement)
175
- # but only if "Always ask" is not explicitly enabled in settings.
176
- if not settings.get("always_ask_charge", False):
177
- try:
178
- final_mol = _process(0, use_rd_determine=True)
179
- except (RuntimeError, ValueError, TypeError):
180
- final_mol = None
181
-
182
- # If still no final_mol (because always_ask is True, or charge 0 failed)
183
- if final_mol is None:
184
- while True:
185
- prompt_fn = getattr(self, "prompt_for_charge", None)
186
- if callable(prompt_fn):
187
- result = prompt_fn()
188
- if isinstance(result, tuple) and len(result) == 3:
189
- charge_val, ok, skip_flag = result
190
- else:
191
- charge_val, ok, skip_flag = 0, True, False
183
+ self.estimate_bonds_from_distances(mol)
184
+ candidate = mol.GetMol()
185
+ _set_prop(candidate, "_xyz_charge", charge_val)
186
+ return candidate
187
+
188
+ if skip_checks or has_dummy_atoms:
189
+ final_mol = _process(0, use_rd_determine=False)
190
+ _set_prop(final_mol, "_xyz_skip_checks", 1)
191
+ else:
192
+ final_mol = None
193
+ # First try with charge 0 (per user's 'first try with 0 then ask' requirement)
194
+ # but only if "Always ask" is not explicitly enabled in settings.
195
+ if not settings.get("always_ask_charge", False):
196
+ try:
197
+ final_mol = _process(0, use_rd_determine=True)
198
+ except (RuntimeError, ValueError, TypeError):
199
+ final_mol = None
200
+
201
+ # If still no final_mol (because always_ask is True, or charge 0 failed)
202
+ if final_mol is None:
203
+ while True:
204
+ prompt_fn = getattr(self, "prompt_for_charge", None)
205
+ if callable(prompt_fn):
206
+ result = prompt_fn()
207
+ if isinstance(result, tuple) and len(result) == 3:
208
+ charge_val, ok, skip_flag = result
192
209
  else:
193
210
  charge_val, ok, skip_flag = 0, True, False
211
+ else:
212
+ charge_val, ok, skip_flag = 0, True, False
213
+
214
+ if not ok:
215
+ return None
216
+ if skip_flag:
217
+ final_mol = _process(0, use_rd_determine=False)
218
+ _set_prop(final_mol, "_xyz_skip_checks", 1)
219
+ break
220
+ try:
221
+ final_mol = _process(charge_val, use_rd_determine=True)
222
+ break
223
+ except (RuntimeError, ValueError, TypeError) as e:
224
+ if self.host.statusBar():
225
+ self.host.statusBar().showMessage(
226
+ f"Chemistry failed for charge {charge_val}: {e}. Try a different charge or skip."
227
+ )
228
+ if not callable(prompt_fn):
229
+ raise e
194
230
 
195
- if not ok:
196
- return None
197
- if skip_flag:
198
- final_mol = _process(0, use_rd_determine=False)
199
- _set_prop(final_mol, "_xyz_skip_checks", 1)
200
- break
201
- try:
202
- final_mol = _process(charge_val, use_rd_determine=True)
203
- break
204
- except (RuntimeError, ValueError, TypeError) as e:
205
- if self.host.statusBar():
206
- self.host.statusBar().showMessage(
207
- f"Chemistry failed for charge {charge_val}: {e}. Try a different charge or skip."
208
- )
209
- if not callable(prompt_fn):
210
- raise e
211
-
212
- if final_mol:
213
- final_mol.xyz_atom_data = atoms_data
214
- return final_mol
231
+ if final_mol:
232
+ final_mol.xyz_atom_data = atoms_data
233
+ return final_mol
234
+
235
+ def load_xyz_file(self, file_path: str) -> Optional[Any]:
236
+ """Load XYZ file and create RDKit Mol with charge prompt and bond determination."""
237
+ if not self.host.state_manager.check_unsaved_changes():
238
+ return None
215
239
 
240
+ try:
241
+ with open(file_path, "r", encoding="utf-8") as f:
242
+ return self._mol_from_xyz_lines(f.readlines())
216
243
  except (RuntimeError, TypeError, ValueError, UnicodeDecodeError) as e:
217
244
  self.host.statusBar().showMessage(f"Error parsing XYZ file: {e}")
218
245
  return None
219
246
 
247
+ def load_xyz_block(self, xyz_text: str) -> Optional[Any]:
248
+ """Load XYZ text and create an RDKit Mol without opening a file dialog."""
249
+ try:
250
+ return self._mol_from_xyz_lines(xyz_text.splitlines())
251
+ except (RuntimeError, TypeError, ValueError, UnicodeDecodeError) as e:
252
+ self.host.statusBar().showMessage(f"Error parsing XYZ data: {e}")
253
+ return None
254
+
255
+ def show_xyz_data(
256
+ self, xyz_text: str, source_name: str = "XYZ data"
257
+ ) -> Optional[Any]:
258
+ """Load XYZ text, set it as the current molecule, and draw it in 3D."""
259
+ try:
260
+ mol = self.load_xyz_block(xyz_text)
261
+ if mol is None:
262
+ return None
263
+
264
+ self.host.edit_actions_manager.clear_all(skip_check=True)
265
+ self.host.set_current_molecule(mol)
266
+ self.host.set_atom_id_to_rdkit_idx_map({})
267
+
268
+ skip_flag = False
269
+ if mol.HasProp("_xyz_skip_checks"):
270
+ skip_flag = bool(mol.GetIntProp("_xyz_skip_checks"))
271
+ self.host.is_xyz_derived = skip_flag or (mol.GetNumBonds() == 0)
272
+
273
+ self.host.view_3d_manager.draw_molecule_3d(mol)
274
+ self.host.ui_manager.enter_3d_viewer_mode()
275
+ self.host.ui_manager.enable_3d_features(True)
276
+ self.host.view_3d_manager.update_atom_id_menu_text()
277
+ self.host.view_3d_manager.update_atom_id_menu_state()
278
+
279
+ if self.host.statusBar():
280
+ self.host.statusBar().showMessage(
281
+ f"3D Viewer Mode: Loaded {source_name}"
282
+ )
283
+ self.host.set_has_unsaved_changes(False)
284
+ self.host.state_manager.update_window_title()
285
+ return mol
286
+ except (RuntimeError, TypeError, ValueError, AttributeError) as e:
287
+ if self.host.statusBar():
288
+ self.host.statusBar().showMessage(f"XYZ display failed: {e}")
289
+ return None
290
+
220
291
  def prompt_for_charge(self) -> Tuple[Optional[int], bool, bool]:
221
292
  """Show dialog to prompt user for molecular charge when loading XYZ files."""
222
293
  dialog = QDialog(self.host)
@@ -261,6 +332,8 @@ class IOManager:
261
332
  for j in range(i + 1, num_atoms):
262
333
  atom_i = mol.GetAtomWithIdx(i)
263
334
  atom_j = mol.GetAtomWithIdx(j)
335
+ if atom_i.GetAtomicNum() == 0 or atom_j.GetAtomicNum() == 0:
336
+ continue
264
337
  distance = rdMolTransforms.GetBondLength(conf, i, j)
265
338
  symbol_i = atom_i.GetSymbol()
266
339
  symbol_j = atom_j.GetSymbol()
@@ -55,6 +55,11 @@ BOND_OFFSET = 3.5
55
55
  DEFAULT_BOND_LENGTH = 75 # Standard bond length used in templates
56
56
  CLIPBOARD_MIME_TYPE = "application/x-moleditpy-fragment"
57
57
 
58
+ # XYZ dummy/pseudo-atom labels that map to RDKit wildcard atom (*)
59
+ DUMMY_XYZ_SYMBOLS: frozenset[str] = frozenset(
60
+ {"*", "-", "X", "DA", "DU", "DUM", "DUMMY", "Q", "BQ", "LP"}
61
+ )
62
+
58
63
  # Physical bond length (approximate) used to convert scene pixels to angstroms.
59
64
  # DEFAULT_BOND_LENGTH is the length in pixels used in the editor UI for a typical bond.
60
65
  # Many molecular file formats expect coordinates in angstroms; use ~1.5 Å as a typical single-bond length.
File without changes