Spaces:
Sleeping
Sleeping
Upload 690 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +30 -0
- Dockerfile +18 -0
- README.md +27 -5
- app.py +45 -0
- autodE/mcp_output/README_MCP.md +59 -0
- autodE/mcp_output/analysis.json +992 -0
- autodE/mcp_output/diff_report.md +68 -0
- autodE/mcp_output/mcp_plugin/__init__.py +0 -0
- autodE/mcp_output/mcp_plugin/adapter.py +248 -0
- autodE/mcp_output/mcp_plugin/main.py +13 -0
- autodE/mcp_output/mcp_plugin/mcp_service.py +69 -0
- autodE/mcp_output/requirements.txt +13 -0
- autodE/mcp_output/start_mcp.py +30 -0
- autodE/mcp_output/workflow_summary.json +216 -0
- autodE/source/.pre-commit-config.yaml +19 -0
- autodE/source/CONTRIBUTING.md +5 -0
- autodE/source/LICENSE.md +22 -0
- autodE/source/README.md +100 -0
- autodE/source/__init__.py +4 -0
- autodE/source/autode/__init__.py +71 -0
- autodE/source/autode/atoms.py +1865 -0
- autodE/source/autode/bond_rearrangement.py +876 -0
- autodE/source/autode/bonds.py +87 -0
- autodE/source/autode/bracket/__init__.py +3 -0
- autodE/source/autode/bracket/base.py +316 -0
- autodE/source/autode/bracket/dhs.py +764 -0
- autodE/source/autode/bracket/ieip.py +601 -0
- autodE/source/autode/bracket/imagepair.py +628 -0
- autodE/source/autode/calculations/__init__.py +5 -0
- autodE/source/autode/calculations/calculation.py +328 -0
- autodE/source/autode/calculations/executors.py +524 -0
- autodE/source/autode/calculations/input.py +72 -0
- autodE/source/autode/calculations/output.py +87 -0
- autodE/source/autode/calculations/types.py +10 -0
- autodE/source/autode/common/NEB.pdf +3 -0
- autodE/source/autode/common/NEB.tex +60 -0
- autodE/source/autode/common/adaptive_path.pdf +0 -0
- autodE/source/autode/common/adaptive_path.tex +32 -0
- autodE/source/autode/common/hessians.pdf +3 -0
- autodE/source/autode/common/hessians.tex +192 -0
- autodE/source/autode/common/llogo.png +3 -0
- autodE/source/autode/common/logo.pages +3 -0
- autodE/source/autode/common/thermochemistry.pdf +3 -0
- autodE/source/autode/common/thermochemistry.tex +91 -0
- autodE/source/autode/config.py +459 -0
- autodE/source/autode/conformers/__init__.py +4 -0
- autodE/source/autode/conformers/cconf_gen.pyx +131 -0
- autodE/source/autode/conformers/conf_gen.py +537 -0
- autodE/source/autode/conformers/conformer.py +184 -0
- autodE/source/autode/conformers/conformers.py +360 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,33 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
autodE/source/autode/common/hessians.pdf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
autodE/source/autode/common/llogo.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
autodE/source/autode/common/logo.pages filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
autodE/source/autode/common/NEB.pdf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
autodE/source/autode/common/thermochemistry.pdf filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
autodE/source/doc/common/adapt_surface_sn2.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
autodE/source/doc/common/claisen_neb_optimised.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
autodE/source/doc/common/conformers.png filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
autodE/source/doc/common/curtius_ts.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
autodE/source/doc/common/curtius.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
autodE/source/doc/common/DA_surface_interpolated.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
autodE/source/doc/common/DA_surface.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
autodE/source/doc/common/diels_alder_quickstart.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
autodE/source/doc/common/diels_alder.png filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
autodE/source/doc/common/functionalisation.png filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
autodE/source/doc/common/logo.png filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
autodE/source/doc/common/molfunc_functionalisation.png filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
autodE/source/doc/common/na_h2o_3_confomers.png filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
autodE/source/doc/common/OH_PES_relaxed.png filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
autodE/source/doc/common/OH_PES_unrelaxed_DFT.png filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
autodE/source/doc/common/OH_PES_unrelaxed.png filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
autodE/source/doc/common/opt_convergence_3500_ORCA.png filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
autodE/source/doc/common/sn2_image.png filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
autodE/source/doc/common/vaskas_conformers.png filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
autodE/source/doc/common/vaskas.png filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
autodE/source/doc/common/water_opt_energy.png filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
autodE/source/doc/common/water_shift.png filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
autodE/source/doc/common/water_trimer_expl.png filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
autodE/source/doc/common/water_trimer.png filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
autodE/source/doc/common/XY_bde_XTB.png filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
|
| 4 |
+
USER user
|
| 5 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 10 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY --chown=user . /app
|
| 13 |
+
ENV MCP_TRANSPORT=http
|
| 14 |
+
ENV MCP_PORT=7860
|
| 15 |
+
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
CMD ["python", "autodE/mcp_output/start_mcp.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,32 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Autode MCP
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
sdk_version: "4.26.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Autode MCP Service
|
| 13 |
+
|
| 14 |
+
Auto-generated MCP service for autodE.
|
| 15 |
+
|
| 16 |
+
## Usage
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
https://None-autodE-mcp.hf.space/mcp
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Connect with Cursor
|
| 23 |
+
|
| 24 |
+
```json
|
| 25 |
+
{
|
| 26 |
+
"mcpServers": {
|
| 27 |
+
"autodE": {
|
| 28 |
+
"url": "https://None-autodE-mcp.hf.space/mcp"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
mcp_plugin_path = os.path.join(os.path.dirname(__file__), "autodE", "mcp_output", "mcp_plugin")
|
| 6 |
+
sys.path.insert(0, mcp_plugin_path)
|
| 7 |
+
|
| 8 |
+
app = FastAPI(
|
| 9 |
+
title="Autode MCP Service",
|
| 10 |
+
description="Auto-generated MCP service for autodE",
|
| 11 |
+
version="1.0.0"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
@app.get("/")
|
| 15 |
+
def root():
|
| 16 |
+
return {
|
| 17 |
+
"service": "Autode MCP Service",
|
| 18 |
+
"version": "1.0.0",
|
| 19 |
+
"status": "running",
|
| 20 |
+
"transport": os.environ.get("MCP_TRANSPORT", "http")
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@app.get("/health")
|
| 24 |
+
def health_check():
|
| 25 |
+
return {"status": "healthy", "service": "autodE MCP"}
|
| 26 |
+
|
| 27 |
+
@app.get("/tools")
|
| 28 |
+
def list_tools():
|
| 29 |
+
try:
|
| 30 |
+
from mcp_service import create_app
|
| 31 |
+
mcp_app = create_app()
|
| 32 |
+
tools = []
|
| 33 |
+
for tool_name, tool_func in mcp_app.tools.items():
|
| 34 |
+
tools.append({
|
| 35 |
+
"name": tool_name,
|
| 36 |
+
"description": tool_func.__doc__ or "No description available"
|
| 37 |
+
})
|
| 38 |
+
return {"tools": tools}
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return {"error": f"Failed to load tools: {str(e)}"}
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
import uvicorn
|
| 44 |
+
port = int(os.environ.get("PORT", 7860))
|
| 45 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
autodE/mcp_output/README_MCP.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# autodE
|
| 2 |
+
|
| 3 |
+
## Project Introduction
|
| 4 |
+
|
| 5 |
+
autodE is a Python module designed for the automated calculation of reaction profiles from SMILES strings of reactants and products. It automates the complex process of finding transition states, performing conformer searches, and generating complete reaction energy profiles using quantum chemical calculations. The core functionalities include handling atomic properties, managing quantum chemical calculations, defining and analyzing chemical reactions, and optimizing transition states.
|
| 6 |
+
|
| 7 |
+
## Installation Method
|
| 8 |
+
|
| 9 |
+
To install autodE, ensure you have Python installed and then use the following pip command:
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
pip install autodE
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
### Dependencies
|
| 16 |
+
|
| 17 |
+
autodE requires the following dependencies:
|
| 18 |
+
- Required: numpy, scipy, ase
|
| 19 |
+
- Optional: matplotlib, pandas
|
| 20 |
+
|
| 21 |
+
Ensure these dependencies are installed in your environment.
|
| 22 |
+
|
| 23 |
+
## Quick Start
|
| 24 |
+
|
| 25 |
+
Here's a simple example to get started with autodE:
|
| 26 |
+
|
| 27 |
+
1. Import autodE and define reactants and products using SMILES strings.
|
| 28 |
+
2. Create a Reaction object and calculate the reaction profile.
|
| 29 |
+
|
| 30 |
+
```
|
| 31 |
+
import autode as ade
|
| 32 |
+
|
| 33 |
+
# Define reactants and products
|
| 34 |
+
reactant = ade.Reactant(smiles='CC[H]')
|
| 35 |
+
product = ade.Product(smiles='C[H]C')
|
| 36 |
+
|
| 37 |
+
# Create reaction and calculate profile
|
| 38 |
+
reaction = ade.Reaction(reactant, product, name='1-2_shift')
|
| 39 |
+
reaction.calculate_reaction_profile()
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
This high-level interface abstracts the complexity of transition state location, conformer generation, and thermochemical analysis while providing full control over the underlying quantum chemical calculations.
|
| 43 |
+
|
| 44 |
+
## Available Tools and Endpoints List
|
| 45 |
+
|
| 46 |
+
- **autode-calculate**: Runs a quantum chemical calculation using specified parameters.
|
| 47 |
+
- **autode-reaction**: Analyzes a chemical reaction and computes its properties.
|
| 48 |
+
|
| 49 |
+
## Common Issues and Notes
|
| 50 |
+
|
| 51 |
+
- Ensure all required dependencies are installed to avoid import errors.
|
| 52 |
+
- The performance of calculations can be affected by the computational resources available. Adjust the number of cores and memory settings in the configuration if necessary.
|
| 53 |
+
- If using optional dependencies like matplotlib or pandas, ensure they are installed for enhanced functionality such as plotting and data manipulation.
|
| 54 |
+
|
| 55 |
+
## Reference Links or Documentation
|
| 56 |
+
|
| 57 |
+
For more detailed information, visit the autodE GitHub repository: [autodE GitHub](https://github.com/duartegroup/autodE)
|
| 58 |
+
|
| 59 |
+
For comprehensive documentation, refer to the autodE documentation available in the repository.
|
autodE/mcp_output/analysis.json
ADDED
|
@@ -0,0 +1,992 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"summary": {
|
| 3 |
+
"repository_url": "https://github.com/duartegroup/autodE",
|
| 4 |
+
"summary": "Imported via zip fallback, file count: 275",
|
| 5 |
+
"file_tree": {
|
| 6 |
+
".github/ISSUE_TEMPLATE/bug_report.md": {
|
| 7 |
+
"size": 567
|
| 8 |
+
},
|
| 9 |
+
".github/ISSUE_TEMPLATE/feature_request.md": {
|
| 10 |
+
"size": 194
|
| 11 |
+
},
|
| 12 |
+
".github/pull_request_template.md": {
|
| 13 |
+
"size": 273
|
| 14 |
+
},
|
| 15 |
+
".github/workflows/catch.yml": {
|
| 16 |
+
"size": 651
|
| 17 |
+
},
|
| 18 |
+
".github/workflows/deploy.yml": {
|
| 19 |
+
"size": 824
|
| 20 |
+
},
|
| 21 |
+
".github/workflows/lint.yml": {
|
| 22 |
+
"size": 683
|
| 23 |
+
},
|
| 24 |
+
".github/workflows/pytest.yml": {
|
| 25 |
+
"size": 1191
|
| 26 |
+
},
|
| 27 |
+
".github/workflows/pytest_cov.yml": {
|
| 28 |
+
"size": 1396
|
| 29 |
+
},
|
| 30 |
+
".pre-commit-config.yaml": {
|
| 31 |
+
"size": 446
|
| 32 |
+
},
|
| 33 |
+
"CONTRIBUTING.md": {
|
| 34 |
+
"size": 228
|
| 35 |
+
},
|
| 36 |
+
"LICENSE.md": {
|
| 37 |
+
"size": 1067
|
| 38 |
+
},
|
| 39 |
+
"README.md": {
|
| 40 |
+
"size": 4804
|
| 41 |
+
},
|
| 42 |
+
"autode/__init__.py": {
|
| 43 |
+
"size": 1744
|
| 44 |
+
},
|
| 45 |
+
"autode/atoms.py": {
|
| 46 |
+
"size": 46059
|
| 47 |
+
},
|
| 48 |
+
"autode/bond_rearrangement.py": {
|
| 49 |
+
"size": 28652
|
| 50 |
+
},
|
| 51 |
+
"autode/bonds.py": {
|
| 52 |
+
"size": 2554
|
| 53 |
+
},
|
| 54 |
+
"autode/bracket/__init__.py": {
|
| 55 |
+
"size": 70
|
| 56 |
+
},
|
| 57 |
+
"autode/bracket/base.py": {
|
| 58 |
+
"size": 11755
|
| 59 |
+
},
|
| 60 |
+
"autode/bracket/dhs.py": {
|
| 61 |
+
"size": 28076
|
| 62 |
+
},
|
| 63 |
+
"autode/bracket/ieip.py": {
|
| 64 |
+
"size": 22489
|
| 65 |
+
},
|
| 66 |
+
"autode/bracket/imagepair.py": {
|
| 67 |
+
"size": 21276
|
| 68 |
+
},
|
| 69 |
+
"autode/calculations/__init__.py": {
|
| 70 |
+
"size": 236
|
| 71 |
+
},
|
| 72 |
+
"autode/calculations/calculation.py": {
|
| 73 |
+
"size": 10489
|
| 74 |
+
},
|
| 75 |
+
"autode/calculations/executors.py": {
|
| 76 |
+
"size": 18044
|
| 77 |
+
},
|
| 78 |
+
"autode/calculations/input.py": {
|
| 79 |
+
"size": 2394
|
| 80 |
+
},
|
| 81 |
+
"autode/calculations/output.py": {
|
| 82 |
+
"size": 2371
|
| 83 |
+
},
|
| 84 |
+
"autode/calculations/types.py": {
|
| 85 |
+
"size": 167
|
| 86 |
+
},
|
| 87 |
+
"autode/config.py": {
|
| 88 |
+
"size": 19291
|
| 89 |
+
},
|
| 90 |
+
"autode/conformers/__init__.py": {
|
| 91 |
+
"size": 141
|
| 92 |
+
},
|
| 93 |
+
"autode/conformers/conf_gen.py": {
|
| 94 |
+
"size": 16976
|
| 95 |
+
},
|
| 96 |
+
"autode/conformers/conformer.py": {
|
| 97 |
+
"size": 5866
|
| 98 |
+
},
|
| 99 |
+
"autode/conformers/conformers.py": {
|
| 100 |
+
"size": 12027
|
| 101 |
+
},
|
| 102 |
+
"autode/constants.py": {
|
| 103 |
+
"size": 965
|
| 104 |
+
},
|
| 105 |
+
"autode/constraints.py": {
|
| 106 |
+
"size": 5418
|
| 107 |
+
},
|
| 108 |
+
"autode/exceptions.py": {
|
| 109 |
+
"size": 4054
|
| 110 |
+
},
|
| 111 |
+
"autode/ext/CMakeLists.txt": {
|
| 112 |
+
"size": 941
|
| 113 |
+
},
|
| 114 |
+
"autode/ext/README.md": {
|
| 115 |
+
"size": 374
|
| 116 |
+
},
|
| 117 |
+
"autode/ext/__init__.py": {
|
| 118 |
+
"size": 0
|
| 119 |
+
},
|
| 120 |
+
"autode/geom.py": {
|
| 121 |
+
"size": 9283
|
| 122 |
+
},
|
| 123 |
+
"autode/hessians.py": {
|
| 124 |
+
"size": 23080
|
| 125 |
+
},
|
| 126 |
+
"autode/input_output.py": {
|
| 127 |
+
"size": 5343
|
| 128 |
+
},
|
| 129 |
+
"autode/log/__init__.py": {
|
| 130 |
+
"size": 57
|
| 131 |
+
},
|
| 132 |
+
"autode/log/log.py": {
|
| 133 |
+
"size": 2015
|
| 134 |
+
},
|
| 135 |
+
"autode/log/methods.py": {
|
| 136 |
+
"size": 1210
|
| 137 |
+
},
|
| 138 |
+
"autode/methods.py": {
|
| 139 |
+
"size": 4468
|
| 140 |
+
},
|
| 141 |
+
"autode/mol_graphs.py": {
|
| 142 |
+
"size": 24611
|
| 143 |
+
},
|
| 144 |
+
"autode/neb/__init__.py": {
|
| 145 |
+
"size": 96
|
| 146 |
+
},
|
| 147 |
+
"autode/neb/ci.py": {
|
| 148 |
+
"size": 3882
|
| 149 |
+
},
|
| 150 |
+
"autode/neb/idpp.py": {
|
| 151 |
+
"size": 5325
|
| 152 |
+
},
|
| 153 |
+
"autode/neb/neb.py": {
|
| 154 |
+
"size": 1899
|
| 155 |
+
},
|
| 156 |
+
"autode/neb/original.py": {
|
| 157 |
+
"size": 25297
|
| 158 |
+
},
|
| 159 |
+
"autode/opt/__init__.py": {
|
| 160 |
+
"size": 164
|
| 161 |
+
},
|
| 162 |
+
"autode/opt/coordinates/__init__.py": {
|
| 163 |
+
"size": 184
|
| 164 |
+
},
|
| 165 |
+
"autode/opt/coordinates/_autodiff.py": {
|
| 166 |
+
"size": 24107
|
| 167 |
+
},
|
| 168 |
+
"autode/opt/coordinates/base.py": {
|
| 169 |
+
"size": 14924
|
| 170 |
+
},
|
| 171 |
+
"autode/opt/coordinates/cartesian.py": {
|
| 172 |
+
"size": 4114
|
| 173 |
+
},
|
| 174 |
+
"autode/opt/coordinates/dic.py": {
|
| 175 |
+
"size": 18255
|
| 176 |
+
},
|
| 177 |
+
"autode/opt/coordinates/dimer.py": {
|
| 178 |
+
"size": 8581
|
| 179 |
+
},
|
| 180 |
+
"autode/opt/coordinates/internals.py": {
|
| 181 |
+
"size": 19160
|
| 182 |
+
},
|
| 183 |
+
"autode/opt/coordinates/primitives.py": {
|
| 184 |
+
"size": 19426
|
| 185 |
+
},
|
| 186 |
+
"autode/opt/optimisers/__init__.py": {
|
| 187 |
+
"size": 565
|
| 188 |
+
},
|
| 189 |
+
"autode/opt/optimisers/base.py": {
|
| 190 |
+
"size": 43844
|
| 191 |
+
},
|
| 192 |
+
"autode/opt/optimisers/crfo.py": {
|
| 193 |
+
"size": 9294
|
| 194 |
+
},
|
| 195 |
+
"autode/opt/optimisers/dimer.py": {
|
| 196 |
+
"size": 13546
|
| 197 |
+
},
|
| 198 |
+
"autode/opt/optimisers/hessian_update.py": {
|
| 199 |
+
"size": 17503
|
| 200 |
+
},
|
| 201 |
+
"autode/opt/optimisers/prfo.py": {
|
| 202 |
+
"size": 4780
|
| 203 |
+
},
|
| 204 |
+
"autode/opt/optimisers/qa.py": {
|
| 205 |
+
"size": 4388
|
| 206 |
+
},
|
| 207 |
+
"autode/opt/optimisers/rfo.py": {
|
| 208 |
+
"size": 5307
|
| 209 |
+
},
|
| 210 |
+
"autode/opt/optimisers/steepest_descent.py": {
|
| 211 |
+
"size": 1992
|
| 212 |
+
},
|
| 213 |
+
"autode/opt/optimisers/utils.py": {
|
| 214 |
+
"size": 5039
|
| 215 |
+
},
|
| 216 |
+
"autode/path/__init__.py": {
|
| 217 |
+
"size": 117
|
| 218 |
+
},
|
| 219 |
+
"autode/path/adaptive.py": {
|
| 220 |
+
"size": 12943
|
| 221 |
+
},
|
| 222 |
+
"autode/path/interpolation.py": {
|
| 223 |
+
"size": 8476
|
| 224 |
+
},
|
| 225 |
+
"autode/path/path.py": {
|
| 226 |
+
"size": 5925
|
| 227 |
+
},
|
| 228 |
+
"autode/pes/__init__.py": {
|
| 229 |
+
"size": 139
|
| 230 |
+
},
|
| 231 |
+
"autode/pes/mep.py": {
|
| 232 |
+
"size": 1239
|
| 233 |
+
},
|
| 234 |
+
"autode/pes/pes_nd.py": {
|
| 235 |
+
"size": 29328
|
| 236 |
+
},
|
| 237 |
+
"autode/pes/reactive.py": {
|
| 238 |
+
"size": 13627
|
| 239 |
+
},
|
| 240 |
+
"autode/pes/relaxed.py": {
|
| 241 |
+
"size": 7520
|
| 242 |
+
},
|
| 243 |
+
"autode/pes/unrelaxed.py": {
|
| 244 |
+
"size": 4701
|
| 245 |
+
},
|
| 246 |
+
"autode/plotting.py": {
|
| 247 |
+
"size": 16231
|
| 248 |
+
},
|
| 249 |
+
"autode/point_charges.py": {
|
| 250 |
+
"size": 1084
|
| 251 |
+
},
|
| 252 |
+
"autode/reactions/__init__.py": {
|
| 253 |
+
"size": 150
|
| 254 |
+
},
|
| 255 |
+
"autode/reactions/multistep.py": {
|
| 256 |
+
"size": 7474
|
| 257 |
+
},
|
| 258 |
+
"autode/reactions/reaction.py": {
|
| 259 |
+
"size": 32095
|
| 260 |
+
},
|
| 261 |
+
"autode/reactions/reaction_types.py": {
|
| 262 |
+
"size": 2720
|
| 263 |
+
},
|
| 264 |
+
"autode/smiles/__init__.py": {
|
| 265 |
+
"size": 115
|
| 266 |
+
},
|
| 267 |
+
"autode/smiles/angles.py": {
|
| 268 |
+
"size": 9039
|
| 269 |
+
},
|
| 270 |
+
"autode/smiles/atom_types.py": {
|
| 271 |
+
"size": 12856
|
| 272 |
+
},
|
| 273 |
+
"autode/smiles/base.py": {
|
| 274 |
+
"size": 8439
|
| 275 |
+
},
|
| 276 |
+
"autode/smiles/builder.py": {
|
| 277 |
+
"size": 36448
|
| 278 |
+
},
|
| 279 |
+
"autode/smiles/parser.py": {
|
| 280 |
+
"size": 18540
|
| 281 |
+
},
|
| 282 |
+
"autode/smiles/smiles.py": {
|
| 283 |
+
"size": 5932
|
| 284 |
+
},
|
| 285 |
+
"autode/solvent/__init__.py": {
|
| 286 |
+
"size": 211
|
| 287 |
+
},
|
| 288 |
+
"autode/solvent/explicit_solvent.py": {
|
| 289 |
+
"size": 8622
|
| 290 |
+
},
|
| 291 |
+
"autode/solvent/solvents.py": {
|
| 292 |
+
"size": 65287
|
| 293 |
+
},
|
| 294 |
+
"autode/species/__init__.py": {
|
| 295 |
+
"size": 522
|
| 296 |
+
},
|
| 297 |
+
"autode/species/complex.py": {
|
| 298 |
+
"size": 15627
|
| 299 |
+
},
|
| 300 |
+
"autode/species/molecule.py": {
|
| 301 |
+
"size": 9914
|
| 302 |
+
},
|
| 303 |
+
"autode/species/species.py": {
|
| 304 |
+
"size": 54754
|
| 305 |
+
},
|
| 306 |
+
"autode/substitution.py": {
|
| 307 |
+
"size": 10025
|
| 308 |
+
},
|
| 309 |
+
"autode/thermochemistry/__init__.py": {
|
| 310 |
+
"size": 177
|
| 311 |
+
},
|
| 312 |
+
"autode/thermochemistry/igm.py": {
|
| 313 |
+
"size": 17425
|
| 314 |
+
},
|
| 315 |
+
"autode/thermochemistry/symmetry.py": {
|
| 316 |
+
"size": 8040
|
| 317 |
+
},
|
| 318 |
+
"autode/transition_states/__init__.py": {
|
| 319 |
+
"size": 258
|
| 320 |
+
},
|
| 321 |
+
"autode/transition_states/base.py": {
|
| 322 |
+
"size": 19578
|
| 323 |
+
},
|
| 324 |
+
"autode/transition_states/lib/template0.txt": {
|
| 325 |
+
"size": 497
|
| 326 |
+
},
|
| 327 |
+
"autode/transition_states/locate_tss.py": {
|
| 328 |
+
"size": 12006
|
| 329 |
+
},
|
| 330 |
+
"autode/transition_states/templates.py": {
|
| 331 |
+
"size": 14335
|
| 332 |
+
},
|
| 333 |
+
"autode/transition_states/transition_state.py": {
|
| 334 |
+
"size": 13849
|
| 335 |
+
},
|
| 336 |
+
"autode/transition_states/transition_states.py": {
|
| 337 |
+
"size": 863
|
| 338 |
+
},
|
| 339 |
+
"autode/transition_states/truncation.py": {
|
| 340 |
+
"size": 11766
|
| 341 |
+
},
|
| 342 |
+
"autode/transition_states/ts_guess.py": {
|
| 343 |
+
"size": 8954
|
| 344 |
+
},
|
| 345 |
+
"autode/units.py": {
|
| 346 |
+
"size": 7919
|
| 347 |
+
},
|
| 348 |
+
"autode/utils.py": {
|
| 349 |
+
"size": 22566
|
| 350 |
+
},
|
| 351 |
+
"autode/values.py": {
|
| 352 |
+
"size": 23224
|
| 353 |
+
},
|
| 354 |
+
"autode/wrappers/G09.py": {
|
| 355 |
+
"size": 24012
|
| 356 |
+
},
|
| 357 |
+
"autode/wrappers/G16.py": {
|
| 358 |
+
"size": 503
|
| 359 |
+
},
|
| 360 |
+
"autode/wrappers/MOPAC.py": {
|
| 361 |
+
"size": 12114
|
| 362 |
+
},
|
| 363 |
+
"autode/wrappers/NWChem.py": {
|
| 364 |
+
"size": 15218
|
| 365 |
+
},
|
| 366 |
+
"autode/wrappers/ORCA.py": {
|
| 367 |
+
"size": 20640
|
| 368 |
+
},
|
| 369 |
+
"autode/wrappers/QChem.py": {
|
| 370 |
+
"size": 20416
|
| 371 |
+
},
|
| 372 |
+
"autode/wrappers/XTB.py": {
|
| 373 |
+
"size": 14414
|
| 374 |
+
},
|
| 375 |
+
"autode/wrappers/__init__.py": {
|
| 376 |
+
"size": 0
|
| 377 |
+
},
|
| 378 |
+
"autode/wrappers/keywords/__init__.py": {
|
| 379 |
+
"size": 1238
|
| 380 |
+
},
|
| 381 |
+
"autode/wrappers/keywords/basis_sets.py": {
|
| 382 |
+
"size": 1051
|
| 383 |
+
},
|
| 384 |
+
"autode/wrappers/keywords/dispersion.py": {
|
| 385 |
+
"size": 226
|
| 386 |
+
},
|
| 387 |
+
"autode/wrappers/keywords/functionals.py": {
|
| 388 |
+
"size": 662
|
| 389 |
+
},
|
| 390 |
+
"autode/wrappers/keywords/implicit_solvent_types.py": {
|
| 391 |
+
"size": 358
|
| 392 |
+
},
|
| 393 |
+
"autode/wrappers/keywords/keywords.py": {
|
| 394 |
+
"size": 19880
|
| 395 |
+
},
|
| 396 |
+
"autode/wrappers/keywords/ri.py": {
|
| 397 |
+
"size": 146
|
| 398 |
+
},
|
| 399 |
+
"autode/wrappers/keywords/wf.py": {
|
| 400 |
+
"size": 92
|
| 401 |
+
},
|
| 402 |
+
"autode/wrappers/methods.py": {
|
| 403 |
+
"size": 10180
|
| 404 |
+
},
|
| 405 |
+
"doc/README.md": {
|
| 406 |
+
"size": 318
|
| 407 |
+
},
|
| 408 |
+
"doc/common/DA_2d.py": {
|
| 409 |
+
"size": 368
|
| 410 |
+
},
|
| 411 |
+
"doc/common/DA_2d_interp.py": {
|
| 412 |
+
"size": 134
|
| 413 |
+
},
|
| 414 |
+
"doc/common/OH_PES_relaxed.py": {
|
| 415 |
+
"size": 614
|
| 416 |
+
},
|
| 417 |
+
"doc/common/OH_PES_unrelaxed.py": {
|
| 418 |
+
"size": 667
|
| 419 |
+
},
|
| 420 |
+
"doc/common/OH_PES_unrelaxed_DFT.py": {
|
| 421 |
+
"size": 734
|
| 422 |
+
},
|
| 423 |
+
"doc/common/XY_bde_XTB.py": {
|
| 424 |
+
"size": 2167
|
| 425 |
+
},
|
| 426 |
+
"doc/common/claisen_cineb.py": {
|
| 427 |
+
"size": 505
|
| 428 |
+
},
|
| 429 |
+
"doc/common/curtius.py": {
|
| 430 |
+
"size": 169
|
| 431 |
+
},
|
| 432 |
+
"doc/common/methane_molfunc.py": {
|
| 433 |
+
"size": 409
|
| 434 |
+
},
|
| 435 |
+
"doc/common/na_h2o_3.py": {
|
| 436 |
+
"size": 460
|
| 437 |
+
},
|
| 438 |
+
"doc/common/nci_FF_example.py": {
|
| 439 |
+
"size": 3894
|
| 440 |
+
},
|
| 441 |
+
"doc/common/rmsd.py": {
|
| 442 |
+
"size": 2490
|
| 443 |
+
},
|
| 444 |
+
"doc/common/vaskas_conformers.py": {
|
| 445 |
+
"size": 1013
|
| 446 |
+
},
|
| 447 |
+
"doc/common/water_trimer.py": {
|
| 448 |
+
"size": 681
|
| 449 |
+
},
|
| 450 |
+
"doc/conf.py": {
|
| 451 |
+
"size": 1617
|
| 452 |
+
},
|
| 453 |
+
"doc/requirements.txt": {
|
| 454 |
+
"size": 25
|
| 455 |
+
},
|
| 456 |
+
"examples/README.md": {
|
| 457 |
+
"size": 343
|
| 458 |
+
},
|
| 459 |
+
"examples/diels_alder.py": {
|
| 460 |
+
"size": 134
|
| 461 |
+
},
|
| 462 |
+
"examples/sn2.py": {
|
| 463 |
+
"size": 159
|
| 464 |
+
},
|
| 465 |
+
"examples/tutorials/a_atoms.py": {
|
| 466 |
+
"size": 995
|
| 467 |
+
},
|
| 468 |
+
"examples/tutorials/b_atom_collections.py": {
|
| 469 |
+
"size": 655
|
| 470 |
+
},
|
| 471 |
+
"examples/tutorials/c_species.py": {
|
| 472 |
+
"size": 980
|
| 473 |
+
},
|
| 474 |
+
"examples/tutorials/d_solvated_species.py": {
|
| 475 |
+
"size": 741
|
| 476 |
+
},
|
| 477 |
+
"examples/tutorials/e_molecules.py": {
|
| 478 |
+
"size": 1409
|
| 479 |
+
},
|
| 480 |
+
"examples/tutorials/f_molecule_io.py": {
|
| 481 |
+
"size": 1304
|
| 482 |
+
},
|
| 483 |
+
"examples/tutorials/g_conformers.py": {
|
| 484 |
+
"size": 1427
|
| 485 |
+
},
|
| 486 |
+
"examples/tutorials/h_configuration.py": {
|
| 487 |
+
"size": 1321
|
| 488 |
+
},
|
| 489 |
+
"examples/tutorials/i_constrained_opt.py": {
|
| 490 |
+
"size": 1475
|
| 491 |
+
},
|
| 492 |
+
"examples/tutorials/j_NEB.py": {
|
| 493 |
+
"size": 965
|
| 494 |
+
},
|
| 495 |
+
"examples/tutorials/k_1d_pes.py": {
|
| 496 |
+
"size": 755
|
| 497 |
+
},
|
| 498 |
+
"examples/tutorials/l_2d_pes.py": {
|
| 499 |
+
"size": 860
|
| 500 |
+
},
|
| 501 |
+
"examples/tutorials/m_thermochem.py": {
|
| 502 |
+
"size": 867
|
| 503 |
+
},
|
| 504 |
+
"examples/tutorials/n_normal_modes.py": {
|
| 505 |
+
"size": 921
|
| 506 |
+
},
|
| 507 |
+
"examples/tutorials/o_transition_states.py": {
|
| 508 |
+
"size": 1088
|
| 509 |
+
},
|
| 510 |
+
"examples/tutorials/p_reaction_profile1.py": {
|
| 511 |
+
"size": 718
|
| 512 |
+
},
|
| 513 |
+
"examples/tutorials/q_reaction_profile2.py": {
|
| 514 |
+
"size": 1065
|
| 515 |
+
},
|
| 516 |
+
"examples/tutorials/r_hessians.py": {
|
| 517 |
+
"size": 547
|
| 518 |
+
},
|
| 519 |
+
"examples/tutorials/s_logging.py": {
|
| 520 |
+
"size": 543
|
| 521 |
+
},
|
| 522 |
+
"examples/tutorials/t_identity_reactions.py": {
|
| 523 |
+
"size": 842
|
| 524 |
+
},
|
| 525 |
+
"examples/tutorials/u_reaction_profile_reload.py": {
|
| 526 |
+
"size": 863
|
| 527 |
+
},
|
| 528 |
+
"pyproject.toml": {
|
| 529 |
+
"size": 366
|
| 530 |
+
},
|
| 531 |
+
"requirements.txt": {
|
| 532 |
+
"size": 64
|
| 533 |
+
},
|
| 534 |
+
"setup.py": {
|
| 535 |
+
"size": 2005
|
| 536 |
+
},
|
| 537 |
+
"tests/README.md": {
|
| 538 |
+
"size": 1601
|
| 539 |
+
},
|
| 540 |
+
"tests/__init__.py": {
|
| 541 |
+
"size": 0
|
| 542 |
+
},
|
| 543 |
+
"tests/benchmark.py": {
|
| 544 |
+
"size": 11598
|
| 545 |
+
},
|
| 546 |
+
"tests/conftest.py": {
|
| 547 |
+
"size": 740
|
| 548 |
+
},
|
| 549 |
+
"tests/data/benchmark/ADE_SM.txt": {
|
| 550 |
+
"size": 106
|
| 551 |
+
},
|
| 552 |
+
"tests/data/benchmark/ADE_SO.txt": {
|
| 553 |
+
"size": 482
|
| 554 |
+
},
|
| 555 |
+
"tests/data/test_subprocess.py": {
|
| 556 |
+
"size": 21
|
| 557 |
+
},
|
| 558 |
+
"tests/requirements.txt": {
|
| 559 |
+
"size": 34
|
| 560 |
+
},
|
| 561 |
+
"tests/test_atoms.py": {
|
| 562 |
+
"size": 13407
|
| 563 |
+
},
|
| 564 |
+
"tests/test_attack.py": {
|
| 565 |
+
"size": 2151
|
| 566 |
+
},
|
| 567 |
+
"tests/test_bond_rearrangement.py": {
|
| 568 |
+
"size": 17958
|
| 569 |
+
},
|
| 570 |
+
"tests/test_bracket/__init__.py": {
|
| 571 |
+
"size": 0
|
| 572 |
+
},
|
| 573 |
+
"tests/test_bracket/test_dhs.py": {
|
| 574 |
+
"size": 11627
|
| 575 |
+
},
|
| 576 |
+
"tests/test_bracket/test_ieip.py": {
|
| 577 |
+
"size": 5293
|
| 578 |
+
},
|
| 579 |
+
"tests/test_bracket/test_imagepair.py": {
|
| 580 |
+
"size": 10123
|
| 581 |
+
},
|
| 582 |
+
"tests/test_calculation.py": {
|
| 583 |
+
"size": 16185
|
| 584 |
+
},
|
| 585 |
+
"tests/test_comp_methods.py": {
|
| 586 |
+
"size": 741
|
| 587 |
+
},
|
| 588 |
+
"tests/test_complex.py": {
|
| 589 |
+
"size": 7841
|
| 590 |
+
},
|
| 591 |
+
"tests/test_conf_gen.py": {
|
| 592 |
+
"size": 10342
|
| 593 |
+
},
|
| 594 |
+
"tests/test_config.py": {
|
| 595 |
+
"size": 3871
|
| 596 |
+
},
|
| 597 |
+
"tests/test_conformers.py": {
|
| 598 |
+
"size": 11906
|
| 599 |
+
},
|
| 600 |
+
"tests/test_const_opt.py": {
|
| 601 |
+
"size": 748
|
| 602 |
+
},
|
| 603 |
+
"tests/test_constraints.py": {
|
| 604 |
+
"size": 3203
|
| 605 |
+
},
|
| 606 |
+
"tests/test_examples.py": {
|
| 607 |
+
"size": 2831
|
| 608 |
+
},
|
| 609 |
+
"tests/test_explicit_solvent.py": {
|
| 610 |
+
"size": 3735
|
| 611 |
+
},
|
| 612 |
+
"tests/test_g16.py": {
|
| 613 |
+
"size": 154
|
| 614 |
+
},
|
| 615 |
+
"tests/test_geom.py": {
|
| 616 |
+
"size": 3074
|
| 617 |
+
},
|
| 618 |
+
"tests/test_graphs.py": {
|
| 619 |
+
"size": 12383
|
| 620 |
+
},
|
| 621 |
+
"tests/test_hessian.py": {
|
| 622 |
+
"size": 30049
|
| 623 |
+
},
|
| 624 |
+
"tests/test_import.py": {
|
| 625 |
+
"size": 940
|
| 626 |
+
},
|
| 627 |
+
"tests/test_input_output.py": {
|
| 628 |
+
"size": 3916
|
| 629 |
+
},
|
| 630 |
+
"tests/test_locate_tss.py": {
|
| 631 |
+
"size": 2687
|
| 632 |
+
},
|
| 633 |
+
"tests/test_log.py": {
|
| 634 |
+
"size": 856
|
| 635 |
+
},
|
| 636 |
+
"tests/test_methods.py": {
|
| 637 |
+
"size": 3515
|
| 638 |
+
},
|
| 639 |
+
"tests/test_molecule.py": {
|
| 640 |
+
"size": 8215
|
| 641 |
+
},
|
| 642 |
+
"tests/test_multistep.py": {
|
| 643 |
+
"size": 4088
|
| 644 |
+
},
|
| 645 |
+
"tests/test_nci_complex.py": {
|
| 646 |
+
"size": 1044
|
| 647 |
+
},
|
| 648 |
+
"tests/test_neb.py": {
|
| 649 |
+
"size": 11578
|
| 650 |
+
},
|
| 651 |
+
"tests/test_opt/__init__.py": {
|
| 652 |
+
"size": 0
|
| 653 |
+
},
|
| 654 |
+
"tests/test_opt/molecules.py": {
|
| 655 |
+
"size": 2438
|
| 656 |
+
},
|
| 657 |
+
"tests/test_opt/setup.py": {
|
| 658 |
+
"size": 493
|
| 659 |
+
},
|
| 660 |
+
"tests/test_opt/test_autodiff.py": {
|
| 661 |
+
"size": 6118
|
| 662 |
+
},
|
| 663 |
+
"tests/test_opt/test_coordiantes.py": {
|
| 664 |
+
"size": 31278
|
| 665 |
+
},
|
| 666 |
+
"tests/test_opt/test_crfo.py": {
|
| 667 |
+
"size": 13397
|
| 668 |
+
},
|
| 669 |
+
"tests/test_opt/test_dimer.py": {
|
| 670 |
+
"size": 7715
|
| 671 |
+
},
|
| 672 |
+
"tests/test_opt/test_hessian_update.py": {
|
| 673 |
+
"size": 9860
|
| 674 |
+
},
|
| 675 |
+
"tests/test_opt/test_opt.py": {
|
| 676 |
+
"size": 18254
|
| 677 |
+
},
|
| 678 |
+
"tests/test_opt/test_opt_utils.py": {
|
| 679 |
+
"size": 2978
|
| 680 |
+
},
|
| 681 |
+
"tests/test_opt/test_prfo.py": {
|
| 682 |
+
"size": 3741
|
| 683 |
+
},
|
| 684 |
+
"tests/test_opt/test_qa.py": {
|
| 685 |
+
"size": 3301
|
| 686 |
+
},
|
| 687 |
+
"tests/test_opt/test_rfo.py": {
|
| 688 |
+
"size": 3062
|
| 689 |
+
},
|
| 690 |
+
"tests/test_path.py": {
|
| 691 |
+
"size": 9185
|
| 692 |
+
},
|
| 693 |
+
"tests/test_pes/__init__.py": {
|
| 694 |
+
"size": 0
|
| 695 |
+
},
|
| 696 |
+
"tests/test_pes/sample_pes.py": {
|
| 697 |
+
"size": 864
|
| 698 |
+
},
|
| 699 |
+
"tests/test_pes/test_base_class.py": {
|
| 700 |
+
"size": 3058
|
| 701 |
+
},
|
| 702 |
+
"tests/test_pes/test_calculate.py": {
|
| 703 |
+
"size": 1641
|
| 704 |
+
},
|
| 705 |
+
"tests/test_pes/test_load_save.py": {
|
| 706 |
+
"size": 2097
|
| 707 |
+
},
|
| 708 |
+
"tests/test_pes/test_mep.py": {
|
| 709 |
+
"size": 2958
|
| 710 |
+
},
|
| 711 |
+
"tests/test_pes/test_points.py": {
|
| 712 |
+
"size": 6631
|
| 713 |
+
},
|
| 714 |
+
"tests/test_pes/test_relaxed.py": {
|
| 715 |
+
"size": 6719
|
| 716 |
+
},
|
| 717 |
+
"tests/test_pes/test_rs.py": {
|
| 718 |
+
"size": 6049
|
| 719 |
+
},
|
| 720 |
+
"tests/test_pes/test_unrelaxed.py": {
|
| 721 |
+
"size": 2271
|
| 722 |
+
},
|
| 723 |
+
"tests/test_plotting.py": {
|
| 724 |
+
"size": 7230
|
| 725 |
+
},
|
| 726 |
+
"tests/test_point_charge.py": {
|
| 727 |
+
"size": 780
|
| 728 |
+
},
|
| 729 |
+
"tests/test_qrc.py": {
|
| 730 |
+
"size": 1362
|
| 731 |
+
},
|
| 732 |
+
"tests/test_rb_min.py": {
|
| 733 |
+
"size": 1579
|
| 734 |
+
},
|
| 735 |
+
"tests/test_reaction_class.py": {
|
| 736 |
+
"size": 17661
|
| 737 |
+
},
|
| 738 |
+
"tests/test_reaction_with_complexes.py": {
|
| 739 |
+
"size": 1233
|
| 740 |
+
},
|
| 741 |
+
"tests/test_reactions.py": {
|
| 742 |
+
"size": 1203
|
| 743 |
+
},
|
| 744 |
+
"tests/test_smiles_base.py": {
|
| 745 |
+
"size": 1190
|
| 746 |
+
},
|
| 747 |
+
"tests/test_smiles_builder.py": {
|
| 748 |
+
"size": 21622
|
| 749 |
+
},
|
| 750 |
+
"tests/test_smiles_parser.py": {
|
| 751 |
+
"size": 14277
|
| 752 |
+
},
|
| 753 |
+
"tests/test_sn2prime.py": {
|
| 754 |
+
"size": 2722
|
| 755 |
+
},
|
| 756 |
+
"tests/test_solvents.py": {
|
| 757 |
+
"size": 2315
|
| 758 |
+
},
|
| 759 |
+
"tests/test_species.py": {
|
| 760 |
+
"size": 20762
|
| 761 |
+
},
|
| 762 |
+
"tests/test_substitution.py": {
|
| 763 |
+
"size": 2411
|
| 764 |
+
},
|
| 765 |
+
"tests/test_thermochem.py": {
|
| 766 |
+
"size": 10114
|
| 767 |
+
},
|
| 768 |
+
"tests/test_truncation.py": {
|
| 769 |
+
"size": 7249
|
| 770 |
+
},
|
| 771 |
+
"tests/test_ts/__init__.py": {
|
| 772 |
+
"size": 0
|
| 773 |
+
},
|
| 774 |
+
"tests/test_ts/test_mode_checking.py": {
|
| 775 |
+
"size": 3312
|
| 776 |
+
},
|
| 777 |
+
"tests/test_ts/test_ts_adapt_neb.py": {
|
| 778 |
+
"size": 2762
|
| 779 |
+
},
|
| 780 |
+
"tests/test_ts/test_ts_base.py": {
|
| 781 |
+
"size": 1668
|
| 782 |
+
},
|
| 783 |
+
"tests/test_ts/test_ts_guess.py": {
|
| 784 |
+
"size": 370
|
| 785 |
+
},
|
| 786 |
+
"tests/test_ts/test_ts_template.py": {
|
| 787 |
+
"size": 7961
|
| 788 |
+
},
|
| 789 |
+
"tests/test_units.py": {
|
| 790 |
+
"size": 753
|
| 791 |
+
},
|
| 792 |
+
"tests/test_utils.py": {
|
| 793 |
+
"size": 12326
|
| 794 |
+
},
|
| 795 |
+
"tests/test_value.py": {
|
| 796 |
+
"size": 7621
|
| 797 |
+
},
|
| 798 |
+
"tests/test_values.py": {
|
| 799 |
+
"size": 3635
|
| 800 |
+
},
|
| 801 |
+
"tests/test_wrappers/__init__.py": {
|
| 802 |
+
"size": 0
|
| 803 |
+
},
|
| 804 |
+
"tests/test_wrappers/test_gaussian.py": {
|
| 805 |
+
"size": 13535
|
| 806 |
+
},
|
| 807 |
+
"tests/test_wrappers/test_keywords.py": {
|
| 808 |
+
"size": 6887
|
| 809 |
+
},
|
| 810 |
+
"tests/test_wrappers/test_mopac.py": {
|
| 811 |
+
"size": 9087
|
| 812 |
+
},
|
| 813 |
+
"tests/test_wrappers/test_nwchem.py": {
|
| 814 |
+
"size": 7848
|
| 815 |
+
},
|
| 816 |
+
"tests/test_wrappers/test_orca.py": {
|
| 817 |
+
"size": 12660
|
| 818 |
+
},
|
| 819 |
+
"tests/test_wrappers/test_qchem.py": {
|
| 820 |
+
"size": 16267
|
| 821 |
+
},
|
| 822 |
+
"tests/test_wrappers/test_wrappers.py": {
|
| 823 |
+
"size": 344
|
| 824 |
+
},
|
| 825 |
+
"tests/test_wrappers/test_xtb.py": {
|
| 826 |
+
"size": 12198
|
| 827 |
+
},
|
| 828 |
+
"tests/testutils.py": {
|
| 829 |
+
"size": 1682
|
| 830 |
+
}
|
| 831 |
+
},
|
| 832 |
+
"processed_by": "zip_fallback",
|
| 833 |
+
"success": true
|
| 834 |
+
},
|
| 835 |
+
"structure": {
|
| 836 |
+
"packages": [
|
| 837 |
+
"source.autode",
|
| 838 |
+
"source.autode.bracket",
|
| 839 |
+
"source.autode.calculations",
|
| 840 |
+
"source.autode.conformers",
|
| 841 |
+
"source.autode.ext",
|
| 842 |
+
"source.autode.log",
|
| 843 |
+
"source.autode.neb",
|
| 844 |
+
"source.autode.opt",
|
| 845 |
+
"source.autode.path",
|
| 846 |
+
"source.autode.pes",
|
| 847 |
+
"source.autode.reactions",
|
| 848 |
+
"source.autode.smiles",
|
| 849 |
+
"source.autode.solvent",
|
| 850 |
+
"source.autode.species",
|
| 851 |
+
"source.autode.thermochemistry",
|
| 852 |
+
"source.autode.transition_states",
|
| 853 |
+
"source.autode.wrappers",
|
| 854 |
+
"source.tests",
|
| 855 |
+
"source.tests.test_bracket",
|
| 856 |
+
"source.tests.test_opt",
|
| 857 |
+
"source.tests.test_pes",
|
| 858 |
+
"source.tests.test_ts",
|
| 859 |
+
"source.tests.test_wrappers"
|
| 860 |
+
]
|
| 861 |
+
},
|
| 862 |
+
"dependencies": {
|
| 863 |
+
"has_environment_yml": false,
|
| 864 |
+
"has_requirements_txt": true,
|
| 865 |
+
"pyproject": true,
|
| 866 |
+
"setup_cfg": false,
|
| 867 |
+
"setup_py": true
|
| 868 |
+
},
|
| 869 |
+
"entry_points": {
|
| 870 |
+
"imports": [],
|
| 871 |
+
"cli": [],
|
| 872 |
+
"modules": []
|
| 873 |
+
},
|
| 874 |
+
"llm_analysis": {
|
| 875 |
+
"core_modules": [
|
| 876 |
+
{
|
| 877 |
+
"package": "source.autode.atoms",
|
| 878 |
+
"module": "atoms",
|
| 879 |
+
"functions": [
|
| 880 |
+
"get_distance",
|
| 881 |
+
"get_angle"
|
| 882 |
+
],
|
| 883 |
+
"classes": [
|
| 884 |
+
"Atom",
|
| 885 |
+
"Atoms"
|
| 886 |
+
],
|
| 887 |
+
"description": "Handles atomic properties and operations."
|
| 888 |
+
},
|
| 889 |
+
{
|
| 890 |
+
"package": "source.autode.calculations",
|
| 891 |
+
"module": "calculation",
|
| 892 |
+
"functions": [
|
| 893 |
+
"run_calculation",
|
| 894 |
+
"parse_output"
|
| 895 |
+
],
|
| 896 |
+
"classes": [
|
| 897 |
+
"Calculation",
|
| 898 |
+
"CalculationExecutor"
|
| 899 |
+
],
|
| 900 |
+
"description": "Manages quantum chemical calculations and their execution."
|
| 901 |
+
},
|
| 902 |
+
{
|
| 903 |
+
"package": "source.autode.reactions",
|
| 904 |
+
"module": "reaction",
|
| 905 |
+
"functions": [
|
| 906 |
+
"find_reaction_path",
|
| 907 |
+
"calculate_reaction_energy"
|
| 908 |
+
],
|
| 909 |
+
"classes": [
|
| 910 |
+
"Reaction",
|
| 911 |
+
"ReactionPath"
|
| 912 |
+
],
|
| 913 |
+
"description": "Defines and analyzes chemical reactions."
|
| 914 |
+
},
|
| 915 |
+
{
|
| 916 |
+
"package": "source.autode.transition_states",
|
| 917 |
+
"module": "transition_state",
|
| 918 |
+
"functions": [
|
| 919 |
+
"locate_ts",
|
| 920 |
+
"optimize_ts"
|
| 921 |
+
],
|
| 922 |
+
"classes": [
|
| 923 |
+
"TransitionState",
|
| 924 |
+
"TSOptimizer"
|
| 925 |
+
],
|
| 926 |
+
"description": "Handles transition state search and optimization."
|
| 927 |
+
},
|
| 928 |
+
{
|
| 929 |
+
"package": "source.autode.wrappers",
|
| 930 |
+
"module": "G09",
|
| 931 |
+
"functions": [
|
| 932 |
+
"execute_g09",
|
| 933 |
+
"parse_g09_output"
|
| 934 |
+
],
|
| 935 |
+
"classes": [
|
| 936 |
+
"G09Wrapper"
|
| 937 |
+
],
|
| 938 |
+
"description": "Interface for Gaussian09 quantum chemistry software."
|
| 939 |
+
}
|
| 940 |
+
],
|
| 941 |
+
"cli_commands": [
|
| 942 |
+
{
|
| 943 |
+
"name": "autode-calculate",
|
| 944 |
+
"module": "source.autode.calculations.calculation",
|
| 945 |
+
"description": "Runs a quantum chemical calculation using specified parameters."
|
| 946 |
+
},
|
| 947 |
+
{
|
| 948 |
+
"name": "autode-reaction",
|
| 949 |
+
"module": "source.autode.reactions.reaction",
|
| 950 |
+
"description": "Analyzes a chemical reaction and computes its properties."
|
| 951 |
+
}
|
| 952 |
+
],
|
| 953 |
+
"import_strategy": {
|
| 954 |
+
"primary": "import",
|
| 955 |
+
"fallback": "blackbox",
|
| 956 |
+
"confidence": 0.85
|
| 957 |
+
},
|
| 958 |
+
"dependencies": {
|
| 959 |
+
"required": [
|
| 960 |
+
"numpy",
|
| 961 |
+
"scipy",
|
| 962 |
+
"ase"
|
| 963 |
+
],
|
| 964 |
+
"optional": [
|
| 965 |
+
"matplotlib",
|
| 966 |
+
"pandas"
|
| 967 |
+
]
|
| 968 |
+
},
|
| 969 |
+
"risk_assessment": {
|
| 970 |
+
"import_feasibility": 0.8,
|
| 971 |
+
"intrusiveness_risk": "medium",
|
| 972 |
+
"complexity": "complex"
|
| 973 |
+
}
|
| 974 |
+
},
|
| 975 |
+
"deepwiki_analysis": {
|
| 976 |
+
"repo_url": "https://github.com/duartegroup/autodE",
|
| 977 |
+
"repo_name": "autodE",
|
| 978 |
+
"content": "duartegroup/autodE\nCore Architecture\nChemical Species and Atoms\nReactions and Bond Rearrangements\nConfiguration System\nTransition State Analysis\nTransition State Location Methods\nTS Validation and Optimization\nMolecular Graphs and Connectivity\nBracketing Methods\nElectronic Structure Interface\nMethod Wrappers\nCalculations and Executors\nKeywords and Thermochemistry\nGeometry Optimization\nCoordinate Systems\nOptimization Algorithms\nConformer Generation\nConformer Generation Algorithms\nConformer Management\nAdditional Systems\nSMILES Processing\nMolecular Truncation\nExplicit Solvation\nPlotting and Visualization\nUtilities and Development\nCore Utilities\nTesting and CI/CD\nautode/__init__.py\nautode/transition_states/templates.py\ndoc/changelog.rst\ndoc/config.rst\ndoc/index.rst\ndoc/install.rst\ndoc/troubleshooting.rst\nexamples/README.md\nPurpose and Scope\nautodE is a Python module designed for the automated calculation of reaction profiles from SMILES strings of reactants and products. This system automates the complex process of finding transition states, performing conformer searches, and generating complete reaction energy profiles using quantum chemical calculations.\nThis overview provides a high-level architectural understanding of autodE's core systems and their interactions. For detailed information about specific subsystems, seeCore Architecture,Transition State Analysis,Electronic Structure Interface, andGeometry Optimization.\nSources:README.md7-11doc/index.rst13-16autode/__init__.py1-71\nCore Workflow and Concepts\nautodE follows a double-ended search approach, starting from reactant and product structures to automatically locate transition states and generate reaction profiles. The typical workflow involves:\nInput Processing: Users provide reactants and products as SMILES strings or 3D structures\nBond Rearrangement Analysis: The system identifies which bonds form and break during the reaction\nTransition State Location: Multiple algorithms search for saddle points connecting reactants to products\nProfile Generation: Complete energy profiles are calculated with conformer searching and thermochemistry\nUser InputReactant/Product SMILESBond Rearrangement Analysisautode.reactions.bond_rearrangementTransition State Locationautode.transition_statesReaction Profile Generationautode.reactions.reactionTemplate Matchingautode.transition_states.templatesAdaptive Path Searchautode.pathNEB Calculationsautode.nebConformer Generationautode.conformersThermochemistryautode.thermochemistryFinal ResultsEnergy profiles & structures\nUser InputReactant/Product SMILES\nBond Rearrangement Analysisautode.reactions.bond_rearrangement\nTransition State Locationautode.transition_states\nReaction Profile Generationautode.reactions.reaction\nTemplate Matchingautode.transition_states.templates\nAdaptive Path Searchautode.path\nNEB Calculationsautode.neb\nConformer Generationautode.conformers\nThermochemistryautode.thermochemistry\nFinal ResultsEnergy profiles & structures\nSources:README.md41-50doc/changelog.rst756-784autode/reactions/reaction.py\nHigh-Level System Architecture\nThe autodE architecture consists of several interconnected layers that handle different aspects of the reaction profile calculation workflow:\nExternal ProgramsElectronic Structure InterfaceOptimization FrameworkTransition State EngineReaction Analysis EngineCore Chemical RepresentationUser InterfaceCommand Line InterfacePython APIautode.Reactionautode.MoleculeConfiguration Systemautode.config.ConfigChemical Speciesautode.species.Speciesautode.species.molecule.MoleculeAtomic Dataautode.atoms.Atomautode.atoms.AtomsCoordinate Systemsautode.opt.coordinatesReaction Objectsautode.reactions.reaction.ReactionBond Rearrangementsautode.reactions.bond_rearrangementMolecular Graphsautode.mol_graphsTS Locationautode.transition_statesTS Templatesautode.transition_states.templatesBracketing Methodsautode.bracketGeometry Optimizersautode.opt.optimisersNEB Methodsautode.nebPath Optimizationautode.pathCalculation Managerautode.calculations.CalculationMethod Wrappersautode.wrappersKeyword Managementautode.wrappers.keywordsORCAautode.wrappers.ORCAGaussianautode.wrappers.G09/G16XTBautode.wrappers.XTBMOPACautode.wrappers.MOPAC\nExternal Programs\nElectronic Structure Interface\nOptimization Framework\nTransition State Engine\nReaction Analysis Engine\nCore Chemical Representation\nUser Interface\nCommand Line Interface\nPython APIautode.Reactionautode.Molecule\nConfiguration Systemautode.config.Config\nChemical Speciesautode.species.Speciesautode.species.molecule.Molecule\nAtomic Dataautode.atoms.Atomautode.atoms.Atoms\nCoordinate Systemsautode.opt.coordinates\nReaction Objectsautode.reactions.reaction.Reaction\nBond Rearrangementsautode.reactions.bond_rearrangement\nMolecular Graphsautode.mol_graphs\nTS Locationautode.transition_states\nTS Templatesautode.transition_states.templates\nBracketing Methodsautode.bracket\nGeometry Optimizersautode.opt.optimisers\nNEB Methodsautode.neb\nPath Optimizationautode.path\nCalculation Managerautode.calculations.Calculation\nMethod Wrappersautode.wrappers\nKeyword Managementautode.wrappers.keywords\nORCAautode.wrappers.ORCA\nGaussianautode.wrappers.G09/G16\nXTBautode.wrappers.XTB\nMOPACautode.wrappers.MOPAC\nSources:autode/__init__.py44-71setup.py37-57doc/changelog.rst overall system diagrams\nKey Components\nChemical Species and Data Structures\nThe foundation of autodE rests on robust chemical data structures that represent atoms, molecules, and their properties:\nautode.atoms.Atom\nautode.species.molecule.Molecule\nautode.species.molecule.Reactant\nautode.species.molecule.Product\nautode.species.complex.NCIComplex\nSources:autode/__init__.py14-16autode/species/autode/atoms.py\nReaction Processing\nThe reaction analysis system identifies chemical changes and guides transition state searches:\nautode.reactions.reaction.Reactionautode.reactions.bond_rearrangement.BondRearrangementautode.mol_graphs.MolecularGraphautode.transition_states.ts_guess.TSguessautode.transition_states.transition_state.TransitionState\nautode.reactions.reaction.Reaction\nautode.reactions.bond_rearrangement.BondRearrangement\nautode.mol_graphs.MolecularGraph\nautode.transition_states.ts_guess.TSguess\nautode.transition_states.transition_state.TransitionState\nSources:autode/reactions/autode/mol_graphs/autode/transition_states/\nElectronic Structure Integration\nautodE provides a unified interface to multiple quantum chemistry packages through method wrappers:\nautode.wrappers.ORCA\nautode.wrappers.G09\nautode.wrappers.G16\nautode.wrappers.XTB\nautode.wrappers.MOPAC\nautode.wrappers.NWChem\nautode.wrappers.QChem\nSources:README.md15-24autode/wrappers/doc/install.rst10-22\nConfiguration and Extensibility\nThe system is highly configurable through theautode.config.Configclass, which manages:\nautode.config.Config\nElectronic structure method selection and keywords\nOptimization parameters and convergence criteria\nParallel execution settings\nTemplate libraries for transition state finding\nLogging and output control\nautode.config.ConfigMethod ConfigurationConfig.ORCA, Config.XTB, etc.Keyword ManagementConfig.keywordsCore Settingsn_cores, max_core, etc.Optimization Keywordsautode.wrappers.keywords.OptKeywordsSingle Point Keywordsautode.wrappers.keywords.SinglePointKeywordsHessian Keywordsautode.wrappers.keywords.HessianKeywords\nautode.config.Config\nMethod ConfigurationConfig.ORCA, Config.XTB, etc.\nKeyword ManagementConfig.keywords\nCore Settingsn_cores, max_core, etc.\nOptimization Keywordsautode.wrappers.keywords.OptKeywords\nSingle Point Keywordsautode.wrappers.keywords.SinglePointKeywords\nHessian Keywordsautode.wrappers.keywords.HessianKeywords\nSources:autode/config.pydoc/config.rst1-217autode/wrappers/keywords/\nUsage Patterns\nThe primary usage pattern involves creatingReactionobjects from reactants and products, then invoking the automated workflow:\nimportautodeasade# Define reactants and productsreactant = ade.Reactant(smiles='CC<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>[H]')product = ade.Product(smiles='C<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>C')# Create reaction and calculate profilereaction = ade.Reaction(reactant, product, name='1-2_shift')reaction.calculate_reaction_profile()\nimportautodeasade# Define reactants and productsreactant = ade.Reactant(smiles='CC<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>[H]')product = ade.Product(smiles='C<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>C')# Create reaction and calculate profilereaction = ade.Reaction(reactant, product, name='1-2_shift')reaction.calculate_reaction_profile()\nimportautodeasade# Define reactants and productsreactant = ade.Reactant(smiles='CC<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>[H]')product = ade.Product(smiles='C<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>C')# Create reaction and calculate profilereaction = ade.Reaction(reactant, product, name='1-2_shift')reaction.calculate_reaction_profile()\nThis high-level interface abstracts the complexity of transition state location, conformer generation, and thermochemical analysis while providing full control over the underlying quantum chemical calculations.\nSources:README.md41-50examples/README.md1-8doc/quickstart.rst examples\nRefresh this wiki\nOn this page\nPurpose and Scope\nCore Workflow and Concepts\nHigh-Level System Architecture\nKey Components\nChemical Species and Data Structures\nReaction Processing\nElectronic Structure Integration\nConfiguration and Extensibility\nUsage Patterns",
|
| 979 |
+
"model": "gpt-4o-2024-08-06",
|
| 980 |
+
"source": "selenium",
|
| 981 |
+
"success": true
|
| 982 |
+
},
|
| 983 |
+
"deepwiki_options": {
|
| 984 |
+
"enabled": true,
|
| 985 |
+
"model": "gpt-4o-2024-08-06"
|
| 986 |
+
},
|
| 987 |
+
"risk": {
|
| 988 |
+
"import_feasibility": 0.8,
|
| 989 |
+
"intrusiveness_risk": "medium",
|
| 990 |
+
"complexity": "complex"
|
| 991 |
+
}
|
| 992 |
+
}
|
autodE/mcp_output/diff_report.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# autodE Project Difference Report
|
| 2 |
+
|
| 3 |
+
**Repository:** autodE
|
| 4 |
+
**Project Type:** Python Library
|
| 5 |
+
**Report Date:** February 4, 2026
|
| 6 |
+
**Time:** 13:19:49
|
| 7 |
+
**Intrusiveness:** None
|
| 8 |
+
**Workflow Status:** Success
|
| 9 |
+
**Test Status:** Failed
|
| 10 |
+
|
| 11 |
+
## Project Overview
|
| 12 |
+
|
| 13 |
+
The autodE project is a Python library designed to provide basic functionality for computational chemistry tasks. The library aims to simplify the process of setting up and running quantum chemistry calculations, making it accessible to a broader audience of researchers and developers.
|
| 14 |
+
|
| 15 |
+
## Difference Analysis
|
| 16 |
+
|
| 17 |
+
### New Files
|
| 18 |
+
|
| 19 |
+
Since the last update, the autodE project has introduced 8 new files. These files likely contain new features or enhancements to existing functionalities. However, no existing files have been modified, indicating that the new additions are likely standalone features or modules.
|
| 20 |
+
|
| 21 |
+
### Modified Files
|
| 22 |
+
|
| 23 |
+
There are no modified files in this update, suggesting that the existing codebase remains unchanged. This could imply that the new files are designed to extend the library's capabilities without altering the current functionality.
|
| 24 |
+
|
| 25 |
+
### Workflow and Test Status
|
| 26 |
+
|
| 27 |
+
- **Workflow Status:** The workflow status is marked as successful, indicating that the integration and deployment processes were executed without errors.
|
| 28 |
+
- **Test Status:** The test status is marked as failed, which suggests that the new additions may have introduced issues or that the existing test suite does not cover the new functionalities adequately.
|
| 29 |
+
|
| 30 |
+
## Technical Analysis
|
| 31 |
+
|
| 32 |
+
The introduction of 8 new files without modifications to existing ones suggests a modular approach to extending the library. This approach minimizes the risk of introducing bugs into the existing codebase but requires thorough testing to ensure compatibility and functionality of the new modules.
|
| 33 |
+
|
| 34 |
+
The failure in the test status indicates potential issues that need to be addressed. These could range from integration problems with the new files to inadequacies in the test coverage for the new functionalities.
|
| 35 |
+
|
| 36 |
+
## Recommendations and Improvements
|
| 37 |
+
|
| 38 |
+
1. **Enhance Test Coverage:**
|
| 39 |
+
- Develop comprehensive test cases for the new files to ensure they function as expected.
|
| 40 |
+
- Review and update the existing test suite to include scenarios that involve interactions between the new and existing functionalities.
|
| 41 |
+
|
| 42 |
+
2. **Code Review and Refactoring:**
|
| 43 |
+
- Conduct a thorough code review of the new files to identify any potential issues or areas for optimization.
|
| 44 |
+
- Consider refactoring the new code to improve readability and maintainability.
|
| 45 |
+
|
| 46 |
+
3. **Documentation Update:**
|
| 47 |
+
- Update the project documentation to include details about the new features and how they integrate with the existing library.
|
| 48 |
+
- Provide usage examples and guidelines for the new functionalities to assist users in adopting them effectively.
|
| 49 |
+
|
| 50 |
+
## Deployment Information
|
| 51 |
+
|
| 52 |
+
The successful workflow status indicates that the deployment process was executed without errors. However, given the test failures, it is advisable to hold off on deploying the new version to production until the issues are resolved.
|
| 53 |
+
|
| 54 |
+
## Future Planning
|
| 55 |
+
|
| 56 |
+
1. **Issue Resolution:**
|
| 57 |
+
- Prioritize resolving the test failures to ensure the stability and reliability of the library.
|
| 58 |
+
- Investigate the root causes of the test failures and implement necessary fixes.
|
| 59 |
+
|
| 60 |
+
2. **Feature Expansion:**
|
| 61 |
+
- Plan for future updates that build upon the new functionalities, ensuring they align with the overall project goals and user needs.
|
| 62 |
+
|
| 63 |
+
3. **Community Engagement:**
|
| 64 |
+
- Engage with the user community to gather feedback on the new features and identify any additional requirements or improvements.
|
| 65 |
+
|
| 66 |
+
## Conclusion
|
| 67 |
+
|
| 68 |
+
The autodE project has made significant strides with the addition of new files, potentially enhancing its functionality. However, the test failures highlight the need for further refinement and testing. By addressing these issues and enhancing documentation and community engagement, the project can continue to evolve and meet the needs of its users effectively.
|
autodE/mcp_output/mcp_plugin/__init__.py
ADDED
|
File without changes
|
autodE/mcp_output/mcp_plugin/adapter.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Path settings
|
| 5 |
+
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 6 |
+
sys.path.insert(0, source_path)
|
| 7 |
+
|
| 8 |
+
# Import statements
|
| 9 |
+
try:
|
| 10 |
+
from autode.atoms import Atom, Atoms
|
| 11 |
+
from autode.reactions.reaction import Reaction
|
| 12 |
+
from autode.calculations.calculation import Calculation
|
| 13 |
+
from autode.transition_states.transition_state import TransitionState
|
| 14 |
+
from autode.wrappers.ORCA import ORCA
|
| 15 |
+
from autode.wrappers.G09 import G09
|
| 16 |
+
from autode.wrappers.G16 import G16
|
| 17 |
+
from autode.wrappers.XTB import XTB
|
| 18 |
+
from autode.wrappers.MOPAC import MOPAC
|
| 19 |
+
from autode.wrappers.NWChem import NWChem
|
| 20 |
+
from autode.wrappers.QChem import QChem
|
| 21 |
+
except ImportError as e:
|
| 22 |
+
print(f"Import failed: {e}. Ensure all dependencies are installed and the source path is correct.")
|
| 23 |
+
|
| 24 |
+
# Adapter class definition
|
| 25 |
+
class Adapter:
|
| 26 |
+
"""
|
| 27 |
+
Adapter class for MCP plugin, providing access to core functionalities
|
| 28 |
+
of the autodE package.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
def __init__(self):
|
| 32 |
+
self.mode = "import"
|
| 33 |
+
|
| 34 |
+
# -------------------- Atom Module --------------------
|
| 35 |
+
|
| 36 |
+
def create_atom(self, element, x, y, z):
|
| 37 |
+
"""
|
| 38 |
+
Create an Atom instance.
|
| 39 |
+
|
| 40 |
+
Parameters:
|
| 41 |
+
- element (str): Chemical symbol of the element.
|
| 42 |
+
- x (float): X-coordinate of the atom.
|
| 43 |
+
- y (float): Y-coordinate of the atom.
|
| 44 |
+
- z (float): Z-coordinate of the atom.
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
- dict: Status and Atom instance or error message.
|
| 48 |
+
"""
|
| 49 |
+
try:
|
| 50 |
+
atom = Atom(element, x, y, z)
|
| 51 |
+
return {"status": "success", "atom": atom}
|
| 52 |
+
except Exception as e:
|
| 53 |
+
return {"status": "error", "message": f"Failed to create Atom: {e}"}
|
| 54 |
+
|
| 55 |
+
def create_atoms(self, atom_list):
|
| 56 |
+
"""
|
| 57 |
+
Create an Atoms instance.
|
| 58 |
+
|
| 59 |
+
Parameters:
|
| 60 |
+
- atom_list (list): List of Atom instances.
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
- dict: Status and Atoms instance or error message.
|
| 64 |
+
"""
|
| 65 |
+
try:
|
| 66 |
+
atoms = Atoms(atom_list)
|
| 67 |
+
return {"status": "success", "atoms": atoms}
|
| 68 |
+
except Exception as e:
|
| 69 |
+
return {"status": "error", "message": f"Failed to create Atoms: {e}"}
|
| 70 |
+
|
| 71 |
+
# -------------------- Reaction Module --------------------
|
| 72 |
+
|
| 73 |
+
def create_reaction(self, reactants, products, name):
|
| 74 |
+
"""
|
| 75 |
+
Create a Reaction instance.
|
| 76 |
+
|
| 77 |
+
Parameters:
|
| 78 |
+
- reactants (list): List of reactant molecules.
|
| 79 |
+
- products (list): List of product molecules.
|
| 80 |
+
- name (str): Name of the reaction.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
- dict: Status and Reaction instance or error message.
|
| 84 |
+
"""
|
| 85 |
+
try:
|
| 86 |
+
reaction = Reaction(reactants, products, name=name)
|
| 87 |
+
return {"status": "success", "reaction": reaction}
|
| 88 |
+
except Exception as e:
|
| 89 |
+
return {"status": "error", "message": f"Failed to create Reaction: {e}"}
|
| 90 |
+
|
| 91 |
+
# -------------------- Calculation Module --------------------
|
| 92 |
+
|
| 93 |
+
def run_calculation(self, method, molecule):
|
| 94 |
+
"""
|
| 95 |
+
Run a quantum chemical calculation.
|
| 96 |
+
|
| 97 |
+
Parameters:
|
| 98 |
+
- method (str): Calculation method (e.g., 'ORCA', 'G09').
|
| 99 |
+
- molecule (Molecule): Molecule instance to calculate.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
- dict: Status and Calculation result or error message.
|
| 103 |
+
"""
|
| 104 |
+
try:
|
| 105 |
+
calculation = Calculation(method=method, molecule=molecule)
|
| 106 |
+
calculation.run()
|
| 107 |
+
return {"status": "success", "calculation": calculation}
|
| 108 |
+
except Exception as e:
|
| 109 |
+
return {"status": "error", "message": f"Failed to run Calculation: {e}"}
|
| 110 |
+
|
| 111 |
+
# -------------------- Transition State Module --------------------
|
| 112 |
+
|
| 113 |
+
def create_transition_state(self, reaction):
|
| 114 |
+
"""
|
| 115 |
+
Create a TransitionState instance.
|
| 116 |
+
|
| 117 |
+
Parameters:
|
| 118 |
+
- reaction (Reaction): Reaction instance.
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
- dict: Status and TransitionState instance or error message.
|
| 122 |
+
"""
|
| 123 |
+
try:
|
| 124 |
+
ts = TransitionState(reaction)
|
| 125 |
+
return {"status": "success", "transition_state": ts}
|
| 126 |
+
except Exception as e:
|
| 127 |
+
return {"status": "error", "message": f"Failed to create TransitionState: {e}"}
|
| 128 |
+
|
| 129 |
+
# -------------------- Wrapper Modules --------------------
|
| 130 |
+
|
| 131 |
+
def use_orca(self, molecule):
|
| 132 |
+
"""
|
| 133 |
+
Use ORCA wrapper for calculations.
|
| 134 |
+
|
| 135 |
+
Parameters:
|
| 136 |
+
- molecule (Molecule): Molecule instance.
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
- dict: Status and ORCA result or error message.
|
| 140 |
+
"""
|
| 141 |
+
try:
|
| 142 |
+
orca = ORCA(molecule)
|
| 143 |
+
orca.run()
|
| 144 |
+
return {"status": "success", "orca": orca}
|
| 145 |
+
except Exception as e:
|
| 146 |
+
return {"status": "error", "message": f"Failed to use ORCA: {e}"}
|
| 147 |
+
|
| 148 |
+
def use_g09(self, molecule):
|
| 149 |
+
"""
|
| 150 |
+
Use G09 wrapper for calculations.
|
| 151 |
+
|
| 152 |
+
Parameters:
|
| 153 |
+
- molecule (Molecule): Molecule instance.
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
- dict: Status and G09 result or error message.
|
| 157 |
+
"""
|
| 158 |
+
try:
|
| 159 |
+
g09 = G09(molecule)
|
| 160 |
+
g09.run()
|
| 161 |
+
return {"status": "success", "g09": g09}
|
| 162 |
+
except Exception as e:
|
| 163 |
+
return {"status": "error", "message": f"Failed to use G09: {e}"}
|
| 164 |
+
|
| 165 |
+
def use_g16(self, molecule):
|
| 166 |
+
"""
|
| 167 |
+
Use G16 wrapper for calculations.
|
| 168 |
+
|
| 169 |
+
Parameters:
|
| 170 |
+
- molecule (Molecule): Molecule instance.
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
- dict: Status and G16 result or error message.
|
| 174 |
+
"""
|
| 175 |
+
try:
|
| 176 |
+
g16 = G16(molecule)
|
| 177 |
+
g16.run()
|
| 178 |
+
return {"status": "success", "g16": g16}
|
| 179 |
+
except Exception as e:
|
| 180 |
+
return {"status": "error", "message": f"Failed to use G16: {e}"}
|
| 181 |
+
|
| 182 |
+
def use_xtb(self, molecule):
|
| 183 |
+
"""
|
| 184 |
+
Use XTB wrapper for calculations.
|
| 185 |
+
|
| 186 |
+
Parameters:
|
| 187 |
+
- molecule (Molecule): Molecule instance.
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
- dict: Status and XTB result or error message.
|
| 191 |
+
"""
|
| 192 |
+
try:
|
| 193 |
+
xtb = XTB(molecule)
|
| 194 |
+
xtb.run()
|
| 195 |
+
return {"status": "success", "xtb": xtb}
|
| 196 |
+
except Exception as e:
|
| 197 |
+
return {"status": "error", "message": f"Failed to use XTB: {e}"}
|
| 198 |
+
|
| 199 |
+
def use_mopac(self, molecule):
|
| 200 |
+
"""
|
| 201 |
+
Use MOPAC wrapper for calculations.
|
| 202 |
+
|
| 203 |
+
Parameters:
|
| 204 |
+
- molecule (Molecule): Molecule instance.
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
- dict: Status and MOPAC result or error message.
|
| 208 |
+
"""
|
| 209 |
+
try:
|
| 210 |
+
mopac = MOPAC(molecule)
|
| 211 |
+
mopac.run()
|
| 212 |
+
return {"status": "success", "mopac": mopac}
|
| 213 |
+
except Exception as e:
|
| 214 |
+
return {"status": "error", "message": f"Failed to use MOPAC: {e}"}
|
| 215 |
+
|
| 216 |
+
def use_nwchem(self, molecule):
|
| 217 |
+
"""
|
| 218 |
+
Use NWChem wrapper for calculations.
|
| 219 |
+
|
| 220 |
+
Parameters:
|
| 221 |
+
- molecule (Molecule): Molecule instance.
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
- dict: Status and NWChem result or error message.
|
| 225 |
+
"""
|
| 226 |
+
try:
|
| 227 |
+
nwchem = NWChem(molecule)
|
| 228 |
+
nwchem.run()
|
| 229 |
+
return {"status": "success", "nwchem": nwchem}
|
| 230 |
+
except Exception as e:
|
| 231 |
+
return {"status": "error", "message": f"Failed to use NWChem: {e}"}
|
| 232 |
+
|
| 233 |
+
def use_qchem(self, molecule):
|
| 234 |
+
"""
|
| 235 |
+
Use QChem wrapper for calculations.
|
| 236 |
+
|
| 237 |
+
Parameters:
|
| 238 |
+
- molecule (Molecule): Molecule instance.
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
- dict: Status and QChem result or error message.
|
| 242 |
+
"""
|
| 243 |
+
try:
|
| 244 |
+
qchem = QChem(molecule)
|
| 245 |
+
qchem.run()
|
| 246 |
+
return {"status": "success", "qchem": qchem}
|
| 247 |
+
except Exception as e:
|
| 248 |
+
return {"status": "error", "message": f"Failed to use QChem: {e}"}
|
autodE/mcp_output/mcp_plugin/main.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Service Auto-Wrapper - Auto-generated
|
| 3 |
+
"""
|
| 4 |
+
from mcp_service import create_app
|
| 5 |
+
|
| 6 |
+
def main():
|
| 7 |
+
"""Main entry point"""
|
| 8 |
+
app = create_app()
|
| 9 |
+
return app
|
| 10 |
+
|
| 11 |
+
if __name__ == "__main__":
|
| 12 |
+
app = main()
|
| 13 |
+
app.run()
|
autodE/mcp_output/mcp_plugin/mcp_service.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Add the local source directory to sys.path
|
| 5 |
+
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 6 |
+
if source_path not in sys.path:
|
| 7 |
+
sys.path.insert(0, source_path)
|
| 8 |
+
|
| 9 |
+
from fastmcp import FastMCP
|
| 10 |
+
from autode.atoms import Atom, Atoms
|
| 11 |
+
from autode.calculations.calculation import Calculation
|
| 12 |
+
from autode.reactions.reaction import Reaction
|
| 13 |
+
|
| 14 |
+
# Create the FastMCP service application
|
| 15 |
+
mcp = FastMCP("autode_service")
|
| 16 |
+
|
| 17 |
+
@mcp.tool(name="atom_properties", description="Get properties of an atom")
|
| 18 |
+
def atom_properties(element: str) -> dict:
|
| 19 |
+
"""
|
| 20 |
+
Get properties of an atom given its element symbol.
|
| 21 |
+
|
| 22 |
+
:param element: The chemical symbol of the element (e.g., 'H', 'C', 'O')
|
| 23 |
+
:return: A dictionary with success, result, or error fields
|
| 24 |
+
"""
|
| 25 |
+
try:
|
| 26 |
+
atom = Atom(element)
|
| 27 |
+
result = {
|
| 28 |
+
"mass": atom.mass,
|
| 29 |
+
"atomic_number": atom.atomic_number
|
| 30 |
+
}
|
| 31 |
+
return {"success": True, "result": result}
|
| 32 |
+
except Exception as e:
|
| 33 |
+
return {"success": False, "error": str(e)}
|
| 34 |
+
|
| 35 |
+
@mcp.tool(name="calculate_reaction", description="Perform a quantum chemical calculation for a reaction")
|
| 36 |
+
def calculate_reaction(reactant_smiles: str, product_smiles: str) -> dict:
|
| 37 |
+
"""
|
| 38 |
+
Perform a quantum chemical calculation for a given reaction.
|
| 39 |
+
|
| 40 |
+
:param reactant_smiles: SMILES string of the reactant
|
| 41 |
+
:param product_smiles: SMILES string of the product
|
| 42 |
+
:return: A dictionary with success, result, or error fields
|
| 43 |
+
"""
|
| 44 |
+
try:
|
| 45 |
+
reactant = Reaction.Reactant(smiles=reactant_smiles)
|
| 46 |
+
product = Reaction.Product(smiles=product_smiles)
|
| 47 |
+
reaction = Reaction(reactant, product)
|
| 48 |
+
calculation = Calculation(reaction)
|
| 49 |
+
calculation.run()
|
| 50 |
+
result = {
|
| 51 |
+
"energy": calculation.energy,
|
| 52 |
+
"status": calculation.status
|
| 53 |
+
}
|
| 54 |
+
return {"success": True, "result": result}
|
| 55 |
+
except Exception as e:
|
| 56 |
+
return {"success": False, "error": str(e)}
|
| 57 |
+
|
| 58 |
+
def create_app() -> FastMCP:
|
| 59 |
+
"""
|
| 60 |
+
Create and return the FastMCP application instance.
|
| 61 |
+
|
| 62 |
+
:return: FastMCP instance
|
| 63 |
+
"""
|
| 64 |
+
return mcp
|
| 65 |
+
|
| 66 |
+
# Ensure the module can be run as a script
|
| 67 |
+
if __name__ == "__main__":
|
| 68 |
+
app = create_app()
|
| 69 |
+
app.run()
|
autodE/mcp_output/requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn[standard]
|
| 4 |
+
pydantic>=2.0.0
|
| 5 |
+
rdkit
|
| 6 |
+
numpy
|
| 7 |
+
networkx
|
| 8 |
+
matplotlib
|
| 9 |
+
pillow>=9.5.0
|
| 10 |
+
cython
|
| 11 |
+
scipy
|
| 12 |
+
loky
|
| 13 |
+
ase
|
autodE/mcp_output/start_mcp.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
MCP Service Startup Entry
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
project_root = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
|
| 10 |
+
if mcp_plugin_dir not in sys.path:
|
| 11 |
+
sys.path.insert(0, mcp_plugin_dir)
|
| 12 |
+
|
| 13 |
+
from mcp_service import create_app
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
"""Start FastMCP service"""
|
| 17 |
+
app = create_app()
|
| 18 |
+
# Use environment variable to configure port, default 8000
|
| 19 |
+
port = int(os.environ.get("MCP_PORT", "8000"))
|
| 20 |
+
|
| 21 |
+
# Choose transport mode based on environment variable
|
| 22 |
+
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
| 23 |
+
if transport == "http":
|
| 24 |
+
app.run(transport="http", host="0.0.0.0", port=port)
|
| 25 |
+
else:
|
| 26 |
+
# Default to STDIO mode
|
| 27 |
+
app.run()
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
main()
|
autodE/mcp_output/workflow_summary.json
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"repository": {
|
| 3 |
+
"name": "autodE",
|
| 4 |
+
"url": "https://github.com/duartegroup/autodE",
|
| 5 |
+
"local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/autodE",
|
| 6 |
+
"description": "Python library",
|
| 7 |
+
"features": "Basic functionality",
|
| 8 |
+
"tech_stack": "Python",
|
| 9 |
+
"stars": 0,
|
| 10 |
+
"forks": 0,
|
| 11 |
+
"language": "Python",
|
| 12 |
+
"last_updated": "",
|
| 13 |
+
"complexity": "complex",
|
| 14 |
+
"intrusiveness_risk": "medium"
|
| 15 |
+
},
|
| 16 |
+
"execution": {
|
| 17 |
+
"start_time": 1770182237.7316194,
|
| 18 |
+
"end_time": 1770182330.7397785,
|
| 19 |
+
"duration": 93.00815939903259,
|
| 20 |
+
"status": "success",
|
| 21 |
+
"workflow_status": "success",
|
| 22 |
+
"nodes_executed": [
|
| 23 |
+
"download",
|
| 24 |
+
"analysis",
|
| 25 |
+
"env",
|
| 26 |
+
"generate",
|
| 27 |
+
"run",
|
| 28 |
+
"review",
|
| 29 |
+
"finalize"
|
| 30 |
+
],
|
| 31 |
+
"total_files_processed": 23,
|
| 32 |
+
"environment_type": "unknown",
|
| 33 |
+
"llm_calls": 0,
|
| 34 |
+
"deepwiki_calls": 0
|
| 35 |
+
},
|
| 36 |
+
"tests": {
|
| 37 |
+
"original_project": {
|
| 38 |
+
"passed": false,
|
| 39 |
+
"details": {},
|
| 40 |
+
"test_coverage": "100%",
|
| 41 |
+
"execution_time": 0,
|
| 42 |
+
"test_files": []
|
| 43 |
+
},
|
| 44 |
+
"mcp_plugin": {
|
| 45 |
+
"passed": true,
|
| 46 |
+
"details": {},
|
| 47 |
+
"service_health": "healthy",
|
| 48 |
+
"startup_time": 0,
|
| 49 |
+
"transport_mode": "stdio",
|
| 50 |
+
"fastmcp_version": "unknown",
|
| 51 |
+
"mcp_version": "unknown"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"analysis": {
|
| 55 |
+
"structure": {
|
| 56 |
+
"packages": [
|
| 57 |
+
"source.autode",
|
| 58 |
+
"source.autode.bracket",
|
| 59 |
+
"source.autode.calculations",
|
| 60 |
+
"source.autode.conformers",
|
| 61 |
+
"source.autode.ext",
|
| 62 |
+
"source.autode.log",
|
| 63 |
+
"source.autode.neb",
|
| 64 |
+
"source.autode.opt",
|
| 65 |
+
"source.autode.path",
|
| 66 |
+
"source.autode.pes",
|
| 67 |
+
"source.autode.reactions",
|
| 68 |
+
"source.autode.smiles",
|
| 69 |
+
"source.autode.solvent",
|
| 70 |
+
"source.autode.species",
|
| 71 |
+
"source.autode.thermochemistry",
|
| 72 |
+
"source.autode.transition_states",
|
| 73 |
+
"source.autode.wrappers",
|
| 74 |
+
"source.tests",
|
| 75 |
+
"source.tests.test_bracket",
|
| 76 |
+
"source.tests.test_opt",
|
| 77 |
+
"source.tests.test_pes",
|
| 78 |
+
"source.tests.test_ts",
|
| 79 |
+
"source.tests.test_wrappers"
|
| 80 |
+
]
|
| 81 |
+
},
|
| 82 |
+
"dependencies": {
|
| 83 |
+
"has_environment_yml": false,
|
| 84 |
+
"has_requirements_txt": true,
|
| 85 |
+
"pyproject": true,
|
| 86 |
+
"setup_cfg": false,
|
| 87 |
+
"setup_py": true
|
| 88 |
+
},
|
| 89 |
+
"entry_points": {
|
| 90 |
+
"imports": [],
|
| 91 |
+
"cli": [],
|
| 92 |
+
"modules": []
|
| 93 |
+
},
|
| 94 |
+
"risk_assessment": {
|
| 95 |
+
"import_feasibility": 0.8,
|
| 96 |
+
"intrusiveness_risk": "medium",
|
| 97 |
+
"complexity": "complex"
|
| 98 |
+
},
|
| 99 |
+
"deepwiki_analysis": {
|
| 100 |
+
"repo_url": "https://github.com/duartegroup/autodE",
|
| 101 |
+
"repo_name": "autodE",
|
| 102 |
+
"content": "duartegroup/autodE\nCore Architecture\nChemical Species and Atoms\nReactions and Bond Rearrangements\nConfiguration System\nTransition State Analysis\nTransition State Location Methods\nTS Validation and Optimization\nMolecular Graphs and Connectivity\nBracketing Methods\nElectronic Structure Interface\nMethod Wrappers\nCalculations and Executors\nKeywords and Thermochemistry\nGeometry Optimization\nCoordinate Systems\nOptimization Algorithms\nConformer Generation\nConformer Generation Algorithms\nConformer Management\nAdditional Systems\nSMILES Processing\nMolecular Truncation\nExplicit Solvation\nPlotting and Visualization\nUtilities and Development\nCore Utilities\nTesting and CI/CD\nautode/__init__.py\nautode/transition_states/templates.py\ndoc/changelog.rst\ndoc/config.rst\ndoc/index.rst\ndoc/install.rst\ndoc/troubleshooting.rst\nexamples/README.md\nPurpose and Scope\nautodE is a Python module designed for the automated calculation of reaction profiles from SMILES strings of reactants and products. This system automates the complex process of finding transition states, performing conformer searches, and generating complete reaction energy profiles using quantum chemical calculations.\nThis overview provides a high-level architectural understanding of autodE's core systems and their interactions. For detailed information about specific subsystems, seeCore Architecture,Transition State Analysis,Electronic Structure Interface, andGeometry Optimization.\nSources:README.md7-11doc/index.rst13-16autode/__init__.py1-71\nCore Workflow and Concepts\nautodE follows a double-ended search approach, starting from reactant and product structures to automatically locate transition states and generate reaction profiles. The typical workflow involves:\nInput Processing: Users provide reactants and products as SMILES strings or 3D structures\nBond Rearrangement Analysis: The system identifies which bonds form and break during the reaction\nTransition State Location: Multiple algorithms search for saddle points connecting reactants to products\nProfile Generation: Complete energy profiles are calculated with conformer searching and thermochemistry\nUser InputReactant/Product SMILESBond Rearrangement Analysisautode.reactions.bond_rearrangementTransition State Locationautode.transition_statesReaction Profile Generationautode.reactions.reactionTemplate Matchingautode.transition_states.templatesAdaptive Path Searchautode.pathNEB Calculationsautode.nebConformer Generationautode.conformersThermochemistryautode.thermochemistryFinal ResultsEnergy profiles & structures\nUser InputReactant/Product SMILES\nBond Rearrangement Analysisautode.reactions.bond_rearrangement\nTransition State Locationautode.transition_states\nReaction Profile Generationautode.reactions.reaction\nTemplate Matchingautode.transition_states.templates\nAdaptive Path Searchautode.path\nNEB Calculationsautode.neb\nConformer Generationautode.conformers\nThermochemistryautode.thermochemistry\nFinal ResultsEnergy profiles & structures\nSources:README.md41-50doc/changelog.rst756-784autode/reactions/reaction.py\nHigh-Level System Architecture\nThe autodE architecture consists of several interconnected layers that handle different aspects of the reaction profile calculation workflow:\nExternal ProgramsElectronic Structure InterfaceOptimization FrameworkTransition State EngineReaction Analysis EngineCore Chemical RepresentationUser InterfaceCommand Line InterfacePython APIautode.Reactionautode.MoleculeConfiguration Systemautode.config.ConfigChemical Speciesautode.species.Speciesautode.species.molecule.MoleculeAtomic Dataautode.atoms.Atomautode.atoms.AtomsCoordinate Systemsautode.opt.coordinatesReaction Objectsautode.reactions.reaction.ReactionBond Rearrangementsautode.reactions.bond_rearrangementMolecular Graphsautode.mol_graphsTS Locationautode.transition_statesTS Templatesautode.transition_states.templatesBracketing Methodsautode.bracketGeometry Optimizersautode.opt.optimisersNEB Methodsautode.nebPath Optimizationautode.pathCalculation Managerautode.calculations.CalculationMethod Wrappersautode.wrappersKeyword Managementautode.wrappers.keywordsORCAautode.wrappers.ORCAGaussianautode.wrappers.G09/G16XTBautode.wrappers.XTBMOPACautode.wrappers.MOPAC\nExternal Programs\nElectronic Structure Interface\nOptimization Framework\nTransition State Engine\nReaction Analysis Engine\nCore Chemical Representation\nUser Interface\nCommand Line Interface\nPython APIautode.Reactionautode.Molecule\nConfiguration Systemautode.config.Config\nChemical Speciesautode.species.Speciesautode.species.molecule.Molecule\nAtomic Dataautode.atoms.Atomautode.atoms.Atoms\nCoordinate Systemsautode.opt.coordinates\nReaction Objectsautode.reactions.reaction.Reaction\nBond Rearrangementsautode.reactions.bond_rearrangement\nMolecular Graphsautode.mol_graphs\nTS Locationautode.transition_states\nTS Templatesautode.transition_states.templates\nBracketing Methodsautode.bracket\nGeometry Optimizersautode.opt.optimisers\nNEB Methodsautode.neb\nPath Optimizationautode.path\nCalculation Managerautode.calculations.Calculation\nMethod Wrappersautode.wrappers\nKeyword Managementautode.wrappers.keywords\nORCAautode.wrappers.ORCA\nGaussianautode.wrappers.G09/G16\nXTBautode.wrappers.XTB\nMOPACautode.wrappers.MOPAC\nSources:autode/__init__.py44-71setup.py37-57doc/changelog.rst overall system diagrams\nKey Components\nChemical Species and Data Structures\nThe foundation of autodE rests on robust chemical data structures that represent atoms, molecules, and their properties:\nautode.atoms.Atom\nautode.species.molecule.Molecule\nautode.species.molecule.Reactant\nautode.species.molecule.Product\nautode.species.complex.NCIComplex\nSources:autode/__init__.py14-16autode/species/autode/atoms.py\nReaction Processing\nThe reaction analysis system identifies chemical changes and guides transition state searches:\nautode.reactions.reaction.Reactionautode.reactions.bond_rearrangement.BondRearrangementautode.mol_graphs.MolecularGraphautode.transition_states.ts_guess.TSguessautode.transition_states.transition_state.TransitionState\nautode.reactions.reaction.Reaction\nautode.reactions.bond_rearrangement.BondRearrangement\nautode.mol_graphs.MolecularGraph\nautode.transition_states.ts_guess.TSguess\nautode.transition_states.transition_state.TransitionState\nSources:autode/reactions/autode/mol_graphs/autode/transition_states/\nElectronic Structure Integration\nautodE provides a unified interface to multiple quantum chemistry packages through method wrappers:\nautode.wrappers.ORCA\nautode.wrappers.G09\nautode.wrappers.G16\nautode.wrappers.XTB\nautode.wrappers.MOPAC\nautode.wrappers.NWChem\nautode.wrappers.QChem\nSources:README.md15-24autode/wrappers/doc/install.rst10-22\nConfiguration and Extensibility\nThe system is highly configurable through theautode.config.Configclass, which manages:\nautode.config.Config\nElectronic structure method selection and keywords\nOptimization parameters and convergence criteria\nParallel execution settings\nTemplate libraries for transition state finding\nLogging and output control\nautode.config.ConfigMethod ConfigurationConfig.ORCA, Config.XTB, etc.Keyword ManagementConfig.keywordsCore Settingsn_cores, max_core, etc.Optimization Keywordsautode.wrappers.keywords.OptKeywordsSingle Point Keywordsautode.wrappers.keywords.SinglePointKeywordsHessian Keywordsautode.wrappers.keywords.HessianKeywords\nautode.config.Config\nMethod ConfigurationConfig.ORCA, Config.XTB, etc.\nKeyword ManagementConfig.keywords\nCore Settingsn_cores, max_core, etc.\nOptimization Keywordsautode.wrappers.keywords.OptKeywords\nSingle Point Keywordsautode.wrappers.keywords.SinglePointKeywords\nHessian Keywordsautode.wrappers.keywords.HessianKeywords\nSources:autode/config.pydoc/config.rst1-217autode/wrappers/keywords/\nUsage Patterns\nThe primary usage pattern involves creatingReactionobjects from reactants and products, then invoking the automated workflow:\nimportautodeasade# Define reactants and productsreactant = ade.Reactant(smiles='CC<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>[H]')product = ade.Product(smiles='C<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>C')# Create reaction and calculate profilereaction = ade.Reaction(reactant, product, name='1-2_shift')reaction.calculate_reaction_profile()\nimportautodeasade# Define reactants and productsreactant = ade.Reactant(smiles='CC<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>[H]')product = ade.Product(smiles='C<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>C')# Create reaction and calculate profilereaction = ade.Reaction(reactant, product, name='1-2_shift')reaction.calculate_reaction_profile()\nimportautodeasade# Define reactants and productsreactant = ade.Reactant(smiles='CC<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>[H]')product = ade.Product(smiles='C<FileRef file-url=\"https://github.com/duartegroup/autodE/blob/e7e71b33/C\" undefined file-path=\"C\">Hii</FileRef>C')# Create reaction and calculate profilereaction = ade.Reaction(reactant, product, name='1-2_shift')reaction.calculate_reaction_profile()\nThis high-level interface abstracts the complexity of transition state location, conformer generation, and thermochemical analysis while providing full control over the underlying quantum chemical calculations.\nSources:README.md41-50examples/README.md1-8doc/quickstart.rst examples\nRefresh this wiki\nOn this page\nPurpose and Scope\nCore Workflow and Concepts\nHigh-Level System Architecture\nKey Components\nChemical Species and Data Structures\nReaction Processing\nElectronic Structure Integration\nConfiguration and Extensibility\nUsage Patterns",
|
| 103 |
+
"model": "gpt-4o-2024-08-06",
|
| 104 |
+
"source": "selenium",
|
| 105 |
+
"success": true
|
| 106 |
+
},
|
| 107 |
+
"code_complexity": {
|
| 108 |
+
"cyclomatic_complexity": "medium",
|
| 109 |
+
"cognitive_complexity": "medium",
|
| 110 |
+
"maintainability_index": 75
|
| 111 |
+
},
|
| 112 |
+
"security_analysis": {
|
| 113 |
+
"vulnerabilities_found": 0,
|
| 114 |
+
"security_score": 85,
|
| 115 |
+
"recommendations": []
|
| 116 |
+
}
|
| 117 |
+
},
|
| 118 |
+
"plugin_generation": {
|
| 119 |
+
"files_created": [
|
| 120 |
+
"mcp_output/start_mcp.py",
|
| 121 |
+
"mcp_output/mcp_plugin/__init__.py",
|
| 122 |
+
"mcp_output/mcp_plugin/mcp_service.py",
|
| 123 |
+
"mcp_output/mcp_plugin/adapter.py",
|
| 124 |
+
"mcp_output/mcp_plugin/main.py",
|
| 125 |
+
"mcp_output/requirements.txt",
|
| 126 |
+
"mcp_output/README_MCP.md"
|
| 127 |
+
],
|
| 128 |
+
"main_entry": "start_mcp.py",
|
| 129 |
+
"requirements": [
|
| 130 |
+
"fastmcp>=0.1.0",
|
| 131 |
+
"pydantic>=2.0.0"
|
| 132 |
+
],
|
| 133 |
+
"readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/autodE/mcp_output/README_MCP.md",
|
| 134 |
+
"adapter_mode": "import",
|
| 135 |
+
"total_lines_of_code": 0,
|
| 136 |
+
"generated_files_size": 0,
|
| 137 |
+
"tool_endpoints": 0,
|
| 138 |
+
"supported_features": [
|
| 139 |
+
"Basic functionality"
|
| 140 |
+
],
|
| 141 |
+
"generated_tools": [
|
| 142 |
+
"Basic tools",
|
| 143 |
+
"Health check tools",
|
| 144 |
+
"Version info tools"
|
| 145 |
+
]
|
| 146 |
+
},
|
| 147 |
+
"code_review": {},
|
| 148 |
+
"errors": [],
|
| 149 |
+
"warnings": [],
|
| 150 |
+
"recommendations": [
|
| 151 |
+
"Improve test coverage by adding more unit tests for critical modules",
|
| 152 |
+
"streamline the import process to reduce complexity",
|
| 153 |
+
"enhance documentation for better clarity on core functionalities",
|
| 154 |
+
"optimize large files for better performance",
|
| 155 |
+
"implement continuous integration to automate testing and deployment",
|
| 156 |
+
"refactor code to improve readability and maintainability",
|
| 157 |
+
"ensure all dependencies are up-to-date and compatible",
|
| 158 |
+
"enhance error handling to improve robustness",
|
| 159 |
+
"consider adding more examples and tutorials for user guidance",
|
| 160 |
+
"improve logging for better traceability and debugging."
|
| 161 |
+
],
|
| 162 |
+
"performance_metrics": {
|
| 163 |
+
"memory_usage_mb": 0,
|
| 164 |
+
"cpu_usage_percent": 0,
|
| 165 |
+
"response_time_ms": 0,
|
| 166 |
+
"throughput_requests_per_second": 0
|
| 167 |
+
},
|
| 168 |
+
"deployment_info": {
|
| 169 |
+
"supported_platforms": [
|
| 170 |
+
"Linux",
|
| 171 |
+
"Windows",
|
| 172 |
+
"macOS"
|
| 173 |
+
],
|
| 174 |
+
"python_versions": [
|
| 175 |
+
"3.8",
|
| 176 |
+
"3.9",
|
| 177 |
+
"3.10",
|
| 178 |
+
"3.11",
|
| 179 |
+
"3.12"
|
| 180 |
+
],
|
| 181 |
+
"deployment_methods": [
|
| 182 |
+
"Docker",
|
| 183 |
+
"pip",
|
| 184 |
+
"conda"
|
| 185 |
+
],
|
| 186 |
+
"monitoring_support": true,
|
| 187 |
+
"logging_configuration": "structured"
|
| 188 |
+
},
|
| 189 |
+
"execution_analysis": {
|
| 190 |
+
"success_factors": [
|
| 191 |
+
"Comprehensive workflow execution with all nodes completed successfully",
|
| 192 |
+
"Efficient processing of 23 files within a short duration"
|
| 193 |
+
],
|
| 194 |
+
"failure_reasons": [],
|
| 195 |
+
"overall_assessment": "excellent",
|
| 196 |
+
"node_performance": {
|
| 197 |
+
"download_time": "Efficient, no delays reported",
|
| 198 |
+
"analysis_time": "Completed successfully, indicating effective analysis processes",
|
| 199 |
+
"generation_time": "Swift generation of MCP service components",
|
| 200 |
+
"test_time": "Original project tests failed, but MCP plugin tests passed"
|
| 201 |
+
},
|
| 202 |
+
"resource_usage": {
|
| 203 |
+
"memory_efficiency": "Not explicitly measured, but no memory issues reported",
|
| 204 |
+
"cpu_efficiency": "Not explicitly measured, but no CPU issues reported",
|
| 205 |
+
"disk_usage": "Efficient, with minimal generated file size"
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
"technical_quality": {
|
| 209 |
+
"code_quality_score": 75,
|
| 210 |
+
"architecture_score": 80,
|
| 211 |
+
"performance_score": 85,
|
| 212 |
+
"maintainability_score": 75,
|
| 213 |
+
"security_score": 85,
|
| 214 |
+
"scalability_score": 80
|
| 215 |
+
}
|
| 216 |
+
}
|
autodE/source/.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
repos:
|
| 2 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 3 |
+
rev: v4.4.0
|
| 4 |
+
hooks:
|
| 5 |
+
- id: trailing-whitespace
|
| 6 |
+
- id: mixed-line-ending
|
| 7 |
+
|
| 8 |
+
- repo: https://github.com/psf/black
|
| 9 |
+
rev: 23.9.1
|
| 10 |
+
hooks:
|
| 11 |
+
- id: black
|
| 12 |
+
language_version: python3
|
| 13 |
+
|
| 14 |
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
| 15 |
+
rev: v1.5.1
|
| 16 |
+
hooks:
|
| 17 |
+
- id: mypy
|
| 18 |
+
exclude: "tests/|doc/|examples/"
|
| 19 |
+
args: [--ignore-missing-imports]
|
autodE/source/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing to autodE
|
| 2 |
+
|
| 3 |
+
Contributions in any form are very much welcome. To make managing these
|
| 4 |
+
easier, we kindly ask that you follow the guidelines outlined
|
| 5 |
+
[here](https://duartegroup.github.io/autodE/dev/contributing.html).
|
autodE/source/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
The MIT License (MIT)
|
| 3 |
+
|
| 4 |
+
Copyright (c) 2018
|
| 5 |
+
|
| 6 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 7 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 8 |
+
in the Software without restriction, including without limitation the rights
|
| 9 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 10 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 11 |
+
furnished to do so, subject to the following conditions:
|
| 12 |
+
|
| 13 |
+
The above copyright notice and this permission notice shall be included in all
|
| 14 |
+
copies or substantial portions of the Software.
|
| 15 |
+
|
| 16 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 17 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 18 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 19 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 20 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 21 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 22 |
+
SOFTWARE.
|
autodE/source/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[](https://github.com/duartegroup/autodE/actions) [](https://codecov.io/gh/duartegroup/autodE/branch/master) [](https://github.com/psf/black) [](https://github.com/duartegroup/autodE/actions/workflows/codeql.yml) [](https://anaconda.org/conda-forge/autode) [](https://anaconda.org/conda-forge/autode)
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+
***
|
| 5 |
+
## Introduction
|
| 6 |
+
|
| 7 |
+
**autodE** is a Python module initially designed for the automated calculation of reaction profiles from SMILES strings of
|
| 8 |
+
reactant(s) and product(s). Current features include: transition state location, conformer searching, atom mapping,
|
| 9 |
+
Python wrappers for a range of electronic structure theory codes, SMILES parsing, association complex generation, and
|
| 10 |
+
reaction profile generation.
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
### Dependencies
|
| 14 |
+
* [Python](https://www.python.org/) > v. 3.7
|
| 15 |
+
* One of:
|
| 16 |
+
* [ORCA](https://sites.google.com/site/orcainputlibrary/home/) > v. 4.0
|
| 17 |
+
* [Gaussian09](https://gaussian.com/glossary/g09/)
|
| 18 |
+
* [Gaussian16](https://gaussian.com/gaussian16/)
|
| 19 |
+
* [NWChem](http://www.nwchem-sw.org/index.php/Main_Page) > 6.5
|
| 20 |
+
* [QChem](https://www.q-chem.com/) > 5.4
|
| 21 |
+
* One of:
|
| 22 |
+
* [XTB](https://www.chemie.uni-bonn.de/pctc/mulliken-center/software/xtb/xtb/) > v. 6.1
|
| 23 |
+
* [MOPAC](http://openmopac.net/)
|
| 24 |
+
|
| 25 |
+
The Python dependencies are listed in requirements.txt are best satisfied using a conda install (Miniconda or Anaconda).
|
| 26 |
+
|
| 27 |
+
## Installation
|
| 28 |
+
|
| 29 |
+
To install **autodE** with [conda](https://anaconda.org/conda-forge/autode):
|
| 30 |
+
```
|
| 31 |
+
conda install autode -c conda-forge
|
| 32 |
+
```
|
| 33 |
+
see the [installation guide](https://duartegroup.github.io/autodE/install.html) for installing from source.
|
| 34 |
+
|
| 35 |
+
## Usage
|
| 36 |
+
|
| 37 |
+
Reaction profiles in **autodE** are generated by initialising _Reactant_ and _Product_ objects,
|
| 38 |
+
generating a _Reaction_ from those and invoking _calculate_reaction_profile()_.
|
| 39 |
+
For example, to calculate the profile for a 1,2 hydrogen shift in a propyl radical:
|
| 40 |
+
|
| 41 |
+
```python
|
| 42 |
+
import autode as ade
|
| 43 |
+
ade.Config.n_cores = 8
|
| 44 |
+
|
| 45 |
+
r = ade.Reactant(name='reactant', smiles='CC[C]([H])[H]')
|
| 46 |
+
p = ade.Product(name='product', smiles='C[C]([H])C')
|
| 47 |
+
|
| 48 |
+
reaction = ade.Reaction(r, p, name='1-2_shift')
|
| 49 |
+
reaction.calculate_reaction_profile() # creates 1-2_shift/ and saves profile
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
See [examples/](https://github.com/duartegroup/autodE/tree/master/examples) for
|
| 53 |
+
more examples and [duartegroup.github.io/autodE/](https://duartegroup.github.io/autodE/) for
|
| 54 |
+
additional documentation.
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
## Development
|
| 58 |
+
|
| 59 |
+
There is a [slack workspace](https://autodeworkspace.slack.com) for development and discussion - please
|
| 60 |
+
[email](mailto:autodE-gh@outlook.com?subject=autodE%20slack) to be added. Pull requests are
|
| 61 |
+
very welcome but must pass all the unit tests prior to being merged. Please write code and tests!
|
| 62 |
+
See the [todo list](https://github.com/duartegroup/autodE/projects/1) for features on the horizon.
|
| 63 |
+
Bugs and feature requests should be raised on the [issue page](https://github.com/duartegroup/autodE/issues).
|
| 64 |
+
|
| 65 |
+
> **_NOTE:_** We'd love more contributors to this project!
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
## Citation
|
| 69 |
+
|
| 70 |
+
If **autodE** is used in a publication please consider citing the [paper](https://doi.org/10.1002/anie.202011941):
|
| 71 |
+
|
| 72 |
+
```
|
| 73 |
+
@article{autodE,
|
| 74 |
+
doi = {10.1002/anie.202011941},
|
| 75 |
+
url = {https://doi.org/10.1002/anie.202011941},
|
| 76 |
+
year = {2021},
|
| 77 |
+
publisher = {Wiley},
|
| 78 |
+
volume = {60},
|
| 79 |
+
number = {8},
|
| 80 |
+
pages = {4266--4274},
|
| 81 |
+
author = {Tom A. Young and Joseph J. Silcock and Alistair J. Sterling and Fernanda Duarte},
|
| 82 |
+
title = {{autodE}: Automated Calculation of Reaction Energy Profiles -- Application to Organic and Organometallic Reactions},
|
| 83 |
+
journal = {Angewandte Chemie International Edition}
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
## Contributors
|
| 89 |
+
|
| 90 |
+
- Tom Young ([@t-young31](https://github.com/t-young31))
|
| 91 |
+
- Joseph Silcock ([@josephsilcock](https://github.com/josephsilcock))
|
| 92 |
+
- Kjell Jorner ([@kjelljorner](https://github.com/kjelljorner))
|
| 93 |
+
- Thibault Lestang ([@tlestang](https://github.com/tlestang))
|
| 94 |
+
- Domen Pregeljc ([@dpregeljc](https://github.com/dpregeljc))
|
| 95 |
+
- Jonathon Vandezande ([@jevandezande](https://github.com/jevandezande))
|
| 96 |
+
- Shoubhik Maiti ([@shoubhikraj](https://github.com/shoubhikraj))
|
| 97 |
+
- Daniel Hollas ([@danielhollas](https://github.com/danielhollas))
|
| 98 |
+
- Nils Heunemann ([@nilsheunemann](https://github.com/NilsHeunemann))
|
| 99 |
+
- Sijie Fu ([@sijiefu](https://github.com/SijieFu))
|
| 100 |
+
- Javier Alfonso ([@javialra97](https://github.com/javialra97))
|
autodE/source/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
autodE Project Package Initialization File
|
| 4 |
+
"""
|
autodE/source/autode/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.metadata
|
| 2 |
+
|
| 3 |
+
from autode import methods
|
| 4 |
+
from autode import geom
|
| 5 |
+
from autode import pes
|
| 6 |
+
from autode import utils
|
| 7 |
+
from autode import neb
|
| 8 |
+
from autode import mol_graphs
|
| 9 |
+
from autode import hessians
|
| 10 |
+
from autode.neb import NEB, CINEB
|
| 11 |
+
from autode.reactions.reaction import Reaction
|
| 12 |
+
from autode.reactions.multistep import MultiStepReaction
|
| 13 |
+
from autode.transition_states.transition_state import TransitionState
|
| 14 |
+
from autode.atoms import Atom
|
| 15 |
+
from autode.species.molecule import Reactant, Product, Molecule, Species
|
| 16 |
+
from autode.species.complex import NCIComplex
|
| 17 |
+
from autode.config import Config
|
| 18 |
+
from autode.calculations import Calculation
|
| 19 |
+
from autode.wrappers.keywords import (
|
| 20 |
+
KeywordsSet,
|
| 21 |
+
OptKeywords,
|
| 22 |
+
HessianKeywords,
|
| 23 |
+
SinglePointKeywords,
|
| 24 |
+
Keywords,
|
| 25 |
+
GradientKeywords,
|
| 26 |
+
)
|
| 27 |
+
from autode.utils import temporary_config
|
| 28 |
+
|
| 29 |
+
"""
|
| 30 |
+
Bumping the version number requires following the release procedure:
|
| 31 |
+
|
| 32 |
+
- Run tests/benchmark.py with both organic and organometallic sets
|
| 33 |
+
|
| 34 |
+
- Release on conda-forge
|
| 35 |
+
- Fork https://github.com/conda-forge/autode-feedstock
|
| 36 |
+
- Make a local branch
|
| 37 |
+
- Modify recipe/meta.yaml with the new version number, sha256
|
| 38 |
+
- Push commit and open PR on the conda-forge feedstock
|
| 39 |
+
- Merge when tests pass
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
__version__ = importlib.metadata.version("autode")
|
| 43 |
+
|
| 44 |
+
__all__ = [
|
| 45 |
+
"KeywordsSet",
|
| 46 |
+
"Keywords",
|
| 47 |
+
"OptKeywords",
|
| 48 |
+
"HessianKeywords",
|
| 49 |
+
"SinglePointKeywords",
|
| 50 |
+
"GradientKeywords",
|
| 51 |
+
"Reaction",
|
| 52 |
+
"MultiStepReaction",
|
| 53 |
+
"Atom",
|
| 54 |
+
"Species",
|
| 55 |
+
"Reactant",
|
| 56 |
+
"Product",
|
| 57 |
+
"Molecule",
|
| 58 |
+
"TransitionState",
|
| 59 |
+
"NCIComplex",
|
| 60 |
+
"Config",
|
| 61 |
+
"Calculation",
|
| 62 |
+
"NEB",
|
| 63 |
+
"CINEB",
|
| 64 |
+
"pes",
|
| 65 |
+
"neb",
|
| 66 |
+
"geom",
|
| 67 |
+
"methods",
|
| 68 |
+
"mol_graphs",
|
| 69 |
+
"utils",
|
| 70 |
+
"hessians",
|
| 71 |
+
]
|
autodE/source/autode/atoms.py
ADDED
|
@@ -0,0 +1,1865 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from copy import deepcopy
|
| 3 |
+
from typing import Union, Optional, List, Sequence, Any
|
| 4 |
+
from autode.log import logger
|
| 5 |
+
from autode.geom import get_rot_mat_euler
|
| 6 |
+
from autode.values import (
|
| 7 |
+
Distance,
|
| 8 |
+
Angle,
|
| 9 |
+
Mass,
|
| 10 |
+
Coordinate,
|
| 11 |
+
Coordinates,
|
| 12 |
+
MomentOfInertia,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Atom:
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
atomic_symbol: str,
|
| 20 |
+
x: Any = 0.0,
|
| 21 |
+
y: Any = 0.0,
|
| 22 |
+
z: Any = 0.0,
|
| 23 |
+
atom_class: Optional[int] = None,
|
| 24 |
+
partial_charge: Optional[float] = None,
|
| 25 |
+
):
|
| 26 |
+
"""
|
| 27 |
+
Atom class. Centered at the origin by default. Can be initialised from
|
| 28 |
+
positional or keyword arguments:
|
| 29 |
+
|
| 30 |
+
.. code-block:: Python
|
| 31 |
+
|
| 32 |
+
>>> import autode as ade
|
| 33 |
+
>>> ade.Atom('H')
|
| 34 |
+
Atom(H, 0.0000, 0.0000, 0.0000)
|
| 35 |
+
>>>
|
| 36 |
+
>>> ade.Atom('H', x=1.0, y=1.0, z=1.0)
|
| 37 |
+
Atom(H, 1.0000, 1.0000, 1.0000)
|
| 38 |
+
>>>
|
| 39 |
+
>>> ade.Atom('H', 1.0, 1.0, 1.0)
|
| 40 |
+
Atom(H, 1.0000, 1.0000, 1.0000)
|
| 41 |
+
|
| 42 |
+
-----------------------------------------------------------------------
|
| 43 |
+
Arguments:
|
| 44 |
+
atomic_symbol: Symbol of an element e.g. 'C' for carbon
|
| 45 |
+
|
| 46 |
+
x: x coordinate in 3D space (Å)
|
| 47 |
+
|
| 48 |
+
y: y coordinate in 3D space (Å)
|
| 49 |
+
|
| 50 |
+
z: z coordinate in 3D space (Å)
|
| 51 |
+
|
| 52 |
+
atom_class: Fictitious additional labels to distinguish otherwise
|
| 53 |
+
identical atoms. Useful in finding bond isomorphisms
|
| 54 |
+
over identity reactions
|
| 55 |
+
|
| 56 |
+
partial_charge: Partial atomic charge in units of e, determined by
|
| 57 |
+
the atomic envrionment. Not an observable property.
|
| 58 |
+
"""
|
| 59 |
+
assert atomic_symbol in elements
|
| 60 |
+
|
| 61 |
+
self.label = atomic_symbol
|
| 62 |
+
self._coord = Coordinate(float(x), float(y), float(z))
|
| 63 |
+
self.atom_class = atom_class
|
| 64 |
+
self.partial_charge = (
|
| 65 |
+
None if partial_charge is None else float(partial_charge)
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def __repr__(self):
|
| 69 |
+
"""
|
| 70 |
+
Representation of this atom
|
| 71 |
+
|
| 72 |
+
-----------------------------------------------------------------------
|
| 73 |
+
Returns:
|
| 74 |
+
(str): Representation
|
| 75 |
+
"""
|
| 76 |
+
x, y, z = self.coord
|
| 77 |
+
return f"Atom({self.label}, {x:.4f}, {y:.4f}, {z:.4f})"
|
| 78 |
+
|
| 79 |
+
def __str__(self):
|
| 80 |
+
return self.__repr__()
|
| 81 |
+
|
| 82 |
+
def __eq__(self, other: Any):
|
| 83 |
+
"""Equality of another atom to this one"""
|
| 84 |
+
are_equal = (
|
| 85 |
+
isinstance(other, Atom)
|
| 86 |
+
and other.label == self.label
|
| 87 |
+
and other.atom_class == self.atom_class
|
| 88 |
+
and isinstance(other.partial_charge, type(self.partial_charge))
|
| 89 |
+
and (
|
| 90 |
+
(other.partial_charge is None and self.partial_charge is None)
|
| 91 |
+
or np.isclose(other.partial_charge, self.partial_charge)
|
| 92 |
+
)
|
| 93 |
+
and np.allclose(other._coord, self._coord)
|
| 94 |
+
)
|
| 95 |
+
return are_equal
|
| 96 |
+
|
| 97 |
+
@property
|
| 98 |
+
def atomic_number(self) -> int:
|
| 99 |
+
"""
|
| 100 |
+
Atomic numbers are the position in the elements (indexed from zero),
|
| 101 |
+
plus one. Example:
|
| 102 |
+
|
| 103 |
+
.. code-block:: Python
|
| 104 |
+
|
| 105 |
+
>>> import autode as ade
|
| 106 |
+
>>> atom = ade.Atom('C')
|
| 107 |
+
>>> atom.atomic_number
|
| 108 |
+
6
|
| 109 |
+
|
| 110 |
+
-----------------------------------------------------------------------
|
| 111 |
+
Returns:
|
| 112 |
+
(int): Atomic number
|
| 113 |
+
"""
|
| 114 |
+
return elements.index(self.label) + 1
|
| 115 |
+
|
| 116 |
+
@property
|
| 117 |
+
def atomic_symbol(self) -> str:
|
| 118 |
+
"""
|
| 119 |
+
A more interpretable alias for Atom.label. Should be present in the
|
| 120 |
+
elements. Example:
|
| 121 |
+
|
| 122 |
+
.. code-block:: Python
|
| 123 |
+
|
| 124 |
+
>>> import autode as ade
|
| 125 |
+
>>> atom = ade.Atom('Zn')
|
| 126 |
+
>>> atom.atomic_symbol
|
| 127 |
+
'Zn'
|
| 128 |
+
|
| 129 |
+
-----------------------------------------------------------------------
|
| 130 |
+
Returns:
|
| 131 |
+
(str): Atomic symbol
|
| 132 |
+
"""
|
| 133 |
+
return self.label
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def coord(self) -> Coordinate:
|
| 137 |
+
"""
|
| 138 |
+
Position of this atom in space. Coordinate has attributes x, y, z
|
| 139 |
+
for the Cartesian displacements. Example:
|
| 140 |
+
|
| 141 |
+
.. code-block:: Python
|
| 142 |
+
|
| 143 |
+
>>> import autode as ade
|
| 144 |
+
>>> atom = ade.Atom('H')
|
| 145 |
+
>>> atom.coord
|
| 146 |
+
Coordinate([0. 0. 0.] Å)
|
| 147 |
+
|
| 148 |
+
To initialise at a different position away from the origin
|
| 149 |
+
|
| 150 |
+
.. code-block:: Python
|
| 151 |
+
|
| 152 |
+
>>> ade.Atom('H', x=1.0).coord
|
| 153 |
+
Coordinate([1. 0. 0.] Å)
|
| 154 |
+
>>> ade.Atom('H', x=1.0).coord.x
|
| 155 |
+
1.0
|
| 156 |
+
|
| 157 |
+
Coordinates are instances of autode.values.ValueArray, so can
|
| 158 |
+
be converted from the default angstrom units to e.g. Bohr
|
| 159 |
+
|
| 160 |
+
.. code-block:: Python
|
| 161 |
+
|
| 162 |
+
>>> ade.Atom('H', x=1.0, y=-1.0).coord.to('a0')
|
| 163 |
+
Coordinate([1.889 -1.889 0. ] bohr)
|
| 164 |
+
|
| 165 |
+
-----------------------------------------------------------------------
|
| 166 |
+
Returns:
|
| 167 |
+
(autode.values.Coordinate): Coordinate
|
| 168 |
+
"""
|
| 169 |
+
return self._coord
|
| 170 |
+
|
| 171 |
+
@coord.setter
|
| 172 |
+
def coord(self, *args):
|
| 173 |
+
"""
|
| 174 |
+
Coordinate setter
|
| 175 |
+
|
| 176 |
+
-----------------------------------------------------------------------
|
| 177 |
+
Arguments:
|
| 178 |
+
*args (float | list(float) | np.ndarray(float)):
|
| 179 |
+
|
| 180 |
+
Raises:
|
| 181 |
+
(ValueError): If the arguments cannot be coerced into a (3,) shape
|
| 182 |
+
"""
|
| 183 |
+
self._coord = Coordinate(*args)
|
| 184 |
+
|
| 185 |
+
@property
|
| 186 |
+
def is_metal(self) -> bool:
|
| 187 |
+
"""
|
| 188 |
+
Is this atom a metal? Defines metals to be up to and including:
|
| 189 |
+
Ga, Sn, Bi. Example:
|
| 190 |
+
|
| 191 |
+
.. code-block:: Python
|
| 192 |
+
|
| 193 |
+
>>> import autode as ade
|
| 194 |
+
>>> ade.Atom('C').is_metal
|
| 195 |
+
False
|
| 196 |
+
>>> ade.Atom('Zn').is_metal
|
| 197 |
+
True
|
| 198 |
+
|
| 199 |
+
-----------------------------------------------------------------------
|
| 200 |
+
Returns:
|
| 201 |
+
(bool):
|
| 202 |
+
"""
|
| 203 |
+
return self.label in metals
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def group(self) -> int:
|
| 207 |
+
"""
|
| 208 |
+
Group of the periodic table is this atom in. 0 if not found. Example:
|
| 209 |
+
|
| 210 |
+
.. code-block:: Python
|
| 211 |
+
|
| 212 |
+
>>> import autode as ade
|
| 213 |
+
>>> ade.Atom('C').group
|
| 214 |
+
14
|
| 215 |
+
|
| 216 |
+
-----------------------------------------------------------------------
|
| 217 |
+
Returns:
|
| 218 |
+
(int): Group
|
| 219 |
+
"""
|
| 220 |
+
|
| 221 |
+
for group_idx in range(1, 18):
|
| 222 |
+
if self.label in PeriodicTable.group(group_idx):
|
| 223 |
+
return group_idx
|
| 224 |
+
|
| 225 |
+
return 0
|
| 226 |
+
|
| 227 |
+
@property
|
| 228 |
+
def period(self) -> int:
|
| 229 |
+
"""
|
| 230 |
+
Period of the periodic table is this atom in. 0 if not found. Example:
|
| 231 |
+
|
| 232 |
+
.. code-block:: Python
|
| 233 |
+
|
| 234 |
+
>>> import autode as ade
|
| 235 |
+
>>> ade.Atom('C').period
|
| 236 |
+
2
|
| 237 |
+
|
| 238 |
+
-----------------------------------------------------------------------
|
| 239 |
+
Returns:
|
| 240 |
+
(int): Period
|
| 241 |
+
"""
|
| 242 |
+
|
| 243 |
+
for period_idx in range(1, 7):
|
| 244 |
+
if self.label in PeriodicTable.period(period_idx):
|
| 245 |
+
return period_idx
|
| 246 |
+
|
| 247 |
+
return 0
|
| 248 |
+
|
| 249 |
+
@property
|
| 250 |
+
def tm_row(self) -> Optional[int]:
|
| 251 |
+
"""
|
| 252 |
+
Row of transition metals that this element is in. Returns None if
|
| 253 |
+
this atom is not a metal. Example:
|
| 254 |
+
|
| 255 |
+
.. code-block:: Python
|
| 256 |
+
|
| 257 |
+
>>> import autode as ade
|
| 258 |
+
>>> ade.Atom('Zn').tm_row
|
| 259 |
+
1
|
| 260 |
+
|
| 261 |
+
-----------------------------------------------------------------------
|
| 262 |
+
Returns:
|
| 263 |
+
(int | None): Transition metal row
|
| 264 |
+
"""
|
| 265 |
+
for row in [1, 2, 3]:
|
| 266 |
+
if self.label in PeriodicTable.transition_metals(row):
|
| 267 |
+
return row
|
| 268 |
+
|
| 269 |
+
return None
|
| 270 |
+
|
| 271 |
+
@property
|
| 272 |
+
def weight(self) -> Mass:
|
| 273 |
+
"""
|
| 274 |
+
Atomic weight. Example:
|
| 275 |
+
|
| 276 |
+
.. code-block:: Python
|
| 277 |
+
|
| 278 |
+
>>> import autode as ade
|
| 279 |
+
>>> ade.Atom('C').weight
|
| 280 |
+
Mass(12.0107 amu)
|
| 281 |
+
>>>
|
| 282 |
+
>>> ade.Atom('C').weight == ade.Atom('C').mass
|
| 283 |
+
True
|
| 284 |
+
|
| 285 |
+
-----------------------------------------------------------------------
|
| 286 |
+
Returns:
|
| 287 |
+
(autode.values.Mass): Weight
|
| 288 |
+
"""
|
| 289 |
+
|
| 290 |
+
try:
|
| 291 |
+
return Mass(atomic_weights[self.label])
|
| 292 |
+
|
| 293 |
+
except KeyError:
|
| 294 |
+
logger.warning(
|
| 295 |
+
f"Could not find a valid weight for {self.label}. "
|
| 296 |
+
f"Guessing at 70"
|
| 297 |
+
)
|
| 298 |
+
return Mass(70)
|
| 299 |
+
|
| 300 |
+
@property
|
| 301 |
+
def mass(self) -> Mass:
|
| 302 |
+
"""Alias of weight. Returns Atom.weight an so can be converted
|
| 303 |
+
to different units. For example, to convert the mass to electron masses:
|
| 304 |
+
|
| 305 |
+
.. code-block:: Python
|
| 306 |
+
|
| 307 |
+
>>> import autode as ade
|
| 308 |
+
>>> ade.Atom('H').mass.to('me')
|
| 309 |
+
Mass(1837.36222 m_e)
|
| 310 |
+
|
| 311 |
+
-----------------------------------------------------------------------
|
| 312 |
+
Returns:
|
| 313 |
+
(autode.values.Mass): Mass
|
| 314 |
+
"""
|
| 315 |
+
return self.weight
|
| 316 |
+
|
| 317 |
+
@property
|
| 318 |
+
def maximal_valance(self) -> int:
|
| 319 |
+
"""
|
| 320 |
+
The maximum/maximal valance that this atom supports in any charge
|
| 321 |
+
state (most commonly). i.e. for H the maximal_valance=1. Useful for
|
| 322 |
+
generating molecular graphs
|
| 323 |
+
|
| 324 |
+
-----------------------------------------------------------------------
|
| 325 |
+
Returns:
|
| 326 |
+
(int): Maximal valance
|
| 327 |
+
"""
|
| 328 |
+
|
| 329 |
+
if self.is_metal:
|
| 330 |
+
return 6
|
| 331 |
+
|
| 332 |
+
if self.label in _max_valances:
|
| 333 |
+
return _max_valances[self.label]
|
| 334 |
+
|
| 335 |
+
logger.warning(
|
| 336 |
+
f"Could not find a valid valance for {self}. " f"Guessing at 6"
|
| 337 |
+
)
|
| 338 |
+
return 6
|
| 339 |
+
|
| 340 |
+
@property
|
| 341 |
+
def vdw_radius(self) -> Distance:
|
| 342 |
+
"""
|
| 343 |
+
Van der Waals radius for this atom. Example:
|
| 344 |
+
|
| 345 |
+
.. code-block:: Python
|
| 346 |
+
|
| 347 |
+
>>> import autode as ade
|
| 348 |
+
>>> ade.Atom('H').vdw_radius
|
| 349 |
+
Distance(1.1 Å)
|
| 350 |
+
|
| 351 |
+
-----------------------------------------------------------------------
|
| 352 |
+
Returns:
|
| 353 |
+
(autode.values.Distance): Van der Waals radius
|
| 354 |
+
"""
|
| 355 |
+
|
| 356 |
+
if self.label in vdw_radii:
|
| 357 |
+
radius = vdw_radii[self.label]
|
| 358 |
+
else:
|
| 359 |
+
logger.error(
|
| 360 |
+
f"Couldn't find the VdV radii for {self}. "
|
| 361 |
+
f"Guessing at 2.3 Å"
|
| 362 |
+
)
|
| 363 |
+
radius = 2.3
|
| 364 |
+
|
| 365 |
+
return Distance(radius, "Å")
|
| 366 |
+
|
| 367 |
+
@property
|
| 368 |
+
def covalent_radius(self) -> Distance:
|
| 369 |
+
"""
|
| 370 |
+
Covalent radius for this atom. Example:
|
| 371 |
+
|
| 372 |
+
.. code-block:: Python
|
| 373 |
+
|
| 374 |
+
>>> import autode as ade
|
| 375 |
+
>>> ade.Atom('H').covalent_radius
|
| 376 |
+
Distance(0.31 Å)
|
| 377 |
+
|
| 378 |
+
-----------------------------------------------------------------------
|
| 379 |
+
Returns:
|
| 380 |
+
(autode.values.Distance): Van der Waals radius
|
| 381 |
+
"""
|
| 382 |
+
radius = Distance(
|
| 383 |
+
_covalent_radii_pm[self.atomic_number - 1], units="pm"
|
| 384 |
+
)
|
| 385 |
+
return radius.to("Å")
|
| 386 |
+
|
| 387 |
+
def is_pi(self, valency: int) -> bool:
|
| 388 |
+
"""
|
| 389 |
+
Determine if this atom is a 'π-atom' i.e. is unsaturated. Only
|
| 390 |
+
approximate! Example:
|
| 391 |
+
|
| 392 |
+
.. code-block:: Python
|
| 393 |
+
|
| 394 |
+
>>> import autode as ade
|
| 395 |
+
>>> ade.Atom('C').is_pi(valency=3)
|
| 396 |
+
True
|
| 397 |
+
>>> ade.Atom('H').is_pi(valency=1)
|
| 398 |
+
False
|
| 399 |
+
|
| 400 |
+
-----------------------------------------------------------------------
|
| 401 |
+
Arguments:
|
| 402 |
+
valency (int):
|
| 403 |
+
|
| 404 |
+
Returns:
|
| 405 |
+
(bool):
|
| 406 |
+
"""
|
| 407 |
+
|
| 408 |
+
if self.label in non_pi_elements:
|
| 409 |
+
return False
|
| 410 |
+
|
| 411 |
+
if self.label not in pi_valencies:
|
| 412 |
+
logger.warning(
|
| 413 |
+
f"{self.label} not found in π valency dictionary - "
|
| 414 |
+
f"assuming not a π-atom"
|
| 415 |
+
)
|
| 416 |
+
return False
|
| 417 |
+
|
| 418 |
+
if valency in pi_valencies[self.label]:
|
| 419 |
+
return True
|
| 420 |
+
|
| 421 |
+
return False
|
| 422 |
+
|
| 423 |
+
def translate(self, *args, **kwargs) -> None:
|
| 424 |
+
"""
|
| 425 |
+
Translate this atom by a vector in place. Arguments should be
|
| 426 |
+
coercible into a coordinate (i.e. length 3). Example:
|
| 427 |
+
|
| 428 |
+
.. code-block:: Python
|
| 429 |
+
|
| 430 |
+
>>> import autode as ade
|
| 431 |
+
>>> atom = ade.Atom('H')
|
| 432 |
+
>>> atom.translate(1.0, 0.0, 0.0)
|
| 433 |
+
>>> atom.coord
|
| 434 |
+
Coordinate([1. 0. 0.] Å)
|
| 435 |
+
|
| 436 |
+
Atoms can also be translated using numpy arrays:
|
| 437 |
+
|
| 438 |
+
.. code-block:: Python
|
| 439 |
+
|
| 440 |
+
>>> import autode as ade
|
| 441 |
+
>>> import numpy as np
|
| 442 |
+
>>>
|
| 443 |
+
>>> atom = ade.Atom('H')
|
| 444 |
+
>>> atom.translate(np.ones(3))
|
| 445 |
+
>>> atom.coord
|
| 446 |
+
Coordinate([1. 1. 1.] Å)
|
| 447 |
+
>>>
|
| 448 |
+
>>> atom.translate(vec=-atom.coord)
|
| 449 |
+
>>> atom.coord
|
| 450 |
+
Coordinate([0. 0. 0.] Å)
|
| 451 |
+
|
| 452 |
+
-----------------------------------------------------------------------
|
| 453 |
+
Arguments:
|
| 454 |
+
*args (float | np.ndarray | list(float)):
|
| 455 |
+
|
| 456 |
+
Keyword Arguments:
|
| 457 |
+
vec (np.ndarray): Shape = (3,)
|
| 458 |
+
"""
|
| 459 |
+
if "vec" in kwargs:
|
| 460 |
+
# Assume the vec is cast-able to a numpy array which can be added
|
| 461 |
+
self.coord += np.asarray(kwargs["vec"])
|
| 462 |
+
|
| 463 |
+
elif len(kwargs) > 0:
|
| 464 |
+
raise ValueError(
|
| 465 |
+
f"Expecting only a vec keyword argument. " f"Had {kwargs}"
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
else:
|
| 469 |
+
self.coord += Coordinate(*args)
|
| 470 |
+
|
| 471 |
+
return None
|
| 472 |
+
|
| 473 |
+
def rotate(
|
| 474 |
+
self,
|
| 475 |
+
axis: Union[np.ndarray, Sequence],
|
| 476 |
+
theta: Union[Angle, float],
|
| 477 |
+
origin: Union[np.ndarray, Sequence, None] = None,
|
| 478 |
+
) -> None:
|
| 479 |
+
"""
|
| 480 |
+
Rotate this atom theta radians around an axis given an origin. By
|
| 481 |
+
default the rotation is applied around the origin with the angle
|
| 482 |
+
in radians (unless an autode.values.Angle). Rotation is applied in
|
| 483 |
+
place. To rotate a H atom around the z-axis:
|
| 484 |
+
|
| 485 |
+
.. code-block:: Python
|
| 486 |
+
|
| 487 |
+
>>> import autode as ade
|
| 488 |
+
>>> atom = ade.Atom('H', x=1.0)
|
| 489 |
+
>>> atom.rotate(axis=[0.0, 0.0, 1.0], theta=3.14)
|
| 490 |
+
>>> atom.coord
|
| 491 |
+
Coordinate([-1. 0. 0.] Å)
|
| 492 |
+
|
| 493 |
+
With an origin:
|
| 494 |
+
|
| 495 |
+
.. code-block:: Python
|
| 496 |
+
|
| 497 |
+
>>> import autode as ade
|
| 498 |
+
>>> atom = ade.Atom('H')
|
| 499 |
+
>>> atom.rotate(axis=[0.0, 0.0, 1.0], theta=3.14, origin=[1.0, 0.0, 0.0])
|
| 500 |
+
>>> atom.coord
|
| 501 |
+
Coordinate([2. 0. 0.] Å)
|
| 502 |
+
|
| 503 |
+
And with an angle not in radians:
|
| 504 |
+
|
| 505 |
+
.. code-block:: Python
|
| 506 |
+
|
| 507 |
+
>>> import autode as ade
|
| 508 |
+
>>> from autode.values import Angle
|
| 509 |
+
>>>
|
| 510 |
+
>>> atom = ade.Atom('H', x=1.0)
|
| 511 |
+
>>> atom.rotate(axis=[0.0, 0.0, 1.0], theta=Angle(180, units='deg'))
|
| 512 |
+
>>> atom.coord
|
| 513 |
+
Coordinate([-1. 0. 0.] Å)
|
| 514 |
+
|
| 515 |
+
-----------------------------------------------------------------------
|
| 516 |
+
Arguments:
|
| 517 |
+
axis: Axis to rotate in. shape = (3,)
|
| 518 |
+
|
| 519 |
+
theta: Angle to rotate by
|
| 520 |
+
|
| 521 |
+
origin: Rotate about this origin. shape = (3,) if no origin is
|
| 522 |
+
specified then the atom is rotated without translation.
|
| 523 |
+
"""
|
| 524 |
+
# If specified, shift so that the origin is at (0, 0, 0)
|
| 525 |
+
if origin is not None:
|
| 526 |
+
self.translate(vec=-np.asarray(origin))
|
| 527 |
+
|
| 528 |
+
# apply the rotation
|
| 529 |
+
rot_matrix = get_rot_mat_euler(axis=axis, theta=theta)
|
| 530 |
+
self.coord = np.matmul(rot_matrix, self.coord)
|
| 531 |
+
|
| 532 |
+
# and shift back, if required
|
| 533 |
+
if origin is not None:
|
| 534 |
+
self.translate(vec=np.asarray(origin))
|
| 535 |
+
|
| 536 |
+
return None
|
| 537 |
+
|
| 538 |
+
def copy(self) -> "Atom":
|
| 539 |
+
return deepcopy(self)
|
| 540 |
+
|
| 541 |
+
# --- Method aliases ---
|
| 542 |
+
coordinate = coord
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
class DummyAtom(Atom):
|
| 546 |
+
def __init__(self, x, y, z):
|
| 547 |
+
"""
|
| 548 |
+
Dummy atom
|
| 549 |
+
|
| 550 |
+
-----------------------------------------------------------------------
|
| 551 |
+
Arguments:
|
| 552 |
+
x (float): x coordinate in 3D space (Å)
|
| 553 |
+
y (float): y
|
| 554 |
+
z (float): z
|
| 555 |
+
"""
|
| 556 |
+
# Superclass constructor called with a valid element...
|
| 557 |
+
super().__init__("H", x, y, z)
|
| 558 |
+
|
| 559 |
+
# then re-assigned
|
| 560 |
+
self.label = "D"
|
| 561 |
+
|
| 562 |
+
@property
|
| 563 |
+
def atomic_number(self):
|
| 564 |
+
"""The atomic number is defined as 0 for a dummy atom"""
|
| 565 |
+
return 0
|
| 566 |
+
|
| 567 |
+
@property
|
| 568 |
+
def weight(self) -> Mass:
|
| 569 |
+
"""Dummy atoms do not have any weight/mass"""
|
| 570 |
+
return Mass(0.0)
|
| 571 |
+
|
| 572 |
+
@property
|
| 573 |
+
def mass(self) -> Mass:
|
| 574 |
+
"""Dummy atoms do not have any weight/mass"""
|
| 575 |
+
return Mass(0.0)
|
| 576 |
+
|
| 577 |
+
@property
|
| 578 |
+
def vdw_radius(self) -> Distance:
|
| 579 |
+
"""Dummy atoms have no radius"""
|
| 580 |
+
return Distance(0.0, units="Å")
|
| 581 |
+
|
| 582 |
+
@property
|
| 583 |
+
def covalent_radius(self) -> Distance:
|
| 584 |
+
"""Dummy atoms have no radius"""
|
| 585 |
+
return Distance(0.0, units="Å")
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
class Atoms(list):
|
| 589 |
+
def __repr__(self):
|
| 590 |
+
"""Representation"""
|
| 591 |
+
return f"Atoms(n_atoms={len(self)}, {super().__repr__()})"
|
| 592 |
+
|
| 593 |
+
def __add__(self, other):
|
| 594 |
+
"""Add another set of Atoms to this one. Can add None"""
|
| 595 |
+
if other is None:
|
| 596 |
+
return self
|
| 597 |
+
|
| 598 |
+
return super().__add__(other)
|
| 599 |
+
|
| 600 |
+
def __radd__(self, other):
|
| 601 |
+
"""Add another set of Atoms to this one. Can add None"""
|
| 602 |
+
return self.__add__(other)
|
| 603 |
+
|
| 604 |
+
def copy(self) -> "Atoms":
|
| 605 |
+
"""
|
| 606 |
+
Copy these atoms, deeply
|
| 607 |
+
|
| 608 |
+
-----------------------------------------------------------------------
|
| 609 |
+
Returns:
|
| 610 |
+
(autode.atoms.Atoms):
|
| 611 |
+
"""
|
| 612 |
+
return deepcopy(self)
|
| 613 |
+
|
| 614 |
+
def remove_dummy(self) -> None:
|
| 615 |
+
"""Remove all the dummy atoms from this list of atoms"""
|
| 616 |
+
|
| 617 |
+
for i, atom in enumerate(self):
|
| 618 |
+
if isinstance(atom, DummyAtom):
|
| 619 |
+
del self[i]
|
| 620 |
+
return
|
| 621 |
+
|
| 622 |
+
@property
|
| 623 |
+
def coordinates(self) -> Coordinates:
|
| 624 |
+
return Coordinates(np.array([a.coord for a in self]))
|
| 625 |
+
|
| 626 |
+
@coordinates.setter
|
| 627 |
+
def coordinates(self, value: np.ndarray):
|
| 628 |
+
"""Set the coordinates from a numpy array
|
| 629 |
+
|
| 630 |
+
-----------------------------------------------------------------------
|
| 631 |
+
Arguments:
|
| 632 |
+
value (np.ndarray): Shape = (n_atoms, 3) or (3*n_atoms) as a
|
| 633 |
+
row major vector
|
| 634 |
+
"""
|
| 635 |
+
|
| 636 |
+
if value.ndim == 1:
|
| 637 |
+
assert value.shape == (3 * len(self),)
|
| 638 |
+
value = value.reshape((-1, 3))
|
| 639 |
+
|
| 640 |
+
elif value.ndim == 2:
|
| 641 |
+
assert value.shape == (len(self), 3)
|
| 642 |
+
|
| 643 |
+
else:
|
| 644 |
+
raise AssertionError(
|
| 645 |
+
"Cannot set coordinates from a array with"
|
| 646 |
+
f"shape: {value.shape}. Must be 1 or 2 "
|
| 647 |
+
f"dimensional"
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
for i, atom in enumerate(self):
|
| 651 |
+
atom.coord = Coordinate(*value[i])
|
| 652 |
+
|
| 653 |
+
@property
|
| 654 |
+
def com(self) -> Coordinate:
|
| 655 |
+
r"""
|
| 656 |
+
Centre of mass of these coordinates
|
| 657 |
+
|
| 658 |
+
.. math::
|
| 659 |
+
\text{COM} = \frac{1}{M} \sum_i m_i R_i
|
| 660 |
+
|
| 661 |
+
where M is the total mass, m_i the mass of atom i and R_i it's
|
| 662 |
+
coordinate
|
| 663 |
+
|
| 664 |
+
-----------------------------------------------------------------------
|
| 665 |
+
Returns:
|
| 666 |
+
(autode.values.Coordinate): COM
|
| 667 |
+
"""
|
| 668 |
+
if len(self) == 0:
|
| 669 |
+
raise ValueError("Undefined centre of mass with no atoms")
|
| 670 |
+
|
| 671 |
+
com = Coordinate(0.0, 0.0, 0.0)
|
| 672 |
+
|
| 673 |
+
for atom in self:
|
| 674 |
+
com += atom.mass * atom.coord
|
| 675 |
+
|
| 676 |
+
return Coordinate(com / sum(atom.mass for atom in self))
|
| 677 |
+
|
| 678 |
+
@property
|
| 679 |
+
def moi(self) -> MomentOfInertia:
|
| 680 |
+
"""
|
| 681 |
+
Moment of inertia matrix (I)::
|
| 682 |
+
|
| 683 |
+
(I_00 I_01 I_02)
|
| 684 |
+
I = (I_10 I_11 I_12)
|
| 685 |
+
(I_20 I_21 I_22)
|
| 686 |
+
|
| 687 |
+
Returns:
|
| 688 |
+
(autode.values.MomentOfInertia):
|
| 689 |
+
"""
|
| 690 |
+
moi = MomentOfInertia(np.zeros(shape=(3, 3)), units="amu Å^2")
|
| 691 |
+
|
| 692 |
+
for atom in self:
|
| 693 |
+
mass, (x, y, z) = atom.mass, atom.coord
|
| 694 |
+
|
| 695 |
+
moi[0, 0] += mass * (y**2 + z**2)
|
| 696 |
+
moi[0, 1] -= mass * (x * y)
|
| 697 |
+
moi[0, 2] -= mass * (x * z)
|
| 698 |
+
|
| 699 |
+
moi[1, 0] -= mass * (y * x)
|
| 700 |
+
moi[1, 1] += mass * (x**2 + z**2)
|
| 701 |
+
moi[1, 2] -= mass * (y * z)
|
| 702 |
+
|
| 703 |
+
moi[2, 0] -= mass * (z * x)
|
| 704 |
+
moi[2, 1] -= mass * (z * y)
|
| 705 |
+
moi[2, 2] += mass * (x**2 + y**2)
|
| 706 |
+
|
| 707 |
+
return moi
|
| 708 |
+
|
| 709 |
+
@property
|
| 710 |
+
def contain_metals(self) -> bool:
|
| 711 |
+
"""
|
| 712 |
+
Do these atoms contain at least a single metal atom?
|
| 713 |
+
|
| 714 |
+
-----------------------------------------------------------------------
|
| 715 |
+
Returns:
|
| 716 |
+
(bool):
|
| 717 |
+
"""
|
| 718 |
+
return any(atom.label in metals for atom in self)
|
| 719 |
+
|
| 720 |
+
def idxs_are_present(self, *args: int) -> bool:
|
| 721 |
+
"""Are all these indexes present in this set of atoms"""
|
| 722 |
+
return set(args).issubset(set(range(len(self))))
|
| 723 |
+
|
| 724 |
+
def eqm_bond_distance(self, i: int, j: int) -> Distance:
|
| 725 |
+
"""
|
| 726 |
+
Equilibrium distance between two atoms. If known then use the
|
| 727 |
+
experimental dimer distance, otherwise estimate if from the
|
| 728 |
+
covalent radii of the two atoms. Example
|
| 729 |
+
|
| 730 |
+
Example:
|
| 731 |
+
|
| 732 |
+
.. code-block:: Python
|
| 733 |
+
|
| 734 |
+
>>> import autode as ade
|
| 735 |
+
>>> mol = ade.Molecule(atoms=[ade.Atom('H'), ade.Atom('H')])
|
| 736 |
+
>>> mol.distance(0, 1)
|
| 737 |
+
Distance(0.0 Å)
|
| 738 |
+
>>> mol.eqm_bond_distance(0, 1)
|
| 739 |
+
Distance(0.741 Å)
|
| 740 |
+
|
| 741 |
+
-----------------------------------------------------------------------
|
| 742 |
+
Returns:
|
| 743 |
+
(autode.values.Distance): Equlirbium distance
|
| 744 |
+
"""
|
| 745 |
+
if not self.idxs_are_present(i, j):
|
| 746 |
+
raise ValueError(
|
| 747 |
+
f"Cannot calculate the equilibrium distance "
|
| 748 |
+
f"between {i}-{j}. At least one atom not present"
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
if i == j:
|
| 752 |
+
return Distance(0.0, units="Å")
|
| 753 |
+
|
| 754 |
+
symbols = f"{self[i].atomic_symbol}{self[j].atomic_symbol}"
|
| 755 |
+
|
| 756 |
+
if symbols in _bond_lengths:
|
| 757 |
+
return Distance(_bond_lengths[symbols], units="Å")
|
| 758 |
+
|
| 759 |
+
# TODO: Something more accurate here
|
| 760 |
+
return self[i].covalent_radius + self[j].covalent_radius
|
| 761 |
+
|
| 762 |
+
def distance(self, i: int, j: int) -> Distance:
|
| 763 |
+
"""
|
| 764 |
+
Distance between two atoms (Å), indexed from 0.
|
| 765 |
+
|
| 766 |
+
.. code-block:: Python
|
| 767 |
+
|
| 768 |
+
>>> import autode as ade
|
| 769 |
+
>>> mol = ade.Molecule(atoms=[ade.Atom('H'), ade.Atom('H', x=1.0)])
|
| 770 |
+
>>> mol.distance(0, 1)
|
| 771 |
+
Distance(1.0 Å)
|
| 772 |
+
|
| 773 |
+
-----------------------------------------------------------------------
|
| 774 |
+
Arguments:
|
| 775 |
+
i (int): Atom index of the first atom
|
| 776 |
+
j (int): Atom index of the second atom
|
| 777 |
+
|
| 778 |
+
Returns:
|
| 779 |
+
(autode.values.Distance): Distance
|
| 780 |
+
|
| 781 |
+
Raises:
|
| 782 |
+
(ValueError):
|
| 783 |
+
"""
|
| 784 |
+
if not self.idxs_are_present(i, j):
|
| 785 |
+
raise ValueError(
|
| 786 |
+
f"Cannot calculate the distance between {i}-{j}. "
|
| 787 |
+
f"At least one atom not present"
|
| 788 |
+
)
|
| 789 |
+
|
| 790 |
+
return Distance(np.linalg.norm(self[i].coord - self[j].coord))
|
| 791 |
+
|
| 792 |
+
def vector(self, i: int, j: int) -> np.ndarray:
|
| 793 |
+
"""
|
| 794 |
+
Vector from atom i to atom j
|
| 795 |
+
|
| 796 |
+
-----------------------------------------------------------------------
|
| 797 |
+
Arguments:
|
| 798 |
+
i (int):
|
| 799 |
+
j (int):
|
| 800 |
+
|
| 801 |
+
Returns:
|
| 802 |
+
(np.ndarray):
|
| 803 |
+
|
| 804 |
+
Raises:
|
| 805 |
+
(IndexError): If i or j are not present
|
| 806 |
+
"""
|
| 807 |
+
return np.asarray(self[j].coord - self[i].coord)
|
| 808 |
+
|
| 809 |
+
def nvector(self, i: int, j: int) -> np.ndarray:
|
| 810 |
+
"""
|
| 811 |
+
Normalised vector from atom i to atom j
|
| 812 |
+
|
| 813 |
+
-----------------------------------------------------------------------
|
| 814 |
+
Arguments:
|
| 815 |
+
i (int):
|
| 816 |
+
j (int):
|
| 817 |
+
|
| 818 |
+
Returns:
|
| 819 |
+
(np.ndarray):
|
| 820 |
+
|
| 821 |
+
Raises:
|
| 822 |
+
(IndexError): If i or j are not present
|
| 823 |
+
"""
|
| 824 |
+
vec = self.vector(i, j)
|
| 825 |
+
return vec / np.linalg.norm(vec)
|
| 826 |
+
|
| 827 |
+
def are_linear(self, angle_tol: Angle = Angle(1, "º")) -> bool:
|
| 828 |
+
"""
|
| 829 |
+
Are these set of atoms colinear?
|
| 830 |
+
|
| 831 |
+
-----------------------------------------------------------------------
|
| 832 |
+
Arguments:
|
| 833 |
+
angle_tol (autode.values.Angle): Tolerance on the angle
|
| 834 |
+
|
| 835 |
+
Returns:
|
| 836 |
+
(bool): Whether the atoms are linear
|
| 837 |
+
"""
|
| 838 |
+
if len(self) < 2: # Must have at least 2 atoms colinear
|
| 839 |
+
return False
|
| 840 |
+
|
| 841 |
+
if len(self) == 2: # Two atoms must be linear
|
| 842 |
+
return True
|
| 843 |
+
|
| 844 |
+
tol = np.abs(1.0 - np.cos(angle_tol.to("rad")))
|
| 845 |
+
|
| 846 |
+
vec0 = self.nvector(0, 1) # Normalised first vector
|
| 847 |
+
|
| 848 |
+
for atom in self[2:]:
|
| 849 |
+
vec = atom.coord - self[0].coord
|
| 850 |
+
cos_theta = np.dot(vec, vec0) / np.linalg.norm(vec)
|
| 851 |
+
|
| 852 |
+
# Both e.g. <179° and >1° should satisfy this condition for
|
| 853 |
+
# angle_tol = 1°
|
| 854 |
+
if np.abs(np.abs(cos_theta) - 1) > tol:
|
| 855 |
+
return False
|
| 856 |
+
|
| 857 |
+
return True
|
| 858 |
+
|
| 859 |
+
def are_planar(self, distance_tol: Distance = Distance(1e-3, "Å")) -> bool:
|
| 860 |
+
"""
|
| 861 |
+
Do all the atoms in this set lie in a single plane?
|
| 862 |
+
|
| 863 |
+
-----------------------------------------------------------------------
|
| 864 |
+
Arguments:
|
| 865 |
+
distance_tol (autode.values.Distance):
|
| 866 |
+
|
| 867 |
+
Returns:
|
| 868 |
+
(bool):
|
| 869 |
+
"""
|
| 870 |
+
if len(self) < 4: # 3 points must lie in a plane
|
| 871 |
+
return True
|
| 872 |
+
|
| 873 |
+
arr = self.coordinates.to("Å")
|
| 874 |
+
|
| 875 |
+
if isinstance(distance_tol, Distance):
|
| 876 |
+
distance_tol_float = float(distance_tol.to("Å"))
|
| 877 |
+
|
| 878 |
+
else:
|
| 879 |
+
logger.warning("Assuming a distance tolerance in units of Å")
|
| 880 |
+
distance_tol_float = float(distance_tol)
|
| 881 |
+
|
| 882 |
+
# Calculate a normal vector to the first two atomic vectors from atom 0
|
| 883 |
+
x0 = arr[0, :]
|
| 884 |
+
normal_vec = np.cross(arr[1, :] - x0, arr[2, :] - x0)
|
| 885 |
+
|
| 886 |
+
for i in range(3, len(self)):
|
| 887 |
+
# Calculate the 0->i atomic vector, which must not have any
|
| 888 |
+
# component in the direction in the normal if the atoms are planar
|
| 889 |
+
if np.dot(normal_vec, arr[i, :] - x0) > distance_tol_float:
|
| 890 |
+
return False
|
| 891 |
+
|
| 892 |
+
return True
|
| 893 |
+
|
| 894 |
+
|
| 895 |
+
class AtomCollection:
|
| 896 |
+
def __init__(self, atoms: Union[List[Atom], Atoms, None] = None):
|
| 897 |
+
"""
|
| 898 |
+
Collection of atoms, used as a base class for a species, complex
|
| 899 |
+
or transition state.
|
| 900 |
+
|
| 901 |
+
-----------------------------------------------------------------------
|
| 902 |
+
Arguments:
|
| 903 |
+
atoms (autode.atoms.Atoms | list(autode.atoms.Atom) | None):
|
| 904 |
+
"""
|
| 905 |
+
self._atoms = Atoms(atoms) if atoms is not None else None
|
| 906 |
+
|
| 907 |
+
@property
|
| 908 |
+
def n_atoms(self) -> int:
|
| 909 |
+
"""Number of atoms in this collection"""
|
| 910 |
+
return 0 if self.atoms is None else len(self.atoms)
|
| 911 |
+
|
| 912 |
+
@property
|
| 913 |
+
def coordinates(self) -> Optional[Coordinates]:
|
| 914 |
+
"""Numpy array of coordinates"""
|
| 915 |
+
if self.atoms is None:
|
| 916 |
+
return None
|
| 917 |
+
|
| 918 |
+
return self.atoms.coordinates
|
| 919 |
+
|
| 920 |
+
@coordinates.setter
|
| 921 |
+
def coordinates(self, value: np.ndarray):
|
| 922 |
+
"""Set the coordinates from a numpy array
|
| 923 |
+
|
| 924 |
+
-----------------------------------------------------------------------
|
| 925 |
+
Arguments:
|
| 926 |
+
value (np.ndarray): Shape = (n_atoms, 3) or (3*n_atoms) as a
|
| 927 |
+
row major vector
|
| 928 |
+
"""
|
| 929 |
+
if self._atoms is None:
|
| 930 |
+
raise ValueError(
|
| 931 |
+
"Must have atoms set to be able to set the "
|
| 932 |
+
"coordinates of them"
|
| 933 |
+
)
|
| 934 |
+
|
| 935 |
+
self._atoms.coordinates = value
|
| 936 |
+
|
| 937 |
+
@property
|
| 938 |
+
def atoms(self) -> Optional[Atoms]:
|
| 939 |
+
"""Constituent atoms of this collection"""
|
| 940 |
+
return self._atoms
|
| 941 |
+
|
| 942 |
+
@atoms.setter
|
| 943 |
+
def atoms(self, value: Union[List[Atom], Atoms, None]):
|
| 944 |
+
"""Set the constituent atoms of this collection"""
|
| 945 |
+
self._atoms = Atoms(value) if value is not None else None
|
| 946 |
+
|
| 947 |
+
@property
|
| 948 |
+
def com(self) -> Optional[Coordinate]:
|
| 949 |
+
"""Centre of mass of this atom collection
|
| 950 |
+
|
| 951 |
+
-----------------------------------------------------------------------
|
| 952 |
+
Returns:
|
| 953 |
+
(autode.values.Coordinate): COM
|
| 954 |
+
|
| 955 |
+
Raises:
|
| 956 |
+
(ValueError): If there are no atoms
|
| 957 |
+
"""
|
| 958 |
+
return None if self.atoms is None else self.atoms.com
|
| 959 |
+
|
| 960 |
+
@property
|
| 961 |
+
def moi(self) -> Optional[MomentOfInertia]:
|
| 962 |
+
"""
|
| 963 |
+
Moment of inertia matrix (I)
|
| 964 |
+
|
| 965 |
+
-----------------------------------------------------------------------
|
| 966 |
+
Returns:
|
| 967 |
+
(autode.values.MomentOfInertia):
|
| 968 |
+
"""
|
| 969 |
+
return None if self.atoms is None else self.atoms.moi
|
| 970 |
+
|
| 971 |
+
@property
|
| 972 |
+
def weight(self) -> Mass:
|
| 973 |
+
"""
|
| 974 |
+
Molecular weight
|
| 975 |
+
|
| 976 |
+
-----------------------------------------------------------------------
|
| 977 |
+
Returns:
|
| 978 |
+
(autode.values.Mass):
|
| 979 |
+
"""
|
| 980 |
+
if self.n_atoms == 0:
|
| 981 |
+
return Mass(0.0)
|
| 982 |
+
|
| 983 |
+
return sum(atom.mass for atom in self.atoms) # type: ignore
|
| 984 |
+
|
| 985 |
+
def distance(self, i: int, j: int) -> Distance:
|
| 986 |
+
assert self.atoms is not None, "Must have atoms"
|
| 987 |
+
return self.atoms.distance(i, j)
|
| 988 |
+
|
| 989 |
+
def eqm_bond_distance(self, i: int, j: int) -> Distance:
|
| 990 |
+
assert self.atoms is not None, "Must have atoms"
|
| 991 |
+
return self.atoms.eqm_bond_distance(i, j)
|
| 992 |
+
|
| 993 |
+
def angle(self, i: int, j: int, k: int) -> Angle:
|
| 994 |
+
r"""
|
| 995 |
+
Angle between three atoms i-j-k, where the atoms are indexed from
|
| 996 |
+
zero::
|
| 997 |
+
|
| 998 |
+
E_i --- E_j
|
| 999 |
+
\
|
| 1000 |
+
θ E_k
|
| 1001 |
+
|
| 1002 |
+
|
| 1003 |
+
Example:
|
| 1004 |
+
|
| 1005 |
+
.. code-block:: Python
|
| 1006 |
+
|
| 1007 |
+
>>> from autode import Atom, Molecule
|
| 1008 |
+
>>> h2o = Molecule(atoms=[Atom('H', x=-1), Atom('O'), Atom('H', x=1)])
|
| 1009 |
+
>>> h2o.angle(0, 1, 2).to('deg')
|
| 1010 |
+
Angle(180.0 °)
|
| 1011 |
+
|
| 1012 |
+
|
| 1013 |
+
-----------------------------------------------------------------------
|
| 1014 |
+
Arguments:
|
| 1015 |
+
i (int): Atom index of the left hand side in the angle
|
| 1016 |
+
j (int): --- middle
|
| 1017 |
+
k (int): --- right
|
| 1018 |
+
|
| 1019 |
+
Returns:
|
| 1020 |
+
(autode.values.Angle): Angle
|
| 1021 |
+
|
| 1022 |
+
Raises:
|
| 1023 |
+
(ValueError): If any of the atom indexes are not present
|
| 1024 |
+
"""
|
| 1025 |
+
assert self.atoms is not None, "Must have atoms"
|
| 1026 |
+
|
| 1027 |
+
if not self.atoms.idxs_are_present(i, j, k):
|
| 1028 |
+
raise ValueError(
|
| 1029 |
+
f"Cannot calculate the angle between {i}-{j}-{k}."
|
| 1030 |
+
f" At least one atom not present"
|
| 1031 |
+
)
|
| 1032 |
+
|
| 1033 |
+
vec1 = self.atoms[i].coord - self.atoms[j].coord
|
| 1034 |
+
vec2 = self.atoms[k].coord - self.atoms[j].coord
|
| 1035 |
+
|
| 1036 |
+
norms = np.linalg.norm(vec1) * np.linalg.norm(vec2)
|
| 1037 |
+
|
| 1038 |
+
if np.isclose(norms, 0.0):
|
| 1039 |
+
raise ValueError(
|
| 1040 |
+
f"Cannot calculate the angle {i}-{j}-{k} - at "
|
| 1041 |
+
f"least one zero vector"
|
| 1042 |
+
)
|
| 1043 |
+
|
| 1044 |
+
# Cos(theta) must lie within [-1, 1]
|
| 1045 |
+
cos_value = np.clip(np.dot(vec1, vec2) / norms, a_min=-1, a_max=1)
|
| 1046 |
+
|
| 1047 |
+
return Angle(np.arccos(cos_value))
|
| 1048 |
+
|
| 1049 |
+
def dihedral(self, w: int, x: int, y: int, z: int) -> Angle:
|
| 1050 |
+
r"""
|
| 1051 |
+
Dihedral angle between four atoms (x, y, z, w), where the atoms are
|
| 1052 |
+
indexed from zero::
|
| 1053 |
+
|
| 1054 |
+
E_w --- E_x
|
| 1055 |
+
\ φ
|
| 1056 |
+
\
|
| 1057 |
+
E_y ---- E_z
|
| 1058 |
+
|
| 1059 |
+
Example:
|
| 1060 |
+
|
| 1061 |
+
.. code-block:: Python
|
| 1062 |
+
|
| 1063 |
+
>>> from autode import Atom, Molecule
|
| 1064 |
+
>>> h2s2 = Molecule(atoms=[Atom('S', 0.1527, 0.9668, -0.9288),
|
| 1065 |
+
... Atom('S', 2.0024, 0.0443, -0.4227),
|
| 1066 |
+
... Atom('H', -0.5802, 0.0234, -0.1850),
|
| 1067 |
+
... Atom('H', 2.1446, 0.8424, 0.7276)])
|
| 1068 |
+
>>> h2s2.dihedral(2, 0, 1, 3).to('deg')
|
| 1069 |
+
Angle(-90.0 °)
|
| 1070 |
+
|
| 1071 |
+
-----------------------------------------------------------------------
|
| 1072 |
+
Arguments:
|
| 1073 |
+
w (int): Atom index of the first atom in the dihedral
|
| 1074 |
+
x (int): -- second --
|
| 1075 |
+
y (int): -- third --
|
| 1076 |
+
z (int): -- fourth --
|
| 1077 |
+
|
| 1078 |
+
Returns:
|
| 1079 |
+
(autode.values.Angle): Dihedral angle
|
| 1080 |
+
|
| 1081 |
+
Raises:
|
| 1082 |
+
(ValueError): If any of the atom indexes are not present in the
|
| 1083 |
+
molecule
|
| 1084 |
+
"""
|
| 1085 |
+
assert self.atoms is not None, "Must have atoms"
|
| 1086 |
+
|
| 1087 |
+
if not self.atoms.idxs_are_present(w, x, y, z):
|
| 1088 |
+
raise ValueError(
|
| 1089 |
+
f"Cannot calculate the dihedral angle involving "
|
| 1090 |
+
f"atoms {z}-{w}-{x}-{y}. At least one atom not "
|
| 1091 |
+
f"present"
|
| 1092 |
+
)
|
| 1093 |
+
|
| 1094 |
+
vec_xw = self.atoms[w].coord - self.atoms[x].coord
|
| 1095 |
+
vec_yz = self.atoms[z].coord - self.atoms[y].coord
|
| 1096 |
+
vec_xy = self.atoms[y].coord - self.atoms[x].coord
|
| 1097 |
+
|
| 1098 |
+
vec1, vec2 = np.cross(vec_xw, vec_xy), np.cross(-vec_xy, vec_yz)
|
| 1099 |
+
|
| 1100 |
+
# Normalise and ensure no zero vectors, for which the dihedral is not
|
| 1101 |
+
# defined
|
| 1102 |
+
for vec in (vec1, vec2, vec_xy):
|
| 1103 |
+
norm = np.linalg.norm(vec)
|
| 1104 |
+
|
| 1105 |
+
if np.isclose(norm, 0.0):
|
| 1106 |
+
raise ValueError(
|
| 1107 |
+
f"Cannot calculate the dihedral angle "
|
| 1108 |
+
f"{z}-{w}-{x}-{y} - one zero vector"
|
| 1109 |
+
)
|
| 1110 |
+
vec /= norm
|
| 1111 |
+
|
| 1112 |
+
"""
|
| 1113 |
+
Dihedral angles are defined as from the IUPAC gold book: "the torsion
|
| 1114 |
+
angle between groups A and D is then considered to be positive if
|
| 1115 |
+
the bond A-B is rotated in a clockwise direction through less than
|
| 1116 |
+
180 degrees"
|
| 1117 |
+
"""
|
| 1118 |
+
value = -np.arctan2(
|
| 1119 |
+
np.dot(np.cross(vec1, vec_xy), vec2), np.dot(vec1, vec2)
|
| 1120 |
+
)
|
| 1121 |
+
|
| 1122 |
+
return Angle(value)
|
| 1123 |
+
|
| 1124 |
+
# --- Method aliases ---
|
| 1125 |
+
centre_of_mass = com
|
| 1126 |
+
moment_of_inertia = moi
|
| 1127 |
+
mass = weight
|
| 1128 |
+
|
| 1129 |
+
|
| 1130 |
+
elements = [
|
| 1131 |
+
"H",
|
| 1132 |
+
"He",
|
| 1133 |
+
"Li",
|
| 1134 |
+
"Be",
|
| 1135 |
+
"B",
|
| 1136 |
+
"C",
|
| 1137 |
+
"N",
|
| 1138 |
+
"O",
|
| 1139 |
+
"F",
|
| 1140 |
+
"Ne",
|
| 1141 |
+
"Na",
|
| 1142 |
+
"Mg",
|
| 1143 |
+
"Al",
|
| 1144 |
+
"Si",
|
| 1145 |
+
"P",
|
| 1146 |
+
"S",
|
| 1147 |
+
"Cl",
|
| 1148 |
+
"Ar",
|
| 1149 |
+
"K",
|
| 1150 |
+
"Ca",
|
| 1151 |
+
"Sc",
|
| 1152 |
+
"Ti",
|
| 1153 |
+
"V",
|
| 1154 |
+
"Cr",
|
| 1155 |
+
"Mn",
|
| 1156 |
+
"Fe",
|
| 1157 |
+
"Co",
|
| 1158 |
+
"Ni",
|
| 1159 |
+
"Cu",
|
| 1160 |
+
"Zn",
|
| 1161 |
+
"Ga",
|
| 1162 |
+
"Ge",
|
| 1163 |
+
"As",
|
| 1164 |
+
"Se",
|
| 1165 |
+
"Br",
|
| 1166 |
+
"Kr",
|
| 1167 |
+
"Rb",
|
| 1168 |
+
"Sr",
|
| 1169 |
+
"Y",
|
| 1170 |
+
"Zr",
|
| 1171 |
+
"Nb",
|
| 1172 |
+
"Mo",
|
| 1173 |
+
"Tc",
|
| 1174 |
+
"Ru",
|
| 1175 |
+
"Rh",
|
| 1176 |
+
"Pd",
|
| 1177 |
+
"Ag",
|
| 1178 |
+
"Cd",
|
| 1179 |
+
"In",
|
| 1180 |
+
"Sn",
|
| 1181 |
+
"Sb",
|
| 1182 |
+
"Te",
|
| 1183 |
+
"I",
|
| 1184 |
+
"Xe",
|
| 1185 |
+
"Cs",
|
| 1186 |
+
"Ba",
|
| 1187 |
+
"La",
|
| 1188 |
+
"Ce",
|
| 1189 |
+
"Pr",
|
| 1190 |
+
"Nd",
|
| 1191 |
+
"Pm",
|
| 1192 |
+
"Sm",
|
| 1193 |
+
"Eu",
|
| 1194 |
+
"Gd",
|
| 1195 |
+
"Tb",
|
| 1196 |
+
"Dy",
|
| 1197 |
+
"Ho",
|
| 1198 |
+
"Er",
|
| 1199 |
+
"Tm",
|
| 1200 |
+
"Yb",
|
| 1201 |
+
"Lu",
|
| 1202 |
+
"Hf",
|
| 1203 |
+
"Ta",
|
| 1204 |
+
"W",
|
| 1205 |
+
"Re",
|
| 1206 |
+
"Os",
|
| 1207 |
+
"Ir",
|
| 1208 |
+
"Pt",
|
| 1209 |
+
"Au",
|
| 1210 |
+
"Hg",
|
| 1211 |
+
"Tl",
|
| 1212 |
+
"Pb",
|
| 1213 |
+
"Bi",
|
| 1214 |
+
"Po",
|
| 1215 |
+
"At",
|
| 1216 |
+
"Rn",
|
| 1217 |
+
"Fr",
|
| 1218 |
+
"Ra",
|
| 1219 |
+
"Ac",
|
| 1220 |
+
"Th",
|
| 1221 |
+
"Pa",
|
| 1222 |
+
"U",
|
| 1223 |
+
"Np",
|
| 1224 |
+
"Pu",
|
| 1225 |
+
"Am",
|
| 1226 |
+
"Cm",
|
| 1227 |
+
"Bk",
|
| 1228 |
+
"Cf",
|
| 1229 |
+
"Es",
|
| 1230 |
+
"Fm",
|
| 1231 |
+
"Md",
|
| 1232 |
+
"No",
|
| 1233 |
+
"Lr",
|
| 1234 |
+
"Rf",
|
| 1235 |
+
"Db",
|
| 1236 |
+
"Sg",
|
| 1237 |
+
"Bh",
|
| 1238 |
+
"Hs",
|
| 1239 |
+
"Mt",
|
| 1240 |
+
"Ds",
|
| 1241 |
+
"Rg",
|
| 1242 |
+
"Cn",
|
| 1243 |
+
"Nh",
|
| 1244 |
+
"Fl",
|
| 1245 |
+
"Mc",
|
| 1246 |
+
"Lv",
|
| 1247 |
+
"Ts",
|
| 1248 |
+
"Og",
|
| 1249 |
+
]
|
| 1250 |
+
|
| 1251 |
+
|
| 1252 |
+
class PeriodicTable:
|
| 1253 |
+
# fmt: off
|
| 1254 |
+
table = np.array(
|
| 1255 |
+
[['H', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'He'],
|
| 1256 |
+
['Li', 'Be', '', '', '', '', '', '', '', '', '', '', 'B', 'C', 'N', 'O', 'F', 'Ne'],
|
| 1257 |
+
['Na', 'Mg', '', '', '', '', '', '', '', '', '', '', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar'],
|
| 1258 |
+
['K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr'],
|
| 1259 |
+
['Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe'],
|
| 1260 |
+
['Cs', 'Ba', '', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn'],
|
| 1261 |
+
['Fr', 'Ra', '', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Nh', 'Fl', 'Mc', 'Lv', 'Ts', 'Og']],
|
| 1262 |
+
dtype=str
|
| 1263 |
+
)
|
| 1264 |
+
# fmt: on
|
| 1265 |
+
|
| 1266 |
+
@classmethod
|
| 1267 |
+
def period(cls, n: int):
|
| 1268 |
+
"""
|
| 1269 |
+
Period of the periodic table, with 1 being the first period
|
| 1270 |
+
|
| 1271 |
+
-----------------------------------------------------------------------
|
| 1272 |
+
Arguments:
|
| 1273 |
+
n (int):
|
| 1274 |
+
|
| 1275 |
+
Returns:
|
| 1276 |
+
(np.ndarray(str)):
|
| 1277 |
+
|
| 1278 |
+
Raises:
|
| 1279 |
+
(ValueError): If n is not valid period index
|
| 1280 |
+
"""
|
| 1281 |
+
if n < 1 or n > 7:
|
| 1282 |
+
raise ValueError("Not a valid period. Must be 1-7")
|
| 1283 |
+
|
| 1284 |
+
# Exclude the empty strings of non-present elements
|
| 1285 |
+
return np.array([elem for elem in cls.table[n - 1, :] if elem != ""])
|
| 1286 |
+
|
| 1287 |
+
@classmethod
|
| 1288 |
+
def group(cls, n: int):
|
| 1289 |
+
"""
|
| 1290 |
+
Group of the periodic table, with 1 being the first period
|
| 1291 |
+
|
| 1292 |
+
-----------------------------------------------------------------------
|
| 1293 |
+
Arguments:
|
| 1294 |
+
n (int):
|
| 1295 |
+
|
| 1296 |
+
Returns:
|
| 1297 |
+
(np.ndarray(str)):
|
| 1298 |
+
|
| 1299 |
+
Raises:
|
| 1300 |
+
(ValueError): If n is not valid group index
|
| 1301 |
+
"""
|
| 1302 |
+
if n < 1 or n > 18:
|
| 1303 |
+
raise ValueError("Not a valid group. Must be 1-18")
|
| 1304 |
+
|
| 1305 |
+
# Exclude the empty strings of non-present elements
|
| 1306 |
+
return np.array([elem for elem in cls.table[:, n - 1] if elem != ""])
|
| 1307 |
+
|
| 1308 |
+
@classmethod
|
| 1309 |
+
def element(cls, period: int, group: int):
|
| 1310 |
+
"""
|
| 1311 |
+
Element given it's index in the periodic table, excluding
|
| 1312 |
+
lanthanides and actinides.
|
| 1313 |
+
|
| 1314 |
+
-----------------------------------------------------------------------
|
| 1315 |
+
Arguments:
|
| 1316 |
+
period (int):
|
| 1317 |
+
|
| 1318 |
+
group (int):
|
| 1319 |
+
|
| 1320 |
+
Returns:
|
| 1321 |
+
(str): Atomic symbol of the element
|
| 1322 |
+
|
| 1323 |
+
Raises:
|
| 1324 |
+
(IndexError): If such an element does not exist
|
| 1325 |
+
"""
|
| 1326 |
+
try:
|
| 1327 |
+
elem = cls.table[
|
| 1328 |
+
period - 1, group - 1
|
| 1329 |
+
] # Convert from 1 -> 0 indexing
|
| 1330 |
+
assert elem != ""
|
| 1331 |
+
|
| 1332 |
+
except (IndexError, AssertionError):
|
| 1333 |
+
raise IndexError("Index of the element not found")
|
| 1334 |
+
|
| 1335 |
+
return elem
|
| 1336 |
+
|
| 1337 |
+
@classmethod
|
| 1338 |
+
def transition_metals(cls, row: int):
|
| 1339 |
+
"""
|
| 1340 |
+
Collection of transition metals (TMs) of a defined row. e.g.
|
| 1341 |
+
|
| 1342 |
+
row = 1 -> [Sc, Ti .. Zn]
|
| 1343 |
+
|
| 1344 |
+
-----------------------------------------------------------------------
|
| 1345 |
+
Arguments:
|
| 1346 |
+
row (int): Colloquial name for TMs period
|
| 1347 |
+
|
| 1348 |
+
Returns:
|
| 1349 |
+
(np.ndarray(str)):
|
| 1350 |
+
|
| 1351 |
+
Raises:
|
| 1352 |
+
(ValueError): If the row is not valid
|
| 1353 |
+
"""
|
| 1354 |
+
if row < 1 or row > 3:
|
| 1355 |
+
raise ValueError("Not a valid row of TMs. Must be 1-3")
|
| 1356 |
+
|
| 1357 |
+
tms = [elem for elem in cls.period(row + 3) if elem in metals]
|
| 1358 |
+
return np.array(tms, dtype=str)
|
| 1359 |
+
|
| 1360 |
+
lanthanoids = lanthanides = np.array(
|
| 1361 |
+
[
|
| 1362 |
+
"La",
|
| 1363 |
+
"Ce",
|
| 1364 |
+
"Pr",
|
| 1365 |
+
"Nd",
|
| 1366 |
+
"Pm",
|
| 1367 |
+
"Sm",
|
| 1368 |
+
"Eu",
|
| 1369 |
+
"Gd",
|
| 1370 |
+
"Tb",
|
| 1371 |
+
"Dy",
|
| 1372 |
+
"Ho",
|
| 1373 |
+
"Er",
|
| 1374 |
+
"Tm",
|
| 1375 |
+
"Yb",
|
| 1376 |
+
"Lu",
|
| 1377 |
+
],
|
| 1378 |
+
dtype=str,
|
| 1379 |
+
)
|
| 1380 |
+
actinoids = actinides = np.array(
|
| 1381 |
+
[
|
| 1382 |
+
"Ac",
|
| 1383 |
+
"Th",
|
| 1384 |
+
"Pa",
|
| 1385 |
+
"U",
|
| 1386 |
+
"Np",
|
| 1387 |
+
"Pu",
|
| 1388 |
+
"Am",
|
| 1389 |
+
"Cm",
|
| 1390 |
+
"Bk",
|
| 1391 |
+
"Cf",
|
| 1392 |
+
"Es",
|
| 1393 |
+
"Fm",
|
| 1394 |
+
"Md",
|
| 1395 |
+
"No",
|
| 1396 |
+
"Lr",
|
| 1397 |
+
],
|
| 1398 |
+
dtype=str,
|
| 1399 |
+
)
|
| 1400 |
+
|
| 1401 |
+
|
| 1402 |
+
# A set of reasonable valances for anionic/neutral/cationic atoms
|
| 1403 |
+
valid_valances = {
|
| 1404 |
+
"H": [0, 1],
|
| 1405 |
+
"B": [3, 4],
|
| 1406 |
+
"C": [2, 3, 4],
|
| 1407 |
+
"N": [2, 3, 4],
|
| 1408 |
+
"O": [1, 2, 3],
|
| 1409 |
+
"F": [0, 1],
|
| 1410 |
+
"Si": [2, 3, 4],
|
| 1411 |
+
"P": [2, 3, 4, 5, 6],
|
| 1412 |
+
"S": [2, 3, 4, 5, 6],
|
| 1413 |
+
"Cl": [0, 1, 2, 3, 4],
|
| 1414 |
+
"Br": [0, 1, 2, 3, 4],
|
| 1415 |
+
"I": [0, 1, 2, 3, 4, 5, 6],
|
| 1416 |
+
"Rh": [0, 1, 2, 3, 4, 5, 6],
|
| 1417 |
+
}
|
| 1418 |
+
|
| 1419 |
+
# Atomic weights in amu from:
|
| 1420 |
+
# IUPAC-CIAWW's Atomic weights of the elements: Review 2000
|
| 1421 |
+
atomic_weights = {
|
| 1422 |
+
"H": 1.00794,
|
| 1423 |
+
"He": 4.002602,
|
| 1424 |
+
"Li": 6.941,
|
| 1425 |
+
"Be": 9.012182,
|
| 1426 |
+
"B": 10.811,
|
| 1427 |
+
"C": 12.0107,
|
| 1428 |
+
"N": 14.0067,
|
| 1429 |
+
"O": 15.9994,
|
| 1430 |
+
"F": 18.9984032,
|
| 1431 |
+
"Ne": 2.01797,
|
| 1432 |
+
"Na": 22.989770,
|
| 1433 |
+
"Mg": 24.3050,
|
| 1434 |
+
"Al": 26.981538,
|
| 1435 |
+
"Si": 28.0855,
|
| 1436 |
+
"P": 30.973761,
|
| 1437 |
+
"S": 32.065,
|
| 1438 |
+
"Cl": 35.453,
|
| 1439 |
+
"Ar": 39.948,
|
| 1440 |
+
"K": 39.0983,
|
| 1441 |
+
"Ca": 40.078,
|
| 1442 |
+
"Sc": 44.955910,
|
| 1443 |
+
"Ti": 47.867,
|
| 1444 |
+
"V": 50.9415,
|
| 1445 |
+
"Cr": 51.9961,
|
| 1446 |
+
"Mn": 54.938049,
|
| 1447 |
+
"Fe": 55.845,
|
| 1448 |
+
"Co": 58.933200,
|
| 1449 |
+
"Ni": 58.6934,
|
| 1450 |
+
"Cu": 63.546,
|
| 1451 |
+
"Zn": 65.409,
|
| 1452 |
+
"Ga": 69.723,
|
| 1453 |
+
"Ge": 72.64,
|
| 1454 |
+
"As": 74.92160,
|
| 1455 |
+
"Se": 78.96,
|
| 1456 |
+
"Br": 79.904,
|
| 1457 |
+
"Kr": 83.798,
|
| 1458 |
+
"Rb": 85.4678,
|
| 1459 |
+
"Sr": 87.62,
|
| 1460 |
+
"Y": 88.90585,
|
| 1461 |
+
"Zr": 91.224,
|
| 1462 |
+
"Nb": 92.90638,
|
| 1463 |
+
"Mo": 95.94,
|
| 1464 |
+
"Ru": 101.07,
|
| 1465 |
+
"Rh": 102.90550,
|
| 1466 |
+
"Pd": 106.42,
|
| 1467 |
+
"Ag": 107.8682,
|
| 1468 |
+
"Cd": 112.411,
|
| 1469 |
+
"In": 114.818,
|
| 1470 |
+
"Sn": 118.710,
|
| 1471 |
+
"Sb": 121.760,
|
| 1472 |
+
"Te": 127.60,
|
| 1473 |
+
"I": 126.90447,
|
| 1474 |
+
"Xe": 131.293,
|
| 1475 |
+
"Cs": 132.90545,
|
| 1476 |
+
"Ba": 137.327,
|
| 1477 |
+
"La": 138.9055,
|
| 1478 |
+
"Ce": 140.116,
|
| 1479 |
+
"Pr": 140.90765,
|
| 1480 |
+
"Nd": 144.24,
|
| 1481 |
+
"Sm": 150.36,
|
| 1482 |
+
"Eu": 151.964,
|
| 1483 |
+
"Gd": 157.25,
|
| 1484 |
+
"Tb": 158.92534,
|
| 1485 |
+
"Dy": 162.500,
|
| 1486 |
+
"Ho": 164.93032,
|
| 1487 |
+
"Er": 167.259,
|
| 1488 |
+
"Tm": 168.93421,
|
| 1489 |
+
"Yb": 173.04,
|
| 1490 |
+
"Lu": 174.967,
|
| 1491 |
+
"Hf": 178.49,
|
| 1492 |
+
"Ta": 180.9479,
|
| 1493 |
+
"W": 183.84,
|
| 1494 |
+
"Re": 186.207,
|
| 1495 |
+
"Os": 190.23,
|
| 1496 |
+
"Ir": 192.217,
|
| 1497 |
+
"Pt": 195.078,
|
| 1498 |
+
"Au": 196.96655,
|
| 1499 |
+
"Hg": 200.59,
|
| 1500 |
+
"Tl": 204.3833,
|
| 1501 |
+
"Pb": 207.2,
|
| 1502 |
+
"Bi": 208.98038,
|
| 1503 |
+
"Th": 232.0381,
|
| 1504 |
+
"Pa": 231.03588,
|
| 1505 |
+
"U": 238.02891,
|
| 1506 |
+
# Remainder from https://ciaaw.org/atomic-masses.htm
|
| 1507 |
+
"Np": 237.0,
|
| 1508 |
+
"Pu": 244.0,
|
| 1509 |
+
"Am": 243.0,
|
| 1510 |
+
"Cm": 247.0,
|
| 1511 |
+
"Bk": 247.0,
|
| 1512 |
+
"Cf": 251.0,
|
| 1513 |
+
"Es": 252.0,
|
| 1514 |
+
"Fm": 257.0,
|
| 1515 |
+
"Md": 258.0,
|
| 1516 |
+
"No": 259.0,
|
| 1517 |
+
"Lr": 262.0,
|
| 1518 |
+
"Rf": 267.0,
|
| 1519 |
+
"Db": 268.0,
|
| 1520 |
+
"Sg": 271.0,
|
| 1521 |
+
"Bh": 274.0,
|
| 1522 |
+
"Hs": 269.0,
|
| 1523 |
+
"Mt": 276.0,
|
| 1524 |
+
"Ds": 281.0,
|
| 1525 |
+
"Rg": 281.0,
|
| 1526 |
+
"Cn": 285.0,
|
| 1527 |
+
"Nh": 286.0,
|
| 1528 |
+
"Fl": 289.0,
|
| 1529 |
+
"Mc": 288.0,
|
| 1530 |
+
"Lv": 293.0,
|
| 1531 |
+
"Ts": 294.0,
|
| 1532 |
+
"Og": 294.0,
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
# van der Walls radii from https://books.google.no/books?id=bNDMBQAAQBAJ
|
| 1536 |
+
vdw_radii = {
|
| 1537 |
+
"H": 1.1,
|
| 1538 |
+
"He": 1.4,
|
| 1539 |
+
"Li": 1.82,
|
| 1540 |
+
"Be": 1.53,
|
| 1541 |
+
"B": 1.92,
|
| 1542 |
+
"C": 1.7,
|
| 1543 |
+
"N": 1.55,
|
| 1544 |
+
"O": 1.52,
|
| 1545 |
+
"F": 1.47,
|
| 1546 |
+
"Ne": 1.54,
|
| 1547 |
+
"Na": 2.27,
|
| 1548 |
+
"Mg": 1.73,
|
| 1549 |
+
"Al": 1.84,
|
| 1550 |
+
"Si": 2.1,
|
| 1551 |
+
"P": 1.8,
|
| 1552 |
+
"S": 1.8,
|
| 1553 |
+
"Cl": 1.75,
|
| 1554 |
+
"Ar": 1.88,
|
| 1555 |
+
"K": 2.75,
|
| 1556 |
+
"Ca": 2.31,
|
| 1557 |
+
"Sc": 2.15,
|
| 1558 |
+
"Ti": 2.11,
|
| 1559 |
+
"V": 2.07,
|
| 1560 |
+
"Cr": 2.06,
|
| 1561 |
+
"Mn": 2.05,
|
| 1562 |
+
"Fe": 2.04,
|
| 1563 |
+
"Co": 2.0,
|
| 1564 |
+
"Ni": 1.97,
|
| 1565 |
+
"Cu": 1.96,
|
| 1566 |
+
"Zn": 2.01,
|
| 1567 |
+
"Ga": 1.87,
|
| 1568 |
+
"Ge": 2.11,
|
| 1569 |
+
"As": 1.85,
|
| 1570 |
+
"Se": 1.9,
|
| 1571 |
+
"Br": 1.85,
|
| 1572 |
+
"Kr": 2.02,
|
| 1573 |
+
"Rb": 3.03,
|
| 1574 |
+
"Sr": 2.49,
|
| 1575 |
+
"Y": 2.32,
|
| 1576 |
+
"Zr": 2.23,
|
| 1577 |
+
"Nb": 2.18,
|
| 1578 |
+
"Mo": 2.17,
|
| 1579 |
+
"Tc": 2.16,
|
| 1580 |
+
"Ru": 2.13,
|
| 1581 |
+
"Rh": 2.1,
|
| 1582 |
+
"Pd": 2.1,
|
| 1583 |
+
"Ag": 2.11,
|
| 1584 |
+
"Cd": 2.18,
|
| 1585 |
+
"In": 1.93,
|
| 1586 |
+
"Sn": 2.17,
|
| 1587 |
+
"Sb": 2.06,
|
| 1588 |
+
"Te": 2.06,
|
| 1589 |
+
"I": 1.98,
|
| 1590 |
+
"Xe": 2.16,
|
| 1591 |
+
"Cs": 3.43,
|
| 1592 |
+
"Ba": 2.68,
|
| 1593 |
+
"La": 2.43,
|
| 1594 |
+
"Ce": 2.42,
|
| 1595 |
+
"Pr": 2.4,
|
| 1596 |
+
"Nd": 2.39,
|
| 1597 |
+
"Pm": 2.38,
|
| 1598 |
+
"Sm": 2.36,
|
| 1599 |
+
"Eu": 2.35,
|
| 1600 |
+
"Gd": 2.34,
|
| 1601 |
+
"Tb": 2.33,
|
| 1602 |
+
"Dy": 2.31,
|
| 1603 |
+
"Ho": 2.3,
|
| 1604 |
+
"Er": 2.29,
|
| 1605 |
+
"Tm": 2.27,
|
| 1606 |
+
"Yb": 2.26,
|
| 1607 |
+
"Lu": 2.24,
|
| 1608 |
+
"Hf": 2.23,
|
| 1609 |
+
"Ta": 2.22,
|
| 1610 |
+
"W": 2.18,
|
| 1611 |
+
"Re": 2.16,
|
| 1612 |
+
"Os": 2.16,
|
| 1613 |
+
"Ir": 2.13,
|
| 1614 |
+
"Pt": 2.13,
|
| 1615 |
+
"Au": 2.14,
|
| 1616 |
+
"Hg": 2.23,
|
| 1617 |
+
"Tl": 1.96,
|
| 1618 |
+
"Pb": 2.02,
|
| 1619 |
+
"Bi": 2.07,
|
| 1620 |
+
"Po": 1.97,
|
| 1621 |
+
"At": 2.02,
|
| 1622 |
+
"Rn": 2.2,
|
| 1623 |
+
"Fr": 3.48,
|
| 1624 |
+
"Ra": 2.83,
|
| 1625 |
+
"Ac": 2.47,
|
| 1626 |
+
"Th": 2.45,
|
| 1627 |
+
"Pa": 2.43,
|
| 1628 |
+
"U": 2.41,
|
| 1629 |
+
"Np": 2.39,
|
| 1630 |
+
"Pu": 2.43,
|
| 1631 |
+
"Am": 2.44,
|
| 1632 |
+
"Cm": 2.45,
|
| 1633 |
+
"Bk": 2.44,
|
| 1634 |
+
"Cf": 2.45,
|
| 1635 |
+
"Es": 2.45,
|
| 1636 |
+
"Fm": 2.45,
|
| 1637 |
+
"Md": 2.46,
|
| 1638 |
+
"No": 2.46,
|
| 1639 |
+
"Lr": 2.46,
|
| 1640 |
+
}
|
| 1641 |
+
|
| 1642 |
+
"""
|
| 1643 |
+
Although a π-bond may not be well defined, it is useful to have a notion of
|
| 1644 |
+
a bond about which there is restricted rotation. The below sets are used to
|
| 1645 |
+
define which atoms may be π-bonded to another
|
| 1646 |
+
"""
|
| 1647 |
+
non_pi_elements = ["H", "He"]
|
| 1648 |
+
pi_valencies = {
|
| 1649 |
+
"B": [1, 2],
|
| 1650 |
+
"N": [1, 2],
|
| 1651 |
+
"O": [1],
|
| 1652 |
+
"C": [1, 2, 3],
|
| 1653 |
+
"P": [1, 2, 3, 4],
|
| 1654 |
+
"S": [1, 3, 4, 5],
|
| 1655 |
+
"Si": [1, 2, 3],
|
| 1656 |
+
}
|
| 1657 |
+
|
| 1658 |
+
# Standard definition of metallic elements: https://en.wikipedia.org/wiki/Metal
|
| 1659 |
+
# (all semi-metals not included)
|
| 1660 |
+
metals = [
|
| 1661 |
+
"Li",
|
| 1662 |
+
"Be",
|
| 1663 |
+
"Na",
|
| 1664 |
+
"Mg",
|
| 1665 |
+
"Al",
|
| 1666 |
+
"K",
|
| 1667 |
+
"Ca",
|
| 1668 |
+
"Sc",
|
| 1669 |
+
"Ti",
|
| 1670 |
+
"V",
|
| 1671 |
+
"Cr",
|
| 1672 |
+
"Mn",
|
| 1673 |
+
"Fe",
|
| 1674 |
+
"Co",
|
| 1675 |
+
"Ni",
|
| 1676 |
+
"Cu",
|
| 1677 |
+
"Zn",
|
| 1678 |
+
"Ga",
|
| 1679 |
+
"Rb",
|
| 1680 |
+
"Sr",
|
| 1681 |
+
"Y",
|
| 1682 |
+
"Zr",
|
| 1683 |
+
"Nb",
|
| 1684 |
+
"Mo",
|
| 1685 |
+
"Tc",
|
| 1686 |
+
"Ru",
|
| 1687 |
+
"Rh",
|
| 1688 |
+
"Pd",
|
| 1689 |
+
"Ag",
|
| 1690 |
+
"Cd",
|
| 1691 |
+
"In",
|
| 1692 |
+
"Sn",
|
| 1693 |
+
"Cs",
|
| 1694 |
+
"Ba",
|
| 1695 |
+
"La",
|
| 1696 |
+
"Ce",
|
| 1697 |
+
"Pr",
|
| 1698 |
+
"Nd",
|
| 1699 |
+
"Pm",
|
| 1700 |
+
"Sm",
|
| 1701 |
+
"Eu",
|
| 1702 |
+
"Gd",
|
| 1703 |
+
"Tb",
|
| 1704 |
+
"Dy",
|
| 1705 |
+
"Ho",
|
| 1706 |
+
"Er",
|
| 1707 |
+
"Tm",
|
| 1708 |
+
"Yb",
|
| 1709 |
+
"Lu",
|
| 1710 |
+
"Hf",
|
| 1711 |
+
"Ta",
|
| 1712 |
+
"W",
|
| 1713 |
+
"Re",
|
| 1714 |
+
"Os",
|
| 1715 |
+
"Ir",
|
| 1716 |
+
"Pt",
|
| 1717 |
+
"Au",
|
| 1718 |
+
"Hg",
|
| 1719 |
+
"Tl",
|
| 1720 |
+
"Pb",
|
| 1721 |
+
"Bi",
|
| 1722 |
+
"Po",
|
| 1723 |
+
"Fr",
|
| 1724 |
+
"Ra",
|
| 1725 |
+
"Ac",
|
| 1726 |
+
"Th",
|
| 1727 |
+
"Pa",
|
| 1728 |
+
"U",
|
| 1729 |
+
"Np",
|
| 1730 |
+
"Pu",
|
| 1731 |
+
"Am",
|
| 1732 |
+
"Cm",
|
| 1733 |
+
"Bk",
|
| 1734 |
+
"Cf",
|
| 1735 |
+
"Es",
|
| 1736 |
+
"Fm",
|
| 1737 |
+
"Md",
|
| 1738 |
+
"No",
|
| 1739 |
+
"Lr",
|
| 1740 |
+
"Rf",
|
| 1741 |
+
"Db",
|
| 1742 |
+
"Sg",
|
| 1743 |
+
"Bh",
|
| 1744 |
+
"Hs",
|
| 1745 |
+
"Mt",
|
| 1746 |
+
"Ds",
|
| 1747 |
+
"Rg",
|
| 1748 |
+
"Cn",
|
| 1749 |
+
"Nh",
|
| 1750 |
+
"Fl",
|
| 1751 |
+
"Mc",
|
| 1752 |
+
"Lv",
|
| 1753 |
+
]
|
| 1754 |
+
|
| 1755 |
+
# Covalent radii in picometers from https://en.wikipedia.org/wiki/Covalent_radius
|
| 1756 |
+
_covalent_radii_pm = [
|
| 1757 |
+
31.0,
|
| 1758 |
+
28.0,
|
| 1759 |
+
128.0,
|
| 1760 |
+
96.0,
|
| 1761 |
+
84.0,
|
| 1762 |
+
76.0,
|
| 1763 |
+
71.0,
|
| 1764 |
+
66.0,
|
| 1765 |
+
57.0,
|
| 1766 |
+
58.0,
|
| 1767 |
+
166.0,
|
| 1768 |
+
141.0,
|
| 1769 |
+
121.0,
|
| 1770 |
+
111.0,
|
| 1771 |
+
107.0,
|
| 1772 |
+
105.0,
|
| 1773 |
+
102.0,
|
| 1774 |
+
106.0,
|
| 1775 |
+
102.0,
|
| 1776 |
+
203.0,
|
| 1777 |
+
176.0,
|
| 1778 |
+
170.0,
|
| 1779 |
+
160.0,
|
| 1780 |
+
153.0,
|
| 1781 |
+
139.0,
|
| 1782 |
+
161.0,
|
| 1783 |
+
152.0,
|
| 1784 |
+
150.0,
|
| 1785 |
+
124.0,
|
| 1786 |
+
132.0,
|
| 1787 |
+
122.0,
|
| 1788 |
+
122.0,
|
| 1789 |
+
120.0,
|
| 1790 |
+
119.0,
|
| 1791 |
+
120.0,
|
| 1792 |
+
116.0,
|
| 1793 |
+
220.0,
|
| 1794 |
+
195.0,
|
| 1795 |
+
190.0,
|
| 1796 |
+
175.0,
|
| 1797 |
+
164.0,
|
| 1798 |
+
154.0,
|
| 1799 |
+
147.0,
|
| 1800 |
+
146.0,
|
| 1801 |
+
142.0,
|
| 1802 |
+
139.0,
|
| 1803 |
+
145.0,
|
| 1804 |
+
144.0,
|
| 1805 |
+
142.0,
|
| 1806 |
+
139.0,
|
| 1807 |
+
139.0,
|
| 1808 |
+
138.0,
|
| 1809 |
+
139.0,
|
| 1810 |
+
140.0,
|
| 1811 |
+
244.0,
|
| 1812 |
+
215.0,
|
| 1813 |
+
207.0,
|
| 1814 |
+
204.0,
|
| 1815 |
+
203.0,
|
| 1816 |
+
201.0,
|
| 1817 |
+
199.0,
|
| 1818 |
+
198.0,
|
| 1819 |
+
198.0,
|
| 1820 |
+
196.0,
|
| 1821 |
+
194.0,
|
| 1822 |
+
192.0,
|
| 1823 |
+
192.0,
|
| 1824 |
+
189.0,
|
| 1825 |
+
190.0,
|
| 1826 |
+
187.0,
|
| 1827 |
+
175.0,
|
| 1828 |
+
187.0,
|
| 1829 |
+
170.0,
|
| 1830 |
+
162.0,
|
| 1831 |
+
151.0,
|
| 1832 |
+
144.0,
|
| 1833 |
+
141.0,
|
| 1834 |
+
136.0,
|
| 1835 |
+
136.0,
|
| 1836 |
+
132.0,
|
| 1837 |
+
145.0,
|
| 1838 |
+
146.0,
|
| 1839 |
+
148.0,
|
| 1840 |
+
140.0,
|
| 1841 |
+
150.0,
|
| 1842 |
+
150.0,
|
| 1843 |
+
]
|
| 1844 |
+
|
| 1845 |
+
# Experimental bond lengths from https://cccbdb.nist.gov/diatomicexpbondx.asp
|
| 1846 |
+
_bond_lengths = {"HH": 0.741, "FF": 1.412, "ClCl": 1.988, "II": 2.665}
|
| 1847 |
+
|
| 1848 |
+
|
| 1849 |
+
_max_valances = {
|
| 1850 |
+
"H": 1,
|
| 1851 |
+
"He": 0,
|
| 1852 |
+
"B": 4,
|
| 1853 |
+
"C": 4,
|
| 1854 |
+
"N": 4,
|
| 1855 |
+
"O": 3,
|
| 1856 |
+
"F": 1,
|
| 1857 |
+
"Si": 4,
|
| 1858 |
+
"P": 6,
|
| 1859 |
+
"S": 6,
|
| 1860 |
+
"Cl": 4,
|
| 1861 |
+
"Br": 4,
|
| 1862 |
+
"I": 6,
|
| 1863 |
+
"Xe": 6,
|
| 1864 |
+
"Al": 4,
|
| 1865 |
+
}
|
autodE/source/autode/bond_rearrangement.py
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import itertools
|
| 2 |
+
import os
|
| 3 |
+
from autode.geom import get_neighbour_list
|
| 4 |
+
from autode.log import logger
|
| 5 |
+
from autode.config import Config
|
| 6 |
+
from autode.mol_graphs import (
|
| 7 |
+
get_bond_type_list,
|
| 8 |
+
get_fbonds,
|
| 9 |
+
is_isomorphic,
|
| 10 |
+
find_cycles,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def get_bond_rearrangs(reactant, product, name, save=True):
|
| 15 |
+
"""For a reactant and product (mol_complex) find the set of breaking and
|
| 16 |
+
forming bonds that will turn reactants into products. This works by
|
| 17 |
+
determining the types of bonds that have been made/broken (i.e CH) and
|
| 18 |
+
then only considering rearrangements involving those bonds.
|
| 19 |
+
|
| 20 |
+
---------------------------------------------------------------------------
|
| 21 |
+
Arguments:
|
| 22 |
+
reactant (autode.species.ReactantComplex):
|
| 23 |
+
|
| 24 |
+
product (autode.species.ProductComplex):
|
| 25 |
+
|
| 26 |
+
name (str):
|
| 27 |
+
|
| 28 |
+
Keyword Arguments:
|
| 29 |
+
save (bool): Save bond rearrangements to a file for fast reloading
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
(list(autode.bond_rearrangements.BondRearrangement)):
|
| 33 |
+
"""
|
| 34 |
+
logger.info(f"Finding the possible forming and breaking bonds for {name}")
|
| 35 |
+
|
| 36 |
+
if os.path.exists(f"{name}_bond_rearrangs.txt"):
|
| 37 |
+
return get_bond_rearrangs_from_file(f"{name}_bond_rearrangs.txt")
|
| 38 |
+
|
| 39 |
+
if is_isomorphic(reactant.graph, product.graph) and product.n_atoms > 3:
|
| 40 |
+
logger.error(
|
| 41 |
+
"Reactant (complex) is isomorphic to product (complex). "
|
| 42 |
+
"Bond rearrangement cannot be determined unless the "
|
| 43 |
+
"substrates are limited in size"
|
| 44 |
+
)
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
possible_brs = []
|
| 48 |
+
|
| 49 |
+
reac_bond_dict = get_bond_type_list(reactant.graph)
|
| 50 |
+
prod_bond_dict = get_bond_type_list(product.graph)
|
| 51 |
+
|
| 52 |
+
# list of bonds where this type of bond (e.g C-H) has less bonds in
|
| 53 |
+
# products than reactants
|
| 54 |
+
all_possible_bbonds = []
|
| 55 |
+
|
| 56 |
+
# list of bonds that can be formed of this bond type. This is only used
|
| 57 |
+
# if there is only one type of bbond, so can be overwritten for each new
|
| 58 |
+
# type of bbond
|
| 59 |
+
bbond_atom_type_fbonds = None
|
| 60 |
+
|
| 61 |
+
# list of bonds where this type of bond (e.g C-H) has more bonds in
|
| 62 |
+
# products than reactants
|
| 63 |
+
all_possible_fbonds = []
|
| 64 |
+
|
| 65 |
+
# list of bonds that can be broken of this bond type. This is only used
|
| 66 |
+
# if there is only one type of fbond, so can be overwritten for each new
|
| 67 |
+
# type of fbond
|
| 68 |
+
fbond_atom_type_bbonds = None
|
| 69 |
+
|
| 70 |
+
# list of bonds where this type of bond (e.g C-H) has the same number of
|
| 71 |
+
# bonds in products and reactants
|
| 72 |
+
possible_bbond_and_fbonds = []
|
| 73 |
+
|
| 74 |
+
for reac_key, reac_bonds in reac_bond_dict.items():
|
| 75 |
+
prod_bonds = prod_bond_dict[reac_key]
|
| 76 |
+
possible_fbonds = get_fbonds(reactant.graph, reac_key)
|
| 77 |
+
if len(prod_bonds) < len(reac_bonds):
|
| 78 |
+
all_possible_bbonds.append(reac_bonds)
|
| 79 |
+
bbond_atom_type_fbonds = possible_fbonds
|
| 80 |
+
elif len(prod_bonds) > len(reac_bonds):
|
| 81 |
+
all_possible_fbonds.append(possible_fbonds)
|
| 82 |
+
fbond_atom_type_bbonds = reac_bonds
|
| 83 |
+
else:
|
| 84 |
+
if len(reac_bonds) != 0:
|
| 85 |
+
possible_bbond_and_fbonds.append([reac_bonds, possible_fbonds])
|
| 86 |
+
|
| 87 |
+
# The change in the number of bonds is > 0 as in the reaction
|
| 88 |
+
# initialisation reacs/prods are swapped if this is < 0
|
| 89 |
+
delta_n_bonds = (
|
| 90 |
+
reactant.graph.number_of_edges() - product.graph.number_of_edges()
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
if delta_n_bonds == 0:
|
| 94 |
+
funcs = [get_fbonds_bbonds_1b1f, get_fbonds_bbonds_2b2f]
|
| 95 |
+
elif delta_n_bonds == 1:
|
| 96 |
+
funcs = [get_fbonds_bbonds_1b, get_fbonds_bbonds_2b1f]
|
| 97 |
+
elif delta_n_bonds == 2:
|
| 98 |
+
funcs = [get_fbonds_bbonds_2b]
|
| 99 |
+
else:
|
| 100 |
+
logger.error(
|
| 101 |
+
f"Cannot treat a change in bonds "
|
| 102 |
+
f"reactant <- product of {delta_n_bonds}"
|
| 103 |
+
)
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
for func in funcs:
|
| 107 |
+
possible_brs = func(
|
| 108 |
+
reactant,
|
| 109 |
+
product,
|
| 110 |
+
possible_brs,
|
| 111 |
+
all_possible_bbonds,
|
| 112 |
+
all_possible_fbonds,
|
| 113 |
+
possible_bbond_and_fbonds,
|
| 114 |
+
bbond_atom_type_fbonds,
|
| 115 |
+
fbond_atom_type_bbonds,
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
if len(possible_brs) > 0:
|
| 119 |
+
logger.info(
|
| 120 |
+
f"Found a molecular graph rearrangement to products "
|
| 121 |
+
f"with {func.__name__}"
|
| 122 |
+
)
|
| 123 |
+
# This function will return with the first bond rearrangement
|
| 124 |
+
# that leads to products
|
| 125 |
+
|
| 126 |
+
n_bond_rearrangs = len(possible_brs)
|
| 127 |
+
if n_bond_rearrangs > 1:
|
| 128 |
+
logger.info(
|
| 129 |
+
f"Multiple *{n_bond_rearrangs}* possible bond "
|
| 130 |
+
f"breaking/makings are possible"
|
| 131 |
+
)
|
| 132 |
+
possible_brs = strip_equiv_bond_rearrs(possible_brs, reactant)
|
| 133 |
+
prune_small_ring_rearrs(possible_brs, reactant)
|
| 134 |
+
|
| 135 |
+
if save:
|
| 136 |
+
save_bond_rearrangs_to_file(
|
| 137 |
+
possible_brs, filename=f"{name}_BRs.txt"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
logger.info(
|
| 141 |
+
f"Found *{len(possible_brs)}* bond "
|
| 142 |
+
f"rearrangement(s) that lead to products"
|
| 143 |
+
)
|
| 144 |
+
return possible_brs
|
| 145 |
+
|
| 146 |
+
return None
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def save_bond_rearrangs_to_file(brs, filename="bond_rearrangs.txt"):
|
| 150 |
+
"""
|
| 151 |
+
Save a list of bond rearrangements to a file in plane text
|
| 152 |
+
|
| 153 |
+
---------------------------------------------------------------------------
|
| 154 |
+
Arguments:
|
| 155 |
+
brs (list(autode.bond_rearrangements.BondRearrangement)):
|
| 156 |
+
|
| 157 |
+
filename (str):
|
| 158 |
+
"""
|
| 159 |
+
logger.info(f"Saving bond rearrangements to {filename}")
|
| 160 |
+
|
| 161 |
+
with open(filename, "w") as file:
|
| 162 |
+
for bond_rearrang in brs:
|
| 163 |
+
print("fbonds", file=file)
|
| 164 |
+
for fbond in bond_rearrang.fbonds:
|
| 165 |
+
print(*fbond, file=file)
|
| 166 |
+
print("bbonds", file=file)
|
| 167 |
+
for bbond in bond_rearrang.bbonds:
|
| 168 |
+
print(*bbond, file=file)
|
| 169 |
+
print("end", file=file)
|
| 170 |
+
|
| 171 |
+
return None
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def get_bond_rearrangs_from_file(filename="bond_rearrangs.txt"):
|
| 175 |
+
"""
|
| 176 |
+
Extract a list of bond rearrangements from a file
|
| 177 |
+
|
| 178 |
+
---------------------------------------------------------------------------
|
| 179 |
+
Keyword Arguments:
|
| 180 |
+
filename (str):
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
(list(autode.bond_rearrangements.BondRearrangement)):
|
| 184 |
+
"""
|
| 185 |
+
logger.info("Getting bond rearrangements from file")
|
| 186 |
+
|
| 187 |
+
if not os.path.exists(filename):
|
| 188 |
+
logger.error("No bond rearrangements file")
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
bond_rearrangs = []
|
| 192 |
+
|
| 193 |
+
with open(filename, "r") as br_file:
|
| 194 |
+
fbonds_block = False
|
| 195 |
+
fbonds, bbonds = [], []
|
| 196 |
+
for line in br_file:
|
| 197 |
+
if "fbonds" in line:
|
| 198 |
+
fbonds_block = True
|
| 199 |
+
|
| 200 |
+
if "bbonds" in line:
|
| 201 |
+
fbonds_block = False
|
| 202 |
+
|
| 203 |
+
if len(line.split()) == 2:
|
| 204 |
+
atom_idx0, atom_idx1 = (int(val) for val in line.split())
|
| 205 |
+
|
| 206 |
+
if fbonds_block:
|
| 207 |
+
fbonds.append((atom_idx0, atom_idx1))
|
| 208 |
+
if not fbonds_block:
|
| 209 |
+
bbonds.append((atom_idx0, atom_idx1))
|
| 210 |
+
|
| 211 |
+
if "end" in line:
|
| 212 |
+
bond_rearrangs.append(
|
| 213 |
+
BondRearrangement(
|
| 214 |
+
forming_bonds=fbonds, breaking_bonds=bbonds
|
| 215 |
+
)
|
| 216 |
+
)
|
| 217 |
+
fbonds = []
|
| 218 |
+
bbonds = []
|
| 219 |
+
|
| 220 |
+
return bond_rearrangs
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def add_bond_rearrangment(bond_rearrangs, reactant, product, fbonds, bbonds):
|
| 224 |
+
"""
|
| 225 |
+
For a possible bond rearrangement, sees if the products are made, and
|
| 226 |
+
adds it to the bond rearrang list if it does
|
| 227 |
+
|
| 228 |
+
---------------------------------------------------------------------------
|
| 229 |
+
Arguments:
|
| 230 |
+
bond_rearrangs (list(autode.bond_rearrangements.BondRearrangement)):
|
| 231 |
+
list of working bond rearrangements
|
| 232 |
+
|
| 233 |
+
reactant (autode.species.Complex): Reactant complex
|
| 234 |
+
|
| 235 |
+
product (autode.species.Complex): Product complex
|
| 236 |
+
|
| 237 |
+
fbonds (list(tuple)): list of bonds to be made
|
| 238 |
+
|
| 239 |
+
bbonds (list(tuple)): list of bonds to be broken
|
| 240 |
+
|
| 241 |
+
Returns:
|
| 242 |
+
(list(autode.bond_rearrangements.BondRearrangement)):
|
| 243 |
+
"""
|
| 244 |
+
|
| 245 |
+
# Check that the bond rearrangement doesn't exceed standard atom valances
|
| 246 |
+
bbond_atoms = [atom for bbond in bbonds for atom in bbond]
|
| 247 |
+
for fbond in fbonds:
|
| 248 |
+
for idx in fbond:
|
| 249 |
+
if (
|
| 250 |
+
reactant.graph.degree(idx)
|
| 251 |
+
== reactant.atoms[idx].maximal_valance
|
| 252 |
+
and idx not in bbond_atoms
|
| 253 |
+
):
|
| 254 |
+
# If we are here then there is at least one atom that will
|
| 255 |
+
# exceed it's maximal valance, therefore
|
| 256 |
+
# we don't need to run isomorphism
|
| 257 |
+
return bond_rearrangs
|
| 258 |
+
|
| 259 |
+
rearranged_graph = generate_rearranged_graph(
|
| 260 |
+
reactant.graph, fbonds=fbonds, bbonds=bbonds
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
if is_isomorphic(rearranged_graph, product.graph):
|
| 264 |
+
ordered_fbonds = []
|
| 265 |
+
ordered_bbonds = []
|
| 266 |
+
for fbond in fbonds:
|
| 267 |
+
if fbond[0] < fbond[1]:
|
| 268 |
+
ordered_fbonds.append((fbond[0], fbond[1]))
|
| 269 |
+
else:
|
| 270 |
+
ordered_fbonds.append((fbond[1], fbond[0]))
|
| 271 |
+
for bbond in bbonds:
|
| 272 |
+
if bbond[0] < bbond[1]:
|
| 273 |
+
ordered_bbonds.append((bbond[0], bbond[1]))
|
| 274 |
+
else:
|
| 275 |
+
ordered_bbonds.append((bbond[1], bbond[0]))
|
| 276 |
+
|
| 277 |
+
ordered_fbonds.sort()
|
| 278 |
+
ordered_bbonds.sort()
|
| 279 |
+
bond_rearrangs.append(
|
| 280 |
+
BondRearrangement(
|
| 281 |
+
forming_bonds=ordered_fbonds, breaking_bonds=ordered_bbonds
|
| 282 |
+
)
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
return bond_rearrangs
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def generate_rearranged_graph(graph, fbonds, bbonds):
|
| 289 |
+
"""Generate a rearranged graph by breaking bonds (edge) and forming others
|
| 290 |
+
(edge)
|
| 291 |
+
|
| 292 |
+
---------------------------------------------------------------------------
|
| 293 |
+
Arguments:
|
| 294 |
+
graph (nx.Graph): reactant graph
|
| 295 |
+
|
| 296 |
+
fbonds (list(tuple)): list of bonds to be made
|
| 297 |
+
|
| 298 |
+
bbonds (list(tuple)): list of bonds to be broken
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
nx.Graph: rearranged graph
|
| 302 |
+
"""
|
| 303 |
+
|
| 304 |
+
rearranged_graph = graph.copy()
|
| 305 |
+
for fbond in fbonds:
|
| 306 |
+
rearranged_graph.add_edge(*fbond)
|
| 307 |
+
for bbond in bbonds:
|
| 308 |
+
rearranged_graph.remove_edge(*bbond)
|
| 309 |
+
|
| 310 |
+
return rearranged_graph
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def get_fbonds_bbonds_1b(
|
| 314 |
+
reac,
|
| 315 |
+
prod,
|
| 316 |
+
possible_brs,
|
| 317 |
+
all_possible_bbonds,
|
| 318 |
+
all_possible_fbonds,
|
| 319 |
+
possible_bbond_and_fbonds,
|
| 320 |
+
bbond_atom_type_fbonds,
|
| 321 |
+
fbond_atom_type_bbonds,
|
| 322 |
+
):
|
| 323 |
+
logger.info("Getting possible 1 breaking bond rearrangements")
|
| 324 |
+
|
| 325 |
+
for bbond in all_possible_bbonds[0]:
|
| 326 |
+
# Break one bond
|
| 327 |
+
possible_brs = add_bond_rearrangment(
|
| 328 |
+
possible_brs, reac, prod, fbonds=[], bbonds=[bbond]
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
return possible_brs
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def get_fbonds_bbonds_2b(
|
| 335 |
+
reac,
|
| 336 |
+
prod,
|
| 337 |
+
possible_brs,
|
| 338 |
+
all_possible_bbonds,
|
| 339 |
+
all_possible_fbonds,
|
| 340 |
+
possible_bbond_and_fbonds,
|
| 341 |
+
bbond_atom_type_fbonds,
|
| 342 |
+
fbond_atom_type_bbonds,
|
| 343 |
+
):
|
| 344 |
+
logger.info("Getting possible 2 breaking bond rearrangements")
|
| 345 |
+
|
| 346 |
+
if len(all_possible_bbonds) == 1:
|
| 347 |
+
# Break two bonds of the same type
|
| 348 |
+
for bbond1, bbond2 in itertools.combinations(
|
| 349 |
+
all_possible_bbonds[0], 2
|
| 350 |
+
):
|
| 351 |
+
possible_brs = add_bond_rearrangment(
|
| 352 |
+
possible_brs, reac, prod, fbonds=[], bbonds=[bbond1, bbond2]
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
elif len(all_possible_bbonds) == 2:
|
| 356 |
+
# Break two bonds of different types
|
| 357 |
+
for bbond1, bbond2 in itertools.product(
|
| 358 |
+
all_possible_bbonds[0], all_possible_bbonds[1]
|
| 359 |
+
):
|
| 360 |
+
possible_brs = add_bond_rearrangment(
|
| 361 |
+
possible_brs, reac, prod, fbonds=[], bbonds=[bbond1, bbond2]
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
return possible_brs
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def get_fbonds_bbonds_1b1f(
|
| 368 |
+
reac,
|
| 369 |
+
prod,
|
| 370 |
+
possible_brs,
|
| 371 |
+
all_possible_bbonds,
|
| 372 |
+
all_possible_fbonds,
|
| 373 |
+
possible_bbond_and_fbonds,
|
| 374 |
+
bbond_atom_type_fbonds,
|
| 375 |
+
fbond_atom_type_bbonds,
|
| 376 |
+
):
|
| 377 |
+
logger.info(
|
| 378 |
+
"Getting possible 1 breaking and 1 forming bond " "rearrangements"
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
if len(all_possible_bbonds) == 1 and len(all_possible_fbonds) == 1:
|
| 382 |
+
# Make and break a bond of different types
|
| 383 |
+
for fbond, bbond in itertools.product(
|
| 384 |
+
all_possible_fbonds[0], all_possible_bbonds[0]
|
| 385 |
+
):
|
| 386 |
+
possible_brs = add_bond_rearrangment(
|
| 387 |
+
possible_brs, reac, prod, fbonds=[fbond], bbonds=[bbond]
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
elif len(all_possible_bbonds) == 0 and len(all_possible_fbonds) == 0:
|
| 391 |
+
# Make and break a bond of the same type
|
| 392 |
+
for bbonds, fbonds in possible_bbond_and_fbonds:
|
| 393 |
+
for bbond, fbond in itertools.product(bbonds, fbonds):
|
| 394 |
+
possible_brs = add_bond_rearrangment(
|
| 395 |
+
possible_brs, reac, prod, fbonds=[fbond], bbonds=[bbond]
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
return possible_brs
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def get_fbonds_bbonds_2b1f(
|
| 402 |
+
reac,
|
| 403 |
+
prod,
|
| 404 |
+
possible_brs,
|
| 405 |
+
all_possible_bbonds,
|
| 406 |
+
all_possible_fbonds,
|
| 407 |
+
possible_bbond_and_fbonds,
|
| 408 |
+
bbond_atom_type_fbonds,
|
| 409 |
+
fbond_atom_type_bbonds,
|
| 410 |
+
):
|
| 411 |
+
logger.info(
|
| 412 |
+
"Getting possible 2 breaking and 1 forming bond rearrangements"
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
if len(all_possible_bbonds) == 2 and len(all_possible_fbonds) == 1:
|
| 416 |
+
# Make a bond and break two bonds, all of different types
|
| 417 |
+
possibles = itertools.product(
|
| 418 |
+
all_possible_fbonds[0],
|
| 419 |
+
all_possible_bbonds[0],
|
| 420 |
+
all_possible_bbonds[1],
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
for fbond, bbond1, bbond2 in possibles:
|
| 424 |
+
possible_brs = add_bond_rearrangment(
|
| 425 |
+
possible_brs,
|
| 426 |
+
reac,
|
| 427 |
+
prod,
|
| 428 |
+
fbonds=[fbond],
|
| 429 |
+
bbonds=[bbond1, bbond2],
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
elif len(all_possible_bbonds) == 1 and len(all_possible_fbonds) == 1:
|
| 433 |
+
# Make a bond of one type, break two bonds of another type
|
| 434 |
+
two_same_possibles = itertools.combinations(all_possible_bbonds[0], 2)
|
| 435 |
+
possibles = itertools.product(
|
| 436 |
+
all_possible_fbonds[0], two_same_possibles
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
for fbond, (bbond1, bbond2) in possibles:
|
| 440 |
+
possible_brs = add_bond_rearrangment(
|
| 441 |
+
possible_brs,
|
| 442 |
+
reac,
|
| 443 |
+
prod,
|
| 444 |
+
fbonds=[fbond],
|
| 445 |
+
bbonds=[bbond1, bbond2],
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
elif len(all_possible_bbonds) == 1 and len(all_possible_fbonds) == 0:
|
| 449 |
+
for bbonds, fbonds in possible_bbond_and_fbonds:
|
| 450 |
+
# Make and break a bond of one type, break a bond of a different
|
| 451 |
+
# type
|
| 452 |
+
possibles = itertools.product(
|
| 453 |
+
fbonds, all_possible_bbonds[0], bbonds
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
for fbond, bbond1, bbond2 in possibles:
|
| 457 |
+
possible_brs = add_bond_rearrangment(
|
| 458 |
+
possible_brs,
|
| 459 |
+
reac,
|
| 460 |
+
prod,
|
| 461 |
+
fbonds=[fbond],
|
| 462 |
+
bbonds=[bbond1, bbond2],
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
# Make and break two bonds, all of the same type
|
| 466 |
+
two_same_possibles = itertools.combinations(all_possible_bbonds[0], 2)
|
| 467 |
+
possibles = itertools.product(
|
| 468 |
+
bbond_atom_type_fbonds, two_same_possibles
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
for fbond, (bbond1, bbond2) in possibles:
|
| 472 |
+
possible_brs = add_bond_rearrangment(
|
| 473 |
+
possible_brs,
|
| 474 |
+
reac,
|
| 475 |
+
prod,
|
| 476 |
+
fbonds=[fbond],
|
| 477 |
+
bbonds=[bbond1, bbond2],
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
return possible_brs
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def get_fbonds_bbonds_2b2f(
|
| 484 |
+
reac,
|
| 485 |
+
prod,
|
| 486 |
+
possible_brs,
|
| 487 |
+
all_possible_bbonds,
|
| 488 |
+
all_possible_fbonds,
|
| 489 |
+
possible_bbond_and_fbonds,
|
| 490 |
+
bbond_atom_type_fbonds,
|
| 491 |
+
fbond_atom_type_bbonds,
|
| 492 |
+
):
|
| 493 |
+
logger.info(
|
| 494 |
+
"Getting possible 2 breaking and 2 forming bond rearrangements"
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
if len(all_possible_bbonds) == 2 and len(all_possible_fbonds) == 2:
|
| 498 |
+
# Make two bonds and break two bonds, all of different types
|
| 499 |
+
possibles = itertools.product(
|
| 500 |
+
all_possible_fbonds[0],
|
| 501 |
+
all_possible_fbonds[1],
|
| 502 |
+
all_possible_bbonds[0],
|
| 503 |
+
all_possible_bbonds[1],
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
for fbond1, fbond2, bbond1, bbond2 in possibles:
|
| 507 |
+
possible_brs = add_bond_rearrangment(
|
| 508 |
+
possible_brs,
|
| 509 |
+
reac,
|
| 510 |
+
prod,
|
| 511 |
+
fbonds=[fbond1, fbond2],
|
| 512 |
+
bbonds=[bbond1, bbond2],
|
| 513 |
+
)
|
| 514 |
+
|
| 515 |
+
elif len(all_possible_bbonds) == 2 and len(all_possible_fbonds) == 1:
|
| 516 |
+
# Make two bonds of the same type, break two bonds of different types
|
| 517 |
+
two_same_possibles = itertools.combinations(all_possible_fbonds[0], 2)
|
| 518 |
+
possibles = itertools.product(
|
| 519 |
+
all_possible_bbonds[0], all_possible_bbonds[1], two_same_possibles
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
for bbond1, bbond2, (fbond1, fbond2) in possibles:
|
| 523 |
+
possible_brs = add_bond_rearrangment(
|
| 524 |
+
possible_brs,
|
| 525 |
+
reac,
|
| 526 |
+
prod,
|
| 527 |
+
fbonds=[fbond1, fbond2],
|
| 528 |
+
bbonds=[bbond1, bbond2],
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
elif len(all_possible_bbonds) == 1 and len(all_possible_fbonds) == 2:
|
| 532 |
+
# Make two bonds of different types, break two bonds of the same type
|
| 533 |
+
two_same_possibles = itertools.combinations(all_possible_bbonds[0], 2)
|
| 534 |
+
possibles = itertools.product(
|
| 535 |
+
all_possible_fbonds[0], all_possible_fbonds[1], two_same_possibles
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
for fbond1, fbond2, (bbond1, bbond2) in possibles:
|
| 539 |
+
possible_brs = add_bond_rearrangment(
|
| 540 |
+
possible_brs,
|
| 541 |
+
reac,
|
| 542 |
+
prod,
|
| 543 |
+
fbonds=[fbond1, fbond2],
|
| 544 |
+
bbonds=[bbond1, bbond2],
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
elif len(all_possible_bbonds) == 1 and len(all_possible_fbonds) == 1:
|
| 548 |
+
two_f_possibles = itertools.combinations(all_possible_fbonds[0], 2)
|
| 549 |
+
two_b_possibles = itertools.combinations(all_possible_bbonds[0], 2)
|
| 550 |
+
possibles = itertools.product(two_f_possibles, two_b_possibles)
|
| 551 |
+
|
| 552 |
+
for (fbond1, fbond2), (bbond1, bbond2) in possibles:
|
| 553 |
+
# Make two bonds of the same type, break two bonds of another type
|
| 554 |
+
possible_brs = add_bond_rearrangment(
|
| 555 |
+
possible_brs,
|
| 556 |
+
reac,
|
| 557 |
+
prod,
|
| 558 |
+
fbonds=[fbond1, fbond2],
|
| 559 |
+
bbonds=[bbond1, bbond2],
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
for bbonds, fbonds in possible_bbond_and_fbonds:
|
| 563 |
+
# Make one bonds of one type, break one bond of another type, make
|
| 564 |
+
# and break a bond of a third type
|
| 565 |
+
possibles = itertools.product(
|
| 566 |
+
all_possible_fbonds[0], fbonds, all_possible_bbonds[0], bbonds
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
+
for fbond1, fbond2, bbond1, bbond2 in possibles:
|
| 570 |
+
possible_brs = add_bond_rearrangment(
|
| 571 |
+
possible_brs,
|
| 572 |
+
reac,
|
| 573 |
+
prod,
|
| 574 |
+
fbonds=[fbond1, fbond2],
|
| 575 |
+
bbonds=[bbond1, bbond2],
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
# Make a bond of one type, make and break two bonds of another type
|
| 579 |
+
two_b_possibles = itertools.combinations(all_possible_bbonds[0], 2)
|
| 580 |
+
possibles = itertools.product(
|
| 581 |
+
all_possible_fbonds[0], bbond_atom_type_fbonds, two_b_possibles
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
for fbond1, fbond2, (bbond1, bbond2) in possibles:
|
| 585 |
+
possible_brs = add_bond_rearrangment(
|
| 586 |
+
possible_brs,
|
| 587 |
+
reac,
|
| 588 |
+
prod,
|
| 589 |
+
fbonds=[fbond1, fbond2],
|
| 590 |
+
bbonds=[bbond1, bbond2],
|
| 591 |
+
)
|
| 592 |
+
|
| 593 |
+
two_f_possibles = itertools.combinations(all_possible_fbonds[0], 2)
|
| 594 |
+
possibles = itertools.product(
|
| 595 |
+
all_possible_bbonds[0], fbond_atom_type_bbonds, two_f_possibles
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
for bbond1, bbond2, (fbond1, fbond2) in possibles:
|
| 599 |
+
# Break a bond of one type, make two and break one bond of another
|
| 600 |
+
# type
|
| 601 |
+
possible_brs = add_bond_rearrangment(
|
| 602 |
+
possible_brs,
|
| 603 |
+
reac,
|
| 604 |
+
prod,
|
| 605 |
+
fbonds=[fbond1, fbond2],
|
| 606 |
+
bbonds=[bbond1, bbond2],
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
elif len(all_possible_bbonds) == 0 and len(all_possible_fbonds) == 0:
|
| 610 |
+
possibles_b_f = itertools.combinations(possible_bbond_and_fbonds, 2)
|
| 611 |
+
|
| 612 |
+
for (bbonds1, fbonds1), (bbonds2, fbonds2) in possibles_b_f:
|
| 613 |
+
# Make and break a bond of one type, make and break a bond of
|
| 614 |
+
# another type
|
| 615 |
+
possibles = itertools.product(fbonds1, bbonds1, fbonds2, bbonds2)
|
| 616 |
+
|
| 617 |
+
for fbond1, bbond1, fbond2, bbond2 in possibles:
|
| 618 |
+
possible_brs = add_bond_rearrangment(
|
| 619 |
+
possible_brs,
|
| 620 |
+
reac,
|
| 621 |
+
prod,
|
| 622 |
+
fbonds=[fbond1, fbond2],
|
| 623 |
+
bbonds=[bbond1, bbond2],
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
for bbonds, fbonds in possible_bbond_and_fbonds:
|
| 627 |
+
# Make two and break two bonds, all of the same type
|
| 628 |
+
possibles = itertools.product(
|
| 629 |
+
itertools.combinations(fbonds, 2),
|
| 630 |
+
itertools.combinations(bbonds, 2),
|
| 631 |
+
)
|
| 632 |
+
|
| 633 |
+
for (fbond1, fbond2), (bbond1, bbond2) in possibles:
|
| 634 |
+
possible_brs = add_bond_rearrangment(
|
| 635 |
+
possible_brs,
|
| 636 |
+
reac,
|
| 637 |
+
prod,
|
| 638 |
+
fbonds=[fbond1, fbond2],
|
| 639 |
+
bbonds=[bbond1, bbond2],
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
return possible_brs
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
def strip_equiv_bond_rearrs(possible_brs, mol, depth=6):
|
| 646 |
+
"""Remove any bond rearrangement from possible_brs for which
|
| 647 |
+
there is already an equivalent in the unique_bond_rearrangements list
|
| 648 |
+
|
| 649 |
+
---------------------------------------------------------------------------
|
| 650 |
+
Arguments:
|
| 651 |
+
possible_brs (list(BondRearrangement)):
|
| 652 |
+
mol (autode.species.Complex): Reactant
|
| 653 |
+
|
| 654 |
+
Keyword Arguments:
|
| 655 |
+
depth (int): Depth of neighbour list that must be identical for a set
|
| 656 |
+
of atoms to be considered equivalent (default: {6})
|
| 657 |
+
|
| 658 |
+
Returns:
|
| 659 |
+
(list(BondRearrangement)): stripped list of BondRearrangement objects
|
| 660 |
+
"""
|
| 661 |
+
logger.info(
|
| 662 |
+
"Stripping the forming and breaking bond list by discarding "
|
| 663 |
+
"rearrangements with equivalent atoms"
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
unique_brs = []
|
| 667 |
+
|
| 668 |
+
for br in possible_brs:
|
| 669 |
+
bond_rearrang_is_unique = True
|
| 670 |
+
|
| 671 |
+
# Compare bond_rearrang to all those already considered to be unique,
|
| 672 |
+
for unique_br in unique_brs:
|
| 673 |
+
if unique_br.get_active_atom_neighbour_lists(
|
| 674 |
+
species=mol, depth=depth
|
| 675 |
+
) == br.get_active_atom_neighbour_lists(species=mol, depth=depth):
|
| 676 |
+
bond_rearrang_is_unique = False
|
| 677 |
+
|
| 678 |
+
if bond_rearrang_is_unique:
|
| 679 |
+
unique_brs.append(br)
|
| 680 |
+
|
| 681 |
+
logger.info(
|
| 682 |
+
f"Stripped {len(possible_brs) - len(unique_brs)} "
|
| 683 |
+
"bond rearrangements"
|
| 684 |
+
)
|
| 685 |
+
return unique_brs
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def prune_small_ring_rearrs(possible_brs, mol):
|
| 689 |
+
"""
|
| 690 |
+
Remove any bond rearrangements that go via small (3, 4) rings if there is
|
| 691 |
+
an alternative that goes vie
|
| 692 |
+
|
| 693 |
+
---------------------------------------------------------------------------
|
| 694 |
+
Arguments:
|
| 695 |
+
possible_brs (list(BondRearrangement)):
|
| 696 |
+
|
| 697 |
+
mol (autode.species.Complex): Reactant
|
| 698 |
+
"""
|
| 699 |
+
small_ring_sizes = (3, 4)
|
| 700 |
+
|
| 701 |
+
if not Config.skip_small_ring_tss:
|
| 702 |
+
logger.info("Not pruning small ring TSs")
|
| 703 |
+
return None
|
| 704 |
+
|
| 705 |
+
# Membered-ness of rings in each bond rearrangement
|
| 706 |
+
n_mem_rings = [br.n_membered_rings(mol) for br in possible_brs]
|
| 707 |
+
|
| 708 |
+
# Unique elements involved in each bond rearrangement
|
| 709 |
+
elems = [
|
| 710 |
+
set(
|
| 711 |
+
mol.atoms[i].label
|
| 712 |
+
for i in range(mol.n_atoms)
|
| 713 |
+
if i in br.active_atoms
|
| 714 |
+
)
|
| 715 |
+
for br in possible_brs
|
| 716 |
+
]
|
| 717 |
+
|
| 718 |
+
logger.info(
|
| 719 |
+
f"Pruning {len(possible_brs)} to remove any "
|
| 720 |
+
f"{small_ring_sizes}-membered rings where others are possible"
|
| 721 |
+
)
|
| 722 |
+
|
| 723 |
+
excluded_idxs = []
|
| 724 |
+
for i, br in enumerate(possible_brs):
|
| 725 |
+
logger.info(
|
| 726 |
+
f"Checking bond rearrangement {i} with rings:"
|
| 727 |
+
f" {n_mem_rings[i]} and atom indexes: {br}"
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
# Only consider brs with at least one small ring
|
| 731 |
+
if not any(n_mem in small_ring_sizes for n_mem in n_mem_rings[i]):
|
| 732 |
+
continue
|
| 733 |
+
|
| 734 |
+
# Check against all other rearrangements
|
| 735 |
+
for j, other_br in enumerate(possible_brs):
|
| 736 |
+
# Only consider brs with the same set of elements
|
| 737 |
+
if elems[i] != elems[j]:
|
| 738 |
+
continue
|
| 739 |
+
|
| 740 |
+
# Needs to have the same number of rings
|
| 741 |
+
if len(n_mem_rings[i]) != len(n_mem_rings[j]):
|
| 742 |
+
continue
|
| 743 |
+
|
| 744 |
+
# Exclude i if j has a larger smallest ring size
|
| 745 |
+
if min(n_mem_rings[i]) < min(n_mem_rings[j]):
|
| 746 |
+
excluded_idxs.append(i)
|
| 747 |
+
break
|
| 748 |
+
|
| 749 |
+
logger.info(
|
| 750 |
+
f"Excluding {len(excluded_idxs)} bond rearrangements based on "
|
| 751 |
+
f"small rings"
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
# Delete the excluded bond rearrangements (sorted high -> low, so the
|
| 755 |
+
# idxs remain the same while deleting)
|
| 756 |
+
for idx in sorted(excluded_idxs, reverse=True):
|
| 757 |
+
del possible_brs[idx]
|
| 758 |
+
|
| 759 |
+
return None
|
| 760 |
+
|
| 761 |
+
|
| 762 |
+
class BondRearrangement:
|
| 763 |
+
def __eq__(self, other):
|
| 764 |
+
return self.fbonds == other.fbonds and self.bbonds == other.bbonds
|
| 765 |
+
|
| 766 |
+
def __str__(self):
|
| 767 |
+
return "_".join(f"{bond[0]}-{bond[1]}" for bond in self.all)
|
| 768 |
+
|
| 769 |
+
def get_active_atom_neighbour_lists(self, species, depth):
|
| 770 |
+
"""
|
| 771 |
+
Get neighbour lists of all the active atoms in the molecule
|
| 772 |
+
(reactant complex)
|
| 773 |
+
|
| 774 |
+
-----------------------------------------------------------------------
|
| 775 |
+
Arguments:
|
| 776 |
+
species (autode.species.Species | autode.species.Complex):
|
| 777 |
+
depth (int): Depth of the neighbour list to consider
|
| 778 |
+
|
| 779 |
+
Returns:
|
| 780 |
+
(list(list(str))):
|
| 781 |
+
"""
|
| 782 |
+
|
| 783 |
+
def nl(idx):
|
| 784 |
+
mol_idxs = None
|
| 785 |
+
|
| 786 |
+
try:
|
| 787 |
+
mol_idxs = next(
|
| 788 |
+
species.atom_indexes(i)
|
| 789 |
+
for i in range(species.n_molecules)
|
| 790 |
+
if idx in species.atom_indexes(i)
|
| 791 |
+
)
|
| 792 |
+
|
| 793 |
+
except (StopIteration, AttributeError):
|
| 794 |
+
logger.warning("Active atom index not found in any molecules")
|
| 795 |
+
|
| 796 |
+
nl_labels = get_neighbour_list(
|
| 797 |
+
species, atom_i=idx, index_set=mol_idxs
|
| 798 |
+
)
|
| 799 |
+
return nl_labels[:depth]
|
| 800 |
+
|
| 801 |
+
return [nl(idx) for idx in self.active_atoms]
|
| 802 |
+
|
| 803 |
+
def n_membered_rings(self, mol):
|
| 804 |
+
"""
|
| 805 |
+
Find the membered-ness of the rings involved in this bond rearrangement
|
| 806 |
+
will add the forming bonds to the graph to determine
|
| 807 |
+
|
| 808 |
+
-----------------------------------------------------------------------
|
| 809 |
+
Arguments:
|
| 810 |
+
(autode.species.Species):
|
| 811 |
+
|
| 812 |
+
Returns:
|
| 813 |
+
(list(int)):
|
| 814 |
+
"""
|
| 815 |
+
assert mol.graph is not None
|
| 816 |
+
graph = mol.graph.copy()
|
| 817 |
+
|
| 818 |
+
for fbond in self.fbonds:
|
| 819 |
+
if fbond not in graph.edges:
|
| 820 |
+
graph.add_edge(*fbond)
|
| 821 |
+
|
| 822 |
+
rings = find_cycles(graph)
|
| 823 |
+
n_mem_rings = []
|
| 824 |
+
|
| 825 |
+
# Full enumeration over all atoms and rings - could be faster..
|
| 826 |
+
for ring in rings:
|
| 827 |
+
for atom_idx in self.active_atoms:
|
| 828 |
+
if atom_idx in ring:
|
| 829 |
+
# This ring has at least one active atom in
|
| 830 |
+
n_mem_rings.append(len(ring))
|
| 831 |
+
|
| 832 |
+
# don't add the same ring more than once
|
| 833 |
+
break
|
| 834 |
+
|
| 835 |
+
return n_mem_rings
|
| 836 |
+
|
| 837 |
+
@property
|
| 838 |
+
def fatoms(self):
|
| 839 |
+
"""Unique atoms indexes involved in forming bonds"""
|
| 840 |
+
return list(sorted(set([i for bond in self.fbonds for i in bond])))
|
| 841 |
+
|
| 842 |
+
@property
|
| 843 |
+
def batoms(self):
|
| 844 |
+
"""Unique atoms indexes involved in breaking bonds"""
|
| 845 |
+
return list(sorted(set([i for bond in self.bbonds for i in bond])))
|
| 846 |
+
|
| 847 |
+
@property
|
| 848 |
+
def active_atoms(self):
|
| 849 |
+
"""Unique atom indexes in forming or breaking bonds"""
|
| 850 |
+
return list(sorted(set(a for b in self.all for a in b)))
|
| 851 |
+
|
| 852 |
+
@property
|
| 853 |
+
def n_fbonds(self):
|
| 854 |
+
return len(self.fbonds)
|
| 855 |
+
|
| 856 |
+
@property
|
| 857 |
+
def n_bbonds(self):
|
| 858 |
+
return len(self.bbonds)
|
| 859 |
+
|
| 860 |
+
def __init__(self, forming_bonds=None, breaking_bonds=None):
|
| 861 |
+
"""
|
| 862 |
+
Bond rearrangement
|
| 863 |
+
|
| 864 |
+
-----------------------------------------------------------------------
|
| 865 |
+
Keyword Arguments:
|
| 866 |
+
forming_bonds (list(tuple(int))): List of atom pairs that are
|
| 867 |
+
forming in this reaction
|
| 868 |
+
|
| 869 |
+
breaking_bonds (list(tuple(int))): List of atom pairs that are
|
| 870 |
+
breaking in the reaction
|
| 871 |
+
"""
|
| 872 |
+
|
| 873 |
+
self.fbonds = forming_bonds if forming_bonds is not None else []
|
| 874 |
+
self.bbonds = breaking_bonds if breaking_bonds is not None else []
|
| 875 |
+
|
| 876 |
+
self.all = self.fbonds + self.bbonds
|
autodE/source/autode/bonds.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ScannedBond:
|
| 2 |
+
def __str__(self):
|
| 3 |
+
i, j = self.atom_indexes
|
| 4 |
+
return f"{i}-{j}"
|
| 5 |
+
|
| 6 |
+
def __getitem__(self, item):
|
| 7 |
+
return self.atom_indexes[item]
|
| 8 |
+
|
| 9 |
+
@property
|
| 10 |
+
def dr(self):
|
| 11 |
+
"""Change in distance for this bond (∆r / Å)"""
|
| 12 |
+
if self.curr_dist is None or self.final_dist is None:
|
| 13 |
+
return 0
|
| 14 |
+
|
| 15 |
+
return self.final_dist - self.curr_dist
|
| 16 |
+
|
| 17 |
+
def __init__(self, atom_indexes):
|
| 18 |
+
"""
|
| 19 |
+
Bond with a current and final distance which will be scanned over
|
| 20 |
+
|
| 21 |
+
-----------------------------------------------------------------------
|
| 22 |
+
Arguments:
|
| 23 |
+
atom_indexes (tuple(int)): Atom indexes that make this
|
| 24 |
+
'bond' e.g. (0, 1)
|
| 25 |
+
"""
|
| 26 |
+
assert len(atom_indexes) == 2
|
| 27 |
+
|
| 28 |
+
self.atom_indexes = atom_indexes
|
| 29 |
+
|
| 30 |
+
self.curr_dist = None
|
| 31 |
+
self.final_dist = None
|
| 32 |
+
|
| 33 |
+
self.forming = False
|
| 34 |
+
self.breaking = False
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class FormingBond(ScannedBond):
|
| 38 |
+
def __init__(self, atom_indexes, species, final_species=None):
|
| 39 |
+
"""
|
| 40 |
+
Forming bond with current and final distances
|
| 41 |
+
|
| 42 |
+
-----------------------------------------------------------------------
|
| 43 |
+
Arguments:
|
| 44 |
+
atom_indexes (tuple(int)):
|
| 45 |
+
|
| 46 |
+
species (autode.species.Species):
|
| 47 |
+
"""
|
| 48 |
+
super().__init__(atom_indexes)
|
| 49 |
+
self.forming = True
|
| 50 |
+
|
| 51 |
+
i, j = self.atom_indexes
|
| 52 |
+
self.curr_dist = species.distance(i=i, j=j)
|
| 53 |
+
|
| 54 |
+
if final_species is None:
|
| 55 |
+
self.final_dist = species.atoms.eqm_bond_distance(i, j)
|
| 56 |
+
else:
|
| 57 |
+
self.final_dist = final_species.distance(*atom_indexes)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class BreakingBond(ScannedBond):
|
| 61 |
+
def __init__(self, atom_indexes, species, final_species=None):
|
| 62 |
+
"""
|
| 63 |
+
Form a breaking bond with current and final distances
|
| 64 |
+
|
| 65 |
+
-----------------------------------------------------------------------
|
| 66 |
+
Arguments:
|
| 67 |
+
atom_indexes (tuple(int)):
|
| 68 |
+
|
| 69 |
+
species (autode.species.Species):
|
| 70 |
+
|
| 71 |
+
final_species (autode.species.Species | None):
|
| 72 |
+
"""
|
| 73 |
+
super().__init__(atom_indexes)
|
| 74 |
+
self.breaking = True
|
| 75 |
+
|
| 76 |
+
self.curr_dist = species.distance(*self.atom_indexes)
|
| 77 |
+
|
| 78 |
+
if final_species is None:
|
| 79 |
+
self.final_dist = 2.0 * self.curr_dist
|
| 80 |
+
|
| 81 |
+
else:
|
| 82 |
+
# Take the smallest possible final distance, thus the shortest
|
| 83 |
+
# path to traverse
|
| 84 |
+
self.final_dist = min(
|
| 85 |
+
final_species.distance(*self.atom_indexes),
|
| 86 |
+
2.0 * self.curr_dist,
|
| 87 |
+
)
|
autodE/source/autode/bracket/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from autode.bracket.dhs import DHS, DHSGS
|
| 2 |
+
|
| 3 |
+
__all__ = ["DHS", "DHSGS"]
|
autodE/source/autode/bracket/base.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Union, Optional, TYPE_CHECKING
|
| 2 |
+
from abc import ABC, abstractmethod
|
| 3 |
+
|
| 4 |
+
from autode.values import Distance, GradientRMS
|
| 5 |
+
from autode.bracket.imagepair import EuclideanImagePair
|
| 6 |
+
from autode.log import logger
|
| 7 |
+
from autode.utils import work_in
|
| 8 |
+
from autode import Config
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
from autode.species.species import Species
|
| 12 |
+
from autode.wrappers.methods import Method
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class BaseBracketMethod(ABC):
|
| 16 |
+
"""
|
| 17 |
+
Base class for all bracketing methods
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
initial_species: "Species",
|
| 23 |
+
final_species: "Species",
|
| 24 |
+
maxiter: int = 300,
|
| 25 |
+
dist_tol: Union[Distance, float] = Distance(1.0, "ang"),
|
| 26 |
+
gtol: Union[GradientRMS, float] = GradientRMS(1.0e-3, "ha/ang"),
|
| 27 |
+
cineb_at_conv: bool = False,
|
| 28 |
+
barrier_check: bool = True,
|
| 29 |
+
):
|
| 30 |
+
"""
|
| 31 |
+
Bracketing methods find transition state by using two images, one
|
| 32 |
+
for the reactant state and another representing the product state.
|
| 33 |
+
These methods move the images continuously until they converge at
|
| 34 |
+
the transition state (TS), i.e. they bracket the TS from both ends.
|
| 35 |
+
It is optionally possible to run a CI-NEB (with only one intervening
|
| 36 |
+
image) from the end-points of a converged bracketing method
|
| 37 |
+
calculation to get much closer to the actual TS.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
initial_species: The "reactant" species
|
| 41 |
+
final_species: The "product" species
|
| 42 |
+
maxiter: Maximum number of energy-gradient evaluations
|
| 43 |
+
dist_tol: The distance tolerance at which the method
|
| 44 |
+
will stop, in units of Å if not given
|
| 45 |
+
gtol: Gradient tolerance for optimisation steps in
|
| 46 |
+
the method, units Ha/Å if not given
|
| 47 |
+
cineb_at_conv: Whether to run a CI-NEB with from the final points
|
| 48 |
+
barrier_check: Whether to stop the calculation if one image is
|
| 49 |
+
detected to have jumped over the barrier. Do not
|
| 50 |
+
turn this off unless you are absolutely sure!
|
| 51 |
+
"""
|
| 52 |
+
# imgpair type must be set by subclass
|
| 53 |
+
self.imgpair: Optional["EuclideanImagePair"] = None
|
| 54 |
+
self._species: "Species" = initial_species.copy()
|
| 55 |
+
|
| 56 |
+
self._maxiter = int(maxiter)
|
| 57 |
+
self._dist_tol = Distance(dist_tol, units="ang")
|
| 58 |
+
self._gtol = GradientRMS(gtol, units="Ha/ang")
|
| 59 |
+
|
| 60 |
+
self._should_run_cineb = bool(cineb_at_conv)
|
| 61 |
+
self._barrier_check = bool(barrier_check)
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def _name(self) -> str:
|
| 65 |
+
"""Name of the current bracketing method, obtained from class name"""
|
| 66 |
+
return type(self).__name__
|
| 67 |
+
|
| 68 |
+
@property
|
| 69 |
+
def ts_guess(self) -> Optional["Species"]:
|
| 70 |
+
"""Get the TS guess from image-pair"""
|
| 71 |
+
assert self.imgpair is not None, "Must have an image pair for TS guess"
|
| 72 |
+
return self.imgpair.ts_guess
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def converged(self) -> bool:
|
| 76 |
+
"""Whether the bracketing method has converged or not"""
|
| 77 |
+
assert self.imgpair is not None, "Must have an image pair"
|
| 78 |
+
|
| 79 |
+
# NOTE: Usually geometry optimisation is done in separate
|
| 80 |
+
# micro-iters, so gradient is checked elsewhere
|
| 81 |
+
return self.imgpair.dist <= self._dist_tol
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
@abstractmethod
|
| 85 |
+
def _macro_iter(self) -> int:
|
| 86 |
+
"""The number of macro-iterations run with this method"""
|
| 87 |
+
|
| 88 |
+
@property
|
| 89 |
+
@abstractmethod
|
| 90 |
+
def _micro_iter(self) -> int:
|
| 91 |
+
"""Total number of micro-iterations run with this method"""
|
| 92 |
+
|
| 93 |
+
@abstractmethod
|
| 94 |
+
def _initialise_run(self) -> None:
|
| 95 |
+
"""Initialise the bracketing method run"""
|
| 96 |
+
|
| 97 |
+
@abstractmethod
|
| 98 |
+
def _step(self) -> None:
|
| 99 |
+
"""
|
| 100 |
+
One step of the bracket method, with one macro-iteration
|
| 101 |
+
and multiple micro-iterations. This must also set new
|
| 102 |
+
coordinates for the next step
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
def _log_convergence(self) -> None:
|
| 106 |
+
"""
|
| 107 |
+
Log the convergence of the bracket method. Only logs macro-iters,
|
| 108 |
+
subclasses may implement further logging for micro-iters
|
| 109 |
+
"""
|
| 110 |
+
assert self.imgpair is not None, "Must have an image pair to log"
|
| 111 |
+
|
| 112 |
+
logger.info(
|
| 113 |
+
f"{self._name} Macro-iteration #{self._macro_iter}: "
|
| 114 |
+
f"Distance = {self.imgpair.dist:.4f}; Energy (initial species) = "
|
| 115 |
+
f"{self.imgpair.left_coords.e:.6f}; Energy (final species) = "
|
| 116 |
+
f"{self.imgpair.right_coords.e:.6f}"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
@property
|
| 120 |
+
def _exceeded_maximum_iteration(self) -> bool:
|
| 121 |
+
"""Whether it has exceeded the number of maximum micro-iterations"""
|
| 122 |
+
if self._micro_iter >= self._maxiter:
|
| 123 |
+
logger.error(
|
| 124 |
+
f"Reached the maximum number of micro-iterations "
|
| 125 |
+
f"*{self._maxiter}*"
|
| 126 |
+
)
|
| 127 |
+
return True
|
| 128 |
+
else:
|
| 129 |
+
return False
|
| 130 |
+
|
| 131 |
+
def calculate(
|
| 132 |
+
self,
|
| 133 |
+
method: "Method",
|
| 134 |
+
n_cores: Optional[int] = None,
|
| 135 |
+
) -> None:
|
| 136 |
+
"""
|
| 137 |
+
Run the bracketing method calculation using the method for
|
| 138 |
+
energy/gradient calculation, with n_cores. Runs CI-NEB at
|
| 139 |
+
the end if requested; then save the .xyz trajectories,
|
| 140 |
+
plot the energies and finally save the peak as TS guess.
|
| 141 |
+
This function should be called only once!
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
method (Method): Method used for calculating energy/gradients
|
| 145 |
+
n_cores (int): Number of cores to use for calculation
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
@work_in(self._name.lower())
|
| 149 |
+
def run():
|
| 150 |
+
self._calculate(method, n_cores)
|
| 151 |
+
|
| 152 |
+
run()
|
| 153 |
+
return None
|
| 154 |
+
|
| 155 |
+
def _calculate(
|
| 156 |
+
self,
|
| 157 |
+
method: "Method",
|
| 158 |
+
n_cores: Optional[int] = None,
|
| 159 |
+
) -> None:
|
| 160 |
+
"""
|
| 161 |
+
Actually runs the calculation, it is wrapped around in calculate()
|
| 162 |
+
so that the results are placed in a sub-folder
|
| 163 |
+
"""
|
| 164 |
+
assert self.imgpair is not None, "Must have set image pair"
|
| 165 |
+
|
| 166 |
+
n_cores = Config.n_cores if n_cores is None else int(n_cores)
|
| 167 |
+
self.imgpair.set_method_and_n_cores(method, n_cores)
|
| 168 |
+
self.imgpair.initialise_trj(
|
| 169 |
+
f"{self._name}_left_history.zip", f"{self._name}_right_history.zip"
|
| 170 |
+
)
|
| 171 |
+
self._initialise_run()
|
| 172 |
+
|
| 173 |
+
logger.info(f"Starting {self._name} method to find transition state")
|
| 174 |
+
|
| 175 |
+
while not self.converged:
|
| 176 |
+
self._step()
|
| 177 |
+
|
| 178 |
+
if self.imgpair.has_jumped_over_barrier:
|
| 179 |
+
# TODO: implement image pair regeneration
|
| 180 |
+
logger.error(
|
| 181 |
+
"One image has probably jumped over the barrier, in"
|
| 182 |
+
f" {self._name} TS search. Please check the"
|
| 183 |
+
f" results carefully"
|
| 184 |
+
)
|
| 185 |
+
if self._barrier_check:
|
| 186 |
+
logger.info(f"Stopping {self._name} calculation")
|
| 187 |
+
break
|
| 188 |
+
|
| 189 |
+
if self._exceeded_maximum_iteration:
|
| 190 |
+
break
|
| 191 |
+
|
| 192 |
+
self._log_convergence()
|
| 193 |
+
|
| 194 |
+
# exited main loop, run CI-NEB if required and bracket converged
|
| 195 |
+
if self._should_run_cineb:
|
| 196 |
+
if self.converged and not self.imgpair.has_jumped_over_barrier:
|
| 197 |
+
self.run_cineb()
|
| 198 |
+
else:
|
| 199 |
+
logger.warning(
|
| 200 |
+
f"{self._name} calculation has not converged"
|
| 201 |
+
f" properly or one side has jumped over the barrier,"
|
| 202 |
+
f" skipping CI-NEB run"
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
logger.info(
|
| 206 |
+
f"Finished {self._name} procedure in {self._macro_iter} "
|
| 207 |
+
f"macro-iterations consisting of {self._micro_iter} micro-"
|
| 208 |
+
f"iterations (optimiser steps). {self._name} is "
|
| 209 |
+
f"{'converged' if self.converged else 'not converged'}"
|
| 210 |
+
)
|
| 211 |
+
self.imgpair.close_trj()
|
| 212 |
+
self.print_geometries()
|
| 213 |
+
self.plot_energies()
|
| 214 |
+
if self.converged and self.ts_guess is not None:
|
| 215 |
+
self.ts_guess.print_xyz_file(filename=f"{self._name}_ts_guess.xyz")
|
| 216 |
+
return None
|
| 217 |
+
|
| 218 |
+
def print_geometries(
|
| 219 |
+
self,
|
| 220 |
+
init_trj_filename: Optional[str] = None,
|
| 221 |
+
final_trj_filename: Optional[str] = None,
|
| 222 |
+
total_trj_filename: Optional[str] = None,
|
| 223 |
+
) -> None:
|
| 224 |
+
"""
|
| 225 |
+
Write trajectories as *.xyz files, one for the initial species,
|
| 226 |
+
one for final species, and one for the whole trajectory, including
|
| 227 |
+
any CI-NEB run from the final end points. The default names for
|
| 228 |
+
the trajectories must be set in individual subclasses
|
| 229 |
+
"""
|
| 230 |
+
assert self.imgpair is not None, "Must have an image pair to plot"
|
| 231 |
+
|
| 232 |
+
init_trj_filename = (
|
| 233 |
+
init_trj_filename
|
| 234 |
+
if init_trj_filename is not None
|
| 235 |
+
else f"initial_species_{self._name}.trj.xyz"
|
| 236 |
+
)
|
| 237 |
+
final_trj_filename = (
|
| 238 |
+
final_trj_filename
|
| 239 |
+
if final_trj_filename is not None
|
| 240 |
+
else f"final_species_{self._name}.trj.xyz"
|
| 241 |
+
)
|
| 242 |
+
total_trj_filename = (
|
| 243 |
+
total_trj_filename
|
| 244 |
+
if total_trj_filename is not None
|
| 245 |
+
else f"total_trajectory_{self._name}.trj.xyz"
|
| 246 |
+
)
|
| 247 |
+
self.imgpair.print_geometries(
|
| 248 |
+
init_trj_filename, final_trj_filename, total_trj_filename
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
return None
|
| 252 |
+
|
| 253 |
+
def plot_energies(
|
| 254 |
+
self,
|
| 255 |
+
filename: Optional[str] = None,
|
| 256 |
+
distance_metric: str = "relative",
|
| 257 |
+
) -> None:
|
| 258 |
+
"""
|
| 259 |
+
Plot the energies of the bracket method run, taking
|
| 260 |
+
into account any CI-NEB interpolation that may have been
|
| 261 |
+
done.
|
| 262 |
+
|
| 263 |
+
The distance metric chooses what the x-axis means;
|
| 264 |
+
"relative" means that the points will be plotted in the order
|
| 265 |
+
in which they appear in the total history, and the x-axis
|
| 266 |
+
numbers will represent the relative distances between two
|
| 267 |
+
adjacent points (giving an approximate reaction coordinate).
|
| 268 |
+
"from_start" will calculate the distance of each point from
|
| 269 |
+
the starting reactant structure and use that as the x-axis
|
| 270 |
+
position. If distance metric is set to "index", then the x-axis
|
| 271 |
+
will simply be integer numbers representing each point in order
|
| 272 |
+
|
| 273 |
+
Args:
|
| 274 |
+
filename (str|None): Name of the file (optional)
|
| 275 |
+
distance_metric (str): "relative" or "from_start" or "index"
|
| 276 |
+
"""
|
| 277 |
+
assert self.imgpair is not None, "Must have an image pair to plot"
|
| 278 |
+
|
| 279 |
+
filename = (
|
| 280 |
+
filename
|
| 281 |
+
if filename is not None
|
| 282 |
+
else f"{self._name}_path_energy_plot.pdf"
|
| 283 |
+
)
|
| 284 |
+
self.imgpair.plot_energies(filename, distance_metric)
|
| 285 |
+
return None
|
| 286 |
+
|
| 287 |
+
def run_cineb(self) -> None:
|
| 288 |
+
"""
|
| 289 |
+
Run CI-NEB from the end-points of a converged bracketing
|
| 290 |
+
calculation. Uses only one intervening image for the
|
| 291 |
+
CI-NEB calculation (which is okay as the bracketing method
|
| 292 |
+
should bring the ends very close to the TS). The result from
|
| 293 |
+
the CI-NEB calculation is stored as coordinates.
|
| 294 |
+
"""
|
| 295 |
+
assert self.imgpair is not None, "Must have image pair to run CINEB"
|
| 296 |
+
|
| 297 |
+
if not self._micro_iter > 0:
|
| 298 |
+
logger.error(
|
| 299 |
+
f"Must run {self._name} calculation before"
|
| 300 |
+
f"running the CI-NEB calculation"
|
| 301 |
+
)
|
| 302 |
+
return None
|
| 303 |
+
|
| 304 |
+
if not self.converged or self.imgpair.dist > 2.0:
|
| 305 |
+
logger.warning(
|
| 306 |
+
f"{self._name} method has not converged sufficiently,"
|
| 307 |
+
f" running a CI-NEB calculation now may cause errors."
|
| 308 |
+
f" Please check results carefully."
|
| 309 |
+
)
|
| 310 |
+
else:
|
| 311 |
+
logger.info(
|
| 312 |
+
f"{self._name} has converged, running CI-NEB"
|
| 313 |
+
f" calculation from the end points"
|
| 314 |
+
)
|
| 315 |
+
self.imgpair.run_cineb_from_end_points()
|
| 316 |
+
return None
|
autodE/source/autode/bracket/dhs.py
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dewar-Healy-Stewart Method for finding transition states
|
| 3 |
+
|
| 4 |
+
Also implements DHS-GS, CI-DHS and CI-DHS-GS methods
|
| 5 |
+
|
| 6 |
+
[1] M. J. S. Dewar, E. Healy, J. Chem. Soc. Farady Trans. 2, 1984, 80, 227-233
|
| 7 |
+
"""
|
| 8 |
+
import numpy as np
|
| 9 |
+
from typing import Tuple, Union, Optional, Any, TYPE_CHECKING
|
| 10 |
+
from enum import Enum
|
| 11 |
+
|
| 12 |
+
from autode.values import Distance, Angle, GradientRMS, PotentialEnergy
|
| 13 |
+
from autode.bracket.imagepair import EuclideanImagePair
|
| 14 |
+
from autode.opt.coordinates import CartesianCoordinates
|
| 15 |
+
from autode.opt.optimisers.utils import TruncatedTaylor
|
| 16 |
+
from autode.opt.optimisers.hessian_update import BFGSSR1Update
|
| 17 |
+
from autode.bracket.base import BaseBracketMethod
|
| 18 |
+
from autode.opt.optimisers import RFOptimiser, ConvergenceParams
|
| 19 |
+
from autode.exceptions import OptimiserStepError
|
| 20 |
+
from autode.log import logger
|
| 21 |
+
|
| 22 |
+
if TYPE_CHECKING:
|
| 23 |
+
from autode.species.species import Species
|
| 24 |
+
from autode.wrappers.methods import Method
|
| 25 |
+
from autode.opt.optimisers.base import ConvergenceTolStr
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class DistanceConstrainedOptimiser(RFOptimiser):
|
| 29 |
+
"""
|
| 30 |
+
Constrained optimisation of a molecule, with the Euclidean
|
| 31 |
+
distance being kept constrained to a fixed value. The
|
| 32 |
+
constraint is enforced by a Lagrangian multiplier. An optional
|
| 33 |
+
linear search can be done to speed up convergence.
|
| 34 |
+
|
| 35 |
+
Same concept as that used in the corrector step of
|
| 36 |
+
Gonzalez-Schlegel second-order IRC integrator. However,
|
| 37 |
+
current implementation is modified to take steps within
|
| 38 |
+
a trust radius.
|
| 39 |
+
|
| 40 |
+
[1] C. Gonzalez, H. B. Schlegel, J. Chem. Phys., 90, 1989, 2154
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
pivot_point: Optional[CartesianCoordinates],
|
| 46 |
+
init_trust: float = 0.1,
|
| 47 |
+
line_search: bool = True,
|
| 48 |
+
angle_thresh: Angle = Angle(5, units="deg"),
|
| 49 |
+
old_coords_read_hess: Optional[CartesianCoordinates] = None,
|
| 50 |
+
*args,
|
| 51 |
+
**kwargs,
|
| 52 |
+
):
|
| 53 |
+
"""
|
| 54 |
+
Initialise a distance constrained optimiser. The pivot point
|
| 55 |
+
is the point against which the distance is constrained. Optionally
|
| 56 |
+
a linear search can be used to attempt to speed up convergence, but
|
| 57 |
+
it may not improve performance in all cases.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
init_trust: Initial trust radius in Angstrom
|
| 61 |
+
pivot_point: Coordinates of the pivot point
|
| 62 |
+
line_search: Whether to use linear search
|
| 63 |
+
angle_thresh: An angle threshold above which linear search
|
| 64 |
+
will be rejected (in Degrees)
|
| 65 |
+
old_coords_read_hess: Old coordinate with hessian which will
|
| 66 |
+
be used to obtain the initial hessian by a
|
| 67 |
+
Hessian update scheme
|
| 68 |
+
"""
|
| 69 |
+
kwargs.pop("init_alpha", None)
|
| 70 |
+
super().__init__(*args, init_alpha=init_trust, **kwargs)
|
| 71 |
+
|
| 72 |
+
if not isinstance(pivot_point, CartesianCoordinates):
|
| 73 |
+
raise NotImplementedError(
|
| 74 |
+
"Internal coordinates are not implemented in distance"
|
| 75 |
+
"constrained optimiser right now, please use Cartesian"
|
| 76 |
+
)
|
| 77 |
+
self._pivot = pivot_point
|
| 78 |
+
self._do_line_search = bool(line_search)
|
| 79 |
+
self._angle_thresh = Angle(angle_thresh, units="deg").to("radian")
|
| 80 |
+
self._target_dist: Optional[float] = None
|
| 81 |
+
|
| 82 |
+
self._hessian_update_types = [BFGSSR1Update]
|
| 83 |
+
self._old_coords = old_coords_read_hess
|
| 84 |
+
|
| 85 |
+
def _initialise_run(self) -> None:
|
| 86 |
+
"""Initialise self._coords, gradient and hessian"""
|
| 87 |
+
assert self._species is not None, "Must have a species to init run"
|
| 88 |
+
|
| 89 |
+
self._coords = CartesianCoordinates(self._species.coordinates)
|
| 90 |
+
self._target_dist = np.linalg.norm(self.dist_vec)
|
| 91 |
+
self._update_gradient_and_energy()
|
| 92 |
+
|
| 93 |
+
# Update the Hessian from old coordinates, if exists
|
| 94 |
+
if self._old_coords is not None and self._old_coords.h is not None:
|
| 95 |
+
assert isinstance(self._old_coords, CartesianCoordinates)
|
| 96 |
+
self._coords.update_h_from_old_h(
|
| 97 |
+
self._old_coords, self._hessian_update_types
|
| 98 |
+
)
|
| 99 |
+
else:
|
| 100 |
+
# no hessian available, use low level method
|
| 101 |
+
self._coords.update_h_from_cart_h(self._low_level_cart_hessian)
|
| 102 |
+
self._coords.make_hessian_positive_definite()
|
| 103 |
+
|
| 104 |
+
@property
|
| 105 |
+
def converged(self) -> bool:
|
| 106 |
+
"""Has the optimisation converged"""
|
| 107 |
+
assert self._coords is not None
|
| 108 |
+
|
| 109 |
+
# Check only the tangential component of gradient
|
| 110 |
+
g_tau = self.tangent_grad
|
| 111 |
+
rms_g_tau = np.sqrt(np.mean(np.square(g_tau)))
|
| 112 |
+
max_g_tau = np.max(np.abs(g_tau))
|
| 113 |
+
|
| 114 |
+
curr_params = self._history.conv_params()
|
| 115 |
+
curr_params.rms_g = GradientRMS(rms_g_tau, "Ha/ang")
|
| 116 |
+
curr_params.max_g = GradientRMS(max_g_tau, "Ha/ang")
|
| 117 |
+
return self.conv_tol.meets_criteria(curr_params)
|
| 118 |
+
|
| 119 |
+
@property
|
| 120 |
+
def tangent_grad(self) -> np.ndarray:
|
| 121 |
+
"""
|
| 122 |
+
Obtain the component of atomic gradients tangent to the distance
|
| 123 |
+
vector between current coords and pivot point
|
| 124 |
+
"""
|
| 125 |
+
assert self._coords is not None and self._coords.g is not None
|
| 126 |
+
|
| 127 |
+
grad = self._coords.g
|
| 128 |
+
# unit vector in the direction of distance vector
|
| 129 |
+
d_hat = self.dist_vec / np.linalg.norm(self.dist_vec)
|
| 130 |
+
tangent_grad = grad - (grad.dot(d_hat)) * d_hat
|
| 131 |
+
return tangent_grad
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def dist_vec(self) -> np.ndarray:
|
| 135 |
+
"""
|
| 136 |
+
Get the distance vector (p) between the current coordinates
|
| 137 |
+
and the pivot point
|
| 138 |
+
|
| 139 |
+
Returns:
|
| 140 |
+
(np.ndarray):
|
| 141 |
+
"""
|
| 142 |
+
return np.array(self._coords - self._pivot)
|
| 143 |
+
|
| 144 |
+
def _update_gradient_and_energy(self) -> None:
|
| 145 |
+
# Hessian update is done after en grad calculation, not in step
|
| 146 |
+
# so that it is present in the final converged coords, which
|
| 147 |
+
# can be used to start off the next batch of optimisation
|
| 148 |
+
super()._update_gradient_and_energy()
|
| 149 |
+
if self.iteration != 0:
|
| 150 |
+
assert self._coords is not None, "Must have set coordinates"
|
| 151 |
+
self._coords.update_h_from_old_h(
|
| 152 |
+
self._history.penultimate, self._hessian_update_types
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
def _step(self) -> None:
|
| 156 |
+
"""
|
| 157 |
+
A step that maintains the distance of the coordinate from
|
| 158 |
+
the pivot point. A line search is done if it is not the first
|
| 159 |
+
iteration (and it has not been turned off), and then a
|
| 160 |
+
quasi-Newton step with a Lagrangian constraint for the distance
|
| 161 |
+
is taken (falls back to steepest descent with projected gradient
|
| 162 |
+
if this fails).
|
| 163 |
+
"""
|
| 164 |
+
assert self._coords is not None, "Must have set coordinates"
|
| 165 |
+
|
| 166 |
+
# if energy is rising, interpolate halfway between last step
|
| 167 |
+
if self.iteration >= 1 and (
|
| 168 |
+
self.last_energy_change > PotentialEnergy(5, "kcalmol")
|
| 169 |
+
):
|
| 170 |
+
logger.warning("Energy rising, going back half a step")
|
| 171 |
+
half_interp = (self._coords + self._history.penultimate) / 2
|
| 172 |
+
self._coords = half_interp
|
| 173 |
+
return None
|
| 174 |
+
|
| 175 |
+
if self.iteration >= 1 and self._do_line_search:
|
| 176 |
+
coords, grad = self._line_search_on_sphere()
|
| 177 |
+
else:
|
| 178 |
+
coords, grad = self._coords, self._coords.g
|
| 179 |
+
|
| 180 |
+
try:
|
| 181 |
+
step = self._get_lagrangian_step(coords, grad)
|
| 182 |
+
logger.info(
|
| 183 |
+
f"Taking a quasi-Newton step: {np.linalg.norm(step):.3f} Å"
|
| 184 |
+
)
|
| 185 |
+
except OptimiserStepError:
|
| 186 |
+
step = self._get_sd_step(coords, grad)
|
| 187 |
+
logger.warning(
|
| 188 |
+
f"Failed to take quasi-Newton step, taking steepest "
|
| 189 |
+
f"descent step instead: {np.linalg.norm(step):.3f} Å"
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# the step is on the interpolated coordinates (if done)
|
| 193 |
+
actual_step = (coords + step) - self._coords
|
| 194 |
+
self._coords = self._coords + actual_step
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
def _get_sd_step(self, coords, grad) -> np.ndarray:
|
| 198 |
+
"""
|
| 199 |
+
Obtain a steepest descent step minimising the tangential
|
| 200 |
+
gradient. This step cannot perfectly maintain the same
|
| 201 |
+
distance from pivot point. The step size is at most half
|
| 202 |
+
of the trust radius.
|
| 203 |
+
|
| 204 |
+
Args:
|
| 205 |
+
coords: Previous coordinates
|
| 206 |
+
grad: Previous gradient
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
(np.ndarray): Step in Cartesian coordinates
|
| 210 |
+
"""
|
| 211 |
+
dist_vec = coords - self._pivot
|
| 212 |
+
dist_hat = dist_vec / np.linalg.norm(dist_vec)
|
| 213 |
+
perp_grad = grad - np.dot(grad, dist_hat) * dist_hat
|
| 214 |
+
|
| 215 |
+
sd_step = -perp_grad
|
| 216 |
+
if np.linalg.norm(sd_step) > self.alpha / 2:
|
| 217 |
+
sd_step *= (self.alpha / 2) / np.linalg.norm(sd_step)
|
| 218 |
+
|
| 219 |
+
return sd_step
|
| 220 |
+
|
| 221 |
+
def _get_lagrangian_step(self, coords, grad) -> np.ndarray:
|
| 222 |
+
"""
|
| 223 |
+
Obtain the step that will minimise the gradient tangent to
|
| 224 |
+
the distance vector from pivot point, while maintaining the
|
| 225 |
+
same distance from pivot point. Takes the step within current
|
| 226 |
+
trust radius.
|
| 227 |
+
|
| 228 |
+
Args:
|
| 229 |
+
coords: Previous coordinate (either from quasi-NR step
|
| 230 |
+
or from linear search)
|
| 231 |
+
grad: Previous gradient (either from quasi-NR or linear
|
| 232 |
+
search)
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
(np.ndarray): Step in cartesian (or mw-cartesian) coordinates
|
| 236 |
+
|
| 237 |
+
Raises:
|
| 238 |
+
OptimiserStepError: If scipy fails to calculate constrained step
|
| 239 |
+
"""
|
| 240 |
+
from scipy.optimize import minimize
|
| 241 |
+
|
| 242 |
+
assert self._coords is not None, "Must have set coordinates"
|
| 243 |
+
|
| 244 |
+
# NOTE: Since the linear interpolation should produce a point
|
| 245 |
+
# in the vicinity of the last two points, it seems reasonable to
|
| 246 |
+
# also use the hessian from the last point in the case of linear
|
| 247 |
+
# interpolation being done
|
| 248 |
+
taylor_pes = TruncatedTaylor(coords, grad, self._coords.h)
|
| 249 |
+
|
| 250 |
+
def step_size_constr(x):
|
| 251 |
+
"""step size must be <= trust radius"""
|
| 252 |
+
step_est = x - coords
|
| 253 |
+
# inequality constraint, should be > 0
|
| 254 |
+
return self.alpha - np.linalg.norm(step_est)
|
| 255 |
+
|
| 256 |
+
def lagrangian_constr(x):
|
| 257 |
+
"""step must maintain same distance from pivot"""
|
| 258 |
+
p = x - self._pivot
|
| 259 |
+
return np.linalg.norm(p) - self._target_dist
|
| 260 |
+
|
| 261 |
+
constrs = (
|
| 262 |
+
{"type": "ineq", "fun": step_size_constr},
|
| 263 |
+
{"type": "eq", "fun": lagrangian_constr},
|
| 264 |
+
)
|
| 265 |
+
# NOTE: The Lagrangian constraint should be ideally calculated using
|
| 266 |
+
# a multiplier which can be found by a 1-D root search, however, it
|
| 267 |
+
# seems to produce really large steps. So instead the constraint
|
| 268 |
+
# and the trust radius are both enforced by doing a constrained
|
| 269 |
+
# optimisation on the truncated Taylor surface, which should give
|
| 270 |
+
# a quadratic step that follows the constraint and is within trust
|
| 271 |
+
# radius
|
| 272 |
+
|
| 273 |
+
res = minimize(
|
| 274 |
+
fun=taylor_pes.value,
|
| 275 |
+
x0=np.array(self._coords),
|
| 276 |
+
method="slsqp",
|
| 277 |
+
jac=taylor_pes.gradient,
|
| 278 |
+
options={"maxiter": 2000},
|
| 279 |
+
constraints=constrs,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
if not res.success:
|
| 283 |
+
raise OptimiserStepError(
|
| 284 |
+
f"Unable to obtain distance-constrained step\nResult: {res}"
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
step = res.x - coords
|
| 288 |
+
return step
|
| 289 |
+
|
| 290 |
+
def _line_search_on_sphere(
|
| 291 |
+
self,
|
| 292 |
+
) -> Tuple[Optional[CartesianCoordinates], Optional[np.ndarray]]:
|
| 293 |
+
"""
|
| 294 |
+
Linear search on a hypersphere of radius equal to the target
|
| 295 |
+
distance.
|
| 296 |
+
|
| 297 |
+
Returns:
|
| 298 |
+
(Tuple): Interpolated coordinates and gradient as tuple
|
| 299 |
+
"""
|
| 300 |
+
assert self._coords is not None, "Must have set coords to line search"
|
| 301 |
+
|
| 302 |
+
# Eq (12) to (15) in J. Chem. Phys., 90, 1989, 2154
|
| 303 |
+
# Notation follows the publication
|
| 304 |
+
last_coords = self._history[-2]
|
| 305 |
+
assert last_coords is not None
|
| 306 |
+
|
| 307 |
+
p_prime = self.dist_vec
|
| 308 |
+
g_prime_per = self._coords.g - p_prime * (
|
| 309 |
+
np.dot(self._coords.g, p_prime) / np.dot(p_prime, p_prime)
|
| 310 |
+
)
|
| 311 |
+
g_prime_per = np.linalg.norm(g_prime_per)
|
| 312 |
+
p_prime_prime = np.array(last_coords - self._pivot)
|
| 313 |
+
g_prime_prime_per = last_coords.g - p_prime_prime * (
|
| 314 |
+
np.dot(last_coords.g, p_prime_prime)
|
| 315 |
+
/ np.dot(p_prime_prime, p_prime_prime)
|
| 316 |
+
)
|
| 317 |
+
g_prime_prime_per = np.linalg.norm(g_prime_prime_per)
|
| 318 |
+
cos_theta_prime = np.dot(p_prime, p_prime_prime) / (
|
| 319 |
+
np.linalg.norm(p_prime) * np.linalg.norm(p_prime_prime)
|
| 320 |
+
)
|
| 321 |
+
assert -1 < cos_theta_prime < 1
|
| 322 |
+
theta_prime = np.arccos(cos_theta_prime)
|
| 323 |
+
theta = (g_prime_prime_per * theta_prime) / (
|
| 324 |
+
g_prime_prime_per - g_prime_per
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
p_interp = p_prime_prime * (
|
| 328 |
+
np.cos(theta)
|
| 329 |
+
- np.sin(theta) * np.cos(theta_prime) / np.sin(theta_prime)
|
| 330 |
+
)
|
| 331 |
+
p_interp += p_prime * (np.sin(theta) / np.sin(theta_prime))
|
| 332 |
+
|
| 333 |
+
g_interp = last_coords.g * (1 - theta / theta_prime)
|
| 334 |
+
g_interp += self._coords.g * (theta / theta_prime)
|
| 335 |
+
|
| 336 |
+
x_interp = self._pivot + p_interp
|
| 337 |
+
|
| 338 |
+
step_size = np.linalg.norm(x_interp - self._coords)
|
| 339 |
+
angle_change = abs(theta_prime - theta)
|
| 340 |
+
if (
|
| 341 |
+
(
|
| 342 |
+
angle_change > self._angle_thresh
|
| 343 |
+
and abs(theta) > self._angle_thresh
|
| 344 |
+
)
|
| 345 |
+
or (
|
| 346 |
+
theta < 0 # extrapolating instead of interpolating
|
| 347 |
+
and theta_prime < self._angle_thresh
|
| 348 |
+
)
|
| 349 |
+
or (step_size > self.alpha) # larger than trust radius
|
| 350 |
+
):
|
| 351 |
+
logger.info("Linear interpolation step is unstable, skipping")
|
| 352 |
+
return self._coords, self._coords.g
|
| 353 |
+
|
| 354 |
+
logger.info(f"Linear interpolation - step size: {step_size:.3f} Å")
|
| 355 |
+
|
| 356 |
+
return x_interp, g_interp
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class ImageSide(Enum):
|
| 360 |
+
"""Represents one side of the image-pair"""
|
| 361 |
+
|
| 362 |
+
left = 0
|
| 363 |
+
right = 1
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
class DHSImagePair(EuclideanImagePair):
|
| 367 |
+
"""
|
| 368 |
+
Image-pair used for Dewar-Healy-Stewart (DHS) method to
|
| 369 |
+
find transition states. In this method, only one side is
|
| 370 |
+
modified in a step, so functions to work with only one
|
| 371 |
+
side is present here
|
| 372 |
+
"""
|
| 373 |
+
|
| 374 |
+
@property
|
| 375 |
+
def ts_guess(self) -> Optional["Species"]:
|
| 376 |
+
"""
|
| 377 |
+
In DHS method, the images can only rise in energy; therefore,
|
| 378 |
+
the highest energy image is the ts_guess. If CI-NEB is run,
|
| 379 |
+
then that result is returned instead
|
| 380 |
+
"""
|
| 381 |
+
tmp_spc = self._left_image.new_species(name="peak")
|
| 382 |
+
|
| 383 |
+
if self._cineb_coords is not None:
|
| 384 |
+
assert (
|
| 385 |
+
self._cineb_coords is not None
|
| 386 |
+
and self._cineb_coords.e
|
| 387 |
+
and self._cineb_coords.g is not None
|
| 388 |
+
)
|
| 389 |
+
tmp_spc.coordinates = self._cineb_coords
|
| 390 |
+
tmp_spc.energy = self._cineb_coords.e
|
| 391 |
+
tmp_spc.gradient = self._cineb_coords.g.reshape(-1, 3).copy()
|
| 392 |
+
return tmp_spc
|
| 393 |
+
|
| 394 |
+
# NOTE: Even though the final points are probably the highest
|
| 395 |
+
# this is not guaranteed, due to the probability of one end
|
| 396 |
+
# jumping over the barrier. So we iterate through all coords
|
| 397 |
+
|
| 398 |
+
energies = []
|
| 399 |
+
max_e = PotentialEnergy(-np.inf)
|
| 400 |
+
peak_coords: Optional[CartesianCoordinates] = None
|
| 401 |
+
for coord in self._total_history:
|
| 402 |
+
energies.append(coord.e)
|
| 403 |
+
if coord.e is None:
|
| 404 |
+
logger.error(
|
| 405 |
+
"Energy values are missing in the trajectory of this"
|
| 406 |
+
" image-pair. Unable to obtain transition state guess"
|
| 407 |
+
)
|
| 408 |
+
return None
|
| 409 |
+
if coord.e > max_e:
|
| 410 |
+
max_e = coord.e
|
| 411 |
+
peak_coords = coord
|
| 412 |
+
|
| 413 |
+
assert peak_coords is not None
|
| 414 |
+
tmp_spc.coordinates = peak_coords
|
| 415 |
+
tmp_spc.energy = peak_coords.e
|
| 416 |
+
if peak_coords.g is not None:
|
| 417 |
+
tmp_spc.gradient = peak_coords.g.reshape(-1, 3).copy()
|
| 418 |
+
return tmp_spc
|
| 419 |
+
|
| 420 |
+
def get_coord_by_side(self, side: ImageSide) -> CartesianCoordinates:
|
| 421 |
+
"""For external usage, supplies the coordinate object by side"""
|
| 422 |
+
if side == ImageSide.left:
|
| 423 |
+
return self.left_coords
|
| 424 |
+
elif side == ImageSide.right:
|
| 425 |
+
return self.right_coords
|
| 426 |
+
else:
|
| 427 |
+
raise ValueError
|
| 428 |
+
|
| 429 |
+
def put_coord_by_side(
|
| 430 |
+
self, new_coord: CartesianCoordinates, side: ImageSide
|
| 431 |
+
) -> None:
|
| 432 |
+
"""
|
| 433 |
+
For external usage, puts the new coordinate in appropriate side
|
| 434 |
+
|
| 435 |
+
Args:
|
| 436 |
+
new_coord (CartesianCoordinates): New set of coordinates
|
| 437 |
+
side (ImageSide): left or right
|
| 438 |
+
"""
|
| 439 |
+
if side == ImageSide.left:
|
| 440 |
+
self.left_coords = new_coord
|
| 441 |
+
elif side == ImageSide.right:
|
| 442 |
+
self.right_coords = new_coord
|
| 443 |
+
else:
|
| 444 |
+
raise ValueError
|
| 445 |
+
return None
|
| 446 |
+
|
| 447 |
+
def get_last_step_by_side(self, side: ImageSide) -> Optional[np.ndarray]:
|
| 448 |
+
"""
|
| 449 |
+
Obtain the last step on the provided side (for the Growing
|
| 450 |
+
String like step)
|
| 451 |
+
"""
|
| 452 |
+
if side == ImageSide.left:
|
| 453 |
+
hist = self._left_history
|
| 454 |
+
elif side == ImageSide.right:
|
| 455 |
+
hist = self._right_history
|
| 456 |
+
else:
|
| 457 |
+
raise ValueError
|
| 458 |
+
|
| 459 |
+
if len(hist) < 2:
|
| 460 |
+
return None
|
| 461 |
+
return hist.final - hist.penultimate
|
| 462 |
+
|
| 463 |
+
def get_dhs_step_by_side(
|
| 464 |
+
self, side: ImageSide, step_size: float
|
| 465 |
+
) -> np.ndarray:
|
| 466 |
+
"""
|
| 467 |
+
Obtain the DHS extrapolation step on the specified side,
|
| 468 |
+
with the specified step size
|
| 469 |
+
|
| 470 |
+
Args:
|
| 471 |
+
side (ImageSide): left or right
|
| 472 |
+
step_size (float): Step size in Angstrom
|
| 473 |
+
|
| 474 |
+
Returns:
|
| 475 |
+
(np.ndarray): The step
|
| 476 |
+
"""
|
| 477 |
+
dhs_step = self.dist_vec * (step_size / self.dist)
|
| 478 |
+
if side == ImageSide.left:
|
| 479 |
+
dhs_step *= -1.0
|
| 480 |
+
elif side == ImageSide.right:
|
| 481 |
+
pass
|
| 482 |
+
else:
|
| 483 |
+
raise ValueError
|
| 484 |
+
|
| 485 |
+
return dhs_step
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
class DHS(BaseBracketMethod):
|
| 489 |
+
"""
|
| 490 |
+
Dewar-Healy-Stewart method for finding transition states,
|
| 491 |
+
from the reactant and product structures
|
| 492 |
+
"""
|
| 493 |
+
|
| 494 |
+
def __init__(
|
| 495 |
+
self,
|
| 496 |
+
initial_species: "Species",
|
| 497 |
+
final_species: "Species",
|
| 498 |
+
large_step: Union[Distance, float] = Distance(0.2, "ang"),
|
| 499 |
+
small_step: Union[Distance, float] = Distance(0.05, "ang"),
|
| 500 |
+
switch_thresh: Union[Distance, float] = Distance(1.5, "ang"),
|
| 501 |
+
conv_tol: Union["ConvergenceParams", "ConvergenceTolStr"] = "loose",
|
| 502 |
+
**kwargs,
|
| 503 |
+
):
|
| 504 |
+
"""
|
| 505 |
+
Dewar-Healy-Stewart method to find transition states. The distance
|
| 506 |
+
tolerance convergence criteria should not be much lower than 0.5 Angstrom
|
| 507 |
+
as DHS is unstable when the distance is low, and there is a tendency for
|
| 508 |
+
one image to jumpy over the barrier.
|
| 509 |
+
|
| 510 |
+
Args:
|
| 511 |
+
initial_species: The "reactant" species
|
| 512 |
+
|
| 513 |
+
final_species: The "product" species
|
| 514 |
+
|
| 515 |
+
large_step: The size of the DHS step when distance between the
|
| 516 |
+
images is larger than switch_thresh (Angstrom)
|
| 517 |
+
|
| 518 |
+
small_step: The size of the DHS step when distance between the
|
| 519 |
+
images is smaller than swtich_thresh (Angstrom)
|
| 520 |
+
|
| 521 |
+
switch_thresh: When distance between the two images is less than
|
| 522 |
+
this cutoff, smaller DHS extrapolation steps are taken
|
| 523 |
+
|
| 524 |
+
conv_tol: Convergence tolerance for the distance-constrained
|
| 525 |
+
optimiser
|
| 526 |
+
|
| 527 |
+
Keyword Args:
|
| 528 |
+
|
| 529 |
+
maxiter: Maximum number of en/grad evaluations
|
| 530 |
+
|
| 531 |
+
dist_tol: The distance tolerance at which DHS will
|
| 532 |
+
stop, values less than 0.5 Angstrom are not
|
| 533 |
+
recommended.
|
| 534 |
+
|
| 535 |
+
cineb_at_conv: Whether to run CI-NEB calculation from the end
|
| 536 |
+
points after the DHS is converged
|
| 537 |
+
"""
|
| 538 |
+
super().__init__(initial_species, final_species, **kwargs)
|
| 539 |
+
|
| 540 |
+
# imgpair is only used for storing the points here
|
| 541 |
+
self.imgpair: DHSImagePair = DHSImagePair(
|
| 542 |
+
initial_species, final_species
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
# DHS needs to keep an extra reference method and n_cores
|
| 546 |
+
self._method: Optional[Method] = None
|
| 547 |
+
self._n_cores: Optional[int] = None
|
| 548 |
+
|
| 549 |
+
self._large_step = Distance(abs(large_step), "ang")
|
| 550 |
+
self._small_step = Distance(abs(small_step), "ang")
|
| 551 |
+
self._sw_thresh = Distance(abs(switch_thresh), "ang")
|
| 552 |
+
assert self._small_step < self._large_step
|
| 553 |
+
self._conv_tol = conv_tol
|
| 554 |
+
|
| 555 |
+
self._step_size: Optional[Distance] = None
|
| 556 |
+
if self._large_step > self.imgpair.dist:
|
| 557 |
+
logger.warning(
|
| 558 |
+
f"Step size ({self._large_step:.3f} Å) for {self._name}"
|
| 559 |
+
f" is larger than the starting Euclidean distance between"
|
| 560 |
+
f" images ({self.imgpair.dist:.3f} Å). This calculation"
|
| 561 |
+
f" will likely run into errors."
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
# NOTE: In DHS the micro-iterations are done separately in
|
| 565 |
+
# an optimiser, so keep track with local variable
|
| 566 |
+
self._current_microiters: int = 0
|
| 567 |
+
|
| 568 |
+
def _initialise_run(self) -> None:
|
| 569 |
+
"""
|
| 570 |
+
Initialise energies/gradients for the first DHS macro-iteration
|
| 571 |
+
"""
|
| 572 |
+
self.imgpair.update_both_img_engrad()
|
| 573 |
+
return None
|
| 574 |
+
|
| 575 |
+
def _step(self) -> None:
|
| 576 |
+
"""
|
| 577 |
+
A DHS step consists of a macro-iteration step, where a step along
|
| 578 |
+
the linear path between two images is taken, and several micro-iteration
|
| 579 |
+
steps in the distance-constrained optimiser, to return to the MEP
|
| 580 |
+
"""
|
| 581 |
+
assert self._method is not None, "Must have a set method"
|
| 582 |
+
assert self.imgpair.left_coords.e and self.imgpair.right_coords.e
|
| 583 |
+
|
| 584 |
+
if self.imgpair.dist > self._sw_thresh:
|
| 585 |
+
self._step_size = self._large_step
|
| 586 |
+
else:
|
| 587 |
+
self._step_size = self._small_step
|
| 588 |
+
opt_trust = min(self._step_size, Distance(0.1, "ang"))
|
| 589 |
+
|
| 590 |
+
if self.imgpair.left_coords.e < self.imgpair.right_coords.e:
|
| 591 |
+
side = ImageSide.left
|
| 592 |
+
pivot = self.imgpair.right_coords
|
| 593 |
+
else:
|
| 594 |
+
side = ImageSide.right
|
| 595 |
+
pivot = self.imgpair.left_coords
|
| 596 |
+
|
| 597 |
+
old_coords: Any = self.imgpair.get_coord_by_side(side)
|
| 598 |
+
old_coords = old_coords if old_coords.h is not None else None
|
| 599 |
+
# take a DHS step on the side with lower energy
|
| 600 |
+
new_coord = self._get_dhs_step(side)
|
| 601 |
+
|
| 602 |
+
# calculate the number of remaining maxiter to feed into optimiser
|
| 603 |
+
curr_maxiter = self._maxiter - self._current_microiters
|
| 604 |
+
if curr_maxiter <= 0:
|
| 605 |
+
return None
|
| 606 |
+
|
| 607 |
+
opt = DistanceConstrainedOptimiser(
|
| 608 |
+
maxiter=curr_maxiter,
|
| 609 |
+
conv_tol=self._conv_tol,
|
| 610 |
+
init_trust=opt_trust,
|
| 611 |
+
pivot_point=pivot,
|
| 612 |
+
old_coords_read_hess=old_coords,
|
| 613 |
+
)
|
| 614 |
+
tmp_spc = self._species.copy()
|
| 615 |
+
tmp_spc.coordinates = new_coord
|
| 616 |
+
opt.run(tmp_spc, self._method, self._n_cores)
|
| 617 |
+
self._micro_iter = self._micro_iter + opt.iteration
|
| 618 |
+
|
| 619 |
+
# not converged can only happen if exceeded maxiter of optimiser
|
| 620 |
+
if not opt.converged:
|
| 621 |
+
return None
|
| 622 |
+
|
| 623 |
+
rms_g_tau = np.sqrt(np.mean(np.square(opt.tangent_grad)))
|
| 624 |
+
logger.info(
|
| 625 |
+
"Successful optimization after DHS step, final RMS of "
|
| 626 |
+
f"tangential gradient = {rms_g_tau:.6f} "
|
| 627 |
+
f"Ha/angstrom"
|
| 628 |
+
)
|
| 629 |
+
|
| 630 |
+
# put results back into imagepair
|
| 631 |
+
self.imgpair.put_coord_by_side(opt.final_coordinates, side) # type: ignore
|
| 632 |
+
opt.clean_up()
|
| 633 |
+
return None
|
| 634 |
+
|
| 635 |
+
def _calculate(
|
| 636 |
+
self, method: "Method", n_cores: Optional[int] = None
|
| 637 |
+
) -> None:
|
| 638 |
+
"""
|
| 639 |
+
Run the DHS calculation and CI-NEB if requested.
|
| 640 |
+
|
| 641 |
+
Args:
|
| 642 |
+
method (Method): Method used for calculating energy/gradients
|
| 643 |
+
n_cores (int): Number of cores to use for calculation
|
| 644 |
+
"""
|
| 645 |
+
self._method = method
|
| 646 |
+
self._n_cores = n_cores
|
| 647 |
+
super()._calculate(method, n_cores)
|
| 648 |
+
|
| 649 |
+
@property
|
| 650 |
+
def _macro_iter(self):
|
| 651 |
+
"""Total number of DHS steps taken so far"""
|
| 652 |
+
# ImagePair only stores the converged coordinates, which
|
| 653 |
+
# is equal to the number of macro-iterations (DHS steps)
|
| 654 |
+
return self.imgpair.total_iters
|
| 655 |
+
|
| 656 |
+
@property
|
| 657 |
+
def _micro_iter(self) -> int:
|
| 658 |
+
"""Total number of optimiser steps in DHS"""
|
| 659 |
+
return self._current_microiters
|
| 660 |
+
|
| 661 |
+
@_micro_iter.setter
|
| 662 |
+
def _micro_iter(self, value: int):
|
| 663 |
+
"""
|
| 664 |
+
For DHS the number of microiters has to be manually
|
| 665 |
+
set
|
| 666 |
+
|
| 667 |
+
Args:
|
| 668 |
+
value (int):
|
| 669 |
+
"""
|
| 670 |
+
self._current_microiters = int(value)
|
| 671 |
+
|
| 672 |
+
def _get_dhs_step(self, side: ImageSide) -> CartesianCoordinates:
|
| 673 |
+
"""
|
| 674 |
+
Take a DHS step, on the side requested, along the distance
|
| 675 |
+
vector between the two images, and return the new coordinates
|
| 676 |
+
after taking the step
|
| 677 |
+
|
| 678 |
+
Args:
|
| 679 |
+
side (ImageSide): left or right
|
| 680 |
+
|
| 681 |
+
Returns:
|
| 682 |
+
(CartesianCoordinates): New predicted coordinates for that side
|
| 683 |
+
"""
|
| 684 |
+
assert self._step_size is not None
|
| 685 |
+
# take a DHS step of the size given
|
| 686 |
+
dhs_step = self.imgpair.get_dhs_step_by_side(side, self._step_size)
|
| 687 |
+
|
| 688 |
+
old_coord = self.imgpair.get_coord_by_side(side)
|
| 689 |
+
new_coord = old_coord + dhs_step
|
| 690 |
+
|
| 691 |
+
logger.info(
|
| 692 |
+
f"DHS step on {side} image: taking a step of"
|
| 693 |
+
f" size {np.linalg.norm(dhs_step):.4f} Å"
|
| 694 |
+
)
|
| 695 |
+
return new_coord
|
| 696 |
+
|
| 697 |
+
|
| 698 |
+
class DHSGS(DHS):
|
| 699 |
+
"""
|
| 700 |
+
Dewar-Healy-Stewart method, augmented with Growing String (GS)
|
| 701 |
+
method. The DHS step (stepping along the linear interpolated
|
| 702 |
+
path between the two images) is mixed with a GS step (linear
|
| 703 |
+
interpolation along last and current position of one image)
|
| 704 |
+
in a fixed ratio.
|
| 705 |
+
|
| 706 |
+
Proposed by J. Kilmes, D. R. Bowler, A. Michaelides,
|
| 707 |
+
J. Phys.: Condens. Matter, 2010, 22(7), 074203
|
| 708 |
+
"""
|
| 709 |
+
|
| 710 |
+
def __init__(self, *args, gs_mix: float = 0.5, **kwargs):
|
| 711 |
+
"""
|
| 712 |
+
Arguments and other keyword arguments follow DHS, please
|
| 713 |
+
see :py:meth:`DHS <autode.bracket.dhs.DHS.__init__>`
|
| 714 |
+
|
| 715 |
+
Keyword Args:
|
| 716 |
+
gs_mix (float): Represents the percentage of mixing of the
|
| 717 |
+
Growing String step with the DHS step. 0.3
|
| 718 |
+
means 0.3 * GS_step + (1-0.3) * DHS_step
|
| 719 |
+
It is not recommended to set this higher
|
| 720 |
+
than 0.5
|
| 721 |
+
"""
|
| 722 |
+
super().__init__(*args, **kwargs)
|
| 723 |
+
|
| 724 |
+
self._gs_mix = float(gs_mix)
|
| 725 |
+
assert 0.0 < self._gs_mix < 1.0, "Mixing factor must be 0 < fac < 1"
|
| 726 |
+
|
| 727 |
+
def _get_dhs_step(self, side: ImageSide) -> CartesianCoordinates:
|
| 728 |
+
"""
|
| 729 |
+
Take a mixed DHS and GS step (interpolates between the two
|
| 730 |
+
vectors) in the given ratio, and then return the new
|
| 731 |
+
coordinates after taking the step
|
| 732 |
+
|
| 733 |
+
Args:
|
| 734 |
+
side (ImageSide):
|
| 735 |
+
|
| 736 |
+
Returns:
|
| 737 |
+
(CartesianCoordinates): New predicted coordinates for that side
|
| 738 |
+
"""
|
| 739 |
+
assert self.imgpair is not None, "Must have an image pair"
|
| 740 |
+
assert self._step_size is not None
|
| 741 |
+
|
| 742 |
+
dhs_step = self.imgpair.get_dhs_step_by_side(side, self._step_size)
|
| 743 |
+
gs_step = self.imgpair.get_last_step_by_side(side)
|
| 744 |
+
|
| 745 |
+
if gs_step is None:
|
| 746 |
+
gs_step = np.zeros_like(dhs_step)
|
| 747 |
+
# hack to ensure the first step is 100% DHS (as GS is not possible)
|
| 748 |
+
dhs_step = dhs_step / (1 - self._gs_mix)
|
| 749 |
+
else:
|
| 750 |
+
# rescale GS step as well so that one vector doesn't dominate
|
| 751 |
+
gs_step *= np.linalg.norm(dhs_step) / np.linalg.norm(gs_step)
|
| 752 |
+
|
| 753 |
+
old_coord = self.imgpair.get_coord_by_side(side)
|
| 754 |
+
new_coord = (
|
| 755 |
+
old_coord + (1 - self._gs_mix) * dhs_step + self._gs_mix * gs_step
|
| 756 |
+
)
|
| 757 |
+
# step size is variable due to adding GS component
|
| 758 |
+
step_size = np.linalg.norm(new_coord - old_coord)
|
| 759 |
+
logger.info(
|
| 760 |
+
f"DHS-GS step on {side} image: taking a step "
|
| 761 |
+
f"of size {step_size:.4f}"
|
| 762 |
+
)
|
| 763 |
+
|
| 764 |
+
return new_coord
|
autodE/source/autode/bracket/ieip.py
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Improved Elastic Image Pair method for finding transition states.
|
| 3 |
+
|
| 4 |
+
References:
|
| 5 |
+
|
| 6 |
+
[1] Y. Liu, H. Qi, M. Lei, J. Chem. Theory Comput., 2023, 19, 2410-2417
|
| 7 |
+
"""
|
| 8 |
+
from typing import Union, Optional, Tuple, List, TYPE_CHECKING
|
| 9 |
+
import numpy as np
|
| 10 |
+
from autode.methods import get_lmethod
|
| 11 |
+
from autode.bracket.base import BaseBracketMethod
|
| 12 |
+
from autode.bracket.dhs import TruncatedTaylor
|
| 13 |
+
from autode.neb import NEB
|
| 14 |
+
from autode.path.interpolation import CubicPathSpline
|
| 15 |
+
from autode.bracket.imagepair import EuclideanImagePair
|
| 16 |
+
from autode.opt.coordinates import CartesianCoordinates
|
| 17 |
+
from autode.values import Distance, GradientRMS, PotentialEnergy
|
| 18 |
+
from autode.utils import ProcessPool
|
| 19 |
+
from autode.log import logger
|
| 20 |
+
|
| 21 |
+
if TYPE_CHECKING:
|
| 22 |
+
from autode.species.species import Species
|
| 23 |
+
from autode.wrappers.methods import Method
|
| 24 |
+
|
| 25 |
+
_interp_image_density = 1.0 # per Angstrom for initial interpolation
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _calculate_low_sp_energy_for_species(
|
| 29 |
+
species: "Species", method: "Method", n_cores: int
|
| 30 |
+
) -> "PotentialEnergy":
|
| 31 |
+
"""
|
| 32 |
+
Convenience function to calculate the energy for a given species
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
species (Species): The species object
|
| 36 |
+
method (Method): The method (low_sp keywords will be used)
|
| 37 |
+
n_cores (int): The number of cores
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
(PotentialEnergy): The single point energy of the species
|
| 41 |
+
"""
|
| 42 |
+
from autode import Calculation
|
| 43 |
+
|
| 44 |
+
sp_calc = Calculation(
|
| 45 |
+
name=f"{species.name}_sp",
|
| 46 |
+
molecule=species,
|
| 47 |
+
method=method,
|
| 48 |
+
keywords=method.keywords.low_sp, # NOTE: We use low_sp
|
| 49 |
+
n_cores=n_cores,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
sp_calc.run()
|
| 53 |
+
sp_calc.clean_up(force=True, everything=True)
|
| 54 |
+
|
| 55 |
+
assert species.energy is not None
|
| 56 |
+
return species.energy
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _parallel_calc_energies(
|
| 60 |
+
points: List["Species"], method: "Method", n_cores: int
|
| 61 |
+
) -> List["PotentialEnergy"]:
|
| 62 |
+
"""
|
| 63 |
+
Calculate the single point energies on a list of species with
|
| 64 |
+
parallel runs
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
points (list[Species]): A list of the species
|
| 68 |
+
method (Method): The method (low_sp keywords will be used)
|
| 69 |
+
n_cores (int): Total number of cores for all calculations
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
(list[PotentialEnergy]): List of energies in order
|
| 73 |
+
"""
|
| 74 |
+
n_cores_per_pp = max(n_cores // len(points), 1)
|
| 75 |
+
n_procs = min(n_cores, len(points))
|
| 76 |
+
|
| 77 |
+
with ProcessPool(max_workers=n_procs) as pool:
|
| 78 |
+
jobs = [
|
| 79 |
+
pool.submit(
|
| 80 |
+
_calculate_low_sp_energy_for_species,
|
| 81 |
+
species=point,
|
| 82 |
+
method=method,
|
| 83 |
+
n_cores=n_cores_per_pp,
|
| 84 |
+
)
|
| 85 |
+
for point in points
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
energies = [job.result() for job in jobs]
|
| 89 |
+
|
| 90 |
+
return energies
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class ElasticImagePair(EuclideanImagePair):
|
| 94 |
+
"""
|
| 95 |
+
This image-pair used for the Elastic Image Pair calculation. The
|
| 96 |
+
geometries after every macro-iteration are stored
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def last_left_step_size(self) -> float:
|
| 101 |
+
"""The last step size on the left image"""
|
| 102 |
+
return np.linalg.norm(self.left_coords - self._left_history[-2])
|
| 103 |
+
|
| 104 |
+
@property
|
| 105 |
+
def last_right_step_size(self):
|
| 106 |
+
"""The last step size on the right image"""
|
| 107 |
+
return np.linalg.norm(self.right_coords - self._right_history[-2])
|
| 108 |
+
|
| 109 |
+
@property
|
| 110 |
+
def ts_guess(self) -> Optional["Species"]:
|
| 111 |
+
"""
|
| 112 |
+
Obtain the TS guess from the i-EIP image pair. The midpoint
|
| 113 |
+
between the two converged images is considered the TS guess
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
(Species|None): The ts guess species, if images are available
|
| 117 |
+
"""
|
| 118 |
+
# account for initial redistribution
|
| 119 |
+
if self.total_iters <= 2:
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
tmp_spc = self._left_image.new_species(name="peak")
|
| 123 |
+
midpt_coords = np.array(self.left_coords + self.right_coords) / 2
|
| 124 |
+
tmp_spc.coordinates = midpt_coords
|
| 125 |
+
return tmp_spc
|
| 126 |
+
|
| 127 |
+
@property
|
| 128 |
+
def perp_rms_gs(self) -> Tuple[GradientRMS, GradientRMS]:
|
| 129 |
+
"""
|
| 130 |
+
The RMS norms of perpendicular gradient component for left
|
| 131 |
+
and right image, in order. The parallel component against
|
| 132 |
+
the distance vector is projected out.
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
(tuple[GradientRMS, GradientRMS]):
|
| 136 |
+
"""
|
| 137 |
+
perp_gradients = []
|
| 138 |
+
d_hat = self.dist_vec / np.linalg.norm(self.dist_vec)
|
| 139 |
+
for coord in [self.left_coords, self.right_coords]:
|
| 140 |
+
parall_g = d_hat * np.dot(d_hat, coord.g)
|
| 141 |
+
perp_g = coord.g - parall_g
|
| 142 |
+
rms_perp_g = np.sqrt(np.mean(np.square(perp_g)))
|
| 143 |
+
perp_gradients.append(GradientRMS(rms_perp_g))
|
| 144 |
+
|
| 145 |
+
return perp_gradients[0], perp_gradients[1]
|
| 146 |
+
|
| 147 |
+
def redistribute_imagepair(
|
| 148 |
+
self,
|
| 149 |
+
ll_neb_interp: bool = True,
|
| 150 |
+
interp_fraction: float = 1 / 4,
|
| 151 |
+
):
|
| 152 |
+
"""
|
| 153 |
+
Redistribute the image pair by running a NEB calculation at lmethod or
|
| 154 |
+
use only IDPP interpolation, and then fitting a cubic spline on energy
|
| 155 |
+
calculated by the method (low_sp keywords). It generates the image pair
|
| 156 |
+
on both sides of the peak on the fitted spline, with a distance of
|
| 157 |
+
interp_fraction * total path distance on either side.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
ll_neb_interp (bool): Whether to optimise the interpolated path with
|
| 161 |
+
NEB at lmethod for the interpolation.
|
| 162 |
+
|
| 163 |
+
interp_fraction (float): Fraction of total interpolated path distance
|
| 164 |
+
that will be used to generate the image pair
|
| 165 |
+
on either side of the interpolated TS
|
| 166 |
+
"""
|
| 167 |
+
# Use at least 5 images for interpolation
|
| 168 |
+
n_images = int(_interp_image_density * self.dist - 1)
|
| 169 |
+
n_images = max(n_images, 5 + 2)
|
| 170 |
+
|
| 171 |
+
interp = NEB.from_end_points(
|
| 172 |
+
self._left_image.copy(), self._right_image.copy(), n_images
|
| 173 |
+
)
|
| 174 |
+
if ll_neb_interp:
|
| 175 |
+
interp.calculate(method=get_lmethod(), n_cores=self._n_cores)
|
| 176 |
+
|
| 177 |
+
# Only calc intermediate images, initial and final already have energies
|
| 178 |
+
path_points = interp.images[1:-1]
|
| 179 |
+
# Get energies
|
| 180 |
+
assert self.left_coords.e and self.right_coords.e
|
| 181 |
+
assert self._method is not None and self._n_cores is not None
|
| 182 |
+
path_energies = _parallel_calc_energies(
|
| 183 |
+
path_points, method=self._method, n_cores=self._n_cores
|
| 184 |
+
)
|
| 185 |
+
energies = [self.left_coords.e] + path_energies + [self.right_coords.e]
|
| 186 |
+
logger.info(
|
| 187 |
+
f"Fitting parametric spline on {len(interp.images)} points"
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
# NOTE: Here we are fitting a parametric spline, with the parameter
|
| 191 |
+
# being the path length along approx. rxn coordinate and target being all
|
| 192 |
+
# coordinates *and* energy at the points
|
| 193 |
+
path_spline = CubicPathSpline.from_species_list(interp.images)
|
| 194 |
+
path_spline.fit_energies(energies)
|
| 195 |
+
peak_x = path_spline.energy_peak()
|
| 196 |
+
if peak_x is None:
|
| 197 |
+
raise RuntimeError(
|
| 198 |
+
"The fitted spline does not have a peak! Unable to proceed"
|
| 199 |
+
)
|
| 200 |
+
# Check the peak is not at the beginning or end
|
| 201 |
+
assert 0.01 < peak_x < 0.99
|
| 202 |
+
# convert to integrated arc lengths
|
| 203 |
+
peak_pos = path_spline.path_integral(0, peak_x)
|
| 204 |
+
path_length = path_spline.path_integral(0, 1)
|
| 205 |
+
|
| 206 |
+
# Generate new coordinates a fraction (default 1/4) of total distance
|
| 207 |
+
# on each side of the peak (interpolated TS)
|
| 208 |
+
left_span = peak_pos - interp_fraction * path_length
|
| 209 |
+
if left_span <= 0.01:
|
| 210 |
+
l_point = 0.0
|
| 211 |
+
else:
|
| 212 |
+
l_point = path_spline.integrate_upto_length(
|
| 213 |
+
span=left_span,
|
| 214 |
+
)
|
| 215 |
+
r_point = path_spline.integrate_upto_length(
|
| 216 |
+
span=peak_pos + interp_fraction * path_length,
|
| 217 |
+
)
|
| 218 |
+
if r_point > 1:
|
| 219 |
+
r_point = 1
|
| 220 |
+
self.left_coords = CartesianCoordinates(path_spline.coords_at(l_point))
|
| 221 |
+
self.right_coords = CartesianCoordinates(
|
| 222 |
+
path_spline.coords_at(r_point)
|
| 223 |
+
)
|
| 224 |
+
return None
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
class IEIPMicroIters:
|
| 228 |
+
"""
|
| 229 |
+
Class to carry out the micro-iterations for the i-EIP
|
| 230 |
+
method
|
| 231 |
+
"""
|
| 232 |
+
|
| 233 |
+
def __init__(
|
| 234 |
+
self,
|
| 235 |
+
left_coords: CartesianCoordinates,
|
| 236 |
+
right_coords: CartesianCoordinates,
|
| 237 |
+
micro_step_size: Union[Distance, float],
|
| 238 |
+
target_dist: Union[Distance, float],
|
| 239 |
+
):
|
| 240 |
+
# generate the Taylor expansion surface from gradient and hessian
|
| 241 |
+
assert left_coords.g is not None and left_coords.h is not None
|
| 242 |
+
assert right_coords.g is not None and right_coords.h is not None
|
| 243 |
+
self._left_taylor_pes = TruncatedTaylor(
|
| 244 |
+
left_coords, left_coords.g, left_coords.h
|
| 245 |
+
)
|
| 246 |
+
self._right_taylor_pes = TruncatedTaylor(
|
| 247 |
+
right_coords, right_coords.g, right_coords.h
|
| 248 |
+
)
|
| 249 |
+
self._micro_step = float(Distance(micro_step_size, "ang"))
|
| 250 |
+
self._target_dist = float(Distance(target_dist, "ang"))
|
| 251 |
+
self.n_micro_iters = 0 # counter
|
| 252 |
+
self.left_coords = left_coords.copy()
|
| 253 |
+
self.right_coords = right_coords.copy()
|
| 254 |
+
# keep this in memory to calculate how much the coords have moved
|
| 255 |
+
self._start_left_coords = left_coords
|
| 256 |
+
self._start_right_coords = right_coords
|
| 257 |
+
|
| 258 |
+
def update_both_img_engrad(self) -> None:
|
| 259 |
+
"""
|
| 260 |
+
Update the energy and gradient from the Taylor surface
|
| 261 |
+
"""
|
| 262 |
+
self.left_coords.e = PotentialEnergy(
|
| 263 |
+
self._left_taylor_pes.value(self.left_coords)
|
| 264 |
+
)
|
| 265 |
+
self.left_coords.g = self._left_taylor_pes.gradient(self.left_coords)
|
| 266 |
+
self.right_coords.e = PotentialEnergy(
|
| 267 |
+
self._right_taylor_pes.value(self.right_coords)
|
| 268 |
+
)
|
| 269 |
+
self.right_coords.g = self._right_taylor_pes.gradient(
|
| 270 |
+
self.right_coords
|
| 271 |
+
)
|
| 272 |
+
return None
|
| 273 |
+
|
| 274 |
+
@property
|
| 275 |
+
def _n_hat(self):
|
| 276 |
+
dist_vec = np.array(self.left_coords - self.right_coords)
|
| 277 |
+
return dist_vec / np.linalg.norm(dist_vec)
|
| 278 |
+
|
| 279 |
+
def _get_perpendicular_micro_steps(self) -> List[np.ndarray]:
|
| 280 |
+
"""
|
| 281 |
+
Obtain the perpendicular displacement for one i-EIP micro-iteration,
|
| 282 |
+
minimises the energy in the direction perpendicular to the distance
|
| 283 |
+
vector connecting the image pair
|
| 284 |
+
|
| 285 |
+
Returns:
|
| 286 |
+
(list[np.ndarray]): A list of steps for left and right
|
| 287 |
+
image, in order
|
| 288 |
+
"""
|
| 289 |
+
assert self.left_coords.g is not None
|
| 290 |
+
assert self.right_coords.g is not None
|
| 291 |
+
steps = []
|
| 292 |
+
for coord in [self.left_coords, self.right_coords]:
|
| 293 |
+
force = -coord.g # type: ignore
|
| 294 |
+
force_parall = self._n_hat * np.dot(force, self._n_hat)
|
| 295 |
+
force_perp = force - force_parall
|
| 296 |
+
if np.linalg.norm(force_perp) > self._micro_step:
|
| 297 |
+
delta_x_perp = force_perp / np.linalg.norm(force_perp)
|
| 298 |
+
delta_x_perp *= self._micro_step
|
| 299 |
+
else:
|
| 300 |
+
delta_x_perp = force_perp
|
| 301 |
+
steps.append(delta_x_perp)
|
| 302 |
+
|
| 303 |
+
return steps
|
| 304 |
+
|
| 305 |
+
def _get_energy_micro_steps(self) -> List[np.ndarray]:
|
| 306 |
+
"""
|
| 307 |
+
Obtain the energy based displacement term for one i-EIP
|
| 308 |
+
micro-iteration. This term minimises the energy difference
|
| 309 |
+
between the two images
|
| 310 |
+
|
| 311 |
+
Returns:
|
| 312 |
+
(list[np.ndarray]): A list of steps for the left and right
|
| 313 |
+
image, in order
|
| 314 |
+
"""
|
| 315 |
+
# NOTE: The sign is flipped here, because n_hat is
|
| 316 |
+
# defined in the opposite direction i.e. left - right
|
| 317 |
+
assert self.left_coords.e and self.right_coords.e
|
| 318 |
+
dist = np.linalg.norm(self.left_coords - self.right_coords)
|
| 319 |
+
f_de = (self.left_coords.e - self.right_coords.e) / float(dist)
|
| 320 |
+
f_de = self._n_hat * float(f_de)
|
| 321 |
+
if np.linalg.norm(f_de) > self._micro_step:
|
| 322 |
+
delta_x_e = f_de / np.linalg.norm(f_de)
|
| 323 |
+
delta_x_e *= self._micro_step
|
| 324 |
+
else:
|
| 325 |
+
delta_x_e = f_de
|
| 326 |
+
return [delta_x_e, delta_x_e]
|
| 327 |
+
|
| 328 |
+
def _get_distance_micro_steps(self):
|
| 329 |
+
"""
|
| 330 |
+
Obtain the displacement term that controls the distance between
|
| 331 |
+
the two images. This term moves the images so that their distance
|
| 332 |
+
can be closer to the target distance in the current macro-iteration
|
| 333 |
+
|
| 334 |
+
Returns:
|
| 335 |
+
(list[np.ndarray]): A list of steps for the left and right
|
| 336 |
+
images, in order
|
| 337 |
+
"""
|
| 338 |
+
# NOTE: The factor k that appears in eqn.(1) of the i-EIP paper
|
| 339 |
+
# has been absorbed into the term in this function (i.e. the function
|
| 340 |
+
# returns the displacements with the proper sign)
|
| 341 |
+
dist_vec = np.array(self.left_coords - self.right_coords)
|
| 342 |
+
dist = np.linalg.norm(dist_vec)
|
| 343 |
+
f_l = -dist_vec * 2 * (dist - self._target_dist) / dist
|
| 344 |
+
if np.linalg.norm(f_l) > self._micro_step:
|
| 345 |
+
delta_x_l = f_l / np.linalg.norm(f_l)
|
| 346 |
+
delta_x_l *= self._micro_step
|
| 347 |
+
else:
|
| 348 |
+
delta_x_l = f_l
|
| 349 |
+
return [delta_x_l, -delta_x_l]
|
| 350 |
+
|
| 351 |
+
def take_micro_step(self) -> None:
|
| 352 |
+
"""
|
| 353 |
+
Take a single i-EIP micro-iteration step (which is a sum of the
|
| 354 |
+
perpendicular, energy and distance terms)
|
| 355 |
+
"""
|
| 356 |
+
perp_steps = self._get_perpendicular_micro_steps()
|
| 357 |
+
energy_steps = self._get_energy_micro_steps()
|
| 358 |
+
dist_steps = self._get_distance_micro_steps()
|
| 359 |
+
|
| 360 |
+
# sum the micro-iteration step components
|
| 361 |
+
left_step = perp_steps[0] + energy_steps[0] + dist_steps[0]
|
| 362 |
+
right_step = perp_steps[1] + energy_steps[1] + dist_steps[1]
|
| 363 |
+
|
| 364 |
+
# scale the steps within the microiter step size
|
| 365 |
+
if np.linalg.norm(left_step) > self._micro_step:
|
| 366 |
+
left_step *= self._micro_step / np.linalg.norm(left_step)
|
| 367 |
+
if np.linalg.norm(right_step) > self._micro_step:
|
| 368 |
+
right_step *= self._micro_step / np.linalg.norm(right_step)
|
| 369 |
+
|
| 370 |
+
self.left_coords = self.left_coords + left_step
|
| 371 |
+
self.right_coords = self.right_coords + right_step
|
| 372 |
+
self.n_micro_iters += 1
|
| 373 |
+
return None
|
| 374 |
+
|
| 375 |
+
@property
|
| 376 |
+
def max_displacement(self) -> float:
|
| 377 |
+
left_displ = np.linalg.norm(self.left_coords - self._start_left_coords)
|
| 378 |
+
right_displ = np.linalg.norm(
|
| 379 |
+
self.right_coords - self._start_right_coords
|
| 380 |
+
)
|
| 381 |
+
return max(left_displ, right_displ)
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
class IEIP(BaseBracketMethod):
|
| 385 |
+
"""
|
| 386 |
+
Improved Elastic Image Pair Method (i-EIP). It performs an initial
|
| 387 |
+
interpolation followed by spline fitting to redistribute the image
|
| 388 |
+
pair close to the interpolated TS. Then, micro-iterations are performed
|
| 389 |
+
to move the images closer while maintaining the distance
|
| 390 |
+
"""
|
| 391 |
+
|
| 392 |
+
def __init__(
|
| 393 |
+
self,
|
| 394 |
+
initial_species: "Species",
|
| 395 |
+
final_species: "Species",
|
| 396 |
+
micro_step_size: Union[Distance, float] = Distance(1.5e-5, "ang"),
|
| 397 |
+
max_micro_per_macro: int = 2000,
|
| 398 |
+
max_macro_step: Union[Distance, float] = Distance(0.15, "ang"),
|
| 399 |
+
use_ll_neb_interp: bool = True,
|
| 400 |
+
interp_fraction: float = 1 / 4,
|
| 401 |
+
dist_tol: Union[Distance, float] = Distance(0.3, "ang"),
|
| 402 |
+
gtol: Union[GradientRMS, float] = GradientRMS(0.02, "Ha/ang"),
|
| 403 |
+
maxiter: int = 200,
|
| 404 |
+
**kwargs,
|
| 405 |
+
):
|
| 406 |
+
"""
|
| 407 |
+
Initialise an i-EIP calculation from the initial (reactant) and
|
| 408 |
+
final (product) species. Every macro-iteration consists of two
|
| 409 |
+
gradient evaluations on both images, therefore, the total number
|
| 410 |
+
of gradient evaluations would be 2 * maxiter. Increase the
|
| 411 |
+
interp_fraction argument to start the calculation closer to the
|
| 412 |
+
reactant and product, and move ahead less with the initial
|
| 413 |
+
interpolation.
|
| 414 |
+
|
| 415 |
+
Args:
|
| 416 |
+
initial_species: The "reactant" species
|
| 417 |
+
|
| 418 |
+
final_species: The "product" species
|
| 419 |
+
|
| 420 |
+
micro_step_size: The step size for every micro-iteration
|
| 421 |
+
|
| 422 |
+
max_micro_per_macro: The maximum number of micro-iterations
|
| 423 |
+
per macro-iteration
|
| 424 |
+
|
| 425 |
+
max_macro_step: The maximum step size for one macro-iteration
|
| 426 |
+
|
| 427 |
+
use_ll_neb_interp: Whether to use lmethod NEB for the
|
| 428 |
+
initial interpolation instead of only IDPP
|
| 429 |
+
interpolation
|
| 430 |
+
|
| 431 |
+
interp_fraction: Generate image pair on both sides of the
|
| 432 |
+
interpolated peak with this fraction of the
|
| 433 |
+
total interpolated path length (default 1/4)
|
| 434 |
+
|
| 435 |
+
dist_tol: The Euclidean distance tolerance (between images)
|
| 436 |
+
for convergence
|
| 437 |
+
|
| 438 |
+
gtol: The tolerance for perpendicular gradient RMS norm
|
| 439 |
+
|
| 440 |
+
maxiter: For i-EIP maxiter is the maximum number of macro-iterations
|
| 441 |
+
|
| 442 |
+
"""
|
| 443 |
+
assert (
|
| 444 |
+
"cineb_at_conv" not in kwargs.keys()
|
| 445 |
+
), "CI-NEB refinement is not available for i-EIP method!"
|
| 446 |
+
|
| 447 |
+
super().__init__(
|
| 448 |
+
initial_species,
|
| 449 |
+
final_species,
|
| 450 |
+
gtol=gtol,
|
| 451 |
+
dist_tol=dist_tol,
|
| 452 |
+
maxiter=maxiter,
|
| 453 |
+
**kwargs,
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
self.imgpair: ElasticImagePair = ElasticImagePair(
|
| 457 |
+
initial_species, final_species
|
| 458 |
+
)
|
| 459 |
+
self._micro_step_size = Distance(micro_step_size, "ang")
|
| 460 |
+
assert self._micro_step_size > 0
|
| 461 |
+
self._max_micro_per = abs(int(max_micro_per_macro))
|
| 462 |
+
self._max_macro_step = Distance(max_macro_step, "ang")
|
| 463 |
+
assert self._max_macro_step > 0
|
| 464 |
+
self._ll_neb_interp = bool(use_ll_neb_interp)
|
| 465 |
+
self._interp_frac = float(interp_fraction)
|
| 466 |
+
assert 0 < interp_fraction < 1
|
| 467 |
+
|
| 468 |
+
# NOTE: In EIP the microiters are done separately in a throwaway
|
| 469 |
+
# imagepair object, so a variable is required to keep track
|
| 470 |
+
self._current_microiters: int = 0
|
| 471 |
+
|
| 472 |
+
self._target_dist: Optional[float] = None
|
| 473 |
+
self._target_rms_g: Optional[float] = None
|
| 474 |
+
|
| 475 |
+
@property
|
| 476 |
+
def _micro_iter(self) -> int:
|
| 477 |
+
"""
|
| 478 |
+
Total number of micro-iterations. For i-EIP each micro-iteration
|
| 479 |
+
is on both of the images simultaneously
|
| 480 |
+
"""
|
| 481 |
+
return self._current_microiters
|
| 482 |
+
|
| 483 |
+
@_micro_iter.setter
|
| 484 |
+
def _micro_iter(self, value):
|
| 485 |
+
"""Set the total number of micro-iterations"""
|
| 486 |
+
self._current_microiters = int(value)
|
| 487 |
+
|
| 488 |
+
@property
|
| 489 |
+
def _macro_iter(self) -> int:
|
| 490 |
+
"""Total number of macro-iterations taken"""
|
| 491 |
+
# minus 1 due to first redistribution
|
| 492 |
+
return int(self.imgpair.total_iters / 2) - 1
|
| 493 |
+
|
| 494 |
+
@property
|
| 495 |
+
def converged(self) -> bool:
|
| 496 |
+
"""Is the i-EIP method converged"""
|
| 497 |
+
# NOTE: Original publication recommends also checking overlap
|
| 498 |
+
# of image-pair mode with Hessian eigenvalue for convergence, but
|
| 499 |
+
# Hessian is expensive, so we use simpler check
|
| 500 |
+
return self.imgpair.dist < self._dist_tol and all(
|
| 501 |
+
rms_grad <= self._gtol for rms_grad in self.imgpair.perp_rms_gs
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
@property
|
| 505 |
+
def _exceeded_maximum_iteration(self) -> bool:
|
| 506 |
+
"""Whether it has exceeded the number of maximum iterations"""
|
| 507 |
+
if self._macro_iter >= self._maxiter:
|
| 508 |
+
logger.error(
|
| 509 |
+
f"Reached the maximum number of macro-iterations "
|
| 510 |
+
f"*{self._maxiter}*"
|
| 511 |
+
)
|
| 512 |
+
return True
|
| 513 |
+
else:
|
| 514 |
+
return False
|
| 515 |
+
|
| 516 |
+
def _initialise_run(self) -> None:
|
| 517 |
+
"""
|
| 518 |
+
Initialise the i-EIP calculation by redistributing the
|
| 519 |
+
image pair and then estimating a low level hessian
|
| 520 |
+
"""
|
| 521 |
+
self.imgpair.update_both_img_engrad()
|
| 522 |
+
self.imgpair.redistribute_imagepair(
|
| 523 |
+
self._ll_neb_interp, self._interp_frac
|
| 524 |
+
)
|
| 525 |
+
self.imgpair.update_both_img_engrad()
|
| 526 |
+
self.imgpair.update_both_img_hessian_by_calc()
|
| 527 |
+
self._target_dist = self.imgpair.dist
|
| 528 |
+
self._target_rms_g = (
|
| 529 |
+
min(max(self.imgpair.dist / self._dist_tol, 1), 2) * self._gtol
|
| 530 |
+
)
|
| 531 |
+
return None
|
| 532 |
+
|
| 533 |
+
def _step(self) -> None:
|
| 534 |
+
"""
|
| 535 |
+
Take one EIP macro-iteration step and store the new coordinates
|
| 536 |
+
in history and update the energies and gradients
|
| 537 |
+
"""
|
| 538 |
+
self._update_target_distance_and_force()
|
| 539 |
+
|
| 540 |
+
# Turn off logging for micro-iterations
|
| 541 |
+
logger.disabled = True
|
| 542 |
+
assert self._target_dist is not None
|
| 543 |
+
micro_imgpair = IEIPMicroIters(
|
| 544 |
+
left_coords=self.imgpair.left_coords,
|
| 545 |
+
right_coords=self.imgpair.right_coords,
|
| 546 |
+
micro_step_size=self._micro_step_size,
|
| 547 |
+
target_dist=self._target_dist,
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
while not (
|
| 551 |
+
micro_imgpair.n_micro_iters >= self._max_micro_per
|
| 552 |
+
or micro_imgpair.max_displacement > self._max_macro_step
|
| 553 |
+
):
|
| 554 |
+
micro_imgpair.update_both_img_engrad()
|
| 555 |
+
micro_imgpair.take_micro_step()
|
| 556 |
+
self._micro_iter += 1
|
| 557 |
+
logger.disabled = False
|
| 558 |
+
self.imgpair.left_coords = micro_imgpair.left_coords
|
| 559 |
+
self.imgpair.right_coords = micro_imgpair.right_coords
|
| 560 |
+
self.imgpair.update_both_img_engrad()
|
| 561 |
+
self.imgpair.update_both_img_hessian_by_formula()
|
| 562 |
+
|
| 563 |
+
logger.info(
|
| 564 |
+
f"Completed one i-EIP macro-iteration with "
|
| 565 |
+
f"{micro_imgpair.n_micro_iters} micro-iterations; maximum "
|
| 566 |
+
f"image displacement = {micro_imgpair.max_displacement:.3f}.\n"
|
| 567 |
+
f"Left image step: {self.imgpair.last_left_step_size:.3f}, "
|
| 568 |
+
f"Right image step: {self.imgpair.last_right_step_size:.3f}"
|
| 569 |
+
)
|
| 570 |
+
return None
|
| 571 |
+
|
| 572 |
+
def _update_target_distance_and_force(self):
|
| 573 |
+
"""
|
| 574 |
+
Update the target distance tolerance and the RMS gradients
|
| 575 |
+
for the current macro-iteration
|
| 576 |
+
"""
|
| 577 |
+
# only update if target RMS force and distance has been reached
|
| 578 |
+
if not all(
|
| 579 |
+
rms_grad <= self._target_rms_g
|
| 580 |
+
for rms_grad in self.imgpair.perp_rms_gs
|
| 581 |
+
):
|
| 582 |
+
return None
|
| 583 |
+
|
| 584 |
+
# NOTE: target distance near the end of optimisation
|
| 585 |
+
# must be slighly lower than the set dist_tol, otherwise
|
| 586 |
+
# it will never converge (as it won't go below dist_tol)
|
| 587 |
+
self._target_dist = max(
|
| 588 |
+
0.9 * self.imgpair.dist,
|
| 589 |
+
self._dist_tol - 0.015,
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
self._target_rms_g = (
|
| 593 |
+
min(max(self.imgpair.dist / self._dist_tol, 1), 2) * self._gtol
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
logger.info(
|
| 597 |
+
f"Updating target distance to {self._target_dist:.3f} Å"
|
| 598 |
+
f" and updating target RMS gradient to "
|
| 599 |
+
f"{self._target_rms_g:.3f} Ha/Å"
|
| 600 |
+
)
|
| 601 |
+
return None
|
autodE/source/autode/bracket/imagepair.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Base classes for implementing all bracketing methods
|
| 3 |
+
that require a pair of images
|
| 4 |
+
"""
|
| 5 |
+
import itertools
|
| 6 |
+
import numpy as np
|
| 7 |
+
from abc import ABC, abstractmethod
|
| 8 |
+
from typing import Optional, Tuple, TYPE_CHECKING, Union, Iterator
|
| 9 |
+
from enum import Enum
|
| 10 |
+
|
| 11 |
+
from autode.values import Distance, PotentialEnergy, Gradient
|
| 12 |
+
from autode.geom import get_rot_mat_kabsch
|
| 13 |
+
from autode.methods import get_lmethod
|
| 14 |
+
from autode.neb import CINEB
|
| 15 |
+
from autode.opt.coordinates import CartesianCoordinates
|
| 16 |
+
from autode.opt.optimisers.hessian_update import BofillUpdate
|
| 17 |
+
from autode.opt.optimisers.utils import Polynomial2PointFit
|
| 18 |
+
from autode.opt.optimisers.base import OptimiserHistory, print_geometries_from
|
| 19 |
+
from autode.plotting import plot_bracket_method_energy_profile
|
| 20 |
+
from autode.utils import work_in_tmp_dir, ProcessPool
|
| 21 |
+
from autode.log import logger
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from autode.species import Species
|
| 25 |
+
from autode.wrappers.methods import Method
|
| 26 |
+
from autode.hessians import Hessian
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _calculate_engrad_for_species(
|
| 30 |
+
species: "Species",
|
| 31 |
+
method: "Method",
|
| 32 |
+
n_cores: int,
|
| 33 |
+
) -> Tuple[PotentialEnergy, Gradient]:
|
| 34 |
+
"""
|
| 35 |
+
Convenience function for calculating the energy/gradient
|
| 36 |
+
for a molecule; removes all input and output files after
|
| 37 |
+
the calculation is finished
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
(tuple[PotentialEnergy, Gradient]): Energy and gradient as tuple
|
| 41 |
+
"""
|
| 42 |
+
from autode.calculations import Calculation
|
| 43 |
+
|
| 44 |
+
engrad_calc = Calculation(
|
| 45 |
+
name=f"{species.name}_engrad",
|
| 46 |
+
molecule=species,
|
| 47 |
+
method=method,
|
| 48 |
+
keywords=method.keywords.grad,
|
| 49 |
+
n_cores=n_cores,
|
| 50 |
+
)
|
| 51 |
+
engrad_calc.run()
|
| 52 |
+
engrad_calc.clean_up(force=True, everything=True)
|
| 53 |
+
assert species.energy and species.gradient is not None, "Calc must be ok"
|
| 54 |
+
|
| 55 |
+
return species.energy, species.gradient
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@work_in_tmp_dir()
|
| 59 |
+
def _calculate_hessian_for_species(
|
| 60 |
+
species: "Species",
|
| 61 |
+
method: "Method",
|
| 62 |
+
n_cores: int,
|
| 63 |
+
) -> "Hessian":
|
| 64 |
+
"""
|
| 65 |
+
Convenience function for calculating the Hessian for a
|
| 66 |
+
molecule; removes all input and output files after
|
| 67 |
+
the calculation is finished
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
(Hessian): Hessian matrix
|
| 71 |
+
"""
|
| 72 |
+
from autode.calculations import Calculation
|
| 73 |
+
|
| 74 |
+
species = species.new_species()
|
| 75 |
+
|
| 76 |
+
hess_calc = Calculation(
|
| 77 |
+
name=f"{species.name}_hess",
|
| 78 |
+
molecule=species,
|
| 79 |
+
method=method,
|
| 80 |
+
keywords=method.keywords.hess,
|
| 81 |
+
n_cores=n_cores,
|
| 82 |
+
)
|
| 83 |
+
hess_calc.run()
|
| 84 |
+
hess_calc.clean_up(force=True, everything=True)
|
| 85 |
+
assert species.hessian is not None, "Calc must be ok"
|
| 86 |
+
|
| 87 |
+
return species.hessian
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class BaseImagePair(ABC):
|
| 91 |
+
"""
|
| 92 |
+
Base class for a pair of images (e.g., reactant and product) of
|
| 93 |
+
the same species. The images are called 'left' and 'right' to
|
| 94 |
+
distinguish them, but there is no requirement for one to be
|
| 95 |
+
reactant or product. Calculations can be performed on both sides
|
| 96 |
+
parallely
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
def __init__(
|
| 100 |
+
self,
|
| 101 |
+
left_image: "Species",
|
| 102 |
+
right_image: "Species",
|
| 103 |
+
):
|
| 104 |
+
"""
|
| 105 |
+
Initialize the image pair, does not set methods/n_cores
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
left_image: One molecule of the pair
|
| 109 |
+
right_image: Another molecule of the pair
|
| 110 |
+
"""
|
| 111 |
+
from autode.species.species import Species
|
| 112 |
+
|
| 113 |
+
assert isinstance(left_image, Species)
|
| 114 |
+
assert isinstance(right_image, Species)
|
| 115 |
+
self._left_image = left_image.new_species(name="left_image")
|
| 116 |
+
self._right_image = right_image.new_species(name="right_image")
|
| 117 |
+
self._sanity_check()
|
| 118 |
+
self._align_species()
|
| 119 |
+
|
| 120 |
+
# for calculation
|
| 121 |
+
self._method = None
|
| 122 |
+
self._hess_method = None
|
| 123 |
+
self._n_cores = None
|
| 124 |
+
self._hessian_update_types = [BofillUpdate]
|
| 125 |
+
|
| 126 |
+
self._left_history = OptimiserHistory()
|
| 127 |
+
self._right_history = OptimiserHistory()
|
| 128 |
+
# push the first coordinates into history
|
| 129 |
+
self.left_coords = CartesianCoordinates(self._left_image.coordinates)
|
| 130 |
+
self.right_coords = CartesianCoordinates(self._right_image.coordinates)
|
| 131 |
+
|
| 132 |
+
def _sanity_check(self) -> None:
|
| 133 |
+
"""
|
| 134 |
+
Check if the two supplied images have the same solvent,
|
| 135 |
+
charge, multiplicity and the same atoms in the same order
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
if self._left_image.n_atoms != self._right_image.n_atoms:
|
| 139 |
+
raise ValueError(
|
| 140 |
+
"The initial_species and final_species must "
|
| 141 |
+
"have the same number of atoms!"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
if (
|
| 145 |
+
self._left_image.charge != self._right_image.charge
|
| 146 |
+
or self._left_image.mult != self._right_image.mult
|
| 147 |
+
or self._left_image.solvent != self._right_image.solvent
|
| 148 |
+
):
|
| 149 |
+
raise ValueError(
|
| 150 |
+
"Charge/multiplicity/solvent of initial_species "
|
| 151 |
+
"and final_species supplied are not the same"
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
for idx in range(len(self._left_image.atoms)):
|
| 155 |
+
if (
|
| 156 |
+
self._left_image.atoms[idx].label
|
| 157 |
+
!= self._right_image.atoms[idx].label
|
| 158 |
+
):
|
| 159 |
+
raise ValueError(
|
| 160 |
+
"The order of atoms in initial_species "
|
| 161 |
+
"and final_species must be the same. The "
|
| 162 |
+
f"atom at position {idx} is different in"
|
| 163 |
+
"the two species"
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
return None
|
| 167 |
+
|
| 168 |
+
def _align_species(self) -> None:
|
| 169 |
+
"""
|
| 170 |
+
Translates both molecules to origin and then performs
|
| 171 |
+
a Kabsch rotation to orient the molecules as close as
|
| 172 |
+
possible against each other
|
| 173 |
+
"""
|
| 174 |
+
# first translate the molecules to the origin
|
| 175 |
+
logger.info(
|
| 176 |
+
"Translating initial_species (reactant) "
|
| 177 |
+
"and final_species (product) to origin"
|
| 178 |
+
)
|
| 179 |
+
p_mat = self._left_image.coordinates.copy()
|
| 180 |
+
p_mat -= np.average(p_mat, axis=0)
|
| 181 |
+
self._left_image.coordinates = p_mat
|
| 182 |
+
|
| 183 |
+
q_mat = self._right_image.coordinates.copy()
|
| 184 |
+
q_mat -= np.average(q_mat, axis=0)
|
| 185 |
+
self._right_image.coordinates = q_mat
|
| 186 |
+
|
| 187 |
+
logger.info(
|
| 188 |
+
"Rotating initial_species (reactant) "
|
| 189 |
+
"to align with final_species (product) "
|
| 190 |
+
"as much as possible"
|
| 191 |
+
)
|
| 192 |
+
rot_mat = get_rot_mat_kabsch(p_mat, q_mat)
|
| 193 |
+
rotated_p_mat = np.dot(rot_mat, p_mat.T).T
|
| 194 |
+
self._left_image.coordinates = rotated_p_mat
|
| 195 |
+
|
| 196 |
+
def initialise_trj(
|
| 197 |
+
self,
|
| 198 |
+
left_history_name: str = "left_history_save.zip",
|
| 199 |
+
right_history_name: str = "right_history_save.zip",
|
| 200 |
+
) -> None:
|
| 201 |
+
"""
|
| 202 |
+
Initialise the trajectory save files (history of coordinates on
|
| 203 |
+
left and right images)
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
left_history_name: Name of savefile for left history
|
| 207 |
+
right_history_name: Name of savefile for right history
|
| 208 |
+
"""
|
| 209 |
+
self._left_history.open(left_history_name)
|
| 210 |
+
self._right_history.open(right_history_name)
|
| 211 |
+
|
| 212 |
+
def close_trj(self):
|
| 213 |
+
"""
|
| 214 |
+
Put all coordinates in memory onto disk in the trajectory
|
| 215 |
+
save files, and close the trajectories
|
| 216 |
+
"""
|
| 217 |
+
self._left_history.close()
|
| 218 |
+
self._right_history.close()
|
| 219 |
+
|
| 220 |
+
def set_method_and_n_cores(
|
| 221 |
+
self,
|
| 222 |
+
method: "Method",
|
| 223 |
+
n_cores: int,
|
| 224 |
+
hess_method: Optional["Method"] = None,
|
| 225 |
+
) -> None:
|
| 226 |
+
"""
|
| 227 |
+
Sets the methods for en/grad calculation, and the total
|
| 228 |
+
number of cores used for any calculation in this image pair.
|
| 229 |
+
Optionally, also set the method for hessian calculation; if
|
| 230 |
+
not set, the available lmethod will be used.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
method (Method): Method used for calculating energy/gradient
|
| 234 |
+
n_cores (int): Number of cores available
|
| 235 |
+
hess_method (Method|None): Method used for calculating
|
| 236 |
+
Hessian (optional)
|
| 237 |
+
"""
|
| 238 |
+
from autode.wrappers.methods import Method
|
| 239 |
+
|
| 240 |
+
if not isinstance(method, Method):
|
| 241 |
+
raise TypeError(
|
| 242 |
+
f"The method needs to be of type autode."
|
| 243 |
+
f"wrappers.method.Method, But "
|
| 244 |
+
f"{type(method)} was supplied."
|
| 245 |
+
)
|
| 246 |
+
self._method = method
|
| 247 |
+
|
| 248 |
+
if hess_method is None:
|
| 249 |
+
hess_method = get_lmethod()
|
| 250 |
+
|
| 251 |
+
if not isinstance(hess_method, Method):
|
| 252 |
+
raise TypeError(
|
| 253 |
+
f"The hessian method needs to be of type autode."
|
| 254 |
+
f"wrappers.method.Method, But {type(hess_method)}"
|
| 255 |
+
f"was supplied"
|
| 256 |
+
)
|
| 257 |
+
self._hess_method = hess_method
|
| 258 |
+
|
| 259 |
+
self._n_cores = int(n_cores)
|
| 260 |
+
return None
|
| 261 |
+
|
| 262 |
+
@property
|
| 263 |
+
def n_atoms(self) -> int:
|
| 264 |
+
"""Number of atoms"""
|
| 265 |
+
return self._left_image.n_atoms
|
| 266 |
+
|
| 267 |
+
@property
|
| 268 |
+
def total_iters(self) -> int:
|
| 269 |
+
"""Total number of iterations done on this image pair"""
|
| 270 |
+
return len(self._left_history) + len(self._right_history) - 2
|
| 271 |
+
|
| 272 |
+
@property
|
| 273 |
+
def left_coords(self) -> CartesianCoordinates:
|
| 274 |
+
"""The coordinates of the left image"""
|
| 275 |
+
assert isinstance(self._left_history[-1], CartesianCoordinates)
|
| 276 |
+
return self._left_history[-1]
|
| 277 |
+
|
| 278 |
+
@left_coords.setter
|
| 279 |
+
def left_coords(self, value: CartesianCoordinates):
|
| 280 |
+
"""
|
| 281 |
+
Sets the coordinates of the left image, also updates
|
| 282 |
+
the coordinates of the species
|
| 283 |
+
|
| 284 |
+
Args:
|
| 285 |
+
value (CartesianCoordinates|None): new set of coordinates
|
| 286 |
+
|
| 287 |
+
Raises:
|
| 288 |
+
(TypeError): If input is not of type CartesianCoordinates
|
| 289 |
+
(ValueError): If input does not have correct shape
|
| 290 |
+
"""
|
| 291 |
+
if value.shape[0] != 3 * self.n_atoms:
|
| 292 |
+
raise ValueError(f"Must have {self.n_atoms * 3} entries")
|
| 293 |
+
|
| 294 |
+
if isinstance(value, CartesianCoordinates):
|
| 295 |
+
self._left_history.add(value.copy())
|
| 296 |
+
else:
|
| 297 |
+
raise TypeError
|
| 298 |
+
|
| 299 |
+
self._left_image.coordinates = self.left_coords
|
| 300 |
+
|
| 301 |
+
@property
|
| 302 |
+
def right_coords(self) -> CartesianCoordinates:
|
| 303 |
+
"""The coordinates of the right image"""
|
| 304 |
+
assert isinstance(self._right_history[-1], CartesianCoordinates)
|
| 305 |
+
return self._right_history[-1]
|
| 306 |
+
|
| 307 |
+
@right_coords.setter
|
| 308 |
+
def right_coords(self, value: CartesianCoordinates):
|
| 309 |
+
"""
|
| 310 |
+
Sets the coordinates of the right image, also updates
|
| 311 |
+
the coordinates of the species
|
| 312 |
+
|
| 313 |
+
Args:
|
| 314 |
+
value (CartesianCoordinates|None): new set of coordinates
|
| 315 |
+
|
| 316 |
+
Raises:
|
| 317 |
+
(TypeError): If input is not of type CartesianCoordinates
|
| 318 |
+
(ValueError): If input does not have correct shape
|
| 319 |
+
"""
|
| 320 |
+
if value.shape[0] != 3 * self.n_atoms:
|
| 321 |
+
raise ValueError(f"Must have {self.n_atoms * 3} entries")
|
| 322 |
+
|
| 323 |
+
if isinstance(value, CartesianCoordinates):
|
| 324 |
+
self._right_history.add(value.copy())
|
| 325 |
+
else:
|
| 326 |
+
raise TypeError
|
| 327 |
+
|
| 328 |
+
self._right_image.coordinates = self.right_coords
|
| 329 |
+
|
| 330 |
+
@property
|
| 331 |
+
@abstractmethod
|
| 332 |
+
def ts_guess(self) -> Optional["Species"]:
|
| 333 |
+
"""TS guess species for this image-pair"""
|
| 334 |
+
|
| 335 |
+
@property
|
| 336 |
+
@abstractmethod
|
| 337 |
+
def dist_vec(self) -> np.ndarray:
|
| 338 |
+
"""Distance vector defined from left to right image"""
|
| 339 |
+
|
| 340 |
+
@property
|
| 341 |
+
@abstractmethod
|
| 342 |
+
def dist(self) -> Distance:
|
| 343 |
+
"""Distance defined between two images in the image-pair"""
|
| 344 |
+
|
| 345 |
+
@property
|
| 346 |
+
@abstractmethod
|
| 347 |
+
def has_jumped_over_barrier(self) -> bool:
|
| 348 |
+
"""Whether one image has jumped over the barrier on the other side"""
|
| 349 |
+
|
| 350 |
+
def update_both_img_engrad(self):
|
| 351 |
+
"""
|
| 352 |
+
Update the energy/gradient for both images, with parallel processing
|
| 353 |
+
"""
|
| 354 |
+
assert self._method is not None
|
| 355 |
+
assert self._n_cores is not None
|
| 356 |
+
n_cores_per_pp = self._n_cores // 2 if self._n_cores > 1 else 1
|
| 357 |
+
n_procs = 1 if self._n_cores < 2 else 2
|
| 358 |
+
with ProcessPool(max_workers=n_procs) as pool:
|
| 359 |
+
jobs = [
|
| 360 |
+
pool.submit(
|
| 361 |
+
_calculate_engrad_for_species,
|
| 362 |
+
species=img,
|
| 363 |
+
method=self._method,
|
| 364 |
+
n_cores=n_cores_per_pp,
|
| 365 |
+
)
|
| 366 |
+
for img in [self._left_image, self._right_image]
|
| 367 |
+
]
|
| 368 |
+
left_engrad, right_engrad = [job.result() for job in jobs]
|
| 369 |
+
|
| 370 |
+
self.left_coords.e = left_engrad[0]
|
| 371 |
+
self.left_coords.update_g_from_cart_g(left_engrad[1])
|
| 372 |
+
self.right_coords.e = right_engrad[0]
|
| 373 |
+
self.right_coords.update_g_from_cart_g(right_engrad[1])
|
| 374 |
+
return None
|
| 375 |
+
|
| 376 |
+
def update_both_img_hessian_by_calc(self):
|
| 377 |
+
"""
|
| 378 |
+
Update the molecular hessian of both images by calculation
|
| 379 |
+
"""
|
| 380 |
+
# TODO: refactor into ll_hessian code
|
| 381 |
+
assert self._hess_method is not None
|
| 382 |
+
assert self._n_cores is not None
|
| 383 |
+
n_cores_per_pp = self._n_cores // 2 if self._n_cores > 1 else 1
|
| 384 |
+
n_procs = 1 if self._n_cores < 2 else 2
|
| 385 |
+
with ProcessPool(max_workers=n_procs) as pool:
|
| 386 |
+
jobs = [
|
| 387 |
+
pool.submit(
|
| 388 |
+
_calculate_hessian_for_species,
|
| 389 |
+
species=img,
|
| 390 |
+
method=self._hess_method,
|
| 391 |
+
n_cores=n_cores_per_pp,
|
| 392 |
+
)
|
| 393 |
+
for img in [self._left_image, self._right_image]
|
| 394 |
+
]
|
| 395 |
+
left_hess, right_hess = [job.result() for job in jobs]
|
| 396 |
+
|
| 397 |
+
self.left_coords.update_h_from_cart_h(left_hess)
|
| 398 |
+
self.right_coords.update_h_from_cart_h(right_hess)
|
| 399 |
+
return None
|
| 400 |
+
|
| 401 |
+
def update_both_img_hessian_by_formula(self):
|
| 402 |
+
"""
|
| 403 |
+
Update the molecular hessian for both images by update formula
|
| 404 |
+
"""
|
| 405 |
+
for history in [self._left_history, self._right_history]:
|
| 406 |
+
history.final.update_h_from_old_h(
|
| 407 |
+
history.penultimate, self._hessian_update_types
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
return None
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
class EuclideanImagePair(BaseImagePair, ABC):
|
| 414 |
+
"""
|
| 415 |
+
Image-pair that defines the distance between the images as
|
| 416 |
+
the Euclidean distance. It can also run CI-NEB calculation
|
| 417 |
+
from the final two points added to the image-pair, and
|
| 418 |
+
plot the energies of the total path
|
| 419 |
+
"""
|
| 420 |
+
|
| 421 |
+
def __init__(
|
| 422 |
+
self,
|
| 423 |
+
left_image: "Species",
|
| 424 |
+
right_image: "Species",
|
| 425 |
+
):
|
| 426 |
+
super().__init__(left_image=left_image, right_image=right_image)
|
| 427 |
+
|
| 428 |
+
# for storing results from CINEB
|
| 429 |
+
self._cineb_coords: Optional[CartesianCoordinates] = None
|
| 430 |
+
|
| 431 |
+
@property
|
| 432 |
+
def dist_vec(self) -> np.ndarray:
|
| 433 |
+
"""
|
| 434 |
+
Distance vector in cartesian coordinates, it is defined here to
|
| 435 |
+
go from right to left image (i.e. right -> left)
|
| 436 |
+
"""
|
| 437 |
+
return np.array(
|
| 438 |
+
self.left_coords.to("cart") - self.right_coords.to("cart")
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
@property
|
| 442 |
+
def dist(self) -> Distance:
|
| 443 |
+
"""
|
| 444 |
+
Euclidean distance between the images in image-pair
|
| 445 |
+
|
| 446 |
+
Returns:
|
| 447 |
+
(Distance): Distance in Angstrom
|
| 448 |
+
"""
|
| 449 |
+
return Distance(np.linalg.norm(self.dist_vec), units="ang")
|
| 450 |
+
|
| 451 |
+
@property
|
| 452 |
+
def has_jumped_over_barrier(self) -> bool:
|
| 453 |
+
"""
|
| 454 |
+
A quick test of whether the images are still separated by a barrier,
|
| 455 |
+
implemented via fitting a cubic polynomial along the linear path
|
| 456 |
+
connecting the two images and checking for a peak. This is only an
|
| 457 |
+
approximation.
|
| 458 |
+
"""
|
| 459 |
+
assert self.left_coords is not None and self.right_coords is not None
|
| 460 |
+
assert self.left_coords.e and self.right_coords.e
|
| 461 |
+
assert (
|
| 462 |
+
self.left_coords.g is not None and self.right_coords.g is not None
|
| 463 |
+
)
|
| 464 |
+
cubic_poly = Polynomial2PointFit.cubic_fit(
|
| 465 |
+
self.left_coords, self.right_coords
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
# NOTE: Interpolation seems reasonable upto ~1.2 Angstrom. If distance
|
| 469 |
+
# is larger, detecting peak is impossible without calculating energies
|
| 470 |
+
# so we assume there is a barrier between the images
|
| 471 |
+
if self.dist > Distance(1.2, "ang"):
|
| 472 |
+
return False
|
| 473 |
+
else:
|
| 474 |
+
return cubic_poly.get_extremum(0.0, 1.0, get_max=True) is None
|
| 475 |
+
|
| 476 |
+
def run_cineb_from_end_points(self) -> None:
|
| 477 |
+
"""
|
| 478 |
+
Runs a CI-NEB calculation from the end-points of the image-pair
|
| 479 |
+
and then stores the coordinates of the peak point obtained
|
| 480 |
+
from the CI-NEB run
|
| 481 |
+
|
| 482 |
+
Returns:
|
| 483 |
+
(CartesianCoordinates): Coordinates of the peak species obtained
|
| 484 |
+
from the CI-NEB run
|
| 485 |
+
"""
|
| 486 |
+
assert self._method is not None, "Methods must be set"
|
| 487 |
+
assert self._n_cores is not None, "Number of cores must be set"
|
| 488 |
+
|
| 489 |
+
cineb = CINEB.from_end_points(
|
| 490 |
+
self._left_image, self._right_image, num=3
|
| 491 |
+
)
|
| 492 |
+
cineb.calculate(method=self._method, n_cores=self._n_cores)
|
| 493 |
+
|
| 494 |
+
if not cineb.images.contains_peak:
|
| 495 |
+
logger.error("CI-NEB failed to find the peak")
|
| 496 |
+
return None
|
| 497 |
+
|
| 498 |
+
peak = cineb.images[cineb.images.peak_idx] # type: ignore
|
| 499 |
+
ci_coords = CartesianCoordinates(peak.coordinates)
|
| 500 |
+
ci_coords.e = peak.energy
|
| 501 |
+
ci_coords.update_g_from_cart_g(peak.gradient)
|
| 502 |
+
|
| 503 |
+
self._cineb_coords = ci_coords
|
| 504 |
+
return None
|
| 505 |
+
|
| 506 |
+
@property
|
| 507 |
+
def _total_history(self) -> Iterator[CartesianCoordinates]:
|
| 508 |
+
"""
|
| 509 |
+
The total history of the image-pair, including any CI run
|
| 510 |
+
from the endpoints
|
| 511 |
+
"""
|
| 512 |
+
cineb_coords = []
|
| 513 |
+
if self._cineb_coords is not None:
|
| 514 |
+
cineb_coords.append(self._cineb_coords)
|
| 515 |
+
return itertools.chain(
|
| 516 |
+
self._left_history, cineb_coords, reversed(self._right_history)
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
def print_geometries(
|
| 520 |
+
self,
|
| 521 |
+
init_trj_filename: str,
|
| 522 |
+
final_trj_filename: str,
|
| 523 |
+
total_trj_filename: str,
|
| 524 |
+
) -> None:
|
| 525 |
+
"""
|
| 526 |
+
Write trajectories as *.xyz files, one for the initial species,
|
| 527 |
+
one for final species, and one for the whole trajectory, including
|
| 528 |
+
any CI-NEB run from the final end points
|
| 529 |
+
"""
|
| 530 |
+
if self.total_iters < 2:
|
| 531 |
+
logger.warning("Cannot write trajectory, not enough points")
|
| 532 |
+
return None
|
| 533 |
+
|
| 534 |
+
print_geometries_from(
|
| 535 |
+
self._left_history,
|
| 536 |
+
species=self._left_image,
|
| 537 |
+
filename=init_trj_filename,
|
| 538 |
+
)
|
| 539 |
+
print_geometries_from(
|
| 540 |
+
self._right_history,
|
| 541 |
+
species=self._right_image,
|
| 542 |
+
filename=final_trj_filename,
|
| 543 |
+
)
|
| 544 |
+
print_geometries_from(
|
| 545 |
+
self._total_history,
|
| 546 |
+
species=self._left_image,
|
| 547 |
+
filename=total_trj_filename,
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
return None
|
| 551 |
+
|
| 552 |
+
def plot_energies(
|
| 553 |
+
self,
|
| 554 |
+
filename: str,
|
| 555 |
+
distance_metric: str,
|
| 556 |
+
) -> None:
|
| 557 |
+
"""
|
| 558 |
+
Plots the energies of the image-pair, including any CI-NEB
|
| 559 |
+
calculation done at the end. The distance metric argument
|
| 560 |
+
determines how the x-axis values are plotted and their
|
| 561 |
+
meaning (Described in more detail in BaseBracketMethod)
|
| 562 |
+
|
| 563 |
+
Args:
|
| 564 |
+
filename (str): name of the plot file to save
|
| 565 |
+
distance_metric (str): "relative" or "from_start" or "index"
|
| 566 |
+
|
| 567 |
+
See Also:
|
| 568 |
+
:py:meth:`BaseBracketMethod <autode.bracket.base.BaseBracketMethod.plot_energies>`
|
| 569 |
+
"""
|
| 570 |
+
|
| 571 |
+
class Metrics(Enum):
|
| 572 |
+
relative = 1
|
| 573 |
+
from_start = 2
|
| 574 |
+
index = 3
|
| 575 |
+
|
| 576 |
+
metric = Metrics[distance_metric]
|
| 577 |
+
|
| 578 |
+
if self.total_iters < 2:
|
| 579 |
+
logger.warning("Cannot plot energies, not enough points")
|
| 580 |
+
return None
|
| 581 |
+
|
| 582 |
+
all_energies = [coord.e for coord in self._total_history]
|
| 583 |
+
if any(en is None for en in all_energies):
|
| 584 |
+
logger.error(
|
| 585 |
+
"One or more coordinates do not have associated"
|
| 586 |
+
" energies, unable to produce energy plot!"
|
| 587 |
+
)
|
| 588 |
+
return None
|
| 589 |
+
|
| 590 |
+
num_left_points = len(self._left_history)
|
| 591 |
+
num_right_points = len(self._right_history)
|
| 592 |
+
first_point = self._left_history[0]
|
| 593 |
+
points: list = [] # list of tuples
|
| 594 |
+
|
| 595 |
+
lowest_en = min(all_energies) # type: ignore
|
| 596 |
+
|
| 597 |
+
last_coord = None
|
| 598 |
+
for idx, coord in enumerate(self._total_history):
|
| 599 |
+
en = coord.e - lowest_en # type: ignore
|
| 600 |
+
if metric == Metrics.relative:
|
| 601 |
+
if idx == 0:
|
| 602 |
+
x = 0
|
| 603 |
+
else:
|
| 604 |
+
x = np.linalg.norm(coord - last_coord)
|
| 605 |
+
x += points[idx - 1][0] # add previous distance
|
| 606 |
+
elif metric == Metrics.from_start:
|
| 607 |
+
x = np.linalg.norm(coord - first_point)
|
| 608 |
+
else: # metric == Metrics.index:
|
| 609 |
+
x = idx
|
| 610 |
+
points.append((x, en))
|
| 611 |
+
last_coord = coord
|
| 612 |
+
|
| 613 |
+
left_points = points[:num_left_points]
|
| 614 |
+
if self._cineb_coords is not None:
|
| 615 |
+
cineb_point = points[num_left_points]
|
| 616 |
+
else:
|
| 617 |
+
cineb_point = None
|
| 618 |
+
right_points = points[-num_right_points:]
|
| 619 |
+
if distance_metric == "relative":
|
| 620 |
+
x_axis_title = "Change in Euclidean Distance (Å)"
|
| 621 |
+
elif distance_metric == "from_start":
|
| 622 |
+
x_axis_title = "Euclidean Distance from Reactant Structure (Å)"
|
| 623 |
+
else:
|
| 624 |
+
x_axis_title = "Point in Reaction Path"
|
| 625 |
+
|
| 626 |
+
plot_bracket_method_energy_profile(
|
| 627 |
+
filename, left_points, cineb_point, right_points, x_axis_title
|
| 628 |
+
)
|
autodE/source/autode/calculations/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from autode.calculations.calculation import Calculation
|
| 2 |
+
from autode.calculations.input import CalculationInput
|
| 3 |
+
from autode.calculations.output import CalculationOutput
|
| 4 |
+
|
| 5 |
+
__all__ = ["Calculation", "CalculationInput", "CalculationOutput"]
|
autodE/source/autode/calculations/calculation.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import autode.wrappers.keywords as kws
|
| 2 |
+
import autode.exceptions as ex
|
| 3 |
+
|
| 4 |
+
from copy import deepcopy
|
| 5 |
+
from typing import Optional, List, TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
from autode.point_charges import PointCharge
|
| 8 |
+
from autode.log import logger
|
| 9 |
+
from autode.calculations.types import CalculationType
|
| 10 |
+
from autode.calculations.executors import (
|
| 11 |
+
CalculationExecutor,
|
| 12 |
+
CalculationExecutorO,
|
| 13 |
+
CalculationExecutorG,
|
| 14 |
+
CalculationExecutorH,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
if TYPE_CHECKING:
|
| 18 |
+
from autode.species.species import Species
|
| 19 |
+
from autode.wrappers.methods import Method
|
| 20 |
+
from autode.wrappers.keywords import Keywords
|
| 21 |
+
from autode.calculations.input import CalculationInput
|
| 22 |
+
from autode.calculations.output import CalculationOutput
|
| 23 |
+
from autode.calculations.executors import CalculationExecutor
|
| 24 |
+
from autode.opt.optimisers.base import BaseOptimiser
|
| 25 |
+
|
| 26 |
+
output_exts = (
|
| 27 |
+
".out",
|
| 28 |
+
".hess",
|
| 29 |
+
".xyz",
|
| 30 |
+
".inp",
|
| 31 |
+
".com",
|
| 32 |
+
".log",
|
| 33 |
+
".nw",
|
| 34 |
+
".pc",
|
| 35 |
+
".grad",
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class Calculation:
|
| 40 |
+
def __init__(
|
| 41 |
+
self,
|
| 42 |
+
name: str,
|
| 43 |
+
molecule: "Species",
|
| 44 |
+
method: "Method",
|
| 45 |
+
keywords: "Keywords",
|
| 46 |
+
n_cores: int = 1,
|
| 47 |
+
point_charges: Optional[List[PointCharge]] = None,
|
| 48 |
+
):
|
| 49 |
+
"""
|
| 50 |
+
Calculation e.g. single point energy evaluation on a molecule. This
|
| 51 |
+
will update the molecule inplace. For example, an optimisation will
|
| 52 |
+
alter molecule.atoms.
|
| 53 |
+
|
| 54 |
+
-----------------------------------------------------------------------
|
| 55 |
+
Arguments:
|
| 56 |
+
name: Name of the calculation. Will be modified with a method
|
| 57 |
+
suffix
|
| 58 |
+
|
| 59 |
+
molecule: Molecule to be calculated. This may have a set of
|
| 60 |
+
associated cartesian or distance constraints
|
| 61 |
+
|
| 62 |
+
method: Wrapped electronic structure method, or other e.g.
|
| 63 |
+
forcefield capable of calculating energies and gradients
|
| 64 |
+
|
| 65 |
+
keywords: Keywords defining the type of calculation and e.g. what
|
| 66 |
+
basis set and functional to use.
|
| 67 |
+
|
| 68 |
+
n_cores: Number of cores available (default: {1})
|
| 69 |
+
|
| 70 |
+
point_charges: List of float of point charges
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
self.name = name
|
| 74 |
+
self.n_cores = int(n_cores)
|
| 75 |
+
self.point_charges = point_charges
|
| 76 |
+
self._executor = self._executor_for(molecule, method, keywords)
|
| 77 |
+
|
| 78 |
+
self._check()
|
| 79 |
+
|
| 80 |
+
def _executor_for(
|
| 81 |
+
self,
|
| 82 |
+
molecule: "Species",
|
| 83 |
+
method: "Method",
|
| 84 |
+
keywords: "Keywords",
|
| 85 |
+
) -> "CalculationExecutor":
|
| 86 |
+
"""
|
| 87 |
+
Return a calculation executor depending on the calculation modes
|
| 88 |
+
implemented in the wrapped method. For instance if the method does not
|
| 89 |
+
implement any optimisation then use an executor that uses the in built
|
| 90 |
+
autodE optimisers (in autode/opt/). Equally if the method does not
|
| 91 |
+
implement way of calculating Hessians then use a numerical evaluation
|
| 92 |
+
of the Hessian
|
| 93 |
+
"""
|
| 94 |
+
_type = CalculationExecutor # base type, implements all calc types
|
| 95 |
+
|
| 96 |
+
if _are_opt(keywords) and not method.implements(CalculationType.opt):
|
| 97 |
+
_type = CalculationExecutorO
|
| 98 |
+
|
| 99 |
+
if _are_grad(keywords) and not method.implements(
|
| 100 |
+
CalculationType.gradient
|
| 101 |
+
):
|
| 102 |
+
_type = CalculationExecutorG
|
| 103 |
+
|
| 104 |
+
if _are_hess(keywords) and not method.implements(
|
| 105 |
+
CalculationType.hessian
|
| 106 |
+
):
|
| 107 |
+
_type = CalculationExecutorH
|
| 108 |
+
|
| 109 |
+
return _type(
|
| 110 |
+
self.name,
|
| 111 |
+
molecule,
|
| 112 |
+
method,
|
| 113 |
+
keywords,
|
| 114 |
+
self.n_cores,
|
| 115 |
+
self.point_charges,
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
def run(self) -> None:
|
| 119 |
+
"""Run the calculation using the EST method"""
|
| 120 |
+
logger.info(f"Running calculation: {self.name}")
|
| 121 |
+
|
| 122 |
+
self._executor.run()
|
| 123 |
+
self._check_properties_exist()
|
| 124 |
+
self._add_to_comp_methods()
|
| 125 |
+
|
| 126 |
+
return None
|
| 127 |
+
|
| 128 |
+
def clean_up(self, force: bool = False, everything: bool = False) -> None:
|
| 129 |
+
"""
|
| 130 |
+
Clean up input and output files, if Config.keep_input_files is False
|
| 131 |
+
(and not force=True)
|
| 132 |
+
|
| 133 |
+
-----------------------------------------------------------------------
|
| 134 |
+
Keyword Arguments:
|
| 135 |
+
|
| 136 |
+
force (bool): If True then override Config.keep_input_files
|
| 137 |
+
|
| 138 |
+
everything (bool): Remove both input and output files
|
| 139 |
+
"""
|
| 140 |
+
return self._executor.clean_up(force, everything)
|
| 141 |
+
|
| 142 |
+
def generate_input(self) -> None:
|
| 143 |
+
"""Generate the input required for this calculation"""
|
| 144 |
+
|
| 145 |
+
if not self.method.uses_external_io:
|
| 146 |
+
logger.warning(
|
| 147 |
+
"Calculation does not create an input file. No "
|
| 148 |
+
"input has been generated"
|
| 149 |
+
)
|
| 150 |
+
else:
|
| 151 |
+
self._executor.generate_input()
|
| 152 |
+
|
| 153 |
+
@property
|
| 154 |
+
def terminated_normally(self) -> bool:
|
| 155 |
+
"""
|
| 156 |
+
Determine if the calculation terminated without error
|
| 157 |
+
|
| 158 |
+
-----------------------------------------------------------------------
|
| 159 |
+
Returns:
|
| 160 |
+
(bool): Normal termination of the calculation?
|
| 161 |
+
"""
|
| 162 |
+
return self._executor.terminated_normally
|
| 163 |
+
|
| 164 |
+
@property
|
| 165 |
+
def input(self) -> "CalculationInput":
|
| 166 |
+
"""The input used to run this calculation"""
|
| 167 |
+
return self._executor.input
|
| 168 |
+
|
| 169 |
+
@property
|
| 170 |
+
def output(self) -> "CalculationOutput":
|
| 171 |
+
"""The output generated by this calculation"""
|
| 172 |
+
return self._executor.output
|
| 173 |
+
|
| 174 |
+
def set_output_filename(self, filename: str) -> None:
|
| 175 |
+
"""
|
| 176 |
+
Set the output filename. If it exists then the properties of
|
| 177 |
+
the molecule this calculation was created with from will be
|
| 178 |
+
set
|
| 179 |
+
"""
|
| 180 |
+
self._executor.output.filename = filename
|
| 181 |
+
self._executor.set_properties()
|
| 182 |
+
self._check_properties_exist()
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
@property
|
| 186 |
+
def optimiser(self) -> "BaseOptimiser":
|
| 187 |
+
"""The optimiser used to run this calculation"""
|
| 188 |
+
return self._executor.optimiser
|
| 189 |
+
|
| 190 |
+
def copy(self) -> "Calculation":
|
| 191 |
+
return deepcopy(self)
|
| 192 |
+
|
| 193 |
+
@property
|
| 194 |
+
def molecule(self) -> "Species":
|
| 195 |
+
return self._executor.molecule
|
| 196 |
+
|
| 197 |
+
@molecule.setter
|
| 198 |
+
def molecule(self, value: "Species"):
|
| 199 |
+
self._executor.molecule = value
|
| 200 |
+
|
| 201 |
+
@property
|
| 202 |
+
def keywords(self) -> "Keywords":
|
| 203 |
+
return self._executor.input.keywords
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def method(self) -> "Method":
|
| 207 |
+
return self._executor.method
|
| 208 |
+
|
| 209 |
+
def _check(self) -> None:
|
| 210 |
+
"""
|
| 211 |
+
Ensure the molecule has the required properties and raise exceptions
|
| 212 |
+
if they are not present. Also ensure that the method has the requsted
|
| 213 |
+
solvent available.
|
| 214 |
+
|
| 215 |
+
-----------------------------------------------------------------------
|
| 216 |
+
Raises:
|
| 217 |
+
(ValueError | autode.exceptions.CalculationException):
|
| 218 |
+
"""
|
| 219 |
+
from autode.species.species import Species
|
| 220 |
+
|
| 221 |
+
assert isinstance(self.molecule, Species)
|
| 222 |
+
|
| 223 |
+
if self.molecule.atoms is None or self.molecule.n_atoms == 0:
|
| 224 |
+
raise ex.NoInputError("Have no atoms. Can't form a calculation")
|
| 225 |
+
|
| 226 |
+
if not self.molecule.has_valid_spin_state:
|
| 227 |
+
raise ex.CalculationException(
|
| 228 |
+
f"Cannot execute a calculation without a valid spin state: "
|
| 229 |
+
f"Spin multiplicity (2S+1) = {self.molecule.mult}"
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
return None
|
| 233 |
+
|
| 234 |
+
def _add_to_comp_methods(self) -> None:
|
| 235 |
+
"""Add the methods used in this calculation to the used methods list"""
|
| 236 |
+
from autode.log.methods import methods
|
| 237 |
+
|
| 238 |
+
methods.add(
|
| 239 |
+
f"Calculations were performed using {self.method.name} v. "
|
| 240 |
+
f"{self.method.version_in(self._executor)} "
|
| 241 |
+
f"({self.method.doi_str})."
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
# Type of calculation ----
|
| 245 |
+
if isinstance(self.input.keywords, kws.SinglePointKeywords):
|
| 246 |
+
string = "Single point "
|
| 247 |
+
|
| 248 |
+
elif isinstance(self.input.keywords, kws.OptKeywords):
|
| 249 |
+
string = "Optimisation "
|
| 250 |
+
|
| 251 |
+
else:
|
| 252 |
+
logger.warning(
|
| 253 |
+
"Not adding gradient or hessian to methods section "
|
| 254 |
+
"anticipating that they will be the same as opt"
|
| 255 |
+
)
|
| 256 |
+
# and have been already added to the methods section
|
| 257 |
+
return
|
| 258 |
+
|
| 259 |
+
# Level of theory ----
|
| 260 |
+
string += (
|
| 261 |
+
f"calculations performed at the "
|
| 262 |
+
f"{self.input.keywords.method_string} level"
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
basis = self.input.keywords.basis_set
|
| 266 |
+
if basis is not None:
|
| 267 |
+
string += (
|
| 268 |
+
f" in combination with the {str(basis)} "
|
| 269 |
+
f"({basis.doi_str}) basis set"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
if (
|
| 273 |
+
self.molecule.solvent is not None
|
| 274 |
+
and self.molecule.solvent.is_implicit
|
| 275 |
+
):
|
| 276 |
+
solv_type = self.method.implicit_solvation_type
|
| 277 |
+
assert solv_type is not None, "Must have an implicit solvent type"
|
| 278 |
+
doi = solv_type.doi_str if hasattr(solv_type, "doi_str") else "?"
|
| 279 |
+
|
| 280 |
+
string += (
|
| 281 |
+
f" and {solv_type.upper()} ({doi}) "
|
| 282 |
+
f"solvation, with parameters appropriate for "
|
| 283 |
+
f"{self.molecule.solvent}"
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
methods.add(f"{string}.\n")
|
| 287 |
+
return None
|
| 288 |
+
|
| 289 |
+
def _check_properties_exist(self) -> None:
|
| 290 |
+
"""
|
| 291 |
+
Check that the requested properties, as defined by the type of keywords
|
| 292 |
+
that this calculation was requested with have been set.
|
| 293 |
+
|
| 294 |
+
-----------------------------------------------------------------------
|
| 295 |
+
Raises:
|
| 296 |
+
(CouldNotGetProperty): If the required property couldn't be found
|
| 297 |
+
"""
|
| 298 |
+
logger.info("Checking required properties exist")
|
| 299 |
+
|
| 300 |
+
if not self.terminated_normally:
|
| 301 |
+
logger.error(
|
| 302 |
+
f"Calculation of {self.molecule} did not terminate "
|
| 303 |
+
f"normally"
|
| 304 |
+
)
|
| 305 |
+
raise ex.CouldNotGetProperty()
|
| 306 |
+
|
| 307 |
+
if self.molecule.energy is None:
|
| 308 |
+
raise ex.CouldNotGetProperty(name="energy")
|
| 309 |
+
|
| 310 |
+
if _are_grad(self.keywords) and self.molecule.gradient is None:
|
| 311 |
+
raise ex.CouldNotGetProperty(name="gradient")
|
| 312 |
+
|
| 313 |
+
if _are_hess(self.keywords) and self.molecule.hessian is None:
|
| 314 |
+
raise ex.CouldNotGetProperty(name="Hessian")
|
| 315 |
+
|
| 316 |
+
return None
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def _are_opt(keywords) -> bool:
|
| 320 |
+
return isinstance(keywords, kws.OptKeywords)
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def _are_grad(keywords) -> bool:
|
| 324 |
+
return isinstance(keywords, kws.GradientKeywords)
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def _are_hess(keywords) -> bool:
|
| 328 |
+
return isinstance(keywords, kws.HessianKeywords)
|
autodE/source/autode/calculations/executors.py
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
A collection of calculation executors which can execute the correct set of
|
| 3 |
+
steps to run a calculation for a specific method, depending on what it
|
| 4 |
+
implements
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import hashlib
|
| 8 |
+
import base64
|
| 9 |
+
import autode.exceptions as ex
|
| 10 |
+
import autode.wrappers.keywords as kws
|
| 11 |
+
|
| 12 |
+
from typing import Optional, List, Tuple, TYPE_CHECKING
|
| 13 |
+
from copy import deepcopy
|
| 14 |
+
|
| 15 |
+
from autode.log import logger
|
| 16 |
+
from autode.config import Config
|
| 17 |
+
from autode.values import Distance
|
| 18 |
+
from autode.utils import no_exceptions, requires_output_to_exist
|
| 19 |
+
from autode.point_charges import PointCharge
|
| 20 |
+
from autode.opt.optimisers.base import NullOptimiser, BaseOptimiser
|
| 21 |
+
from autode.calculations.input import CalculationInput
|
| 22 |
+
from autode.calculations.output import (
|
| 23 |
+
CalculationOutput,
|
| 24 |
+
BlankCalculationOutput,
|
| 25 |
+
)
|
| 26 |
+
from autode.values import PotentialEnergy, GradientRMS
|
| 27 |
+
|
| 28 |
+
if TYPE_CHECKING:
|
| 29 |
+
from autode.species.species import Species
|
| 30 |
+
from autode.wrappers.methods import Method
|
| 31 |
+
from autode.wrappers.keywords import Keywords
|
| 32 |
+
from autode.opt.optimisers.base import NDOptimiser
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class CalculationExecutor:
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
name: str,
|
| 39 |
+
molecule: "Species",
|
| 40 |
+
method: "Method",
|
| 41 |
+
keywords: "Keywords",
|
| 42 |
+
n_cores: int = 1,
|
| 43 |
+
point_charges: Optional[List[PointCharge]] = None,
|
| 44 |
+
):
|
| 45 |
+
# Calculation names that start with "-" can break EST methods
|
| 46 |
+
self.name = f"{_string_without_leading_hyphen(name)}_{method.name}"
|
| 47 |
+
|
| 48 |
+
self.molecule = molecule
|
| 49 |
+
self.method = method
|
| 50 |
+
self.optimiser: BaseOptimiser = NullOptimiser()
|
| 51 |
+
self.n_cores = int(n_cores)
|
| 52 |
+
|
| 53 |
+
self.input = CalculationInput(
|
| 54 |
+
keywords=keywords,
|
| 55 |
+
added_internals=_active_bonds(molecule),
|
| 56 |
+
point_charges=point_charges,
|
| 57 |
+
)
|
| 58 |
+
self._external_output = CalculationOutput()
|
| 59 |
+
self._check()
|
| 60 |
+
|
| 61 |
+
def _check(self) -> None:
|
| 62 |
+
"""Check that the method has the required properties to run the calc"""
|
| 63 |
+
|
| 64 |
+
if self.molecule.solvent is None or self.molecule.solvent.is_explicit:
|
| 65 |
+
return
|
| 66 |
+
|
| 67 |
+
if getattr(self.molecule.solvent, self.method.name) is None:
|
| 68 |
+
err_str = (
|
| 69 |
+
f"Could not find {self.molecule.solvent} for "
|
| 70 |
+
f"{self.method.name}. Available solvents for {self.method.name} "
|
| 71 |
+
f"are: {self.method.available_implicit_solvents}"
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
raise ex.SolventUnavailable(err_str)
|
| 75 |
+
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
def run(self) -> None:
|
| 79 |
+
"""Run/execute the calculation"""
|
| 80 |
+
|
| 81 |
+
if self.method.uses_external_io:
|
| 82 |
+
self.generate_input()
|
| 83 |
+
self.output.filename = self.method.output_filename_for(self)
|
| 84 |
+
self._execute_external()
|
| 85 |
+
self.set_properties()
|
| 86 |
+
self.clean_up()
|
| 87 |
+
|
| 88 |
+
else:
|
| 89 |
+
self.method.execute(self)
|
| 90 |
+
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
def generate_input(self) -> None:
|
| 94 |
+
"""Generate the required input file"""
|
| 95 |
+
logger.info(f"Generating input file(s) for {self.name}")
|
| 96 |
+
|
| 97 |
+
# Can switch off uniqueness testing with e.g.
|
| 98 |
+
# export AUTODE_FIXUNIQUE=False used for testing
|
| 99 |
+
if os.getenv("AUTODE_FIXUNIQUE", True) != "False":
|
| 100 |
+
self._fix_unique()
|
| 101 |
+
|
| 102 |
+
self.input.filename = self.method.input_filename_for(self)
|
| 103 |
+
|
| 104 |
+
# Check that if the keyword is a autode.wrappers.keywords.Keyword then
|
| 105 |
+
# it has the required name in the method used for this calculation
|
| 106 |
+
for keyword in self.input.keywords:
|
| 107 |
+
if not isinstance(keyword, kws.Keyword): # allow string keywords
|
| 108 |
+
continue
|
| 109 |
+
|
| 110 |
+
# Allow for the unambiguous setting of a keyword with only a name
|
| 111 |
+
if keyword.has_only_name:
|
| 112 |
+
# set e.g. keyword.orca = 'b3lyp'
|
| 113 |
+
setattr(keyword, self.method.name, keyword.name)
|
| 114 |
+
continue
|
| 115 |
+
|
| 116 |
+
# For a keyword e.g. Keyword(name='pbe', orca='PBE') then the
|
| 117 |
+
# definition in this method is not obvious, so raise an exception
|
| 118 |
+
if getattr(keyword, self.method.name) is None:
|
| 119 |
+
err_str = (
|
| 120 |
+
f"Keyword: {keyword} is not supported set "
|
| 121 |
+
f"{repr(keyword)}.{self.method.name} as a string"
|
| 122 |
+
)
|
| 123 |
+
raise ex.UnsupportedCalculationInput(err_str)
|
| 124 |
+
|
| 125 |
+
return self.method.generate_input_for(self)
|
| 126 |
+
|
| 127 |
+
def _execute_external(self) -> None:
|
| 128 |
+
"""
|
| 129 |
+
Execute an external calculation i.e. one that saves a log file if it
|
| 130 |
+
has not been run, or if it did not finish with a normal termination
|
| 131 |
+
"""
|
| 132 |
+
logger.info(f"Running {self.input.filename} using {self.method.name}")
|
| 133 |
+
|
| 134 |
+
if not self.input.exists:
|
| 135 |
+
raise ex.NoInputError("Input did not exist")
|
| 136 |
+
|
| 137 |
+
if self.output.exists and self.terminated_normally:
|
| 138 |
+
logger.info("Calculation already terminated normally. Skipping")
|
| 139 |
+
return None
|
| 140 |
+
|
| 141 |
+
if not self.method.is_available:
|
| 142 |
+
raise ex.MethodUnavailable(f"{self.method} was not available")
|
| 143 |
+
|
| 144 |
+
self.output.clear()
|
| 145 |
+
self.method.execute(self)
|
| 146 |
+
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
@requires_output_to_exist
|
| 150 |
+
def set_properties(self) -> None:
|
| 151 |
+
"""Set the properties of a molecule from this calculation"""
|
| 152 |
+
keywords = self.input.keywords
|
| 153 |
+
|
| 154 |
+
if isinstance(keywords, kws.OptKeywords):
|
| 155 |
+
self.optimiser = self.method.optimiser_from(self)
|
| 156 |
+
self.molecule.coordinates = self.method.coordinates_from(self)
|
| 157 |
+
|
| 158 |
+
self.molecule.energy = self.method.energy_from(self)
|
| 159 |
+
|
| 160 |
+
if isinstance(keywords, kws.GradientKeywords):
|
| 161 |
+
self.molecule.gradient = self.method.gradient_from(self)
|
| 162 |
+
else: # Try to set the gradient anyway
|
| 163 |
+
self._no_except_set_gradient()
|
| 164 |
+
|
| 165 |
+
if isinstance(keywords, kws.HessianKeywords):
|
| 166 |
+
self.molecule.hessian = self.method.hessian_from(self)
|
| 167 |
+
else: # Try to set hessian anyway
|
| 168 |
+
self._no_except_set_hessian()
|
| 169 |
+
|
| 170 |
+
try:
|
| 171 |
+
self.molecule.partial_charges = self.method.partial_charges_from(
|
| 172 |
+
self
|
| 173 |
+
)
|
| 174 |
+
except (ValueError, IndexError, ex.AutodeException):
|
| 175 |
+
logger.warning("Failed to set partial charges")
|
| 176 |
+
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
@no_exceptions
|
| 180 |
+
def _no_except_set_gradient(self) -> None:
|
| 181 |
+
self.molecule.gradient = self.method.gradient_from(self)
|
| 182 |
+
|
| 183 |
+
@no_exceptions
|
| 184 |
+
def _no_except_set_hessian(self) -> None:
|
| 185 |
+
self.molecule.hessian = self.method.hessian_from(self)
|
| 186 |
+
|
| 187 |
+
def clean_up(self, force: bool = False, everything: bool = False) -> None:
|
| 188 |
+
if not self.method.uses_external_io: # Then there are no i/o files
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
if Config.keep_input_files and not force:
|
| 192 |
+
logger.info("Keeping input files")
|
| 193 |
+
return None
|
| 194 |
+
|
| 195 |
+
filenames = self.input.filenames
|
| 196 |
+
if everything:
|
| 197 |
+
filenames.append(self.output.filename)
|
| 198 |
+
filenames += [
|
| 199 |
+
fn for fn in os.listdir() if fn.startswith(self.name)
|
| 200 |
+
]
|
| 201 |
+
|
| 202 |
+
logger.info(f"Deleting: {set(filenames)}")
|
| 203 |
+
|
| 204 |
+
for filename in [fn for fn in set(filenames) if fn is not None]:
|
| 205 |
+
try:
|
| 206 |
+
os.remove(filename)
|
| 207 |
+
except FileNotFoundError:
|
| 208 |
+
logger.warning(f"Could not delete {filename} it did not exist")
|
| 209 |
+
|
| 210 |
+
return None
|
| 211 |
+
|
| 212 |
+
@property
|
| 213 |
+
def terminated_normally(self) -> bool:
|
| 214 |
+
"""
|
| 215 |
+
Determine if the calculation terminated without error
|
| 216 |
+
|
| 217 |
+
-----------------------------------------------------------------------
|
| 218 |
+
Returns:
|
| 219 |
+
(bool): Normal termination of the calculation?
|
| 220 |
+
"""
|
| 221 |
+
logger.info(f"Checking for {self.output.filename} normal termination")
|
| 222 |
+
|
| 223 |
+
if self.method.uses_external_io and not self.output.exists:
|
| 224 |
+
logger.warning("Calculation did not generate any output")
|
| 225 |
+
return False
|
| 226 |
+
|
| 227 |
+
return self.method.terminated_normally_in(self)
|
| 228 |
+
|
| 229 |
+
@property
|
| 230 |
+
def output(self) -> "CalculationOutput":
|
| 231 |
+
"""
|
| 232 |
+
Calculation output. If the method does not use any external files
|
| 233 |
+
then a blank calculation output is returned
|
| 234 |
+
"""
|
| 235 |
+
|
| 236 |
+
if self.method.uses_external_io:
|
| 237 |
+
return self._external_output
|
| 238 |
+
else:
|
| 239 |
+
return BlankCalculationOutput()
|
| 240 |
+
|
| 241 |
+
@output.setter
|
| 242 |
+
def output(self, value: CalculationOutput):
|
| 243 |
+
"""Set the value of the calculation output"""
|
| 244 |
+
assert isinstance(value, CalculationOutput)
|
| 245 |
+
|
| 246 |
+
self._external_output = value
|
| 247 |
+
|
| 248 |
+
def copy(self) -> "CalculationExecutor":
|
| 249 |
+
return deepcopy(self)
|
| 250 |
+
|
| 251 |
+
def __str__(self):
|
| 252 |
+
"""Create a unique string(/hash) of the calculation"""
|
| 253 |
+
string = (
|
| 254 |
+
f"{self.name}{self.method.name}{repr(self.input.keywords)}"
|
| 255 |
+
f"{self.molecule}{self.method.implicit_solvation_type}"
|
| 256 |
+
f"{self.molecule.constraints}"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
hasher = hashlib.sha1(string.encode()).digest()
|
| 260 |
+
return base64.urlsafe_b64encode(hasher).decode()
|
| 261 |
+
|
| 262 |
+
def _fix_unique(self, register_name=".autode_calculations") -> None:
|
| 263 |
+
"""
|
| 264 |
+
If a calculation has already been run for this molecule then it
|
| 265 |
+
shouldn't be run again, unless the input keywords have changed, in
|
| 266 |
+
which case it should be run while retaining the previous data. This
|
| 267 |
+
function fixes this problem by checking .autode_calculations and adding
|
| 268 |
+
a number to the end of self.name if the calculation input is different
|
| 269 |
+
"""
|
| 270 |
+
|
| 271 |
+
def append_register():
|
| 272 |
+
with open(register_name, "a") as register_file:
|
| 273 |
+
print(self.name, str(self), file=register_file)
|
| 274 |
+
|
| 275 |
+
def exists():
|
| 276 |
+
return any(reg_name == self.name for reg_name in register.keys())
|
| 277 |
+
|
| 278 |
+
def is_identical():
|
| 279 |
+
return any(reg_id == str(self) for reg_id in register.values())
|
| 280 |
+
|
| 281 |
+
# If there is no register yet in this folder then create it
|
| 282 |
+
if not os.path.exists(register_name):
|
| 283 |
+
logger.info("No calculations have been performed here yet")
|
| 284 |
+
append_register()
|
| 285 |
+
return None
|
| 286 |
+
|
| 287 |
+
# Populate a register of calculation names and their unique identifiers
|
| 288 |
+
register = {}
|
| 289 |
+
for line in open(register_name, "r"):
|
| 290 |
+
if len(line.split()) == 2: # Expecting: name id
|
| 291 |
+
calc_name, identifier = line.split()
|
| 292 |
+
register[calc_name] = identifier
|
| 293 |
+
|
| 294 |
+
if is_identical():
|
| 295 |
+
logger.info("Calculation exists in registry")
|
| 296 |
+
return None
|
| 297 |
+
|
| 298 |
+
# If this calculation doesn't yet appear in the register add it
|
| 299 |
+
if not exists():
|
| 300 |
+
logger.info("This calculation has not yet been run")
|
| 301 |
+
append_register()
|
| 302 |
+
return None
|
| 303 |
+
|
| 304 |
+
# If we're here then this calculation - with these input - has not yet
|
| 305 |
+
# been run. Therefore, add an integer to the calculation name until
|
| 306 |
+
# either the calculation has been run before and is the same or it's
|
| 307 |
+
# not been run
|
| 308 |
+
logger.info(
|
| 309 |
+
"Calculation with this name has been run before but "
|
| 310 |
+
"with different input"
|
| 311 |
+
)
|
| 312 |
+
name, n = self.name, 0
|
| 313 |
+
while True:
|
| 314 |
+
self.name = f"{name}{n}"
|
| 315 |
+
logger.info(f"New calculation name is: {self.name}")
|
| 316 |
+
|
| 317 |
+
if is_identical():
|
| 318 |
+
return None
|
| 319 |
+
|
| 320 |
+
if not exists():
|
| 321 |
+
append_register()
|
| 322 |
+
return None
|
| 323 |
+
|
| 324 |
+
n += 1
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
class _IndirectCalculationExecutor(CalculationExecutor):
|
| 328 |
+
"""
|
| 329 |
+
An 'indirect' executor is one that, given a calculation to perform,
|
| 330 |
+
calls the method multiple times and aggregates the results in some way.
|
| 331 |
+
Therefore, there is no direct calculation output.
|
| 332 |
+
"""
|
| 333 |
+
|
| 334 |
+
@property
|
| 335 |
+
def output(self) -> "CalculationOutput":
|
| 336 |
+
return BlankCalculationOutput()
|
| 337 |
+
|
| 338 |
+
@output.setter
|
| 339 |
+
def output(self, value: CalculationOutput):
|
| 340 |
+
raise ValueError("Cannot set the output of an indirect calculation")
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
class CalculationExecutorO(_IndirectCalculationExecutor):
|
| 344 |
+
"""Calculation executor that uses autodE inbuilt optimisation"""
|
| 345 |
+
|
| 346 |
+
def __init__(self, *args, **kwargs):
|
| 347 |
+
super().__init__(*args, **kwargs)
|
| 348 |
+
|
| 349 |
+
self.conv_tol = "normal"
|
| 350 |
+
self._fix_unique()
|
| 351 |
+
|
| 352 |
+
def run(self) -> None:
|
| 353 |
+
"""Run an optimisation with using default autodE optimisers"""
|
| 354 |
+
from autode.opt.optimisers.crfo import CRFOptimiser
|
| 355 |
+
from autode.opt.optimisers.prfo import PRFOptimiser
|
| 356 |
+
|
| 357 |
+
if self._opt_trajectory_exists:
|
| 358 |
+
self.optimiser = CRFOptimiser.from_file(self._opt_trajectory_name)
|
| 359 |
+
self._set_properties_from_optimiser()
|
| 360 |
+
return None
|
| 361 |
+
|
| 362 |
+
type_ = PRFOptimiser if self._calc_is_ts_opt else CRFOptimiser
|
| 363 |
+
|
| 364 |
+
self.optimiser: "NDOptimiser" = type_(
|
| 365 |
+
init_alpha=self._step_size,
|
| 366 |
+
maxiter=self._max_opt_cycles,
|
| 367 |
+
conv_tol=self.conv_tol,
|
| 368 |
+
)
|
| 369 |
+
method = self.method.copy()
|
| 370 |
+
method.keywords.grad = kws.GradientKeywords(self.input.keywords)
|
| 371 |
+
|
| 372 |
+
self.optimiser.run(
|
| 373 |
+
species=self.molecule,
|
| 374 |
+
method=method,
|
| 375 |
+
n_cores=self.n_cores,
|
| 376 |
+
name=self._opt_trajectory_name,
|
| 377 |
+
)
|
| 378 |
+
self.optimiser.print_geometries(
|
| 379 |
+
self._opt_trajectory_name[:-4]
|
| 380 |
+
if self._opt_trajectory_name.endswith(".zip")
|
| 381 |
+
else self._opt_trajectory_name
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
if self.molecule.n_atoms == 1:
|
| 385 |
+
return self._run_single_energy_evaluation()
|
| 386 |
+
|
| 387 |
+
if self._calc_is_ts_opt:
|
| 388 |
+
# If this calculation is a transition state optimisation then a
|
| 389 |
+
# hessian on the final structure is required
|
| 390 |
+
self.molecule.calc_hessian(
|
| 391 |
+
method=self.method, n_cores=self.n_cores
|
| 392 |
+
)
|
| 393 |
+
return None
|
| 394 |
+
|
| 395 |
+
def _run_single_energy_evaluation(self) -> None:
|
| 396 |
+
"""Run a single point energy evaluation, suitable for a single atom"""
|
| 397 |
+
from autode.calculations.calculation import Calculation
|
| 398 |
+
|
| 399 |
+
calc = Calculation(
|
| 400 |
+
name=f"{self.molecule.name}_energy",
|
| 401 |
+
molecule=self.molecule,
|
| 402 |
+
method=self.method,
|
| 403 |
+
keywords=kws.SinglePointKeywords(self.input.keywords),
|
| 404 |
+
n_cores=self.n_cores,
|
| 405 |
+
)
|
| 406 |
+
calc.run()
|
| 407 |
+
return None
|
| 408 |
+
|
| 409 |
+
@property
|
| 410 |
+
def terminated_normally(self) -> bool:
|
| 411 |
+
"""
|
| 412 |
+
Using inbuilt optimisers raise exceptions if something goes wrong, so
|
| 413 |
+
this property is always true, provided the output exists
|
| 414 |
+
|
| 415 |
+
-----------------------------------------------------------------------
|
| 416 |
+
Returns:
|
| 417 |
+
(bool): Normal termination of the calculation?
|
| 418 |
+
"""
|
| 419 |
+
return self._opt_trajectory_exists or self.molecule.n_atoms == 1
|
| 420 |
+
|
| 421 |
+
def set_properties(self) -> None:
|
| 422 |
+
"""
|
| 423 |
+
Nothing needs to be set as the energy/gradient/Hessian of the
|
| 424 |
+
molecule are set within the optimiser
|
| 425 |
+
"""
|
| 426 |
+
return None
|
| 427 |
+
|
| 428 |
+
@property
|
| 429 |
+
def _calc_is_ts_opt(self) -> bool:
|
| 430 |
+
"""Does this calculation correspond to a transition state opt"""
|
| 431 |
+
return isinstance(self.input.keywords, kws.OptTSKeywords)
|
| 432 |
+
|
| 433 |
+
@property
|
| 434 |
+
def _max_opt_cycles(self) -> int:
|
| 435 |
+
"""Get the maximum num of optimisation cycles for this calculation"""
|
| 436 |
+
try:
|
| 437 |
+
return next(
|
| 438 |
+
int(kwd)
|
| 439 |
+
for kwd in self.input.keywords
|
| 440 |
+
if isinstance(kwd, kws.MaxOptCycles)
|
| 441 |
+
)
|
| 442 |
+
except StopIteration:
|
| 443 |
+
return 50
|
| 444 |
+
|
| 445 |
+
@property
|
| 446 |
+
def _step_size(self) -> float:
|
| 447 |
+
return 0.05 if self._calc_is_ts_opt else 0.1
|
| 448 |
+
|
| 449 |
+
@property
|
| 450 |
+
def _opt_trajectory_name(self) -> str:
|
| 451 |
+
return f"{self.name}_opt_trj.zip"
|
| 452 |
+
|
| 453 |
+
@property
|
| 454 |
+
def _opt_trajectory_exists(self) -> bool:
|
| 455 |
+
return os.path.exists(self._opt_trajectory_name)
|
| 456 |
+
|
| 457 |
+
def _set_properties_from_optimiser(self) -> None:
|
| 458 |
+
"""Set the properties from the trajectory file, that must exist"""
|
| 459 |
+
logger.info(
|
| 460 |
+
"Setting optimised coordinates, gradient and energy from "
|
| 461 |
+
"the reloaded optimiser state"
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
final_coords = self.optimiser.final_coordinates
|
| 465 |
+
if final_coords is None:
|
| 466 |
+
raise ex.CalculationException("Final coordinates undefined")
|
| 467 |
+
|
| 468 |
+
cart_coords = final_coords.to("cart")
|
| 469 |
+
self.molecule.coordinates = cart_coords.reshape((-1, 3))
|
| 470 |
+
if cart_coords.g is not None:
|
| 471 |
+
self.molecule.gradient = cart_coords.g.reshape((-1, 3))
|
| 472 |
+
|
| 473 |
+
self.molecule.energy = final_coords.e
|
| 474 |
+
return None
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
class CalculationExecutorG(_IndirectCalculationExecutor):
|
| 478 |
+
"""Calculation executor with a numerical gradient evaluation"""
|
| 479 |
+
|
| 480 |
+
def run(self) -> None:
|
| 481 |
+
raise NotImplementedError
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
class CalculationExecutorH(_IndirectCalculationExecutor):
|
| 485 |
+
"""Calculation executor with a numerical Hessian evaluation"""
|
| 486 |
+
|
| 487 |
+
def run(self) -> None:
|
| 488 |
+
logger.warning(
|
| 489 |
+
f"{self.method} does not implement Hessian "
|
| 490 |
+
f"calculations. Evaluating a numerical Hessian"
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
from autode.hessians import NumericalHessianCalculator
|
| 494 |
+
|
| 495 |
+
nhc = NumericalHessianCalculator(
|
| 496 |
+
species=self.molecule,
|
| 497 |
+
method=self.method,
|
| 498 |
+
keywords=kws.GradientKeywords(self.input.keywords.tolist()),
|
| 499 |
+
do_c_diff=False,
|
| 500 |
+
shift=Distance(2e-3, units="Å"),
|
| 501 |
+
n_cores=self.n_cores,
|
| 502 |
+
)
|
| 503 |
+
nhc.calculate()
|
| 504 |
+
self.molecule.hessian = nhc.hessian
|
| 505 |
+
|
| 506 |
+
@property
|
| 507 |
+
def terminated_normally(self) -> bool:
|
| 508 |
+
"""
|
| 509 |
+
This calculation executor terminated normally if the Hessian exists and
|
| 510 |
+
did not raise any exceptions along the way
|
| 511 |
+
|
| 512 |
+
-----------------------------------------------------------------------
|
| 513 |
+
Returns:
|
| 514 |
+
(bool):
|
| 515 |
+
"""
|
| 516 |
+
return self.molecule.hessian is not None
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
def _string_without_leading_hyphen(s: str) -> str:
|
| 520 |
+
return s if not s.startswith("-") else f"_{s}"
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _active_bonds(molecule: "Species") -> List[Tuple[int, int]]:
|
| 524 |
+
return [] if molecule.graph is None else molecule.graph.active_bonds
|
autodE/source/autode/calculations/input.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import autode.wrappers.keywords as kws
|
| 3 |
+
|
| 4 |
+
from typing import Optional, List, TYPE_CHECKING
|
| 5 |
+
from autode.point_charges import PointCharge
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from autode.wrappers.keywords import Keywords
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class CalculationInput:
|
| 12 |
+
def __init__(
|
| 13 |
+
self,
|
| 14 |
+
keywords: "Keywords",
|
| 15 |
+
added_internals: Optional[list] = None,
|
| 16 |
+
point_charges: Optional[List[PointCharge]] = None,
|
| 17 |
+
):
|
| 18 |
+
"""
|
| 19 |
+
Calculation input
|
| 20 |
+
|
| 21 |
+
-----------------------------------------------------------------------
|
| 22 |
+
Arguments:
|
| 23 |
+
keywords: Keywords that a method will use to run the calculation
|
| 24 |
+
e.g. ['pbe', 'def2-svp'] for an ORCA single point at
|
| 25 |
+
PBE/def2-SVP
|
| 26 |
+
|
| 27 |
+
added_internals: Atom indexes to add to the internal coordinates
|
| 28 |
+
|
| 29 |
+
point_charges: Optional list of float of point charges, x, y, z
|
| 30 |
+
coordinates for each point charge
|
| 31 |
+
"""
|
| 32 |
+
self.keywords: Keywords = keywords.copy()
|
| 33 |
+
|
| 34 |
+
self.added_internals: Optional[list] = None
|
| 35 |
+
if added_internals is not None and len(added_internals) > 0:
|
| 36 |
+
self.added_internals = added_internals
|
| 37 |
+
|
| 38 |
+
self.point_charges = point_charges
|
| 39 |
+
|
| 40 |
+
self.filename: Optional[str] = None
|
| 41 |
+
self.additional_filenames: List[str] = []
|
| 42 |
+
|
| 43 |
+
self._check()
|
| 44 |
+
|
| 45 |
+
def _check(self):
|
| 46 |
+
"""Check that the input parameters have the expected format"""
|
| 47 |
+
if self.keywords is not None:
|
| 48 |
+
assert isinstance(self.keywords, kws.Keywords)
|
| 49 |
+
|
| 50 |
+
# Ensure the point charges are given as a list of PointCharge objects
|
| 51 |
+
if self.point_charges is not None:
|
| 52 |
+
assert type(self.point_charges) is list
|
| 53 |
+
assert all(type(pc) is PointCharge for pc in self.point_charges)
|
| 54 |
+
|
| 55 |
+
if self.added_internals is not None:
|
| 56 |
+
assert type(self.added_internals) is list
|
| 57 |
+
assert all(len(idxs) == 2 for idxs in self.added_internals)
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
def exists(self):
|
| 61 |
+
"""Does the input (files) exist?"""
|
| 62 |
+
return self.filename is not None and all(
|
| 63 |
+
os.path.exists(fn) for fn in self.filenames
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def filenames(self):
|
| 68 |
+
"""Return a list of all the input files"""
|
| 69 |
+
if self.filename is None:
|
| 70 |
+
return self.additional_filenames
|
| 71 |
+
|
| 72 |
+
return [self.filename] + self.additional_filenames
|
autodE/source/autode/calculations/output.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import autode.exceptions as ex
|
| 3 |
+
|
| 4 |
+
from typing import Optional, List
|
| 5 |
+
from functools import cached_property
|
| 6 |
+
from autode.log import logger
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class CalculationOutput:
|
| 10 |
+
def __init__(self, filename: Optional[str] = None):
|
| 11 |
+
self._filename = filename
|
| 12 |
+
|
| 13 |
+
@property
|
| 14 |
+
def filename(self) -> Optional[str]:
|
| 15 |
+
return self._filename
|
| 16 |
+
|
| 17 |
+
@filename.setter
|
| 18 |
+
def filename(self, value: str):
|
| 19 |
+
self._filename = str(value)
|
| 20 |
+
self.clear()
|
| 21 |
+
|
| 22 |
+
@cached_property
|
| 23 |
+
def file_lines(self) -> List[str]:
|
| 24 |
+
"""
|
| 25 |
+
Output files lines. This may be slow for large files but should
|
| 26 |
+
not become a bottleneck when running standard DFT/WF calculations,
|
| 27 |
+
are cached so only read once
|
| 28 |
+
|
| 29 |
+
-----------------------------------------------------------------------
|
| 30 |
+
Returns:
|
| 31 |
+
(list(str)): Lines from the output file
|
| 32 |
+
|
| 33 |
+
Raises:
|
| 34 |
+
(autode.exceptions.NoCalculationOutput): If the file doesn't exist
|
| 35 |
+
"""
|
| 36 |
+
logger.info("Setting output file lines")
|
| 37 |
+
|
| 38 |
+
if self.filename is None or not os.path.exists(self.filename):
|
| 39 |
+
raise ex.NoCalculationOutput
|
| 40 |
+
|
| 41 |
+
file = open(self.filename, "r", encoding="utf-8", errors="ignore")
|
| 42 |
+
return file.readlines()
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def exists(self) -> bool:
|
| 46 |
+
"""Does the calculation output exist?"""
|
| 47 |
+
return self.filename is not None and os.path.exists(self.filename)
|
| 48 |
+
|
| 49 |
+
def clear(self) -> None:
|
| 50 |
+
"""Clear the cached file lines"""
|
| 51 |
+
|
| 52 |
+
if "file_lines" in self.__dict__:
|
| 53 |
+
del self.__dict__["file_lines"]
|
| 54 |
+
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
def try_to_print_final_lines(self, n: int = 50) -> None:
|
| 58 |
+
"""
|
| 59 |
+
Attempt to print the final n output lines, if the output exists
|
| 60 |
+
|
| 61 |
+
-----------------------------------------------------------------------
|
| 62 |
+
Arguments:
|
| 63 |
+
n: Number of lines
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
if self.exists:
|
| 67 |
+
print("".join(self.file_lines[-n:]))
|
| 68 |
+
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class BlankCalculationOutput(CalculationOutput):
|
| 73 |
+
@property
|
| 74 |
+
def filename(self) -> Optional[str]:
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
@filename.setter
|
| 78 |
+
def filename(self, value: str):
|
| 79 |
+
raise ValueError("Cannot set the filename of a blank output")
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def file_lines(self) -> List[str]:
|
| 83 |
+
return []
|
| 84 |
+
|
| 85 |
+
@property
|
| 86 |
+
def exists(self) -> bool:
|
| 87 |
+
return True
|
autodE/source/autode/calculations/types.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class CalculationType(Enum):
|
| 5 |
+
"""Enum defining a mode/type of a calculation"""
|
| 6 |
+
|
| 7 |
+
opt = 0
|
| 8 |
+
energy = 1
|
| 9 |
+
gradient = 2
|
| 10 |
+
hessian = 3
|
autodE/source/autode/common/NEB.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:694f945d2516a306819efe16cca784e12b6d657c7bae52e989ba4b0368b3899a
|
| 3 |
+
size 106121
|
autodE/source/autode/common/NEB.tex
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\documentclass[10pt]{article}
|
| 2 |
+
\usepackage{bm}% bold math
|
| 3 |
+
\usepackage{amsmath}
|
| 4 |
+
|
| 5 |
+
\begin{document}
|
| 6 |
+
|
| 7 |
+
\subsection{Original NEB}
|
| 8 |
+
|
| 9 |
+
Nudged elastic band (NEB) approaches to locating transition states are efficient alternatives to evaluating the PES on a uniform grid over some coordinates of interest. The implementation in \emph{autodE} follows that in [\emph{J. Chem. Phys.}, 2000, {\bfseries{113}}, 9978]
|
| 10 |
+
\\\\
|
| 11 |
+
For an image $i$ in the nudged elastic band
|
| 12 |
+
\begin{equation}
|
| 13 |
+
\boldsymbol{\tau}_i =
|
| 14 |
+
\begin{cases}
|
| 15 |
+
\boldsymbol{\tau}_i^+ &\quad\text{if}\quad V_{i-1} < V_i < V_{i+1} \\
|
| 16 |
+
\boldsymbol{\tau}_i^- &\quad\text{if}\quad V_{i+1} < V_i < V_{i-1} \\
|
| 17 |
+
\boldsymbol{\tau}_i^+\Delta V_i^{max} + \boldsymbol{\tau}_i^-\Delta V_i^{min} &\quad\text{if}\quad V_{i-1} < V_{i+1} \\
|
| 18 |
+
\boldsymbol{\tau}_i^+\Delta V_i^{min} + \boldsymbol{\tau}_i^-\Delta V_i^{max} &\quad\text{if}\quad V_{i+1} < V_{i-1} \\
|
| 19 |
+
\end{cases}
|
| 20 |
+
\end{equation}
|
| 21 |
+
where
|
| 22 |
+
\begin{equation}
|
| 23 |
+
\begin{aligned}
|
| 24 |
+
\boldsymbol{\tau}_i^+ &= \boldsymbol{x}_{i+1} - \boldsymbol{x}_i \\
|
| 25 |
+
\boldsymbol{\tau}_i^- &= \boldsymbol{x}_{i} - \boldsymbol{x}_{i-1}
|
| 26 |
+
\end{aligned}
|
| 27 |
+
\end{equation}
|
| 28 |
+
and
|
| 29 |
+
\begin{equation}
|
| 30 |
+
\begin{aligned}
|
| 31 |
+
\Delta V_i^{max} &= \max(|V_{i+1} - V_i|, |V_{i-1} - V_i|) \\
|
| 32 |
+
\Delta V_i^{min} &= \min(|V_{i+1} - V_i|, |V_{i-1} - V_i|)
|
| 33 |
+
\end{aligned}
|
| 34 |
+
\end{equation}
|
| 35 |
+
and $\boldsymbol{x}_i$ are the coordinates of image $i$. The spring force is
|
| 36 |
+
\begin{equation}
|
| 37 |
+
\boldsymbol{F}^s_i|_{\parallel} = (k_i|\boldsymbol{x}_{i+1} - \boldsymbol{x}_i| - k_{i-1}|\boldsymbol{x}_i - \boldsymbol{x}_{i-1}|) \hat{\boldsymbol{\tau}}_i
|
| 38 |
+
\end{equation}
|
| 39 |
+
and the total force on the image
|
| 40 |
+
\begin{equation}
|
| 41 |
+
\boldsymbol{F}_i = \boldsymbol{F}^s_i|_{\parallel} - \nabla V(\boldsymbol{x}_i)|_\perp
|
| 42 |
+
\end{equation}
|
| 43 |
+
where
|
| 44 |
+
\begin{equation}
|
| 45 |
+
\nabla V(\boldsymbol{x}_i)|_\perp = \nabla V(\boldsymbol{x}_i) - \nabla V(\boldsymbol{x}_i)\cdot \hat{\boldsymbol{\tau}}_i\hat{\boldsymbol{\tau}}_i
|
| 46 |
+
\end{equation}
|
| 47 |
+
and finally $\hat{\boldsymbol{\tau}} = \boldsymbol{\tau}_i/|\boldsymbol{\tau}_i|$.
|
| 48 |
+
\\\\
|
| 49 |
+
\subsection{CI-NEB}
|
| 50 |
+
|
| 51 |
+
The climbing image (CI) NEB implementation follows that in [\emph{J. Chem. Phys.}, 2000, {\bfseries{113}}, 9901] where after a few iterations the force on the maximum energy image ($m$) is given by
|
| 52 |
+
|
| 53 |
+
\begin{equation}
|
| 54 |
+
\boldsymbol{F}_{m} = -\nabla V(\boldsymbol{x}_m) + 2\nabla V(\boldsymbol{x}_m)\cdot \hat{\boldsymbol{\tau}}_i\hat{\boldsymbol{\tau}}_i
|
| 55 |
+
\end{equation}
|
| 56 |
+
|
| 57 |
+
which is the force due to the potential along the band being inverted.
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
\end{document}
|
autodE/source/autode/common/adaptive_path.pdf
ADDED
|
Binary file (95.4 kB). View file
|
|
|
autodE/source/autode/common/adaptive_path.tex
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\documentclass[10pt]{article}
|
| 2 |
+
\usepackage{bm}% bold math
|
| 3 |
+
\usepackage{amsmath}
|
| 4 |
+
\DeclareMathOperator{\sgn}{sgn}
|
| 5 |
+
|
| 6 |
+
\begin{document}
|
| 7 |
+
|
| 8 |
+
\subsection{Adaptive Path}
|
| 9 |
+
|
| 10 |
+
The adaptive path algorithm in \emph{autodE} attempts to traverse the minimum energy pathway from reactants to products with constrained optimisations using a gradient dependent step size. The initial constraints for the first point are
|
| 11 |
+
|
| 12 |
+
\begin{equation}
|
| 13 |
+
r_b^{(1)} = r_b^{(0)} + \sgn(r_b^\text{final} - r_b^{(0)})\Delta r_\text{init}
|
| 14 |
+
\end{equation}
|
| 15 |
+
\\
|
| 16 |
+
for a bond $b$, where the superscript denotes the current step. $\Delta r_\text{init}$ is an initial step size, e.g. 0.2 Å. Constraints for subsequent steps are then given by
|
| 17 |
+
\\\\
|
| 18 |
+
\begin{equation}
|
| 19 |
+
r_b^{(k)} = r_b^{(k-1)} + \sgn(r_b^\text{final} - r_b^{(0)})\Delta r_b^{(k-1)}
|
| 20 |
+
\end{equation}
|
| 21 |
+
|
| 22 |
+
\begin{equation}
|
| 23 |
+
\Delta r_b^{(k)} =
|
| 24 |
+
\begin{cases}
|
| 25 |
+
\Delta r_\text{max} \quad &\text{if } \sgn(r_b^\text{final} - r_b^{(0)}) \nabla E_{j} \cdot \boldsymbol{r}_{ij} > 0 \\
|
| 26 |
+
\Delta r_\text{m}\exp\left[-\left({\nabla E_{j}^{(k)} \cdot \boldsymbol{r}_{ij}}/{g} \right)^2\right] + \Delta r_\text{min} \quad &\text{otherwise}
|
| 27 |
+
\end{cases}
|
| 28 |
+
\end{equation}
|
| 29 |
+
\\
|
| 30 |
+
where $\Delta r_\text{m} = \Delta r_\text{max} - \Delta r_\text{min}$, $E$ the total potential energy (in the absence of any harmonic constraints) and $g$ a parameter to control the interpolation between $\Delta r_\text{max}$ and $\Delta r_\text{min}$ e.g. 0.05 Ha Å$^{-1}$. Atom indices $i, j$ form part of the bond indexed by $b$ with $j$ being an atom not being substituted. In the case that neither $i$ nor $j$ are being substituted the gradient is taken as an average over $i$ and $j$.
|
| 31 |
+
|
| 32 |
+
\end{document}
|
autodE/source/autode/common/hessians.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8dfd38c9dbaf48e024f972a70c7546b08390c644af5cc839712c49bb4532426c
|
| 3 |
+
size 154155
|
autodE/source/autode/common/hessians.tex
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\documentclass[10pt]{article}
|
| 2 |
+
\usepackage{bm}% bold math
|
| 3 |
+
\usepackage{amsmath}
|
| 4 |
+
\usepackage{amssymb}
|
| 5 |
+
\usepackage{color}
|
| 6 |
+
\DeclareMathOperator{\sgn}{sgn}
|
| 7 |
+
\renewcommand{\thefootnote}{\alph{footnote}}
|
| 8 |
+
|
| 9 |
+
\begin{document}
|
| 10 |
+
|
| 11 |
+
\subsection{Hessian Diagonalization}
|
| 12 |
+
|
| 13 |
+
Frequencies and normal modes are obtained from Hessian diagonalization, following the method from {\color{blue} https://tinyurl.com/4a75skfm}, which in turn uses (V. Barone, JCP, 2005, 122, 014108; V. Barone et al. IJQ. Chem., 2012, 112, 2185). Without projection frequencies and normal modes are just (transformed) eigenvalues and eigenvectors of the Hessian,
|
| 14 |
+
|
| 15 |
+
\begin{equation}
|
| 16 |
+
\mathsf{H} = \begin{pmatrix}
|
| 17 |
+
\frac{\partial^2 E}{\partial x_1^2} & \frac{\partial^2 E}{\partial x_1y_1} &
|
| 18 |
+
\frac{\partial^2 E}{\partial x_1z_1} & \cdots\\
|
| 19 |
+
\frac{\partial^2 E}{\partial y_1x_1} & \frac{\partial^2 E}{\partial y_1^2} &
|
| 20 |
+
\frac{\partial^2 E}{\partial y_1z_1} & \cdots\\
|
| 21 |
+
\vdots & \vdots & \vdots & \ddots
|
| 22 |
+
\end{pmatrix}
|
| 23 |
+
\end{equation}
|
| 24 |
+
\\
|
| 25 |
+
appropriately mass weighted,
|
| 26 |
+
\begin{equation}
|
| 27 |
+
\mathsf{H}_\text{w} = \begin{pmatrix}
|
| 28 |
+
\frac{\mathsf{H}_{11}}{\sqrt{m_1 m_1}} & \cdots & \frac{\mathsf{H}_{1,3i}}{\sqrt{m_1 m_i}} & \cdots \\
|
| 29 |
+
\vdots & \vdots & \vdots & \ddots
|
| 30 |
+
\end{pmatrix}
|
| 31 |
+
\end{equation}
|
| 32 |
+
\\
|
| 33 |
+
which is real symmetric so Hermitian ($\mathsf{H} \in \mathbb{R}^{3N\times3N}$ for a system of $N$ atoms). The frequencies are then square roots of the eigenvalues i.e. $\nu_i = \sqrt{\lambda_i}$\footnote{With an appropriate unit conversion.} and the normal modes $\boldsymbol{s}_i$ where,
|
| 34 |
+
|
| 35 |
+
\begin{equation}
|
| 36 |
+
\mathsf{H}_\text{w} = \mathsf{S D S}^T \quad ; \quad \mathsf{D} = \begin{pmatrix}
|
| 37 |
+
\lambda_1 & 0 & \cdots \\
|
| 38 |
+
0 & \lambda_2 & \cdots \\
|
| 39 |
+
\vdots & \vdots & \ddots
|
| 40 |
+
\end{pmatrix}
|
| 41 |
+
%
|
| 42 |
+
\quad ; \quad
|
| 43 |
+
%
|
| 44 |
+
\mathsf{S} = \begin{pmatrix}
|
| 45 |
+
\uparrow & \uparrow & \\
|
| 46 |
+
\boldsymbol{s}_1 & \boldsymbol{s}_2 & \cdots \\
|
| 47 |
+
\downarrow & \downarrow &
|
| 48 |
+
\end{pmatrix}
|
| 49 |
+
\end{equation}
|
| 50 |
+
\\
|
| 51 |
+
To project out translational and rotational motion for a non linear molecule requires a transformation of $\mathsf{H}_\text{w}$,
|
| 52 |
+
|
| 53 |
+
\begin{equation}
|
| 54 |
+
\mathsf{H}_\text{w}' = \mathsf{T}^T \mathsf{H}_\text{w} \mathsf{T} \qquad ; \qquad \mathsf{H}_\text{w}' = \begin{pmatrix}
|
| 55 |
+
\boldsymbol{0} & \boldsymbol{0} \\
|
| 56 |
+
\boldsymbol{0} & \bar{\mathsf{H}}_\text{w}
|
| 57 |
+
\end{pmatrix}
|
| 58 |
+
\end{equation}
|
| 59 |
+
\\
|
| 60 |
+
where
|
| 61 |
+
\begin{equation}
|
| 62 |
+
\mathsf{T} = \begin{pmatrix}
|
| 63 |
+
\uparrow & \uparrow & \\
|
| 64 |
+
\hat{\boldsymbol{t}}_1 & \hat{\boldsymbol{t}}_2 & \cdots \\
|
| 65 |
+
\downarrow & \downarrow &
|
| 66 |
+
\end{pmatrix}
|
| 67 |
+
\end{equation}
|
| 68 |
+
\\
|
| 69 |
+
and the columns of $\mathsf{M}$ are,
|
| 70 |
+
|
| 71 |
+
\begin{equation}
|
| 72 |
+
\boldsymbol{t}_1 = \begin{bmatrix}
|
| 73 |
+
(\hat{\boldsymbol{e}}_1)_1 \\
|
| 74 |
+
\vdots \\
|
| 75 |
+
(\hat{\boldsymbol{e}}_1)_N \\
|
| 76 |
+
\end{bmatrix}
|
| 77 |
+
%
|
| 78 |
+
\quad ; \quad
|
| 79 |
+
%
|
| 80 |
+
\boldsymbol{t}_2 = \begin{bmatrix}
|
| 81 |
+
(\hat{\boldsymbol{e}}_2)_1 \\
|
| 82 |
+
\vdots \\
|
| 83 |
+
(\hat{\boldsymbol{e}}_2)_N \\
|
| 84 |
+
\end{bmatrix}
|
| 85 |
+
%
|
| 86 |
+
\quad ; \quad
|
| 87 |
+
%
|
| 88 |
+
\boldsymbol{t}_3 = \begin{bmatrix}
|
| 89 |
+
(\hat{\boldsymbol{e}}_3)_1 \\
|
| 90 |
+
\vdots \\
|
| 91 |
+
(\hat{\boldsymbol{e}}_3)_N \\
|
| 92 |
+
\end{bmatrix}
|
| 93 |
+
\end{equation}
|
| 94 |
+
\\
|
| 95 |
+
where $\hat{\boldsymbol{e}}_k$ is a unit vector in 3D (i.e. $\hat{\boldsymbol{e}}_1 = (1, 0, 0)^T$). The rotation vectors are
|
| 96 |
+
|
| 97 |
+
\begin{equation}
|
| 98 |
+
\boldsymbol{t}_4 = \begin{bmatrix}
|
| 99 |
+
\boldsymbol{e}_1 \times \boldsymbol{r}_1 \\
|
| 100 |
+
\vdots \\
|
| 101 |
+
\boldsymbol{e}_1 \times \boldsymbol{r}_N \\
|
| 102 |
+
\end{bmatrix}
|
| 103 |
+
%
|
| 104 |
+
\quad ; \quad
|
| 105 |
+
%
|
| 106 |
+
\boldsymbol{t}_5 = \begin{bmatrix}
|
| 107 |
+
\boldsymbol{e}_2 \times \boldsymbol{r}_1 \\
|
| 108 |
+
\vdots \\
|
| 109 |
+
\boldsymbol{e}_2 \times \boldsymbol{r}_N \\
|
| 110 |
+
\end{bmatrix}
|
| 111 |
+
%
|
| 112 |
+
\quad ; \quad
|
| 113 |
+
%
|
| 114 |
+
\boldsymbol{t}_6 = \begin{bmatrix}
|
| 115 |
+
\boldsymbol{e}_3 \times \boldsymbol{r}_1 \\
|
| 116 |
+
\vdots \\
|
| 117 |
+
\boldsymbol{e}_3 \times \boldsymbol{r}_N \\
|
| 118 |
+
\end{bmatrix}
|
| 119 |
+
\end{equation}
|
| 120 |
+
\\
|
| 121 |
+
where $\boldsymbol{r}_i$ is the vector from the centre of mass of the system to the atom $i$. The remaining $\boldsymbol{t}_n$ are filled with random vectors that are orthogonal to $\boldsymbol{t}_1\text{--}\boldsymbol{t}_6$, which can be achieved by QR factorisation once the remaining elements of $\mathsf{T}$ have been seeded with random numbers. Normalisation requires,
|
| 122 |
+
|
| 123 |
+
\begin{equation}
|
| 124 |
+
\hat{\boldsymbol{t}}_i = \frac{\mathsf{M}^{1/2}\boldsymbol{t}_i}{|\mathsf{M}^{1/2}\boldsymbol{t}_i|}
|
| 125 |
+
%
|
| 126 |
+
\qquad ; \qquad
|
| 127 |
+
%
|
| 128 |
+
\mathsf{M} = \begin{pmatrix}
|
| 129 |
+
m_1 & 0 & 0 & 0 &\cdots \\
|
| 130 |
+
0 & m_1 & 0 & 0& \cdots \\
|
| 131 |
+
0 & 0 & m_1 & 0& \cdots \\
|
| 132 |
+
0 & 0 & 0 & m_2 & \cdots \\
|
| 133 |
+
\vdots & \vdots & \vdots & \vdots & \ddots
|
| 134 |
+
\end{pmatrix}
|
| 135 |
+
\end{equation}
|
| 136 |
+
\\
|
| 137 |
+
where $m_i$ is the mass of atom $i$.
|
| 138 |
+
|
| 139 |
+
\vspace{0.4cm}
|
| 140 |
+
|
| 141 |
+
Projected frequencies are then obtained from the submatrix of $\mathsf{H}_\text{w}'$,
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
\begin{equation}
|
| 145 |
+
\bar{\mathsf{H}}_\text{w} = \mathsf{\bar{S} \bar{D}\bar{S}}^T
|
| 146 |
+
%
|
| 147 |
+
\quad ; \quad
|
| 148 |
+
%
|
| 149 |
+
\bar{\mathsf{S}} =
|
| 150 |
+
\begin{pmatrix}
|
| 151 |
+
\uparrow & \\
|
| 152 |
+
\bar{\boldsymbol{s}}_7 & \cdots \\
|
| 153 |
+
\downarrow &
|
| 154 |
+
\end{pmatrix}
|
| 155 |
+
%
|
| 156 |
+
\quad ; \quad
|
| 157 |
+
%
|
| 158 |
+
\bar{\mathsf{D}} =
|
| 159 |
+
\begin{pmatrix}
|
| 160 |
+
\bar{\lambda}_7 & 0& \cdots \\
|
| 161 |
+
0 & \bar{\lambda}_8 & \cdots \\
|
| 162 |
+
\vdots & \vdots & \ddots
|
| 163 |
+
\end{pmatrix}
|
| 164 |
+
\end{equation}
|
| 165 |
+
with $\bar{\nu}_{0\text{--}6} = 0$ cm${}^{-1}$, while the eigenvectors are,
|
| 166 |
+
|
| 167 |
+
\begin{equation}
|
| 168 |
+
\boldsymbol{s}_i = \mathsf{T}\boldsymbol{s}_i'
|
| 169 |
+
%
|
| 170 |
+
\quad ; \quad
|
| 171 |
+
%
|
| 172 |
+
\mathsf{S}' = \begin{pmatrix}
|
| 173 |
+
\uparrow & \\
|
| 174 |
+
\boldsymbol{s}_1' & \cdots\\
|
| 175 |
+
\downarrow &
|
| 176 |
+
\end{pmatrix}
|
| 177 |
+
=
|
| 178 |
+
\begin{pmatrix}
|
| 179 |
+
\boldsymbol{0} & \boldsymbol{0} \\
|
| 180 |
+
\boldsymbol{0} & \bar{\mathsf{S}}
|
| 181 |
+
\end{pmatrix}
|
| 182 |
+
\end{equation}
|
| 183 |
+
\\
|
| 184 |
+
which correspond to the normal modes in the original coordinates. For a linear molecule the vibrational frequencies are then the $3N-5$ modes, rather than $3N-6$, with $\mathsf{H}_w'$ contains a different number of non-zero entries.
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
\end{document}
|
autodE/source/autode/common/llogo.png
ADDED
|
Git LFS Details
|
autodE/source/autode/common/logo.pages
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:408f692d6bc6bc309fcf189fa15a7de7eb44f2e7726c9ada3379d1ff3f8d3d86
|
| 3 |
+
size 101212
|
autodE/source/autode/common/thermochemistry.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3372faf8528e9aa2118cf95289cff147ed553bc10a5f9cad0dd4169184a5dc1c
|
| 3 |
+
size 119353
|
autodE/source/autode/common/thermochemistry.tex
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
\documentclass[10pt]{article}
|
| 2 |
+
\usepackage{bm}% bold math
|
| 3 |
+
\usepackage{amsmath}
|
| 4 |
+
\usepackage{amssymb}
|
| 5 |
+
\usepackage{color}
|
| 6 |
+
\DeclareMathOperator{\sgn}{sgn}
|
| 7 |
+
\renewcommand{\thefootnote}{\alph{footnote}}
|
| 8 |
+
|
| 9 |
+
\begin{document}
|
| 10 |
+
|
| 11 |
+
\subsection{Ideal Gas Model}
|
| 12 |
+
|
| 13 |
+
From (McQuarrie, Statistical mechanics, 2000) the ideal gas method (IGM) for calculating an absolute free energy is outlined below.
|
| 14 |
+
\begin{equation}
|
| 15 |
+
G = H - TS
|
| 16 |
+
\end{equation}
|
| 17 |
+
\begin{equation}
|
| 18 |
+
H = U + RT
|
| 19 |
+
\end{equation}
|
| 20 |
+
\begin{equation}
|
| 21 |
+
U = E_\text{pot} + E_\text{ZPE} + E_\text{trns} + E_\text{rot} + E_\text{vib} \end{equation}
|
| 22 |
+
\begin{equation}
|
| 23 |
+
S = S_\text{trns} + S_\text{rot} + S_\text{vib} + S_\text{elec}
|
| 24 |
+
\end{equation}
|
| 25 |
+
|
| 26 |
+
where $T$ is temperature, $R$ the ideal gas constant and $S_\text{elec}$ is taken to be zero for all molecules. The internal energy components are then
|
| 27 |
+
|
| 28 |
+
\begin{equation}
|
| 29 |
+
E_\text{ZPE} = \frac{N_a}{2}\sum_i h \nu_i
|
| 30 |
+
\end{equation}
|
| 31 |
+
\begin{equation}
|
| 32 |
+
E_\text{trns} = \frac{3}{2}RT
|
| 33 |
+
\end{equation}
|
| 34 |
+
\begin{equation}
|
| 35 |
+
E_\text{rot} =
|
| 36 |
+
\begin{cases}
|
| 37 |
+
0 &\quad \text{if } N = 1 \\
|
| 38 |
+
RT &\quad \text{if linear} \\
|
| 39 |
+
\frac{3}{2} RT &\quad \text{otherwise}
|
| 40 |
+
\end{cases}
|
| 41 |
+
\end{equation}
|
| 42 |
+
\begin{equation}
|
| 43 |
+
E_\text{vib} = R \sum_i \frac{\theta_i}{e^{\theta_i / T} - 1} \quad ,\quad \theta_i = h\nu_i / k_B
|
| 44 |
+
\end{equation}
|
| 45 |
+
|
| 46 |
+
where $N_a$ is Avogadro's's constant, $N$ is the number of atoms in the molecule, $k_B$ Boltzmann's constant, $ \nu_i$ the $i$-th harmonic frequency and $h$ is Planks constant. The entropic components are
|
| 47 |
+
|
| 48 |
+
\begin{equation}
|
| 49 |
+
S_\text{trns} = R \ln(q_\text{trns}) + \frac{5}{2}R
|
| 50 |
+
\end{equation}
|
| 51 |
+
\begin{equation}
|
| 52 |
+
S_\text{rot} = \begin{cases}
|
| 53 |
+
0 &\quad \text{if } N = 1 \\
|
| 54 |
+
R \ln(q_\text{rot}) + R &\quad \text{if linear} \\
|
| 55 |
+
R \ln(q_\text{rot}) + \frac{3}{2}R &\quad \text{otherwise}
|
| 56 |
+
\end{cases}
|
| 57 |
+
\end{equation}
|
| 58 |
+
\begin{equation}
|
| 59 |
+
S_\text{vib}^\text{HO} = R \sum_i \frac{\theta_i}{T(e^{\theta_i / T} - 1)} - \ln(1 - e^{-\theta_i / T})
|
| 60 |
+
\end{equation}
|
| 61 |
+
\begin{equation}
|
| 62 |
+
q_\text{trans} = {\Big (} \frac{2\pi m k_B T}{h^2} {\Big )}^{3/2} V_\text{eff} \quad , \quad V_\text{eff} = \begin{cases}
|
| 63 |
+
k_B T / p^{\circ} \quad&\text{if 1 atm standard state} \\
|
| 64 |
+
1 / c^\circ N_a \quad&\text{if 1 M standard state}
|
| 65 |
+
\end{cases}
|
| 66 |
+
\end{equation}
|
| 67 |
+
\begin{equation}
|
| 68 |
+
q_\text{rot} = \frac{T^{3/2}}{\sigma_r} \sqrt{\frac{\pi}{\omega_r}} \quad,\quad \omega_r = \prod_{k} \frac{h^2}{8 \pi^2 k_B I_k}
|
| 69 |
+
\end{equation}
|
| 70 |
+
|
| 71 |
+
where $q$ are molecular partition functions, $p^{\circ}$ is the standard pressure (1 atm) and $c^\circ$ the standard concentration (1 mol dm$^{-3}$), $\sigma_r$ is the rotational symmetry number for the molecule and $I_k$ a diagonal element of the moment of inertia matrix.
|
| 72 |
+
\\\\
|
| 73 |
+
Due to the vibrational entropy contribution being overestimated for low frequency modes Thrular proposed a correction, which instead of summing over frequencies in $S_\text{vib}^\text{HO}$ does so over $\max(\nu_\text{thresh},\; \nu_i)$ to shift all low frequencies to a threshold value (\emph{J. Phys. Chem. B} 2011, {\bfseries{115}}, 14556). An alternative method from Grimme (\emph{Chem. Eur. J.}, 2012, {\bfseries{18}}, 9955) uses an interpolation between a harmonic oscillator and rigid rotor to scale down the contribution from the low frequency modes as
|
| 74 |
+
\begin{equation}
|
| 75 |
+
S_\text{vib}^\text{Grimme} = \sum_i w_i S_ \text{vib}^\text{HO}(i) + (1-w_i) {\Big (} R\ln {\Big (} \sqrt{\frac{8 \pi^3 \mu_i' k_B T}{h^2}} {\Big )} + \frac{R}{2} {\Big )}
|
| 76 |
+
\end{equation}
|
| 77 |
+
\begin{eqnarray}
|
| 78 |
+
\mu_i' = \frac{\mu_i \bar{B}}{\mu_i + \bar{B}} \quad,\quad \mu_i = \frac{h}{8\pi^2 \nu_i} \quad,\quad \bar{B} = \text{Tr}[I] / 3
|
| 79 |
+
\end{eqnarray}
|
| 80 |
+
\begin{equation}
|
| 81 |
+
w_i = \frac{1}{1 + (\omega_0/ \nu_i)^\alpha}
|
| 82 |
+
\end{equation}
|
| 83 |
+
where $\omega_0$ and $\alpha$ are adjustable parameters.
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
\end{document}
|
autodE/source/autode/config.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Any
|
| 3 |
+
from autode.values import Frequency, Distance, Allocation
|
| 4 |
+
from autode.wrappers.keywords import implicit_solvent_types as solv
|
| 5 |
+
from autode.wrappers.keywords import KeywordsSet, MaxOptCycles
|
| 6 |
+
from autode.wrappers.keywords.basis_sets import (
|
| 7 |
+
def2svp,
|
| 8 |
+
def2tzvp,
|
| 9 |
+
def2ecp,
|
| 10 |
+
def2tzecp,
|
| 11 |
+
)
|
| 12 |
+
from autode.wrappers.keywords.functionals import pbe0
|
| 13 |
+
from autode.wrappers.keywords.dispersion import d3bj
|
| 14 |
+
from autode.wrappers.keywords.ri import rijcosx
|
| 15 |
+
|
| 16 |
+
location = os.path.abspath(__file__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class _ConfigClass:
|
| 20 |
+
# -------------------------------------------------------------------------
|
| 21 |
+
# Total number of cores available
|
| 22 |
+
#
|
| 23 |
+
n_cores = 4
|
| 24 |
+
# -------------------------------------------------------------------------
|
| 25 |
+
# Per core memory available
|
| 26 |
+
#
|
| 27 |
+
max_core = Allocation(4, units="GB")
|
| 28 |
+
# -------------------------------------------------------------------------
|
| 29 |
+
# DFT code to use. If set to None then the highest priority available code
|
| 30 |
+
# will be used:
|
| 31 |
+
# 1. 'orca', 2. 'g09' 3. 'nwchem'
|
| 32 |
+
#
|
| 33 |
+
hcode = None
|
| 34 |
+
# -------------------------------------------------------------------------
|
| 35 |
+
# Semi-empirical/tight binding method to use. If set to None then the
|
| 36 |
+
# highest priority available will be used: 1. 'xtb', 2. 'mopac'
|
| 37 |
+
#
|
| 38 |
+
lcode = None
|
| 39 |
+
# -------------------------------------------------------------------------
|
| 40 |
+
# When using explicit solvent is stable this will be uncommented
|
| 41 |
+
#
|
| 42 |
+
# explicit_solvent = False
|
| 43 |
+
#
|
| 44 |
+
# -------------------------------------------------------------------------
|
| 45 |
+
# Setting to keep input files, otherwise they will be removed
|
| 46 |
+
#
|
| 47 |
+
keep_input_files = True
|
| 48 |
+
# -------------------------------------------------------------------------
|
| 49 |
+
# Use a different base directory for calculations with low-level methods
|
| 50 |
+
# e.g. /dev/shm with a low level method, if None then will use the default
|
| 51 |
+
# in tempfile.mkdtemp
|
| 52 |
+
#
|
| 53 |
+
ll_tmp_dir = None
|
| 54 |
+
# -------------------------------------------------------------------------
|
| 55 |
+
# By default templates are saved to /path/to/autode/transition_states/lib/
|
| 56 |
+
# unless ts_template_folder_path is set
|
| 57 |
+
#
|
| 58 |
+
ts_template_folder_path = None
|
| 59 |
+
# -------------------------------------------------------------------------
|
| 60 |
+
# Whether or not to create and save transition state templates
|
| 61 |
+
#
|
| 62 |
+
make_ts_template = True
|
| 63 |
+
# -------------------------------------------------------------------------
|
| 64 |
+
# Save plots with dpi = 400
|
| 65 |
+
#
|
| 66 |
+
high_quality_plots = True
|
| 67 |
+
# -------------------------------------------------------------------------
|
| 68 |
+
# RMSD in angstroms threshold for conformers. Larger values will remove
|
| 69 |
+
# more conformers that need to be calculated but also reduces the chance
|
| 70 |
+
# that the lowest energy conformer is found
|
| 71 |
+
#
|
| 72 |
+
rmsd_threshold = Distance(0.3, units="Å")
|
| 73 |
+
# -------------------------------------------------------------------------
|
| 74 |
+
# Total number of conformers generated in find_lowest_energy_conformer()
|
| 75 |
+
# for single molecules/TSs
|
| 76 |
+
#
|
| 77 |
+
num_conformers = 300
|
| 78 |
+
# -------------------------------------------------------------------------
|
| 79 |
+
# Maximum random displacement in angstroms for conformational searching
|
| 80 |
+
#
|
| 81 |
+
max_atom_displacement = Distance(4.0, units="Å")
|
| 82 |
+
# -------------------------------------------------------------------------
|
| 83 |
+
# Number of evenly spaced points on a sphere that will be used to generate
|
| 84 |
+
# NCI and Reactant and Product complex conformers. Total number of
|
| 85 |
+
# conformers will be:
|
| 86 |
+
# (num_complex_sphere_points ×
|
| 87 |
+
# num_complex_random_rotations) ^ (n molecules in complex - 1)
|
| 88 |
+
#
|
| 89 |
+
num_complex_sphere_points = 10
|
| 90 |
+
# -------------------------------------------------------------------------
|
| 91 |
+
# Number of random rotations of a molecule that is added to a NCI or
|
| 92 |
+
# Reactant/Product complex
|
| 93 |
+
#
|
| 94 |
+
num_complex_random_rotations = 10
|
| 95 |
+
# -------------------------------------------------------------------------
|
| 96 |
+
# For more than 2 molecules in a complex the conformational space explodes,
|
| 97 |
+
# so limit the maximum number to this value
|
| 98 |
+
#
|
| 99 |
+
max_num_complex_conformers = 300
|
| 100 |
+
# -------------------------------------------------------------------------
|
| 101 |
+
# Use the high + low level method to find the lowest energy
|
| 102 |
+
# conformer, to use energies at the low_opt level of the low level code
|
| 103 |
+
# set this to False
|
| 104 |
+
#
|
| 105 |
+
hmethod_conformers = True
|
| 106 |
+
# -------------------------------------------------------------------------
|
| 107 |
+
# Set to True to use single point energy evaluations to rank conformers and
|
| 108 |
+
# select the lowest energy. Requires keywords.low_sp to be set and
|
| 109 |
+
# hmethod_conformers = True
|
| 110 |
+
# WARNING: This relies on the low-level geometry being accurate enough for
|
| 111 |
+
# the system in question – switching this on without benchmarking may lead
|
| 112 |
+
# to large errors!
|
| 113 |
+
#
|
| 114 |
+
hmethod_sp_conformers = False
|
| 115 |
+
# -------------------------------------------------------------------------
|
| 116 |
+
# Use adaptive force constant modification in NEB calculations to improve
|
| 117 |
+
# sampling around the saddle point
|
| 118 |
+
#
|
| 119 |
+
adaptive_neb_k = True
|
| 120 |
+
# -------------------------------------------------------------------------
|
| 121 |
+
# Minimum and maximum step size to use for the adaptive path search
|
| 122 |
+
#
|
| 123 |
+
min_step_size = Distance(0.05, units="Å")
|
| 124 |
+
max_step_size = Distance(0.3, units="Å")
|
| 125 |
+
# -------------------------------------------------------------------------
|
| 126 |
+
# Heuristic for pruning the bond rearrangement set. If there are only bond
|
| 127 |
+
# rearrangements that involve small rings then TSs involving small rings
|
| 128 |
+
# are possible. However, when there are multiple possibilities involving
|
| 129 |
+
# the same set of atoms then discard any rearrangements that would involve
|
| 130 |
+
# a 3 or 4-membered TS e.g. skip the possible 4-membered TS for a Cope
|
| 131 |
+
# rearrangement in hexadiene
|
| 132 |
+
#
|
| 133 |
+
skip_small_ring_tss = True
|
| 134 |
+
# -------------------------------------------------------------------------
|
| 135 |
+
# Minimum magnitude of the imaginary frequency (cm-1) to consider for a
|
| 136 |
+
# 'true' TS. For very shallow saddle points this may need to be reduced
|
| 137 |
+
# to e.g. -10 cm-1. Although most TSs have |v_imag| > 100 cm-1 this
|
| 138 |
+
# threshold is designed to be conservative
|
| 139 |
+
#
|
| 140 |
+
min_imag_freq = Frequency(-40, units="cm-1")
|
| 141 |
+
# -------------------------------------------------------------------------
|
| 142 |
+
# Configuration parameters for ideal gas free energy calculations. Can be
|
| 143 |
+
# configured to use different standard states, quasi-rigid rotor harmonic
|
| 144 |
+
# oscillator (qRRHO) or pure RRHO
|
| 145 |
+
#
|
| 146 |
+
# One of: '1M', '1atm'
|
| 147 |
+
standard_state = "1M"
|
| 148 |
+
#
|
| 149 |
+
# Method to treat low frequency modes (LFMs). Either standard RRHO ('igm'),
|
| 150 |
+
# Truhlar's method where all frequencies below a threshold are scaled to
|
| 151 |
+
# a shifted value (see J. Phys. Chem. B, 2011, 115, 14556), Grimme's
|
| 152 |
+
# method of interpolating between HO and RR (i.e. qRRHO, see
|
| 153 |
+
# Chem. Eur. J. 2012, 18, 9955), or 'minenkov' where free rotor/vibrational
|
| 154 |
+
# interpolation is useed for U and S (i.e. mRRHO, see
|
| 155 |
+
# J. Comput. Chem., 2023 44, 1807)
|
| 156 |
+
#
|
| 157 |
+
# One of: 'igm', 'truhlar', 'grimme', 'minenkov'
|
| 158 |
+
lfm_method = "grimme"
|
| 159 |
+
#
|
| 160 |
+
# Parameters for Grimme's method (only used when lfm_method='grimme'),
|
| 161 |
+
# w0 is a frequency in cm-1
|
| 162 |
+
grimme_w0 = Frequency(100, units="cm-1")
|
| 163 |
+
grimme_alpha = 4
|
| 164 |
+
#
|
| 165 |
+
# Parameters for Truhlar's method (only used when lfm_method='truhlar')
|
| 166 |
+
# vibrational frequencies below this value (cm-1) will be shifted to this
|
| 167 |
+
# value before the entropy is calculated
|
| 168 |
+
vib_freq_shift = Frequency(100, units="cm-1")
|
| 169 |
+
# -------------------------------------------------------------------------
|
| 170 |
+
# Frequency scale factor, useful for DFT functions known to have a
|
| 171 |
+
# systematic error. This value must be between 0 and 1 inclusive. For
|
| 172 |
+
# example, PBEh-3c has a scale factor of 0.95.
|
| 173 |
+
#
|
| 174 |
+
freq_scale_factor = None
|
| 175 |
+
# -------------------------------------------------------------------------
|
| 176 |
+
# Minimum number of atoms that are removed for truncation to be used in
|
| 177 |
+
# locating TSs. Below this number any truncation is skipped
|
| 178 |
+
#
|
| 179 |
+
min_num_atom_removed_in_truncation = 10
|
| 180 |
+
# -------------------------------------------------------------------------
|
| 181 |
+
# Flag for allowing free energies to be calculated with association
|
| 182 |
+
# complexes. This is *not* recommended to be turned on due to the
|
| 183 |
+
# approximations made in the entropy calculations.
|
| 184 |
+
#
|
| 185 |
+
allow_association_complex_G = False
|
| 186 |
+
# -------------------------------------------------------------------------
|
| 187 |
+
# Flag to allow use of an experimental timeout function wrapper for
|
| 188 |
+
# Windows, using loky. The default case has no timeout for Windows, and
|
| 189 |
+
# timeout only works on Linux/macOS. This flag is ignored on Linux/macOS.
|
| 190 |
+
#
|
| 191 |
+
use_experimental_timeout = False
|
| 192 |
+
# -------------------------------------------------------------------------
|
| 193 |
+
|
| 194 |
+
class ORCA:
|
| 195 |
+
# ---------------------------------------------------------------------
|
| 196 |
+
# Parameters for orca https://sites.google.com/site/orcainputlibrary/
|
| 197 |
+
# ---------------------------------------------------------------------
|
| 198 |
+
#
|
| 199 |
+
# Path can be unset and will be assigned if it can be found in $PATH
|
| 200 |
+
path = None
|
| 201 |
+
#
|
| 202 |
+
# File extensions to copy when a calculation completes
|
| 203 |
+
copied_output_exts = [".out", ".hess", ".xyz", ".inp", ".pc"]
|
| 204 |
+
|
| 205 |
+
optts_block = (
|
| 206 |
+
"\n%geom\n"
|
| 207 |
+
"Calc_Hess true\n"
|
| 208 |
+
"Recalc_Hess 20\n"
|
| 209 |
+
"Trust -0.1\n"
|
| 210 |
+
"MaxIter 100\n"
|
| 211 |
+
"end"
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
keywords = KeywordsSet(
|
| 215 |
+
low_opt=[
|
| 216 |
+
"LooseOpt",
|
| 217 |
+
pbe0,
|
| 218 |
+
rijcosx,
|
| 219 |
+
d3bj,
|
| 220 |
+
def2svp,
|
| 221 |
+
"def2/J",
|
| 222 |
+
MaxOptCycles(10),
|
| 223 |
+
],
|
| 224 |
+
grad=["EnGrad", pbe0, rijcosx, d3bj, def2svp, "def2/J"],
|
| 225 |
+
low_sp=["SP", pbe0, rijcosx, d3bj, def2svp, "def2/J"],
|
| 226 |
+
opt=["Opt", pbe0, rijcosx, d3bj, def2svp, "def2/J"],
|
| 227 |
+
opt_ts=[
|
| 228 |
+
"OptTS",
|
| 229 |
+
"Freq",
|
| 230 |
+
pbe0,
|
| 231 |
+
rijcosx,
|
| 232 |
+
d3bj,
|
| 233 |
+
def2svp,
|
| 234 |
+
"def2/J",
|
| 235 |
+
optts_block,
|
| 236 |
+
],
|
| 237 |
+
hess=["Freq", pbe0, rijcosx, d3bj, def2svp, "def2/J"],
|
| 238 |
+
sp=["SP", pbe0, rijcosx, d3bj, def2tzvp, "def2/J"],
|
| 239 |
+
ecp=def2ecp,
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# Implicit solvent in ORCA is either treated with CPCM or SMD, the
|
| 243 |
+
# former has support for a VdW surface construction which provides
|
| 244 |
+
# better geometry convergence (https://doi.org/10.1002/jcc.26139) SMD
|
| 245 |
+
# is in general more accurate, but does not (yet) have support for the
|
| 246 |
+
# VdW charge scheme. Use either (1) solv.cpcm, (2) solv.smd
|
| 247 |
+
implicit_solvation_type = solv.cpcm
|
| 248 |
+
|
| 249 |
+
class G09:
|
| 250 |
+
# ---------------------------------------------------------------------
|
| 251 |
+
# Parameters for g09 https://gaussian.com/glossary/g09/
|
| 252 |
+
# ---------------------------------------------------------------------
|
| 253 |
+
#
|
| 254 |
+
# path can be unset and will be assigned if it can be found in $PATH
|
| 255 |
+
path = None
|
| 256 |
+
#
|
| 257 |
+
grid = "integral=ultrafinegrid"
|
| 258 |
+
optts_block = (
|
| 259 |
+
"Opt=(TS, CalcFC, NoEigenTest, MaxCycles=100, "
|
| 260 |
+
"MaxStep=10, NoTrustUpdate)"
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
keywords = KeywordsSet(
|
| 264 |
+
low_opt=[pbe0, def2svp, "Opt=Loose", MaxOptCycles(10), d3bj, grid],
|
| 265 |
+
grad=[pbe0, def2svp, "Force(NoStep)", d3bj, grid],
|
| 266 |
+
low_sp=[pbe0, def2svp, d3bj, grid],
|
| 267 |
+
opt=[pbe0, def2svp, "Opt", d3bj, grid],
|
| 268 |
+
opt_ts=[pbe0, def2svp, "Freq", d3bj, grid, optts_block],
|
| 269 |
+
hess=[pbe0, def2svp, "Freq", d3bj, grid],
|
| 270 |
+
sp=[pbe0, def2tzvp, d3bj, grid],
|
| 271 |
+
ecp=def2tzecp,
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Only SMD implemented
|
| 275 |
+
implicit_solvation_type = solv.smd
|
| 276 |
+
|
| 277 |
+
class G16:
|
| 278 |
+
# ---------------------------------------------------------------------
|
| 279 |
+
# Parameters for g16 https://gaussian.com/gaussian16/
|
| 280 |
+
# ---------------------------------------------------------------------
|
| 281 |
+
#
|
| 282 |
+
# path can be unset and will be assigned if it can be found in $PATH
|
| 283 |
+
path = None
|
| 284 |
+
#
|
| 285 |
+
ts_str = (
|
| 286 |
+
"Opt=(TS, CalcFC, NoEigenTest, MaxCycles=100, MaxStep=10, "
|
| 287 |
+
"NoTrustUpdate, RecalcFC=30)"
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
keywords = KeywordsSet(
|
| 291 |
+
low_opt=[pbe0, def2svp, "Opt=Loose", d3bj, MaxOptCycles(10)],
|
| 292 |
+
grad=[pbe0, def2svp, "Force(NoStep)", d3bj],
|
| 293 |
+
low_sp=[pbe0, def2svp, d3bj],
|
| 294 |
+
opt=[pbe0, def2svp, "Opt", d3bj],
|
| 295 |
+
opt_ts=[pbe0, def2svp, "Freq", d3bj, ts_str],
|
| 296 |
+
hess=[pbe0, def2svp, "Freq", d3bj],
|
| 297 |
+
sp=[pbe0, def2tzvp, d3bj],
|
| 298 |
+
ecp=def2tzecp,
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
# Only SMD implemented
|
| 302 |
+
implicit_solvation_type = solv.smd
|
| 303 |
+
|
| 304 |
+
class NWChem:
|
| 305 |
+
# ---------------------------------------------------------------------
|
| 306 |
+
# Parameters for nwchem http://www.nwchem-sw.org/index.php/Main_Page
|
| 307 |
+
# ---------------------------------------------------------------------
|
| 308 |
+
#
|
| 309 |
+
# Path can be unset and will be assigned if it can be found in $PATH
|
| 310 |
+
path = None
|
| 311 |
+
#
|
| 312 |
+
# Note that the default NWChem level is PBE0 and PBE rather than
|
| 313 |
+
# PBE0-D3BJ and PBE-D3BJ as only D3 is available. The optimisation
|
| 314 |
+
# keywords contain 'gradient' as the optimisation is driven by autodE
|
| 315 |
+
keywords = KeywordsSet(
|
| 316 |
+
low_opt=[def2svp, pbe0, MaxOptCycles(10), "task dft gradient"],
|
| 317 |
+
grad=[def2svp, pbe0, "task dft gradient"],
|
| 318 |
+
low_sp=[def2svp, pbe0, "task dft energy"],
|
| 319 |
+
opt=[def2svp, pbe0, MaxOptCycles(100), "task dft gradient"],
|
| 320 |
+
opt_ts=[def2svp, pbe0, MaxOptCycles(50), "task dft gradient"],
|
| 321 |
+
hess=[def2svp, pbe0, "task dft freq"],
|
| 322 |
+
sp=[def2tzvp, pbe0, "task dft energy"],
|
| 323 |
+
ecp=def2ecp,
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
# Only SMD implemented
|
| 327 |
+
implicit_solvation_type = solv.smd
|
| 328 |
+
|
| 329 |
+
class XTB:
|
| 330 |
+
# ---------------------------------------------------------------------
|
| 331 |
+
# Parameters for xtb https://github.com/grimme-lab/xtb
|
| 332 |
+
# ---------------------------------------------------------------------
|
| 333 |
+
#
|
| 334 |
+
# path can be unset and will be assigned if it can be found in $PATH
|
| 335 |
+
path = None
|
| 336 |
+
#
|
| 337 |
+
keywords = KeywordsSet()
|
| 338 |
+
#
|
| 339 |
+
# Only GBSA implemented
|
| 340 |
+
implicit_solvation_type = solv.gbsa
|
| 341 |
+
#
|
| 342 |
+
# Force constant used for harmonic restraints in constrained
|
| 343 |
+
# optimisations (Ha/a0)
|
| 344 |
+
force_constant = 2
|
| 345 |
+
#
|
| 346 |
+
# Electronic temperature for all calculations (Kelvin)
|
| 347 |
+
# None means unset (default), set to 300.0 to have 300K for example
|
| 348 |
+
electronic_temp = None
|
| 349 |
+
#
|
| 350 |
+
# Version of xTB hamiltonian parameterisation: 0,1 or 2
|
| 351 |
+
# corresponding to GFN0-xTB, GFN1-xTB, GFN2-xTB respectively
|
| 352 |
+
# When unset, uses the default
|
| 353 |
+
gfn_version = None
|
| 354 |
+
|
| 355 |
+
class MOPAC:
|
| 356 |
+
# ---------------------------------------------------------------------
|
| 357 |
+
# Parameters for mopac http://openmopac.net
|
| 358 |
+
# ---------------------------------------------------------------------
|
| 359 |
+
#
|
| 360 |
+
# path can be unset and will be assigned if it can be found in $PATH
|
| 361 |
+
path = None
|
| 362 |
+
#
|
| 363 |
+
# Note: all optimisations at this low level will be in the gas phase
|
| 364 |
+
# using the keywords_list specified here. Solvent in mopac is defined
|
| 365 |
+
# by EPS and the dielectric
|
| 366 |
+
keywords = KeywordsSet(low_opt=["PM7", "PRECISE"])
|
| 367 |
+
#
|
| 368 |
+
# Only COSMO implemented
|
| 369 |
+
implicit_solvation_type = solv.cosmo
|
| 370 |
+
|
| 371 |
+
class QChem:
|
| 372 |
+
# ---------------------------------------------------------------------
|
| 373 |
+
# Parameters for QChem https://www.q-chem.com/
|
| 374 |
+
# ---------------------------------------------------------------------
|
| 375 |
+
#
|
| 376 |
+
# path can be unset and will be assigned if it can be found in $PATH
|
| 377 |
+
path = None
|
| 378 |
+
#
|
| 379 |
+
# Default set of keywords to use for different types of calculation
|
| 380 |
+
keywords = KeywordsSet(
|
| 381 |
+
low_opt=[pbe0, def2svp, "jobtype opt", MaxOptCycles(10), d3bj],
|
| 382 |
+
grad=[pbe0, def2svp, "jobtype force", d3bj],
|
| 383 |
+
low_sp=[pbe0, def2svp, d3bj],
|
| 384 |
+
opt=[pbe0, def2svp, "jobtype opt", d3bj],
|
| 385 |
+
opt_ts=[pbe0, def2svp, "jobtype TS", d3bj],
|
| 386 |
+
hess=[pbe0, def2svp, "jobtype Freq", d3bj],
|
| 387 |
+
sp=[pbe0, def2tzvp, d3bj],
|
| 388 |
+
ecp=def2ecp,
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
#
|
| 392 |
+
# Only SMD is implemented
|
| 393 |
+
implicit_solvation_type = solv.smd
|
| 394 |
+
|
| 395 |
+
# =========================================================================
|
| 396 |
+
# ============= End ==================
|
| 397 |
+
# =========================================================================
|
| 398 |
+
|
| 399 |
+
def __setattr__(self, key, value):
|
| 400 |
+
"""Custom setters"""
|
| 401 |
+
|
| 402 |
+
if not hasattr(self, key):
|
| 403 |
+
raise KeyError(f"Cannot set {key}. Not present in ade.Config")
|
| 404 |
+
|
| 405 |
+
if key == "max_core":
|
| 406 |
+
value = Allocation(value).to("MB")
|
| 407 |
+
|
| 408 |
+
if key == "freq_scale_factor":
|
| 409 |
+
if value is not None:
|
| 410 |
+
if not (0.0 < value <= 1.0):
|
| 411 |
+
raise ValueError(
|
| 412 |
+
"Cannot set the frequency scale factor "
|
| 413 |
+
"outside of (0, 1]"
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
value = float(value)
|
| 417 |
+
|
| 418 |
+
if key in ("max_atom_displacement", "min_step_size", "max_step_size"):
|
| 419 |
+
if float(value) < 0:
|
| 420 |
+
raise ValueError(f"Distances cannot be negative. Had: {value}")
|
| 421 |
+
|
| 422 |
+
value = Distance(value).to("ang")
|
| 423 |
+
|
| 424 |
+
return super().__setattr__(key, value)
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def _instantiate_config_opts(cls: type) -> Any:
|
| 428 |
+
"""
|
| 429 |
+
Instantiate a config class containing options defined
|
| 430 |
+
as class variables. It generates an instance of the
|
| 431 |
+
class, and then creates instance variables of the same
|
| 432 |
+
name as class variables, recursively converting any
|
| 433 |
+
nested class into instances.
|
| 434 |
+
(This is required because class variables are not pickled,
|
| 435 |
+
only instance variables are)
|
| 436 |
+
|
| 437 |
+
Args:
|
| 438 |
+
cls (type): Must be a class containing class
|
| 439 |
+
variables (not instance)
|
| 440 |
+
|
| 441 |
+
Returns:
|
| 442 |
+
(Any): The generated class instance
|
| 443 |
+
"""
|
| 444 |
+
if not isinstance(cls, type):
|
| 445 |
+
raise ValueError("Must be a class, not an instance")
|
| 446 |
+
cls_instance = cls()
|
| 447 |
+
for name, attr in cls.__dict__.items():
|
| 448 |
+
if name.startswith("__"):
|
| 449 |
+
continue
|
| 450 |
+
if isinstance(attr, type):
|
| 451 |
+
attr_val = _instantiate_config_opts(attr) # recursive
|
| 452 |
+
else:
|
| 453 |
+
attr_val = attr
|
| 454 |
+
setattr(cls_instance, name, attr_val)
|
| 455 |
+
return cls_instance
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
# Single instance of the configuration
|
| 459 |
+
Config = _instantiate_config_opts(_ConfigClass)
|
autodE/source/autode/conformers/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from autode.conformers.conformer import Conformer
|
| 2 |
+
from autode.conformers.conformers import Conformers
|
| 3 |
+
|
| 4 |
+
__all__ = ["Conformer", "Conformers"]
|
autodE/source/autode/conformers/cconf_gen.pyx
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# cython: boundscheck=False
|
| 2 |
+
# cython: wraparound=False
|
| 3 |
+
# cython: cdivision=True
|
| 4 |
+
from cpython.array cimport array, clone
|
| 5 |
+
from libc.math cimport sqrt, pow
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
cdef calc_energy(int n_atoms, array coords, int[:, :] bond_matrix, double k, double[:, :] d0, double c,
|
| 10 |
+
int exponent):
|
| 11 |
+
|
| 12 |
+
cdef int i, j
|
| 13 |
+
cdef double delta_x = 0.0
|
| 14 |
+
cdef double delta_y = 0.0
|
| 15 |
+
cdef double delta_z = 0.0
|
| 16 |
+
|
| 17 |
+
cdef double d = 0.0
|
| 18 |
+
cdef double repulsion = 0.0
|
| 19 |
+
cdef double bonded = 0.0
|
| 20 |
+
|
| 21 |
+
cdef double energy = 0.0
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
for i in range(n_atoms):
|
| 25 |
+
for j in range(n_atoms):
|
| 26 |
+
if i > j:
|
| 27 |
+
delta_x = coords.data.as_doubles[3*j] - coords.data.as_doubles[3*i]
|
| 28 |
+
delta_y = coords.data.as_doubles[3*j+1] - coords.data.as_doubles[3*i+1]
|
| 29 |
+
delta_z = coords.data.as_doubles[3*j+2] - coords.data.as_doubles[3*i+2]
|
| 30 |
+
d = sqrt(delta_x*delta_x + delta_y*delta_y + delta_z*delta_z)
|
| 31 |
+
|
| 32 |
+
energy += c / pow(d, exponent)
|
| 33 |
+
|
| 34 |
+
if bond_matrix[i][j] == 1:
|
| 35 |
+
energy += k * pow((d - d0[i][j]), 2)
|
| 36 |
+
|
| 37 |
+
if bond_matrix[i][j] == 2:
|
| 38 |
+
energy += 10 * pow((d - d0[i][j]), 2)
|
| 39 |
+
return energy
|
| 40 |
+
|
| 41 |
+
cdef calc_deriv(int n_atoms, array deriv, array coords, int[:, :] bond_matrix,
|
| 42 |
+
double k, double[:, :] d0, double c, int exponent):
|
| 43 |
+
|
| 44 |
+
cdef int i, j
|
| 45 |
+
cdef double delta_x
|
| 46 |
+
cdef double delta_y
|
| 47 |
+
cdef double delta_z
|
| 48 |
+
|
| 49 |
+
cdef double d
|
| 50 |
+
cdef double repulsion
|
| 51 |
+
cdef double bonded
|
| 52 |
+
cdef double fixed
|
| 53 |
+
|
| 54 |
+
exponent_minus_2 = exponent + 2
|
| 55 |
+
|
| 56 |
+
for i in range(n_atoms):
|
| 57 |
+
for j in range(n_atoms):
|
| 58 |
+
if i != j:
|
| 59 |
+
delta_x = coords.data.as_doubles[3*j] - coords.data.as_doubles[3*i]
|
| 60 |
+
delta_y = coords.data.as_doubles[3*j+1] - coords.data.as_doubles[3*i+1]
|
| 61 |
+
delta_z = coords.data.as_doubles[3*j+2] - coords.data.as_doubles[3*i+2]
|
| 62 |
+
d = sqrt(delta_x*delta_x + delta_y*delta_y + delta_z*delta_z)
|
| 63 |
+
|
| 64 |
+
repulsion = -exponent * c / pow(d, exponent_minus_2)
|
| 65 |
+
deriv.data.as_doubles[3*i] += repulsion * delta_x
|
| 66 |
+
deriv.data.as_doubles[3*i+1] += repulsion * delta_y
|
| 67 |
+
deriv.data.as_doubles[3*i+2] += repulsion * delta_z
|
| 68 |
+
|
| 69 |
+
if bond_matrix[i][j] == 1:
|
| 70 |
+
bonded = 2.0 * k * (1.0 - d0[i][j]/d)
|
| 71 |
+
deriv.data.as_doubles[3*i] += bonded * delta_x
|
| 72 |
+
deriv.data.as_doubles[3*i+1] += bonded * delta_y
|
| 73 |
+
deriv.data.as_doubles[3*i+2] += bonded * delta_z
|
| 74 |
+
|
| 75 |
+
if bond_matrix[i][j] == 2:
|
| 76 |
+
fixed = 20.0 * (1.0 - d0[i][j]/d)
|
| 77 |
+
deriv.data.as_doubles[3*i] += fixed * delta_x
|
| 78 |
+
deriv.data.as_doubles[3*i+1] += fixed * delta_y
|
| 79 |
+
deriv.data.as_doubles[3*i+2] += fixed * delta_z
|
| 80 |
+
|
| 81 |
+
return -np.array(deriv)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def dvdr(py_flat_coords, py_bond_matrix, py_k, py_d0, py_c, py_exponent,
|
| 85 |
+
py_fixed_atoms):
|
| 86 |
+
|
| 87 |
+
py_n_atoms = int(len(py_flat_coords) / 3)
|
| 88 |
+
cdef int n_atoms = py_n_atoms
|
| 89 |
+
cdef int[:, :] bond_matrix = py_bond_matrix
|
| 90 |
+
cdef double k = py_k
|
| 91 |
+
cdef double[:, :] d0 = py_d0
|
| 92 |
+
cdef double c = py_c
|
| 93 |
+
cdef int i
|
| 94 |
+
cdef exponent = py_exponent
|
| 95 |
+
|
| 96 |
+
cdef array coords, template = array('d')
|
| 97 |
+
coords = clone(template, 3*n_atoms, False)
|
| 98 |
+
init_array = clone(template, 3*n_atoms, False)
|
| 99 |
+
|
| 100 |
+
# Initalise arrays
|
| 101 |
+
for i in range(3*n_atoms):
|
| 102 |
+
init_array[i] = 0.0
|
| 103 |
+
coords[i] = py_flat_coords[i]
|
| 104 |
+
|
| 105 |
+
dvdr = calc_deriv(n_atoms, init_array, coords, bond_matrix, k, d0, c, exponent)
|
| 106 |
+
|
| 107 |
+
# Zero the gradients for all the fixed atoms
|
| 108 |
+
dvdr = dvdr.reshape(-1, 3)
|
| 109 |
+
dvdr[py_fixed_atoms, :] = 0.0
|
| 110 |
+
|
| 111 |
+
return dvdr.flatten()
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def v(py_flat_coords, py_bond_matrix, py_k, py_d0, py_c, py_exponent, *args):
|
| 115 |
+
|
| 116 |
+
py_n_atoms = int(len(py_flat_coords) / 3)
|
| 117 |
+
cdef int n_atoms = py_n_atoms
|
| 118 |
+
cdef int[:, :] bond_matrix = py_bond_matrix
|
| 119 |
+
cdef double k = py_k
|
| 120 |
+
cdef double[:, :] d0 = py_d0
|
| 121 |
+
cdef double c = py_c
|
| 122 |
+
cdef exponent = py_exponent
|
| 123 |
+
|
| 124 |
+
cdef array coords, template = array('d')
|
| 125 |
+
coords = clone(template, 3*n_atoms, False)
|
| 126 |
+
|
| 127 |
+
cdef int i
|
| 128 |
+
for i in range(3*n_atoms):
|
| 129 |
+
coords[i] = py_flat_coords[i]
|
| 130 |
+
|
| 131 |
+
return calc_energy(n_atoms, coords, bond_matrix, k, d0, c, exponent)
|
autodE/source/autode/conformers/conf_gen.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
import autode as ade
|
| 4 |
+
from copy import deepcopy
|
| 5 |
+
from typing import Dict, Optional, TYPE_CHECKING
|
| 6 |
+
from itertools import combinations
|
| 7 |
+
from scipy.optimize import minimize
|
| 8 |
+
|
| 9 |
+
from autode.conformers import Conformer
|
| 10 |
+
import autode.exceptions as ex
|
| 11 |
+
from autode.utils import log_time
|
| 12 |
+
from autode.input_output import xyz_file_to_atoms, atoms_to_xyz_file
|
| 13 |
+
from autode.mol_graphs import split_mol_across_bond
|
| 14 |
+
from autode.log import logger
|
| 15 |
+
|
| 16 |
+
if TYPE_CHECKING:
|
| 17 |
+
from autode.species.species import Species
|
| 18 |
+
from autode.wrappers.keywords import Keywords
|
| 19 |
+
from autode.wrappers.methods import Method
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _get_bond_matrix(n_atoms, bonds, fixed_bonds):
|
| 23 |
+
"""
|
| 24 |
+
Populate a bond matrix with 1 if i, j are bonded, 2 if i, j are bonded and
|
| 25 |
+
fixed and 0 otherwise. Can support a partial structure with bonds to atoms
|
| 26 |
+
that don't (yet) exist.
|
| 27 |
+
|
| 28 |
+
---------------------------------------------------------------------------
|
| 29 |
+
Arguments:
|
| 30 |
+
n_atoms (int):
|
| 31 |
+
bonds (list(tuple)):
|
| 32 |
+
fixed_bonds (list(tuple)):
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
(np.ndarray): Bond matrix, shape = (n_atoms, n_atoms)
|
| 36 |
+
"""
|
| 37 |
+
bond_matrix = np.zeros((n_atoms, n_atoms), dtype=np.intc)
|
| 38 |
+
|
| 39 |
+
for i, j in bonds:
|
| 40 |
+
if i < n_atoms and j < n_atoms:
|
| 41 |
+
bond_matrix[i, j] = 1
|
| 42 |
+
bond_matrix[j, i] = 1
|
| 43 |
+
for i, j in fixed_bonds:
|
| 44 |
+
if i < n_atoms and j < n_atoms:
|
| 45 |
+
bond_matrix[i, j] = 2
|
| 46 |
+
bond_matrix[j, i] = 2
|
| 47 |
+
|
| 48 |
+
return bond_matrix
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _get_coords_energy(
|
| 52 |
+
coords, bonds, k, c, d0, tol, fixed_bonds, exponent=8, fixed_idxs=None
|
| 53 |
+
):
|
| 54 |
+
"""
|
| 55 |
+
Get the coordinates that minimise a FF with a bonds + repulsion FF
|
| 56 |
+
where the repulsion is c/r^exponent
|
| 57 |
+
|
| 58 |
+
---------------------------------------------------------------------------
|
| 59 |
+
Arguments:
|
| 60 |
+
coords (np.ndarray): Initial coordinates, shape = (n_atoms, 3)
|
| 61 |
+
bonds (list(tuple(int))): List of bonds
|
| 62 |
+
fixed_bonds (list(tuple(int))): List of constrained bonds will use 10k
|
| 63 |
+
as the harmonic force constant
|
| 64 |
+
k (float):
|
| 65 |
+
c (float):
|
| 66 |
+
|
| 67 |
+
Keyword Arguments:
|
| 68 |
+
exponent (int): Exponent in the repulsive pairwise term
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
(np.ndarray): Optimised coordinates, shape = (n_atoms, 3)
|
| 72 |
+
"""
|
| 73 |
+
# TODO divide and conquer?
|
| 74 |
+
from cconf_gen import v
|
| 75 |
+
from cconf_gen import dvdr
|
| 76 |
+
|
| 77 |
+
n_atoms = len(coords)
|
| 78 |
+
os.environ["OMP_NUM_THREADS"] = str(1)
|
| 79 |
+
|
| 80 |
+
bond_matrix = _get_bond_matrix(
|
| 81 |
+
n_atoms=len(coords), bonds=bonds, fixed_bonds=fixed_bonds
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
if fixed_idxs is None:
|
| 85 |
+
fixed_idxs = np.array([], dtype=int)
|
| 86 |
+
|
| 87 |
+
res = minimize(
|
| 88 |
+
v,
|
| 89 |
+
x0=coords.reshape(3 * n_atoms),
|
| 90 |
+
args=(bond_matrix, k, d0, c, exponent, fixed_idxs),
|
| 91 |
+
method="CG",
|
| 92 |
+
tol=tol,
|
| 93 |
+
jac=dvdr,
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
return res.x.reshape(n_atoms, 3), res.fun
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _get_v(coords, bonds, k, c, d0, fixed_bonds, exponent=8):
|
| 100 |
+
"""Get the energy using a bond + repulsion FF where
|
| 101 |
+
|
| 102 |
+
V(r) = Σ_bonds k(d - d0)^2 + Σ_ij c/d^exponent
|
| 103 |
+
|
| 104 |
+
---------------------------------------------------------------------------
|
| 105 |
+
Arguments:
|
| 106 |
+
coords (np.ndarray): shape = (n_atoms, 3)
|
| 107 |
+
bonds (list(tuple(int))): List of bonds
|
| 108 |
+
fixed_bonds (list(tuple(int))): List of constrained bonds will use 10k
|
| 109 |
+
as the harmonic force constant
|
| 110 |
+
k (float):
|
| 111 |
+
c (float):
|
| 112 |
+
exponent (int): Exponent in the repulsive pairwise term
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
(float): Energy
|
| 116 |
+
"""
|
| 117 |
+
from cconf_gen import v
|
| 118 |
+
|
| 119 |
+
n_atoms = len(coords)
|
| 120 |
+
os.environ["OMP_NUM_THREADS"] = str(1)
|
| 121 |
+
|
| 122 |
+
init_coords = coords.reshape(3 * n_atoms)
|
| 123 |
+
bond_matrix = _get_bond_matrix(
|
| 124 |
+
n_atoms=n_atoms, bonds=bonds, fixed_bonds=fixed_bonds
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
return v(init_coords, bond_matrix, k, d0, c, exponent)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _get_atoms_rotated_stereocentres(species, atoms, rand):
|
| 131 |
+
"""If two stereocentres are bonded, rotate them randomly with respect
|
| 132 |
+
to each other
|
| 133 |
+
|
| 134 |
+
---------------------------------------------------------------------------
|
| 135 |
+
Arguments:
|
| 136 |
+
species (autode.species.Species):
|
| 137 |
+
atoms (list(autode.atoms.Atom)):
|
| 138 |
+
rand (np.RandomState): random state
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
(list(autode.atoms.Atom)): Atoms
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
stereocentres = [
|
| 145 |
+
node
|
| 146 |
+
for node in species.graph.nodes
|
| 147 |
+
if species.graph.nodes[node]["stereo"] is True
|
| 148 |
+
]
|
| 149 |
+
|
| 150 |
+
# Check on every pair of stereocenters
|
| 151 |
+
for i, j in combinations(stereocentres, 2):
|
| 152 |
+
if (i, j) not in species.graph.edges:
|
| 153 |
+
continue
|
| 154 |
+
|
| 155 |
+
# Don't rotate if the bond connecting the centers is a π-bond
|
| 156 |
+
if species.graph.edges[i, j]["pi"] is True:
|
| 157 |
+
logger.info("Stereocenters were π bonded – not rotating")
|
| 158 |
+
continue
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
left_idxs, right_idxs = split_mol_across_bond(
|
| 162 |
+
species.graph, bond=(i, j)
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
except ex.CannotSplitAcrossBond:
|
| 166 |
+
logger.warning(
|
| 167 |
+
"Splitting across this bond does not give two "
|
| 168 |
+
"components - could have a ring"
|
| 169 |
+
)
|
| 170 |
+
return atoms
|
| 171 |
+
|
| 172 |
+
# Rotate the left hand side randomly
|
| 173 |
+
rot_axis = atoms[i].coord - atoms[j].coord
|
| 174 |
+
theta = 2 * np.pi * rand.rand()
|
| 175 |
+
idxs_to_rotate = left_idxs if i in left_idxs else right_idxs
|
| 176 |
+
|
| 177 |
+
# Rotate all the atoms to the left of this bond, missing out i as that
|
| 178 |
+
# is the origin for rotation and thus won't move
|
| 179 |
+
for n in idxs_to_rotate:
|
| 180 |
+
if n == i:
|
| 181 |
+
continue
|
| 182 |
+
atoms[n].rotate(axis=rot_axis, theta=theta, origin=atoms[i].coord)
|
| 183 |
+
|
| 184 |
+
return atoms
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _add_dist_consts_for_stereocentres(species, dist_consts):
|
| 188 |
+
"""
|
| 189 |
+
Add distances constraints across two bonded stereocentres, for example
|
| 190 |
+
for a Z alkene, (hopefully) ensuring that in the conformer generation the
|
| 191 |
+
stereochemistry is retained. Will also add distance constraints from
|
| 192 |
+
one nearest neighbour to the other nearest neighbours for that chiral
|
| 193 |
+
centre
|
| 194 |
+
|
| 195 |
+
---------------------------------------------------------------------------
|
| 196 |
+
Arguments:
|
| 197 |
+
species (autode.species.Species):
|
| 198 |
+
dist_consts (dict): keyed with tuple of atom indexes and valued with
|
| 199 |
+
the distance (Å), or None
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
(dict): Distance constraints
|
| 203 |
+
"""
|
| 204 |
+
if not ade.geom.are_coords_reasonable(coords=species.coordinates):
|
| 205 |
+
# TODO generate a reasonable initial structure: molassembler?
|
| 206 |
+
logger.error(
|
| 207 |
+
"Cannot constrain stereochemistry if the initial "
|
| 208 |
+
"structure is not sensible"
|
| 209 |
+
)
|
| 210 |
+
return dist_consts
|
| 211 |
+
|
| 212 |
+
stereocentres = [
|
| 213 |
+
node
|
| 214 |
+
for node in species.graph.nodes
|
| 215 |
+
if species.graph.nodes[node]["stereo"] is True
|
| 216 |
+
]
|
| 217 |
+
|
| 218 |
+
# Get the stereocentres with 4 bonds as ~ chiral centres
|
| 219 |
+
chiral_centres = [
|
| 220 |
+
centre
|
| 221 |
+
for centre in stereocentres
|
| 222 |
+
if len(list(species.graph.neighbors(centre))) == 4
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
# Add distance constraints from one atom to the other 3 atoms to fix the
|
| 226 |
+
# configuration
|
| 227 |
+
for chiral_centre in chiral_centres:
|
| 228 |
+
neighbors = list(species.graph.neighbors(chiral_centre))
|
| 229 |
+
atom_i = neighbors[0]
|
| 230 |
+
|
| 231 |
+
for atom_j in neighbors[1:]:
|
| 232 |
+
dist_consts[(atom_i, atom_j)] = species.distance(atom_i, atom_j)
|
| 233 |
+
|
| 234 |
+
# Check on every pair of stereocenters
|
| 235 |
+
for atom_i, atom_j in combinations(stereocentres, 2):
|
| 236 |
+
# If they are not bonded don't alter
|
| 237 |
+
if (atom_i, atom_j) not in species.graph.edges:
|
| 238 |
+
continue
|
| 239 |
+
|
| 240 |
+
# Add a single distance constraint between the nearest neighbours of
|
| 241 |
+
# each stereocentre
|
| 242 |
+
for i_neighbour in species.graph.neighbors(atom_i):
|
| 243 |
+
for j_neighbour in species.graph.neighbors(atom_j):
|
| 244 |
+
if i_neighbour != atom_j and j_neighbour != atom_i:
|
| 245 |
+
# Fix the distance to the current value
|
| 246 |
+
dist = species.distance(i_neighbour, j_neighbour)
|
| 247 |
+
dist_consts[(i_neighbour, j_neighbour)] = dist
|
| 248 |
+
|
| 249 |
+
logger.info(f"Have {len(dist_consts)} distance constraint(s)")
|
| 250 |
+
return dist_consts
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def _get_non_random_atoms(species):
|
| 254 |
+
"""
|
| 255 |
+
Get the atoms that won't be randomised in the conformer generation.
|
| 256 |
+
Stereocentres and nearest neighbours
|
| 257 |
+
|
| 258 |
+
---------------------------------------------------------------------------
|
| 259 |
+
Arguments:
|
| 260 |
+
species (autode.species.Species):
|
| 261 |
+
|
| 262 |
+
Returns:
|
| 263 |
+
(set(int)): Atoms indexes to not randomise
|
| 264 |
+
"""
|
| 265 |
+
stereocentres = [
|
| 266 |
+
node
|
| 267 |
+
for node in species.graph.nodes
|
| 268 |
+
if species.graph.nodes[node]["stereo"] is True
|
| 269 |
+
]
|
| 270 |
+
|
| 271 |
+
non_rand_atoms = deepcopy(stereocentres)
|
| 272 |
+
for stereocentre in stereocentres:
|
| 273 |
+
non_rand_atoms += list(species.graph.neighbors(stereocentre))
|
| 274 |
+
|
| 275 |
+
if len(non_rand_atoms) > 0:
|
| 276 |
+
logger.info(f"Not randomising atom index(es) {set(non_rand_atoms)}")
|
| 277 |
+
|
| 278 |
+
return np.array(list(set(non_rand_atoms)), dtype=int)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def _get_atoms_from_generated_file(species, xyz_filename):
|
| 282 |
+
"""
|
| 283 |
+
Get atoms from a previously generated .xyz file, if the atoms match
|
| 284 |
+
|
| 285 |
+
---------------------------------------------------------------------------
|
| 286 |
+
Arguments:
|
| 287 |
+
species (autode.species.Species):
|
| 288 |
+
xyz_filename (str):
|
| 289 |
+
|
| 290 |
+
Returns:
|
| 291 |
+
(list(autode.atoms.Atoms)) or None: Atoms from file
|
| 292 |
+
"""
|
| 293 |
+
|
| 294 |
+
if not os.path.exists(xyz_filename):
|
| 295 |
+
return None
|
| 296 |
+
|
| 297 |
+
atoms = xyz_file_to_atoms(filename=xyz_filename)
|
| 298 |
+
|
| 299 |
+
if len(atoms) != species.n_atoms:
|
| 300 |
+
return None
|
| 301 |
+
|
| 302 |
+
all_atoms_match = all(
|
| 303 |
+
atoms[i].label == species.atoms[i].label
|
| 304 |
+
for i in range(species.n_atoms)
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
if all_atoms_match:
|
| 308 |
+
logger.info("Conformer has already been generated")
|
| 309 |
+
return atoms
|
| 310 |
+
|
| 311 |
+
return None
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def _get_coords_no_init_structure(atoms, species, d0, constrained_bonds):
|
| 315 |
+
"""
|
| 316 |
+
Generate coordinates where no initial structure is present - this fixes(?)
|
| 317 |
+
a problem for large molecule where if all the atoms are initially bonded
|
| 318 |
+
and minimised then high energy minima are often found
|
| 319 |
+
|
| 320 |
+
Args:
|
| 321 |
+
atoms (list(autode.atoms.Atom)):
|
| 322 |
+
species (autode.species.Species):
|
| 323 |
+
d0 (np.ndarray):
|
| 324 |
+
constrained_bonds (list):
|
| 325 |
+
|
| 326 |
+
Returns:
|
| 327 |
+
(np.ndarray): Optimised coordinates, shape = (n_atoms, 3)
|
| 328 |
+
"""
|
| 329 |
+
# Minimise atoms with no bonds between them
|
| 330 |
+
far_coords, _ = _get_coords_energy(
|
| 331 |
+
coords=np.array([atom.coord for atom in atoms]),
|
| 332 |
+
bonds=species.graph.edges,
|
| 333 |
+
fixed_bonds=constrained_bonds,
|
| 334 |
+
k=0.0,
|
| 335 |
+
c=0.1,
|
| 336 |
+
d0=d0,
|
| 337 |
+
tol=5e-3,
|
| 338 |
+
exponent=2,
|
| 339 |
+
)
|
| 340 |
+
coords = far_coords[:2]
|
| 341 |
+
|
| 342 |
+
# Add the atoms one by one to the structure. Thanks to Dr. Cyrille Lavigne
|
| 343 |
+
# for this suggestion!
|
| 344 |
+
for n in range(2, species.n_atoms):
|
| 345 |
+
new_coords = np.concatenate((coords, far_coords[len(coords) : n + 1]))
|
| 346 |
+
coords, _ = _get_coords_energy(
|
| 347 |
+
new_coords,
|
| 348 |
+
bonds=species.graph.edges,
|
| 349 |
+
fixed_bonds=constrained_bonds,
|
| 350 |
+
k=0.1,
|
| 351 |
+
c=0.1,
|
| 352 |
+
d0=d0,
|
| 353 |
+
tol=1e-3,
|
| 354 |
+
exponent=2,
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
# Perform a final minimisation
|
| 358 |
+
coords, energy = _get_coords_energy(
|
| 359 |
+
coords=coords,
|
| 360 |
+
bonds=species.graph.edges,
|
| 361 |
+
fixed_bonds=constrained_bonds,
|
| 362 |
+
k=1.0,
|
| 363 |
+
c=0.01,
|
| 364 |
+
d0=d0,
|
| 365 |
+
tol=1e-5,
|
| 366 |
+
)
|
| 367 |
+
return coords, energy
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
@log_time(prefix="Generated RR atoms in:", units="s")
|
| 371 |
+
def get_simanl_atoms(
|
| 372 |
+
species: "Species",
|
| 373 |
+
dist_consts: Optional[Dict] = None,
|
| 374 |
+
conf_n: int = 0,
|
| 375 |
+
save_xyz: bool = True,
|
| 376 |
+
also_return_energy: bool = False,
|
| 377 |
+
):
|
| 378 |
+
r"""
|
| 379 |
+
Use a bonded + repulsive force field to generate 3D structure for a
|
| 380 |
+
species. If the initial coordinates are reasonable e.g. from a previously
|
| 381 |
+
generated 3D structure then add random displacement vectors and minimise
|
| 382 |
+
to generate a conformer. Otherwise add atoms to the box sequentially
|
| 383 |
+
until all atoms have been added, which generates a qualitatively reasonable
|
| 384 |
+
3D geometry which should be optimised using a electronic structure method::
|
| 385 |
+
|
| 386 |
+
V(x) = Σ_bonds k(d - d0)^2 + Σ_ij c/d^n
|
| 387 |
+
|
| 388 |
+
---------------------------------------------------------------------------
|
| 389 |
+
Arguments:
|
| 390 |
+
species (autode.species.Species):
|
| 391 |
+
|
| 392 |
+
dist_consts (dict): Key = tuple of atom indexes, Value = distance
|
| 393 |
+
|
| 394 |
+
conf_n (int): Number of this conformer
|
| 395 |
+
|
| 396 |
+
save_xyz (bool): Whether or not to save a .xyz file of the structure
|
| 397 |
+
for fast reloading
|
| 398 |
+
|
| 399 |
+
also_return_energy (bool): Whether or not to return the energy
|
| 400 |
+
|
| 401 |
+
Returns:
|
| 402 |
+
(list(autode.atoms.Atom)): Atoms
|
| 403 |
+
"""
|
| 404 |
+
xyz_filename = f"{species.name}_conf{conf_n}_siman.xyz"
|
| 405 |
+
|
| 406 |
+
saved_atoms = _get_atoms_from_generated_file(species, xyz_filename)
|
| 407 |
+
if saved_atoms is not None and not also_return_energy:
|
| 408 |
+
return saved_atoms
|
| 409 |
+
|
| 410 |
+
# To generate the potential requires bonds between atoms defined in a
|
| 411 |
+
# molecular graph
|
| 412 |
+
if species.graph is None:
|
| 413 |
+
raise ex.NoMolecularGraph
|
| 414 |
+
|
| 415 |
+
# Initialise a new random seed and make a copy of the species' atoms.
|
| 416 |
+
# RandomState is thread safe
|
| 417 |
+
rand = np.random.RandomState()
|
| 418 |
+
atoms = _get_atoms_rotated_stereocentres(
|
| 419 |
+
species=species, atoms=deepcopy(species.atoms), rand=rand
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
# Add the distance constraints as fixed bonds
|
| 423 |
+
d0 = species.graph.eqm_bond_distance_matrix
|
| 424 |
+
|
| 425 |
+
# Add distance constraints across stereocentres e.g. for a Z double bond
|
| 426 |
+
# then modify d0 appropriately
|
| 427 |
+
curr_dist_consts = {} if dist_consts is None else dist_consts
|
| 428 |
+
distance_constraints = _add_dist_consts_for_stereocentres(
|
| 429 |
+
species=species, dist_consts=curr_dist_consts
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
constrained_bonds = []
|
| 433 |
+
for bond, length in distance_constraints.items():
|
| 434 |
+
i, j = bond
|
| 435 |
+
d0[i, j] = length
|
| 436 |
+
d0[j, i] = length
|
| 437 |
+
constrained_bonds.append(bond)
|
| 438 |
+
|
| 439 |
+
# Randomise coordinates that aren't fixed by shifting a maximum of
|
| 440 |
+
# autode.Config.max_atom_displacement in x, y, z
|
| 441 |
+
fixed_atom_indexes = _get_non_random_atoms(species=species)
|
| 442 |
+
|
| 443 |
+
# Shift by a factor defined in the config file if the coordinates are
|
| 444 |
+
# reasonable but otherwise init in a 10 A cube
|
| 445 |
+
reasonable_init_coords = ade.geom.are_coords_reasonable(
|
| 446 |
+
species.coordinates
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
if reasonable_init_coords:
|
| 450 |
+
factor = ade.Config.max_atom_displacement / np.sqrt(3)
|
| 451 |
+
for i, atom in enumerate(atoms):
|
| 452 |
+
if i not in fixed_atom_indexes:
|
| 453 |
+
atom.translate(vec=factor * rand.uniform(-1, 1, 3))
|
| 454 |
+
else:
|
| 455 |
+
# Randomise in a 10 Å cubic box
|
| 456 |
+
[atom.translate(vec=rand.uniform(-5, 5, 3)) for atom in atoms]
|
| 457 |
+
|
| 458 |
+
if reasonable_init_coords:
|
| 459 |
+
init_coords = np.array([atom.coord for atom in atoms])
|
| 460 |
+
coords, energy = _get_coords_energy(
|
| 461 |
+
coords=init_coords,
|
| 462 |
+
bonds=species.graph.edges,
|
| 463 |
+
k=1.0,
|
| 464 |
+
c=0.01,
|
| 465 |
+
d0=d0,
|
| 466 |
+
tol=1e-5,
|
| 467 |
+
fixed_idxs=fixed_atom_indexes,
|
| 468 |
+
fixed_bonds=constrained_bonds,
|
| 469 |
+
)
|
| 470 |
+
else:
|
| 471 |
+
coords, energy = _get_coords_no_init_structure(
|
| 472 |
+
atoms, species, d0, constrained_bonds
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
# Set the coordinates of the new atoms
|
| 476 |
+
for i, atom in enumerate(atoms):
|
| 477 |
+
atom.coord = coords[i]
|
| 478 |
+
|
| 479 |
+
# Print an xyz file so rerunning will read the file
|
| 480 |
+
if save_xyz:
|
| 481 |
+
atoms_to_xyz_file(atoms=atoms, filename=xyz_filename)
|
| 482 |
+
|
| 483 |
+
if also_return_energy:
|
| 484 |
+
logger.info(f"E_RR = {energy:.6f}")
|
| 485 |
+
return atoms, energy
|
| 486 |
+
|
| 487 |
+
return atoms
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
def get_simanl_conformer(
|
| 491 |
+
species: "Species",
|
| 492 |
+
dist_consts: Optional[Dict] = None,
|
| 493 |
+
conf_n: int = 0,
|
| 494 |
+
save_xyz: bool = True,
|
| 495 |
+
) -> "Conformer":
|
| 496 |
+
"""
|
| 497 |
+
Generate a conformer of a species using randomise+relax with a simple FF
|
| 498 |
+
(see get_simanl_atoms). Example
|
| 499 |
+
|
| 500 |
+
.. code-block:: Python
|
| 501 |
+
>>> import autode as ade
|
| 502 |
+
>>> from autode.conformers.conf_gen import get_simanl_conformer
|
| 503 |
+
>>> mol = ade.Molecule(smiles='CCCC', name='butane')
|
| 504 |
+
>>> conf0 = get_simanl_conformer(mol, conf_n=0, save_xyz=False)
|
| 505 |
+
Conformer(butane_conf0, n_atoms=14, charge=0, mult=1)
|
| 506 |
+
|
| 507 |
+
---------------------------------------------------------------------------
|
| 508 |
+
Arguments:
|
| 509 |
+
species (autode.species.Species):
|
| 510 |
+
|
| 511 |
+
dist_consts (dict): Key = tuple of atom indexes, Value = distance
|
| 512 |
+
|
| 513 |
+
conf_n (int): Number of this conformer
|
| 514 |
+
|
| 515 |
+
save_xyz (bool): Whether or not to save a .xyz file of the structure
|
| 516 |
+
|
| 517 |
+
Returns:
|
| 518 |
+
(autode.conformers.Conformer): Conformer
|
| 519 |
+
"""
|
| 520 |
+
|
| 521 |
+
conformer = Conformer(
|
| 522 |
+
species=species,
|
| 523 |
+
name=f"{species.name}_conf{conf_n}",
|
| 524 |
+
dist_consts=dist_consts,
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
atoms, energy = get_simanl_atoms(
|
| 528 |
+
species,
|
| 529 |
+
dist_consts=dist_consts,
|
| 530 |
+
conf_n=conf_n,
|
| 531 |
+
save_xyz=save_xyz,
|
| 532 |
+
also_return_energy=True,
|
| 533 |
+
)
|
| 534 |
+
conformer.atoms = atoms
|
| 535 |
+
conformer.energy = energy
|
| 536 |
+
|
| 537 |
+
return conformer
|
autodE/source/autode/conformers/conformer.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
from typing import Optional, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
from autode.atoms import Atoms
|
| 6 |
+
from autode.values import Coordinates
|
| 7 |
+
from autode.exceptions import AtomsNotFound
|
| 8 |
+
from autode.log import logger
|
| 9 |
+
from autode.species.species import Species
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
from autode.calculations.calculation import Calculation
|
| 13 |
+
from autode.wrappers.methods import Method
|
| 14 |
+
from autode.wrappers.keywords import Keywords
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class Conformer(Species):
|
| 18 |
+
def __init__(
|
| 19 |
+
self,
|
| 20 |
+
name: str = "conf",
|
| 21 |
+
atoms: Optional["Atoms"] = None,
|
| 22 |
+
solvent_name: Optional[str] = None,
|
| 23 |
+
charge: int = 0,
|
| 24 |
+
mult: int = 1,
|
| 25 |
+
dist_consts: Optional[dict] = None,
|
| 26 |
+
species: Optional[Species] = None,
|
| 27 |
+
):
|
| 28 |
+
"""
|
| 29 |
+
Construct a conformer either using the standard species constructor,
|
| 30 |
+
or from a species directly.
|
| 31 |
+
|
| 32 |
+
-----------------------------------------------------------------------
|
| 33 |
+
See Also:
|
| 34 |
+
(autode.species.species.Species):
|
| 35 |
+
"""
|
| 36 |
+
super().__init__(name, atoms, charge, mult, solvent_name=solvent_name)
|
| 37 |
+
self._parent_atoms = None
|
| 38 |
+
self._coordinates = None
|
| 39 |
+
|
| 40 |
+
if species is not None:
|
| 41 |
+
self._parent_atoms = species.atoms
|
| 42 |
+
self._coordinates = species.coordinates.copy()
|
| 43 |
+
self.charge = species.charge # Require identical charge/mult/solv
|
| 44 |
+
self.mult = species.mult
|
| 45 |
+
self.solvent = species.solvent
|
| 46 |
+
|
| 47 |
+
if atoms is not None: # Specified atoms overrides species
|
| 48 |
+
self.atoms = Atoms(atoms)
|
| 49 |
+
|
| 50 |
+
self.constraints.update(distance=dist_consts)
|
| 51 |
+
|
| 52 |
+
def __repr__(self):
|
| 53 |
+
"""Representation of a conformer"""
|
| 54 |
+
return self._repr(prefix="Conformer")
|
| 55 |
+
|
| 56 |
+
def __eq__(self, other):
|
| 57 |
+
return super().__eq__(other)
|
| 58 |
+
|
| 59 |
+
def single_point(
|
| 60 |
+
self,
|
| 61 |
+
method: "Method",
|
| 62 |
+
keywords: Optional["Keywords"] = None,
|
| 63 |
+
n_cores: Optional[int] = None,
|
| 64 |
+
):
|
| 65 |
+
"""
|
| 66 |
+
Calculate a single point and default to a low level single point method
|
| 67 |
+
|
| 68 |
+
----------------------------------------------------------------------
|
| 69 |
+
Arguments:
|
| 70 |
+
method (autode.wrappers.base.ElectronicStructureMethod):
|
| 71 |
+
|
| 72 |
+
keywords (autode.wrappers.keywords.Keywords):
|
| 73 |
+
|
| 74 |
+
n_cores (int | None): If None then defaults to Config.n_cores
|
| 75 |
+
"""
|
| 76 |
+
keywords = method.keywords.low_sp if keywords is None else keywords
|
| 77 |
+
|
| 78 |
+
return super().single_point(method, keywords, n_cores=n_cores)
|
| 79 |
+
|
| 80 |
+
def optimise(
|
| 81 |
+
self,
|
| 82 |
+
method: Optional["Method"] = None,
|
| 83 |
+
reset_graph: bool = False,
|
| 84 |
+
calc: Optional["Calculation"] = None,
|
| 85 |
+
keywords: Optional["Keywords"] = None,
|
| 86 |
+
n_cores: Optional[int] = None,
|
| 87 |
+
):
|
| 88 |
+
"""
|
| 89 |
+
Optimise the geometry of this conformer using a method. Will use
|
| 90 |
+
low_opt keywords if no keywords are given.
|
| 91 |
+
|
| 92 |
+
-----------------------------------------------------------------------
|
| 93 |
+
Arguments:
|
| 94 |
+
method (autode.wrappers.base.ElectronicStructureMethod):
|
| 95 |
+
|
| 96 |
+
reset_graph (bool):
|
| 97 |
+
|
| 98 |
+
calc (autode.calculation.Calculation):
|
| 99 |
+
|
| 100 |
+
keywords (autode.wrappers.keywords.Keywords):
|
| 101 |
+
|
| 102 |
+
n_cores (int | None): If None then defaults to Config.n_cores
|
| 103 |
+
"""
|
| 104 |
+
try:
|
| 105 |
+
if keywords is None and method is not None:
|
| 106 |
+
keywords = method.keywords.low_opt
|
| 107 |
+
|
| 108 |
+
super().optimise(
|
| 109 |
+
method, keywords=keywords, calc=calc, n_cores=n_cores
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
except AtomsNotFound:
|
| 113 |
+
logger.error(f"Atoms not found for {self.name} but not critical")
|
| 114 |
+
self.atoms = None
|
| 115 |
+
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
@property
|
| 119 |
+
def coordinates(self) -> Optional[Coordinates]:
|
| 120 |
+
"""Coordinates of this conformer"""
|
| 121 |
+
return self._coordinates
|
| 122 |
+
|
| 123 |
+
@coordinates.setter
|
| 124 |
+
def coordinates(self, value: np.ndarray):
|
| 125 |
+
"""Set the coordinates of this conformer"""
|
| 126 |
+
if self._parent_atoms is None:
|
| 127 |
+
raise ValueError(
|
| 128 |
+
"Conformer has no parent atoms. Setting the "
|
| 129 |
+
"coordinates will leave the atoms undefined"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
self._coordinates = Coordinates(value)
|
| 133 |
+
|
| 134 |
+
@property
|
| 135 |
+
def atoms(self) -> Optional[Atoms]:
|
| 136 |
+
"""
|
| 137 |
+
Atoms of this conformer are built from the parent atoms and the
|
| 138 |
+
coordinates that are unique to this conformer.
|
| 139 |
+
"""
|
| 140 |
+
|
| 141 |
+
if self._parent_atoms is None or self._coordinates is None:
|
| 142 |
+
return None
|
| 143 |
+
|
| 144 |
+
atoms = Atoms()
|
| 145 |
+
for parent_atom, coord in zip(self._parent_atoms, self._coordinates):
|
| 146 |
+
atom = parent_atom.copy()
|
| 147 |
+
atom.coord = coord
|
| 148 |
+
|
| 149 |
+
atoms.append(atom)
|
| 150 |
+
|
| 151 |
+
return atoms
|
| 152 |
+
|
| 153 |
+
@atoms.setter
|
| 154 |
+
def atoms(self, value: Optional[Atoms]):
|
| 155 |
+
"""
|
| 156 |
+
Set the atoms of this conformer.
|
| 157 |
+
|
| 158 |
+
If None then set the corresponding coordinates of this conformer to
|
| 159 |
+
None (such that self.atoms is None). If this conformer has coordinates
|
| 160 |
+
then set those from the individual atomic coordinates otherwise
|
| 161 |
+
set the coordinates as a batch
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
if value is None: # Clear the coordinates
|
| 165 |
+
self._coordinates = None
|
| 166 |
+
return
|
| 167 |
+
|
| 168 |
+
if self._parent_atoms is None:
|
| 169 |
+
self._parent_atoms = value
|
| 170 |
+
|
| 171 |
+
if self._coordinates is None:
|
| 172 |
+
self._coordinates = value.coordinates
|
| 173 |
+
return
|
| 174 |
+
|
| 175 |
+
for i, atom in enumerate(value):
|
| 176 |
+
parent_atom = self._parent_atoms[i]
|
| 177 |
+
if atom.label != parent_atom.label:
|
| 178 |
+
raise ValueError(
|
| 179 |
+
"Cannot alter the atomic symbols of a "
|
| 180 |
+
"conformer. Parent molecule was different: "
|
| 181 |
+
f"{atom.label} != {parent_atom.label}"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
self._coordinates[i] = atom.coord.copy()
|
autodE/source/autode/conformers/conformers.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
from typing import Optional, Union, TYPE_CHECKING
|
| 4 |
+
from rdkit import Chem
|
| 5 |
+
|
| 6 |
+
from autode.values import Distance, Energy
|
| 7 |
+
from autode.atoms import Atom, Atoms
|
| 8 |
+
from autode.config import Config
|
| 9 |
+
from autode.mol_graphs import make_graph, is_isomorphic
|
| 10 |
+
from autode.geom import calc_heavy_atom_rmsd
|
| 11 |
+
from autode.log import logger
|
| 12 |
+
from autode.utils import ProcessPool
|
| 13 |
+
from autode.exceptions import NoConformers, CouldNotGetProperty
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
if TYPE_CHECKING:
|
| 17 |
+
from autode.conformers.conformer import Conformer
|
| 18 |
+
from autode.wrappers.methods import Method
|
| 19 |
+
from autode.mol_graphs import MolecularGraph
|
| 20 |
+
from autode.wrappers.keywords import Keywords
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _calc_conformer(conformer, calc_type, method, keywords, n_cores=1):
|
| 24 |
+
"""Top-level hashable function to call in parallel"""
|
| 25 |
+
func = getattr(conformer, calc_type)
|
| 26 |
+
try:
|
| 27 |
+
func(method=method, keywords=keywords, n_cores=n_cores)
|
| 28 |
+
except CouldNotGetProperty as e:
|
| 29 |
+
logger.warning(
|
| 30 |
+
f"Failed to run calculation on conformer {conformer.name} due to {e}"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
return conformer
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class Conformers(list):
|
| 37 |
+
@property
|
| 38 |
+
def lowest_energy(self) -> Optional["Conformer"]:
|
| 39 |
+
"""
|
| 40 |
+
Return the lowest energy conformer state from this set. If no
|
| 41 |
+
conformers have an energy then return None
|
| 42 |
+
|
| 43 |
+
-----------------------------------------------------------------------
|
| 44 |
+
Returns:
|
| 45 |
+
(autode.conformers.Conformer | None): Conformer
|
| 46 |
+
"""
|
| 47 |
+
if all(c.energy is None for c in self):
|
| 48 |
+
logger.error("Have no conformers with an energy, so no lowest")
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
energies = [c.energy if c.energy is not None else np.inf for c in self]
|
| 52 |
+
return self[np.argmin(energies)]
|
| 53 |
+
|
| 54 |
+
def prune(
|
| 55 |
+
self,
|
| 56 |
+
e_tol: Union[Energy, float] = Energy(1.0, "kJ mol-1"),
|
| 57 |
+
rmsd_tol: Union[Distance, float, None] = None,
|
| 58 |
+
n_sigma: float = 5,
|
| 59 |
+
remove_no_energy: bool = False,
|
| 60 |
+
) -> None:
|
| 61 |
+
"""
|
| 62 |
+
Prune conformers based on both energy and root mean squared deviation
|
| 63 |
+
(RMSD) values. Will discard any conformers that are within e_tol in
|
| 64 |
+
energy (Ha) and rmsd in RMSD (Å) to any other
|
| 65 |
+
|
| 66 |
+
-----------------------------------------------------------------------
|
| 67 |
+
Arguments:
|
| 68 |
+
e_tol (Energy): Energy tolerance
|
| 69 |
+
|
| 70 |
+
rmsd_tol (Distance | None): RMSD tolerance. Defaults to
|
| 71 |
+
autode.Config.rmsd_threshold
|
| 72 |
+
|
| 73 |
+
n_sigma (float | int):
|
| 74 |
+
|
| 75 |
+
remove_no_energy (bool):
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
if remove_no_energy:
|
| 79 |
+
self.remove_no_energy()
|
| 80 |
+
|
| 81 |
+
self.prune_on_energy(e_tol=e_tol, n_sigma=n_sigma)
|
| 82 |
+
self.prune_on_rmsd(rmsd_tol=rmsd_tol)
|
| 83 |
+
|
| 84 |
+
return None
|
| 85 |
+
|
| 86 |
+
def prune_on_energy(
|
| 87 |
+
self,
|
| 88 |
+
e_tol: Union[Energy, float] = Energy(1.0, "kJ mol-1"),
|
| 89 |
+
n_sigma: float = 5,
|
| 90 |
+
) -> None:
|
| 91 |
+
"""
|
| 92 |
+
Prune the conformers based on an energy threshold, discarding those
|
| 93 |
+
that have energies that are similar to within e_tol. Also discards
|
| 94 |
+
conformers with very high energies (indicating a problem
|
| 95 |
+
with the calculation) if the are more than n_sigma standard deviations
|
| 96 |
+
away from the mean
|
| 97 |
+
|
| 98 |
+
-----------------------------------------------------------------------
|
| 99 |
+
Arguments:
|
| 100 |
+
e_tol (autode.values.Energy | float | None):
|
| 101 |
+
|
| 102 |
+
n_sigma (int): Number of standard deviations a conformer energy
|
| 103 |
+
must be from the average for it not to be added
|
| 104 |
+
"""
|
| 105 |
+
idxs_with_energy = [
|
| 106 |
+
idx for idx, conf in enumerate(self) if conf.energy is not None
|
| 107 |
+
]
|
| 108 |
+
n_prev_confs = len(self)
|
| 109 |
+
|
| 110 |
+
if len(idxs_with_energy) < 2:
|
| 111 |
+
logger.info(
|
| 112 |
+
f"Only have {len(self)} conformers with an energy. No "
|
| 113 |
+
f"need to prune"
|
| 114 |
+
)
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
energies = [self[idx].energy for idx in idxs_with_energy]
|
| 118 |
+
|
| 119 |
+
# Use a lower-bounded σ to prevent division by zero
|
| 120 |
+
std_dev_e = max(float(np.std(energies)), 1e-8)
|
| 121 |
+
avg_e = np.average(energies)
|
| 122 |
+
|
| 123 |
+
logger.info(
|
| 124 |
+
f"Have {len(energies)} energies with μ={avg_e:.6f} Ha "
|
| 125 |
+
f"σ={std_dev_e:.6f} Ha"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if isinstance(e_tol, Energy):
|
| 129 |
+
e_tol = float(e_tol.to("Ha"))
|
| 130 |
+
else:
|
| 131 |
+
logger.warning(
|
| 132 |
+
f"Assuming energy tolerance {e_tol:.6f} has units " f"of Ha"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Delete from the end of the list to preserve the order when deleting
|
| 136 |
+
for i, idx in enumerate(reversed(idxs_with_energy)):
|
| 137 |
+
conf = self[idx]
|
| 138 |
+
idxs_with_energy = [j for j in idxs_with_energy if j < len(self)]
|
| 139 |
+
|
| 140 |
+
if np.abs(conf.energy - avg_e) / std_dev_e > n_sigma:
|
| 141 |
+
logger.warning(
|
| 142 |
+
f"Conformer {idx} had an energy >{n_sigma}σ "
|
| 143 |
+
f"from the average - removing"
|
| 144 |
+
)
|
| 145 |
+
del self[idx]
|
| 146 |
+
continue
|
| 147 |
+
|
| 148 |
+
if i == 0:
|
| 149 |
+
# The first (last) conformer must be unique
|
| 150 |
+
continue
|
| 151 |
+
|
| 152 |
+
if any(
|
| 153 |
+
np.abs(conf.energy - self[o_idx].energy) < e_tol
|
| 154 |
+
for o_idx in idxs_with_energy
|
| 155 |
+
if o_idx != idx
|
| 156 |
+
):
|
| 157 |
+
logger.info(f"Conformer {idx} had a non unique energy")
|
| 158 |
+
del self[idx]
|
| 159 |
+
continue
|
| 160 |
+
|
| 161 |
+
logger.info(
|
| 162 |
+
f"Stripped {n_prev_confs - len(self)} conformer(s)."
|
| 163 |
+
f" {n_prev_confs} -> {len(self)}"
|
| 164 |
+
)
|
| 165 |
+
return None
|
| 166 |
+
|
| 167 |
+
def prune_on_rmsd(
|
| 168 |
+
self, rmsd_tol: Union[Distance, float, None] = None
|
| 169 |
+
) -> None:
|
| 170 |
+
"""
|
| 171 |
+
Given a list of conformers add those that are unique based on an RMSD
|
| 172 |
+
tolerance. If rmsd=None then use autode.Config.rmsd_threshold
|
| 173 |
+
|
| 174 |
+
-----------------------------------------------------------------------
|
| 175 |
+
Arguments:
|
| 176 |
+
rmsd_tol (autode.values.Distance | float | None):
|
| 177 |
+
"""
|
| 178 |
+
if len(self) < 2:
|
| 179 |
+
logger.info(
|
| 180 |
+
f"Only have {len(self)} conformers. No need to prune "
|
| 181 |
+
f"on RMSD"
|
| 182 |
+
)
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
rmsd_tol = Config.rmsd_threshold if rmsd_tol is None else rmsd_tol
|
| 186 |
+
|
| 187 |
+
if isinstance(rmsd_tol, float):
|
| 188 |
+
logger.warning(
|
| 189 |
+
f"Assuming RMSD tolerance {rmsd_tol:.2f} has units" f" of Å"
|
| 190 |
+
)
|
| 191 |
+
rmsd_tol = Distance(rmsd_tol, "Å")
|
| 192 |
+
|
| 193 |
+
logger.info(
|
| 194 |
+
f'Removing conformers with RMSD < {rmsd_tol.to("ang")} Å '
|
| 195 |
+
f"to any other (heavy atoms only, with no symmetry)"
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# Only enumerate up to but not including the final index, as at
|
| 199 |
+
# least one of the conformers must be unique in geometry
|
| 200 |
+
for idx in reversed(range(len(self) - 1)):
|
| 201 |
+
conf = self[idx]
|
| 202 |
+
|
| 203 |
+
if any(
|
| 204 |
+
calc_heavy_atom_rmsd(conf.atoms, other.atoms) < rmsd_tol
|
| 205 |
+
for o_idx, other in enumerate(self)
|
| 206 |
+
if o_idx != idx
|
| 207 |
+
):
|
| 208 |
+
logger.info(
|
| 209 |
+
f"Conformer {idx} was close in geometry to at "
|
| 210 |
+
f"least one other - removing"
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
del self[idx]
|
| 214 |
+
|
| 215 |
+
logger.info(f"Pruned to {len(self)} unique conformer(s) on RMSD")
|
| 216 |
+
return None
|
| 217 |
+
|
| 218 |
+
def prune_diff_graph(self, graph: "MolecularGraph") -> None:
|
| 219 |
+
"""
|
| 220 |
+
Remove conformers with a different molecular graph to a defined
|
| 221 |
+
reference. Although all conformers should have the same molecular
|
| 222 |
+
graph there are situations where not pruning these is useful
|
| 223 |
+
|
| 224 |
+
-----------------------------------------------------------------------
|
| 225 |
+
|
| 226 |
+
Arguments:
|
| 227 |
+
graph: Reference graph
|
| 228 |
+
"""
|
| 229 |
+
n_prev_confs = len(self)
|
| 230 |
+
|
| 231 |
+
for idx in reversed(range(len(self))):
|
| 232 |
+
conformer = self[idx]
|
| 233 |
+
make_graph(conformer)
|
| 234 |
+
|
| 235 |
+
if not is_isomorphic(
|
| 236 |
+
conformer.graph, graph, ignore_active_bonds=True
|
| 237 |
+
):
|
| 238 |
+
logger.warning("Conformer had a different graph. Ignoring")
|
| 239 |
+
del self[idx]
|
| 240 |
+
|
| 241 |
+
logger.info(f"Pruned on connectivity {n_prev_confs} -> {len(self)}")
|
| 242 |
+
return None
|
| 243 |
+
|
| 244 |
+
def remove_no_energy(self) -> None:
|
| 245 |
+
"""Remove all conformers from this list that do not have an energy"""
|
| 246 |
+
n_conformers_before_remove = len(self)
|
| 247 |
+
|
| 248 |
+
for idx in reversed(range(len(self))): # Enumerate backwards
|
| 249 |
+
if self[idx].energy is None:
|
| 250 |
+
del self[idx]
|
| 251 |
+
|
| 252 |
+
n_conformers = len(self)
|
| 253 |
+
if n_conformers == 0 and n_conformers != n_conformers_before_remove:
|
| 254 |
+
raise NoConformers(
|
| 255 |
+
f"Removed all the conformers "
|
| 256 |
+
f"{n_conformers_before_remove} -> 0"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def _parallel_calc(self, calc_type, method, keywords):
|
| 260 |
+
"""
|
| 261 |
+
Run a set of calculations (single point energy evaluations or geometry
|
| 262 |
+
optimisations) in parallel over every conformer in this set. Will
|
| 263 |
+
attempt to use all autode.Config.n_cores as fully as possible
|
| 264 |
+
|
| 265 |
+
Arguments:
|
| 266 |
+
calc_type (str):
|
| 267 |
+
|
| 268 |
+
method (autode.wrappers.base.ElectronicStructureMethod):
|
| 269 |
+
|
| 270 |
+
keywords (autode.wrappers.keywords.Keywords):
|
| 271 |
+
"""
|
| 272 |
+
# TODO: Test efficiency + improve with dynamic load balancing
|
| 273 |
+
if len(self) == 0:
|
| 274 |
+
logger.error(f"Cannot run {calc_type} over 0 conformers")
|
| 275 |
+
return None
|
| 276 |
+
|
| 277 |
+
n_cores_pp = max(Config.n_cores // len(self), 1)
|
| 278 |
+
|
| 279 |
+
with ProcessPool(max_workers=Config.n_cores // n_cores_pp) as pool:
|
| 280 |
+
jobs = [
|
| 281 |
+
pool.submit(
|
| 282 |
+
_calc_conformer,
|
| 283 |
+
conf,
|
| 284 |
+
calc_type,
|
| 285 |
+
method,
|
| 286 |
+
keywords,
|
| 287 |
+
n_cores=n_cores_pp,
|
| 288 |
+
)
|
| 289 |
+
for conf in self
|
| 290 |
+
]
|
| 291 |
+
|
| 292 |
+
for idx, res in enumerate(jobs):
|
| 293 |
+
self[idx] = res.result()
|
| 294 |
+
|
| 295 |
+
return None
|
| 296 |
+
|
| 297 |
+
def optimise(
|
| 298 |
+
self,
|
| 299 |
+
method: "Method",
|
| 300 |
+
keywords: Optional["Keywords"] = None,
|
| 301 |
+
) -> None:
|
| 302 |
+
"""
|
| 303 |
+
Optimise a set of conformers in parallel
|
| 304 |
+
|
| 305 |
+
-----------------------------------------------------------------------
|
| 306 |
+
Arguments:
|
| 307 |
+
method (autode.wrappers.base.ElectronicStructureMethod):
|
| 308 |
+
|
| 309 |
+
keywords (autode.wrappers.keywords.Keywords):
|
| 310 |
+
"""
|
| 311 |
+
return self._parallel_calc("optimise", method, keywords)
|
| 312 |
+
|
| 313 |
+
def single_point(
|
| 314 |
+
self,
|
| 315 |
+
method: "Method",
|
| 316 |
+
keywords: Optional["Keywords"] = None,
|
| 317 |
+
) -> None:
|
| 318 |
+
"""
|
| 319 |
+
Evaluate single point energies for a set of conformers in parallel
|
| 320 |
+
|
| 321 |
+
-----------------------------------------------------------------------
|
| 322 |
+
Arguments:
|
| 323 |
+
method (autode.wrappers.base.ElectronicStructureMethod):
|
| 324 |
+
|
| 325 |
+
keywords (autode.wrappers.keywords.Keywords):
|
| 326 |
+
"""
|
| 327 |
+
return self._parallel_calc("single_point", method, keywords)
|
| 328 |
+
|
| 329 |
+
def copy(self) -> "Conformers":
|
| 330 |
+
return Conformers([conformer.copy() for conformer in self])
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def atoms_from_rdkit_mol(rdkit_mol_obj: Chem.Mol, conf_id: int = 0) -> Atoms:
|
| 334 |
+
"""
|
| 335 |
+
Generate atoms for a conformer contained within an RDKit molecule object
|
| 336 |
+
|
| 337 |
+
---------------------------------------------------------------------------
|
| 338 |
+
Arguments:
|
| 339 |
+
rdkit_mol_obj (rdkit.Chem.Mol): RDKit molecule
|
| 340 |
+
|
| 341 |
+
conf_id (int): Conformer id to convert to atoms
|
| 342 |
+
|
| 343 |
+
Returns:
|
| 344 |
+
(list(autode.atoms.Atom)): Atoms
|
| 345 |
+
"""
|
| 346 |
+
|
| 347 |
+
mol_block_lines = Chem.MolToMolBlock(rdkit_mol_obj, confId=conf_id).split(
|
| 348 |
+
"\n"
|
| 349 |
+
)
|
| 350 |
+
mol_file_atoms = Atoms()
|
| 351 |
+
|
| 352 |
+
# Extract atoms from the mol block
|
| 353 |
+
for line in mol_block_lines:
|
| 354 |
+
split_line = line.split()
|
| 355 |
+
|
| 356 |
+
if len(split_line) == 16:
|
| 357 |
+
x, y, z, atom_label = split_line[:4]
|
| 358 |
+
mol_file_atoms.append(Atom(atom_label, x=x, y=y, z=z))
|
| 359 |
+
|
| 360 |
+
return mol_file_atoms
|