guohanghui commited on
Commit
bc673f9
·
verified ·
1 Parent(s): 528ce95

Upload 690 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +30 -0
  2. Dockerfile +18 -0
  3. README.md +27 -5
  4. app.py +45 -0
  5. autodE/mcp_output/README_MCP.md +59 -0
  6. autodE/mcp_output/analysis.json +992 -0
  7. autodE/mcp_output/diff_report.md +68 -0
  8. autodE/mcp_output/mcp_plugin/__init__.py +0 -0
  9. autodE/mcp_output/mcp_plugin/adapter.py +248 -0
  10. autodE/mcp_output/mcp_plugin/main.py +13 -0
  11. autodE/mcp_output/mcp_plugin/mcp_service.py +69 -0
  12. autodE/mcp_output/requirements.txt +13 -0
  13. autodE/mcp_output/start_mcp.py +30 -0
  14. autodE/mcp_output/workflow_summary.json +216 -0
  15. autodE/source/.pre-commit-config.yaml +19 -0
  16. autodE/source/CONTRIBUTING.md +5 -0
  17. autodE/source/LICENSE.md +22 -0
  18. autodE/source/README.md +100 -0
  19. autodE/source/__init__.py +4 -0
  20. autodE/source/autode/__init__.py +71 -0
  21. autodE/source/autode/atoms.py +1865 -0
  22. autodE/source/autode/bond_rearrangement.py +876 -0
  23. autodE/source/autode/bonds.py +87 -0
  24. autodE/source/autode/bracket/__init__.py +3 -0
  25. autodE/source/autode/bracket/base.py +316 -0
  26. autodE/source/autode/bracket/dhs.py +764 -0
  27. autodE/source/autode/bracket/ieip.py +601 -0
  28. autodE/source/autode/bracket/imagepair.py +628 -0
  29. autodE/source/autode/calculations/__init__.py +5 -0
  30. autodE/source/autode/calculations/calculation.py +328 -0
  31. autodE/source/autode/calculations/executors.py +524 -0
  32. autodE/source/autode/calculations/input.py +72 -0
  33. autodE/source/autode/calculations/output.py +87 -0
  34. autodE/source/autode/calculations/types.py +10 -0
  35. autodE/source/autode/common/NEB.pdf +3 -0
  36. autodE/source/autode/common/NEB.tex +60 -0
  37. autodE/source/autode/common/adaptive_path.pdf +0 -0
  38. autodE/source/autode/common/adaptive_path.tex +32 -0
  39. autodE/source/autode/common/hessians.pdf +3 -0
  40. autodE/source/autode/common/hessians.tex +192 -0
  41. autodE/source/autode/common/llogo.png +3 -0
  42. autodE/source/autode/common/logo.pages +3 -0
  43. autodE/source/autode/common/thermochemistry.pdf +3 -0
  44. autodE/source/autode/common/thermochemistry.tex +91 -0
  45. autodE/source/autode/config.py +459 -0
  46. autodE/source/autode/conformers/__init__.py +4 -0
  47. autodE/source/autode/conformers/cconf_gen.pyx +131 -0
  48. autodE/source/autode/conformers/conf_gen.py +537 -0
  49. autodE/source/autode/conformers/conformer.py +184 -0
  50. 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: AutodE
3
- emoji: 😻
4
- colorFrom: red
5
- colorTo: yellow
6
  sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ [![Build Status](https://github.com/duartegroup/autodE/actions/workflows/pytest.yml/badge.svg)](https://github.com/duartegroup/autodE/actions) [![codecov](https://codecov.io/gh/duartegroup/autodE/branch/master/graph/badge.svg)](https://codecov.io/gh/duartegroup/autodE/branch/master) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![GitHub CodeQL](https://github.com/duartegroup/autodE/actions/workflows/codeql.yml/badge.svg)](https://github.com/duartegroup/autodE/actions/workflows/codeql.yml) [![Conda Recipe](https://img.shields.io/badge/recipe-autode-green.svg)](https://anaconda.org/conda-forge/autode) [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/autode.svg)](https://anaconda.org/conda-forge/autode)
2
+
3
+ ![alt text](autode/common/llogo.png)
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

  • SHA256: 595f485e0ae062f0783fb1adce4f54ae2aac6b6b0687f44671dc196f5b2ae38f
  • Pointer size: 131 Bytes
  • Size of remote file: 108 kB
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