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.
- peace_protomer-0.1.0/LICENSE +21 -0
- peace_protomer-0.1.0/PKG-INFO +118 -0
- peace_protomer-0.1.0/README.md +87 -0
- peace_protomer-0.1.0/peace/__init__.py +31 -0
- peace_protomer-0.1.0/peace/api.py +110 -0
- peace_protomer-0.1.0/peace/calculators/__init__.py +69 -0
- peace_protomer-0.1.0/peace/calculators/aimnet2.py +95 -0
- peace_protomer-0.1.0/peace/calculators/common.py +59 -0
- peace_protomer-0.1.0/peace/calculators/orca.py +457 -0
- peace_protomer-0.1.0/peace/calculators/settings.py +184 -0
- peace_protomer-0.1.0/peace/calculators/xtb.py +838 -0
- peace_protomer-0.1.0/peace/calculators/xtb2.py +196 -0
- peace_protomer-0.1.0/peace/common.py +172 -0
- peace_protomer-0.1.0/peace/data/allowed_solvents.txt +15 -0
- peace_protomer-0.1.0/peace/data/cpcm_allowed_solvents.txt +93 -0
- peace_protomer-0.1.0/peace/data/cpcm_internal_solvent_names.txt +98 -0
- peace_protomer-0.1.0/peace/engine.py +182 -0
- peace_protomer-0.1.0/peace/logging_utils.py +94 -0
- peace_protomer-0.1.0/peace/main.py +1680 -0
- peace_protomer-0.1.0/peace/pka.py +1167 -0
- peace_protomer-0.1.0/peace/protomer.py +1017 -0
- peace_protomer-0.1.0/peace/solvation.py +3107 -0
- peace_protomer-0.1.0/peace/solvents.py +204 -0
- peace_protomer-0.1.0/peace/visualization.py +928 -0
- peace_protomer-0.1.0/peace_protomer.egg-info/PKG-INFO +118 -0
- peace_protomer-0.1.0/peace_protomer.egg-info/SOURCES.txt +36 -0
- peace_protomer-0.1.0/peace_protomer.egg-info/dependency_links.txt +1 -0
- peace_protomer-0.1.0/peace_protomer.egg-info/entry_points.txt +2 -0
- peace_protomer-0.1.0/peace_protomer.egg-info/requires.txt +9 -0
- peace_protomer-0.1.0/peace_protomer.egg-info/top_level.txt +1 -0
- peace_protomer-0.1.0/pyproject.toml +60 -0
- peace_protomer-0.1.0/setup.cfg +4 -0
- peace_protomer-0.1.0/tests/test_aimnet2.py +30 -0
- peace_protomer-0.1.0/tests/test_api.py +108 -0
- peace_protomer-0.1.0/tests/test_cli.py +520 -0
- peace_protomer-0.1.0/tests/test_enumeration.py +224 -0
- peace_protomer-0.1.0/tests/test_qm_workflow.py +165 -0
- 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
|
+

|
|
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
|
+

|
|
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
|