peace-protomer 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. peace_protomer-0.1.0/LICENSE +21 -0
  2. peace_protomer-0.1.0/PKG-INFO +118 -0
  3. peace_protomer-0.1.0/README.md +87 -0
  4. peace_protomer-0.1.0/peace/__init__.py +31 -0
  5. peace_protomer-0.1.0/peace/api.py +110 -0
  6. peace_protomer-0.1.0/peace/calculators/__init__.py +69 -0
  7. peace_protomer-0.1.0/peace/calculators/aimnet2.py +95 -0
  8. peace_protomer-0.1.0/peace/calculators/common.py +59 -0
  9. peace_protomer-0.1.0/peace/calculators/orca.py +457 -0
  10. peace_protomer-0.1.0/peace/calculators/settings.py +184 -0
  11. peace_protomer-0.1.0/peace/calculators/xtb.py +838 -0
  12. peace_protomer-0.1.0/peace/calculators/xtb2.py +196 -0
  13. peace_protomer-0.1.0/peace/common.py +172 -0
  14. peace_protomer-0.1.0/peace/data/allowed_solvents.txt +15 -0
  15. peace_protomer-0.1.0/peace/data/cpcm_allowed_solvents.txt +93 -0
  16. peace_protomer-0.1.0/peace/data/cpcm_internal_solvent_names.txt +98 -0
  17. peace_protomer-0.1.0/peace/engine.py +182 -0
  18. peace_protomer-0.1.0/peace/logging_utils.py +94 -0
  19. peace_protomer-0.1.0/peace/main.py +1680 -0
  20. peace_protomer-0.1.0/peace/pka.py +1167 -0
  21. peace_protomer-0.1.0/peace/protomer.py +1017 -0
  22. peace_protomer-0.1.0/peace/solvation.py +3107 -0
  23. peace_protomer-0.1.0/peace/solvents.py +204 -0
  24. peace_protomer-0.1.0/peace/visualization.py +928 -0
  25. peace_protomer-0.1.0/peace_protomer.egg-info/PKG-INFO +118 -0
  26. peace_protomer-0.1.0/peace_protomer.egg-info/SOURCES.txt +36 -0
  27. peace_protomer-0.1.0/peace_protomer.egg-info/dependency_links.txt +1 -0
  28. peace_protomer-0.1.0/peace_protomer.egg-info/entry_points.txt +2 -0
  29. peace_protomer-0.1.0/peace_protomer.egg-info/requires.txt +9 -0
  30. peace_protomer-0.1.0/peace_protomer.egg-info/top_level.txt +1 -0
  31. peace_protomer-0.1.0/pyproject.toml +60 -0
  32. peace_protomer-0.1.0/setup.cfg +4 -0
  33. peace_protomer-0.1.0/tests/test_aimnet2.py +30 -0
  34. peace_protomer-0.1.0/tests/test_api.py +108 -0
  35. peace_protomer-0.1.0/tests/test_cli.py +520 -0
  36. peace_protomer-0.1.0/tests/test_enumeration.py +224 -0
  37. peace_protomer-0.1.0/tests/test_qm_workflow.py +165 -0
  38. peace_protomer-0.1.0/tests/test_solvation.py +121 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Zheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: peace-protomer
3
+ Version: 0.1.0
4
+ Summary: Protomer Enumerator & Automatic Calculator for Energies: an atomistic workflow for determining acid-base prototropic tautomer microstate populations
5
+ Author-email: Jonathan Zheng <jonathan.zheng@tuwien.ac.at>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jonwzheng/PEACE
8
+ Project-URL: Repository, https://github.com/jonwzheng/PEACE
9
+ Project-URL: Issues, https://github.com/jonwzheng/PEACE/issues
10
+ Keywords: cheminformatics,tautomer,protomer,zwitterion,pKa,solvation,rdkit,xtb
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
19
+ Requires-Python: <3.15,>=3.12
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: rdkit
23
+ Requires-Dist: pandas
24
+ Requires-Dist: numpy
25
+ Requires-Dist: pillow
26
+ Requires-Dist: prism-pruner
27
+ Requires-Dist: matplotlib
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest; extra == "test"
30
+ Dynamic: license-file
31
+
32
+ # PEACE: Protomer Enumerator & Automatic Calculator for Energies
33
+
34
+ ![PEACE Logo](https://raw.githubusercontent.com/jonwzheng/PEACE/main/docs/static/header.svg)
35
+
36
+ ## Overview
37
+ PEACE is a lightweight cheminformatics workflow for enumerating acid-base tautomers. It also includes a calculator function that orchestrates QM calculations for estimating the microstate populations in numerous solvent systems.
38
+
39
+ For more information, please read the manuscript (and cite, if using this work):
40
+ > Zheng, J. W., & Heid, E. (2026). An Atomistic Workflow for Determining Acid-Base Prototropic Tautomer Microstate Populations. RSC Advances.
41
+
42
+ ## Installation
43
+ 1. Install from PyPI with `pip install peace-protomer`.
44
+ 2. If using free energy simulations, additionally install the appropriate QM software:
45
+ - If using `xtb`-based methods, install `gxtb` version 2.0.1 or greater into `bin` (or specify its location with `--xtb-executable`).
46
+ - If using DFT, install ORCA 6.x and add this to `bin` (or specify its location with `--orca-executable`).
47
+ - If using AIMNet2, install it into the same Python environment as PEACE with `pip install "aimnet[ase]"`.
48
+
49
+ As an alternative to step 1, you can install from source:
50
+ ```
51
+ git clone git@github.com:jonwzheng/PEACE.git
52
+ cd PEACE
53
+ python -m venv peace_env
54
+ source peace_env/bin/activate
55
+ pip install -e ".[test]"
56
+ pytest
57
+ ```
58
+
59
+ Example run:
60
+ `peace --smiles "NCC(=O)O" --solvation` (equivalently `python -m peace.main ...`)
61
+
62
+ CLI commands can be displayed e.g. with:
63
+ `peace --help`
64
+
65
+ ## Python API
66
+ Everything available on the CLI can also be run from Python e.g. (`--charge-min` -> `charge_min`, `--solv-sp-calculator` -> `solv_sp_calculator`). You can check their defaults with `peace.cli_defaults()`. By default, the Python API does not write a CSV or display plots unless you pass `output_csv=...` or `no_plot=False`.
67
+
68
+ ```python
69
+ import peace
70
+
71
+ # Tautomer/protomer enumeration only (no QM)
72
+ result = peace.enumerate_protomers("O=C(O)CN(CCN(CC(=O)O)CC(=O)O)CC(=O)O", charge_min=-4, charge_max=2)
73
+ for charge, species in result.species_by_charge.items():
74
+ print(charge, sum(len(t.protomers) for t in species.tautomers.values()))
75
+
76
+ # Solvation free energies and Boltzmann populations
77
+ result = peace.solvate("NCC(=O)O", max_conformers=2, scratch_root="./solvation_results")
78
+ print(result.dataframe[["protomer_smiles", "boltzmann_fraction"]])
79
+
80
+ # Any CLI option combination, e.g. pKa
81
+ result = peace.solvate("NCC(=O)O", charge_min=0, charge_max=1, pka=True)
82
+ print(result.pka_dataframe)
83
+ ```
84
+
85
+ ## How it works:
86
+
87
+ 1. **Tautomer Enumeration** using RDKit.
88
+ 2. **Protomer Enumeration** by searching each tautomer for acid/base sites and sequentially (de)protonating all possible combinations (this searches for zwitterion forms).
89
+ 3. **(Optional) Microstate Population Estimation** using quantum-chemical calculations, G = E_gas + G_RRHO + dG_solv:
90
+ - **Screening**: KDG conformer -> implicit-solvation SP, gas-phase SP, and frequencies (RRHO) on the screening geometry.
91
+ - **Refinement** (screened-in protomers): MMFF94-ranked conformer ensemble -> implicit-solvent optimization -> implicit-solvation SP on that geometry -> gas-phase re-optimization and gas-phase SP -> frequencies on the gas-phase geometry. Pass `--no-gas-optimize` to skip the gas-phase re-optimization and evaluate the gas SP and frequencies at the implicit-solvent geometry.
92
+ It is planned that a machine learning model will be made available to speed this up.
93
+ 4. **Visualization** of all relevant tautomer-protomers based on their graph representations.
94
+
95
+ ## Calculators
96
+ Each stage of the free-energy workflow has its own calculator:
97
+
98
+ | Stage | Flag | Default | Options |
99
+ | --- | --- | --- | --- |
100
+ | Gas-phase SP + gas-phase re-optimization | `--gas-calculator` | `gxtb` | `gxtb`, `gfn2`, `aimnet2`, `orca` |
101
+ | Frequencies (RRHO) | `--freq-calculator` | `gfn2` | `gfn2`, `gxtb`, `orca` |
102
+ | Implicit-solvent optimization | `--solv-opt-calculator` | `gfn2` (GFN2-xTB/ALPB) | `gfn2`, `aimnet2`, `orca` |
103
+ | Implicit-solvation free energy | `--solv-sp-calculator` | `cpcmx` (CPCM-X) | `cpcmx`, `orca` |
104
+
105
+ `--freq-calculator gxtb` computes the RRHO contribution with g-xTB instead of GFN2-xTB frequencies; it may be slightly more accurate but is about 2x slower.
106
+
107
+ ORCA stages take arbitrary method keywords: `--orca-keywords` sets a default for every ORCA stage, and `--gas-orca-keywords`, `--freq-orca-keywords`, `--solv-opt-orca-keywords`, `--solv-sp-orca-keywords` override it per stage. PEACE adds the job type (`Opt`/`FREQ`) and the solvation model itself: `--solv-opt-orca-model` (`cpcm`, `smd`) and `--solv-sp-orca-model` (`cpcm`, `smd`, `cosmors`). See `python -m peace.main --help` for more runtime options.
108
+
109
+ Example: r2SCAN-3c gas-phase energies and frequencies, keeping GFN2-xTB/ALPB optimization and CPCM-X solvation:
110
+ ```
111
+ python -m peace.main --smiles "NCC(=O)O" --solvation \
112
+ --gas-calculator orca --freq-calculator orca --orca-keywords "r2SCAN-3c"
113
+ ```
114
+
115
+ ## pKa Prediction
116
+ The free energies of tautomers, and their ensembles, can be used to estimate pKa values. It is currently quite inaccurate and should be considered work in progress, or at least a way of estimating which micro-reactions contribute to acidity. Work is ongoing.
117
+
118
+ You can call this with the `--pka` flag as well as `--proton-energy` to estimate the proton free energy in solution.
@@ -0,0 +1,87 @@
1
+ # PEACE: Protomer Enumerator & Automatic Calculator for Energies
2
+
3
+ ![PEACE Logo](https://raw.githubusercontent.com/jonwzheng/PEACE/main/docs/static/header.svg)
4
+
5
+ ## Overview
6
+ PEACE is a lightweight cheminformatics workflow for enumerating acid-base tautomers. It also includes a calculator function that orchestrates QM calculations for estimating the microstate populations in numerous solvent systems.
7
+
8
+ For more information, please read the manuscript (and cite, if using this work):
9
+ > Zheng, J. W., & Heid, E. (2026). An Atomistic Workflow for Determining Acid-Base Prototropic Tautomer Microstate Populations. RSC Advances.
10
+
11
+ ## Installation
12
+ 1. Install from PyPI with `pip install peace-protomer`.
13
+ 2. If using free energy simulations, additionally install the appropriate QM software:
14
+ - If using `xtb`-based methods, install `gxtb` version 2.0.1 or greater into `bin` (or specify its location with `--xtb-executable`).
15
+ - If using DFT, install ORCA 6.x and add this to `bin` (or specify its location with `--orca-executable`).
16
+ - If using AIMNet2, install it into the same Python environment as PEACE with `pip install "aimnet[ase]"`.
17
+
18
+ As an alternative to step 1, you can install from source:
19
+ ```
20
+ git clone git@github.com:jonwzheng/PEACE.git
21
+ cd PEACE
22
+ python -m venv peace_env
23
+ source peace_env/bin/activate
24
+ pip install -e ".[test]"
25
+ pytest
26
+ ```
27
+
28
+ Example run:
29
+ `peace --smiles "NCC(=O)O" --solvation` (equivalently `python -m peace.main ...`)
30
+
31
+ CLI commands can be displayed e.g. with:
32
+ `peace --help`
33
+
34
+ ## Python API
35
+ Everything available on the CLI can also be run from Python e.g. (`--charge-min` -> `charge_min`, `--solv-sp-calculator` -> `solv_sp_calculator`). You can check their defaults with `peace.cli_defaults()`. By default, the Python API does not write a CSV or display plots unless you pass `output_csv=...` or `no_plot=False`.
36
+
37
+ ```python
38
+ import peace
39
+
40
+ # Tautomer/protomer enumeration only (no QM)
41
+ result = peace.enumerate_protomers("O=C(O)CN(CCN(CC(=O)O)CC(=O)O)CC(=O)O", charge_min=-4, charge_max=2)
42
+ for charge, species in result.species_by_charge.items():
43
+ print(charge, sum(len(t.protomers) for t in species.tautomers.values()))
44
+
45
+ # Solvation free energies and Boltzmann populations
46
+ result = peace.solvate("NCC(=O)O", max_conformers=2, scratch_root="./solvation_results")
47
+ print(result.dataframe[["protomer_smiles", "boltzmann_fraction"]])
48
+
49
+ # Any CLI option combination, e.g. pKa
50
+ result = peace.solvate("NCC(=O)O", charge_min=0, charge_max=1, pka=True)
51
+ print(result.pka_dataframe)
52
+ ```
53
+
54
+ ## How it works:
55
+
56
+ 1. **Tautomer Enumeration** using RDKit.
57
+ 2. **Protomer Enumeration** by searching each tautomer for acid/base sites and sequentially (de)protonating all possible combinations (this searches for zwitterion forms).
58
+ 3. **(Optional) Microstate Population Estimation** using quantum-chemical calculations, G = E_gas + G_RRHO + dG_solv:
59
+ - **Screening**: KDG conformer -> implicit-solvation SP, gas-phase SP, and frequencies (RRHO) on the screening geometry.
60
+ - **Refinement** (screened-in protomers): MMFF94-ranked conformer ensemble -> implicit-solvent optimization -> implicit-solvation SP on that geometry -> gas-phase re-optimization and gas-phase SP -> frequencies on the gas-phase geometry. Pass `--no-gas-optimize` to skip the gas-phase re-optimization and evaluate the gas SP and frequencies at the implicit-solvent geometry.
61
+ It is planned that a machine learning model will be made available to speed this up.
62
+ 4. **Visualization** of all relevant tautomer-protomers based on their graph representations.
63
+
64
+ ## Calculators
65
+ Each stage of the free-energy workflow has its own calculator:
66
+
67
+ | Stage | Flag | Default | Options |
68
+ | --- | --- | --- | --- |
69
+ | Gas-phase SP + gas-phase re-optimization | `--gas-calculator` | `gxtb` | `gxtb`, `gfn2`, `aimnet2`, `orca` |
70
+ | Frequencies (RRHO) | `--freq-calculator` | `gfn2` | `gfn2`, `gxtb`, `orca` |
71
+ | Implicit-solvent optimization | `--solv-opt-calculator` | `gfn2` (GFN2-xTB/ALPB) | `gfn2`, `aimnet2`, `orca` |
72
+ | Implicit-solvation free energy | `--solv-sp-calculator` | `cpcmx` (CPCM-X) | `cpcmx`, `orca` |
73
+
74
+ `--freq-calculator gxtb` computes the RRHO contribution with g-xTB instead of GFN2-xTB frequencies; it may be slightly more accurate but is about 2x slower.
75
+
76
+ ORCA stages take arbitrary method keywords: `--orca-keywords` sets a default for every ORCA stage, and `--gas-orca-keywords`, `--freq-orca-keywords`, `--solv-opt-orca-keywords`, `--solv-sp-orca-keywords` override it per stage. PEACE adds the job type (`Opt`/`FREQ`) and the solvation model itself: `--solv-opt-orca-model` (`cpcm`, `smd`) and `--solv-sp-orca-model` (`cpcm`, `smd`, `cosmors`). See `python -m peace.main --help` for more runtime options.
77
+
78
+ Example: r2SCAN-3c gas-phase energies and frequencies, keeping GFN2-xTB/ALPB optimization and CPCM-X solvation:
79
+ ```
80
+ python -m peace.main --smiles "NCC(=O)O" --solvation \
81
+ --gas-calculator orca --freq-calculator orca --orca-keywords "r2SCAN-3c"
82
+ ```
83
+
84
+ ## pKa Prediction
85
+ The free energies of tautomers, and their ensembles, can be used to estimate pKa values. It is currently quite inaccurate and should be considered work in progress, or at least a way of estimating which micro-reactions contribute to acidity. Work is ongoing.
86
+
87
+ You can call this with the `--pka` flag as well as `--proton-energy` to estimate the proton free energy in solution.
@@ -0,0 +1,31 @@
1
+ """
2
+ PEACE - Protomer Enumeration And Charge Engine
3
+ A package for molecular protomer and tautomer enumeration.
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "protomer",
9
+ "engine",
10
+ "common",
11
+ "main",
12
+ "solvation",
13
+ "pka",
14
+ "api",
15
+ "run",
16
+ "enumerate_protomers",
17
+ "solvate",
18
+ "cli_defaults",
19
+ "PeaceResult",
20
+ ]
21
+
22
+ _API_NAMES = {"run", "enumerate_protomers", "solvate", "cli_defaults", "PeaceResult"}
23
+
24
+
25
+ def __getattr__(name):
26
+ # Lazy so that `import peace` stays cheap and peace.main can import __version__.
27
+ if name in _API_NAMES:
28
+ from peace import api
29
+
30
+ return getattr(api, name)
31
+ raise AttributeError(f"module 'peace' has no attribute {name!r}")
@@ -0,0 +1,110 @@
1
+ """
2
+ High-level Python API mirroring the PEACE command-line interface.
3
+
4
+ Every CLI flag is available as a keyword argument named after its argparse
5
+ destination, i.e. the flag with the leading dashes removed and remaining dashes
6
+ replaced by underscores (``--charge-min`` -> ``charge_min``,
7
+ ``--solv-sp-calculator`` -> ``solv_sp_calculator``). Use ``cli_defaults()`` to
8
+ list them.
9
+
10
+ Example::
11
+
12
+ import peace
13
+
14
+ result = peace.solvate("NCC(=O)O", max_conformers=2)
15
+ print(result.dataframe[["protomer_smiles", "boltzmann_fraction"]])
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from os import PathLike
21
+ from typing import Any
22
+
23
+ from peace.main import PeaceResult, _build_cli_parser, run_from_args
24
+
25
+ # Defaults that differ from the CLI: the API does not write files or open
26
+ # image windows unless asked to.
27
+ _API_DEFAULT_OVERRIDES: dict[str, Any] = {
28
+ "no_plot": True,
29
+ "output_csv": None,
30
+ }
31
+
32
+
33
+ def _raise_value_error(message: str) -> None:
34
+ raise ValueError(message)
35
+
36
+
37
+ def _build_api_parser():
38
+ parser = _build_cli_parser()
39
+ parser.error = _raise_value_error
40
+ return parser
41
+
42
+
43
+ def cli_defaults() -> dict[str, Any]:
44
+ """Return every accepted keyword argument with its default value."""
45
+ defaults = vars(_build_cli_parser().parse_args([]))
46
+ defaults.update(_API_DEFAULT_OVERRIDES)
47
+ return defaults
48
+
49
+
50
+ def _normalize_option(key: str, value: Any) -> Any:
51
+ if isinstance(value, PathLike):
52
+ return str(value)
53
+ if key == "add_tautomers" and isinstance(value, str):
54
+ return [value]
55
+ if key == "add_tautomers" and value is not None:
56
+ return [str(v) for v in value]
57
+ return value
58
+
59
+
60
+ def run(smiles: str, **options: Any) -> PeaceResult:
61
+ """
62
+ Run PEACE on ``smiles`` with the same options as the CLI.
63
+
64
+ Raises ``TypeError`` for unknown options and ``ValueError`` for invalid
65
+ option combinations (the cases where the CLI would exit with an error).
66
+ """
67
+ parser = _build_api_parser()
68
+ args = parser.parse_args([])
69
+ for key, value in _API_DEFAULT_OVERRIDES.items():
70
+ setattr(args, key, value)
71
+
72
+ valid_keys = set(vars(args))
73
+ unknown = sorted(set(options) - valid_keys)
74
+ if unknown:
75
+ raise TypeError(
76
+ f"Unknown PEACE option(s): {', '.join(unknown)}. "
77
+ "See peace.cli_defaults() for accepted keyword arguments."
78
+ )
79
+
80
+ args.smiles = str(smiles)
81
+ for key, value in options.items():
82
+ setattr(args, key, _normalize_option(key, value))
83
+
84
+ return run_from_args(parser, args, print_results=False)
85
+
86
+
87
+ def enumerate_protomers(
88
+ smiles: str,
89
+ *,
90
+ charge_min: int = 0,
91
+ charge_max: int = 0,
92
+ **options: Any,
93
+ ) -> PeaceResult:
94
+ """Enumerate tautomers and protomers over ``[charge_min, charge_max]`` without QM calculations."""
95
+ if options.get("solvation"):
96
+ raise ValueError("enumerate_protomers() does not run solvation; use solvate() instead.")
97
+ options.pop("solvation", None)
98
+ return run(smiles, charge_min=charge_min, charge_max=charge_max, solvation=False, **options)
99
+
100
+
101
+ def solvate(
102
+ smiles: str,
103
+ *,
104
+ charge_min: int = 0,
105
+ charge_max: int = 0,
106
+ **options: Any,
107
+ ) -> PeaceResult:
108
+ """Enumerate tautomers/protomers and compute solution-phase free energies and populations."""
109
+ options.pop("solvation", None)
110
+ return run(smiles, charge_min=charge_min, charge_max=charge_max, solvation=True, **options)
@@ -0,0 +1,69 @@
1
+ from .aimnet2 import run_aimnet2_optimization, run_aimnet2_single_point_energy
2
+ from .common import DEFAULT_TEMPERATURE_K, EV_TO_KCAL_MOL, HARTREE_TO_KCAL_MOL, float_regex, parse_last_float
3
+ from .orca import (
4
+ orca_opt_keyword,
5
+ orca_solvent_keyword,
6
+ run_orca_frequencies,
7
+ run_orca_optimization,
8
+ run_orca_single_point_energy,
9
+ run_orca_solvation_single_point,
10
+ spin_multiplicity,
11
+ verify_orca_executable,
12
+ )
13
+ from .settings import CalculatorSettings, OrcaSettings
14
+ from .xtb import (
15
+ XtbFatalError,
16
+ parse_xtb_rrho_contrib_hartree,
17
+ parse_xtb_solvent_free_energy_hartree,
18
+ parse_xtb_total_energy_hartree,
19
+ report_xtb_fatal_and_exit,
20
+ run_cpcmx_single_point,
21
+ run_gxtb_hessian_and_parse_energies,
22
+ run_gxtb_optimization,
23
+ run_gxtb_single_point_energy,
24
+ run_hessian_and_parse_energies,
25
+ run_xtb_command,
26
+ run_xtb_optimization,
27
+ run_xtb_single_point_energy,
28
+ )
29
+ from .xtb2 import (
30
+ run_gxtb_hessian_and_parse_energies as run_gxtb2_hessian_and_parse_energies,
31
+ run_gxtb_optimization as run_gxtb2_optimization,
32
+ run_gxtb_single_point_energy as run_gxtb2_single_point_energy,
33
+ )
34
+
35
+ __all__ = [
36
+ "DEFAULT_TEMPERATURE_K",
37
+ "HARTREE_TO_KCAL_MOL",
38
+ "EV_TO_KCAL_MOL",
39
+ "CalculatorSettings",
40
+ "OrcaSettings",
41
+ "XtbFatalError",
42
+ "float_regex",
43
+ "orca_opt_keyword",
44
+ "orca_solvent_keyword",
45
+ "parse_last_float",
46
+ "parse_xtb_rrho_contrib_hartree",
47
+ "parse_xtb_solvent_free_energy_hartree",
48
+ "parse_xtb_total_energy_hartree",
49
+ "report_xtb_fatal_and_exit",
50
+ "run_aimnet2_optimization",
51
+ "run_aimnet2_single_point_energy",
52
+ "run_cpcmx_single_point",
53
+ "run_gxtb_hessian_and_parse_energies",
54
+ "run_gxtb2_hessian_and_parse_energies",
55
+ "run_gxtb_optimization",
56
+ "run_gxtb2_optimization",
57
+ "run_gxtb_single_point_energy",
58
+ "run_gxtb2_single_point_energy",
59
+ "run_hessian_and_parse_energies",
60
+ "run_orca_frequencies",
61
+ "run_orca_optimization",
62
+ "run_orca_single_point_energy",
63
+ "run_orca_solvation_single_point",
64
+ "run_xtb_command",
65
+ "run_xtb_optimization",
66
+ "run_xtb_single_point_energy",
67
+ "spin_multiplicity",
68
+ "verify_orca_executable",
69
+ ]
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import traceback
4
+ from pathlib import Path
5
+ from typing import Callable, Optional
6
+
7
+ from .common import EV_TO_KCAL_MOL
8
+
9
+
10
+ def run_aimnet2_single_point_energy(
11
+ *,
12
+ scratch_dir: Path,
13
+ xyz_path: Path,
14
+ charge: int,
15
+ dry_run: bool,
16
+ log_paths: list[Path],
17
+ log_status: Callable[[list[Path], str, str], None],
18
+ model: str = "aimnet2",
19
+ ) -> tuple[Optional[float], Optional[float]]:
20
+ if dry_run:
21
+ log_status(log_paths, "SKIP", "dry_run; skipping AIMNet2 SP")
22
+ return None, None
23
+ try:
24
+ from ase.io import read
25
+ from aimnet.calculators import AIMNet2ASE
26
+ except Exception as exc:
27
+ raise RuntimeError(
28
+ "AIMNet2 dependencies are unavailable. Install 'aimnet[ase]' for AIMNet2 support."
29
+ ) from exc
30
+
31
+ log_status(log_paths, "STEP", f"running AIMNet2 SP on {xyz_path.name} model={model} charge={charge}")
32
+ try:
33
+ atoms = read(str(xyz_path))
34
+ atoms.calc = AIMNet2ASE(model, charge=int(charge))
35
+ energy_ev = float(atoms.get_potential_energy())
36
+ energy_kcal_mol = energy_ev * EV_TO_KCAL_MOL
37
+ except Exception as exc:
38
+ err = "".join(traceback.format_exception_only(type(exc), exc)).strip()
39
+ log_status(log_paths, "FAIL", f"AIMNet2 SP failed: {err}")
40
+ raise RuntimeError(f"AIMNet2 SP calculation failed: {err}") from exc
41
+
42
+ log_path = scratch_dir / "aimnet2sp_run.log"
43
+ log_path.write_text(
44
+ f"xyz_path={xyz_path}\nmodel={model}\ncharge={charge}\nenergy_ev={energy_ev}\nenergy_kcal_mol={energy_kcal_mol}\n",
45
+ encoding="utf-8",
46
+ )
47
+ log_status(log_paths, "OK", f"saved AIMNet2 SP summary to {log_path.name}")
48
+ return energy_kcal_mol, energy_ev
49
+
50
+
51
+ def run_aimnet2_optimization(
52
+ *,
53
+ scratch_dir: Path,
54
+ input_xyz_path: Path,
55
+ charge: int,
56
+ dry_run: bool,
57
+ log_paths: list[Path],
58
+ log_status: Callable[[list[Path], str, str], None],
59
+ model: str = "aimnet2",
60
+ fmax: float = 0.01,
61
+ max_steps: int = 200,
62
+ ) -> tuple[Path, Optional[float], Optional[float]]:
63
+ optimized_xyz_path = scratch_dir / "aimnet2opt.xyz"
64
+ if dry_run:
65
+ log_status(log_paths, "SKIP", "dry_run; skipping AIMNet2 optimization")
66
+ return optimized_xyz_path, None, None
67
+ try:
68
+ from ase.io import read, write
69
+ from ase.optimize import LBFGS
70
+ from aimnet.calculators import AIMNet2ASE
71
+ except Exception as exc:
72
+ raise RuntimeError(
73
+ "AIMNet2 dependencies are unavailable. Install 'aimnet[ase]' for AIMNet2 support."
74
+ ) from exc
75
+
76
+ log_status(
77
+ log_paths,
78
+ "STEP",
79
+ f"running AIMNet2 optimization on {input_xyz_path.name} model={model} charge={charge} fmax={fmax}",
80
+ )
81
+ try:
82
+ atoms = read(str(input_xyz_path))
83
+ atoms.calc = AIMNet2ASE(model, charge=int(charge))
84
+ opt = LBFGS(atoms, logfile=str(scratch_dir / "aimnet2opt_run.log"))
85
+ opt.run(fmax=float(fmax), steps=int(max_steps))
86
+ write(str(optimized_xyz_path), atoms)
87
+ energy_ev = float(atoms.get_potential_energy())
88
+ energy_kcal_mol = energy_ev * EV_TO_KCAL_MOL
89
+ except Exception as exc:
90
+ err = "".join(traceback.format_exception_only(type(exc), exc)).strip()
91
+ log_status(log_paths, "FAIL", f"AIMNet2 optimization failed: {err}")
92
+ raise RuntimeError(f"AIMNet2 optimization failed: {err}") from exc
93
+
94
+ log_status(log_paths, "OK", f"AIMNet2 optimization produced {optimized_xyz_path.name}")
95
+ return optimized_xyz_path, energy_kcal_mol, energy_ev
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Optional
5
+
6
+ HARTREE_TO_KCAL_MOL = 627.5094740631
7
+ EV_TO_KCAL_MOL = 23.06054783061903
8
+
9
+ # Default thermochemical / Boltzmann temperature (Kelvin).
10
+ DEFAULT_TEMPERATURE_K = 298.15
11
+
12
+ # xTB --opt convergence presets, loosest to tightest.
13
+ OPT_CONVERGENCE_LEVELS: tuple[str, ...] = (
14
+ "crude",
15
+ "sloppy",
16
+ "loose",
17
+ "lax",
18
+ "normal",
19
+ "tight",
20
+ "vtight",
21
+ "extreme",
22
+ )
23
+
24
+
25
+ def looser_opt_convergence_level(opt_level: str) -> Optional[str]:
26
+ """Return the next-looser convergence preset, or None if already at crude."""
27
+ try:
28
+ idx = OPT_CONVERGENCE_LEVELS.index(opt_level)
29
+ except ValueError:
30
+ return None
31
+ if idx == 0:
32
+ return None
33
+ return OPT_CONVERGENCE_LEVELS[idx - 1]
34
+
35
+
36
+ def opt_convergence_retry_levels(opt_level: str) -> list[str]:
37
+ """Return convergence presets to try, from opt_level down to crude."""
38
+ try:
39
+ idx = OPT_CONVERGENCE_LEVELS.index(opt_level)
40
+ except ValueError:
41
+ return [opt_level]
42
+ return list(OPT_CONVERGENCE_LEVELS[idx::-1])
43
+
44
+
45
+ def float_regex() -> str:
46
+ return r"[-+]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][-+]?\d+)?"
47
+
48
+
49
+ def parse_last_float(patterns: list[str], text: str) -> Optional[float]:
50
+ for pat in patterns:
51
+ matches = list(re.finditer(pat, text, flags=re.IGNORECASE | re.MULTILINE))
52
+ if not matches:
53
+ continue
54
+ for m in reversed(matches):
55
+ try:
56
+ return float(m.group(1))
57
+ except ValueError:
58
+ continue
59
+ return None