chemparseplot 0.0.2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/.gitignore +1 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/PKG-INFO +7 -6
- chemparseplot-0.0.3/_version.py +34 -0
- chemparseplot-0.0.3/chemparseplot/analyze/dist.py +130 -0
- chemparseplot-0.0.3/chemparseplot/analyze/use_ira.py +73 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/basetypes.py +43 -0
- chemparseplot-0.0.3/chemparseplot/parse/eon/gprd.py +329 -0
- chemparseplot-0.0.3/chemparseplot/parse/eon/minimization.py +27 -0
- chemparseplot-0.0.3/chemparseplot/parse/eon/saddle_search.py +341 -0
- chemparseplot-0.0.3/chemparseplot/parse/sella/saddle_search.py +234 -0
- chemparseplot-0.0.3/chemparseplot/plot/__init__.py +9 -0
- chemparseplot-0.0.3/chemparseplot/plot/_aids.py +9 -0
- chemparseplot-0.0.3/chemparseplot/plot/geomscan.py +37 -0
- chemparseplot-0.0.3/chemparseplot/plot/structs.py +89 -0
- chemparseplot-0.0.3/chemparseplot/util.py +103 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/pyproject.toml +13 -12
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/tests/parse/orca/test_interp.py +7 -6
- chemparseplot-0.0.2/_version.py +0 -16
- chemparseplot-0.0.2/chemparseplot/plot/__init__.py +0 -3
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/LICENSE +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/__init__.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/__init__.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/converter.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/orca/__init__.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/orca/geomscan.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/orca/neb/interp.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/patterns.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/units.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/readme.md +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/tests/parse/orca/test_geomscan.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/tests/parse/test_converter.py +0 -0
- {chemparseplot-0.0.2 → chemparseplot-0.0.3}/tests/parse/test_patterns.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: chemparseplot
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Parsers and plotting tools for computational chemistry
|
|
5
5
|
Project-URL: Documentation, https://github.com/HaoZeke/chemparseplot#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/HaoZeke/chemparseplot/issues
|
|
@@ -22,16 +22,17 @@ Requires-Dist: numpy>=1.26.2
|
|
|
22
22
|
Requires-Dist: pint>=0.22
|
|
23
23
|
Provides-Extra: doc
|
|
24
24
|
Requires-Dist: mdit-py-plugins>=0.3.4; extra == 'doc'
|
|
25
|
-
Requires-Dist: myst-nb>=1
|
|
26
|
-
Requires-Dist: myst-parser>=2
|
|
27
|
-
Requires-Dist: sphinx-autodoc2>=0.5
|
|
25
|
+
Requires-Dist: myst-nb>=1; extra == 'doc'
|
|
26
|
+
Requires-Dist: myst-parser>=2; extra == 'doc'
|
|
27
|
+
Requires-Dist: sphinx-autodoc2>=0.5; extra == 'doc'
|
|
28
28
|
Requires-Dist: sphinx-copybutton>=0.5.2; extra == 'doc'
|
|
29
29
|
Requires-Dist: sphinx-library>=1.1.2; extra == 'doc'
|
|
30
30
|
Requires-Dist: sphinx-sitemap>=2.5.1; extra == 'doc'
|
|
31
31
|
Requires-Dist: sphinx-togglebutton>=0.3.2; extra == 'doc'
|
|
32
32
|
Requires-Dist: sphinx>=7.2.6; extra == 'doc'
|
|
33
|
-
Requires-Dist: sphinxcontrib-apidoc>=0.4
|
|
33
|
+
Requires-Dist: sphinxcontrib-apidoc>=0.4; extra == 'doc'
|
|
34
34
|
Provides-Extra: plot
|
|
35
|
+
Requires-Dist: cmcrameri>=1.7; extra == 'plot'
|
|
35
36
|
Requires-Dist: matplotlib>=3.8.2; extra == 'plot'
|
|
36
37
|
Description-Content-Type: text/markdown
|
|
37
38
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.0.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 3)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import ase
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ase.data import covalent_radii
|
|
4
|
+
from ase.neighborlist import NeighborList
|
|
5
|
+
from scipy.spatial.distance import cdist
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def analyze_structure(
|
|
9
|
+
atoms: ase.Atoms, covalent_scale: float = 1.2
|
|
10
|
+
) -> tuple[np.ndarray, np.ndarray, list[list[int]]]:
|
|
11
|
+
"""
|
|
12
|
+
Analyzes an ASE Atoms object to calculate the distance matrix, bond matrix,
|
|
13
|
+
and identify molecular fragments.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
atoms: The ASE Atoms object to analyze.
|
|
17
|
+
covalent_scale: A scaling factor applied to the covalent radii to
|
|
18
|
+
determine the bonding threshold. A bond is formed if the distance
|
|
19
|
+
between two atoms is less than or equal to the sum of their
|
|
20
|
+
covalent radii, multiplied by this factor.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A tuple containing:
|
|
24
|
+
- distance_matrix: A NumPy array representing the pairwise distances
|
|
25
|
+
between all atoms in the system.
|
|
26
|
+
- bond_matrix: A NumPy array representing the adjacency matrix of the
|
|
27
|
+
molecular graph. bond_matrix[i, j] = 1 if atoms i and j are
|
|
28
|
+
bonded, and 0 otherwise.
|
|
29
|
+
- fragments: A list of lists, where each inner list contains the indices
|
|
30
|
+
of the atoms belonging to a connected fragment.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
num_atoms = len(atoms)
|
|
34
|
+
|
|
35
|
+
# 1. Calculate the distance matrix.
|
|
36
|
+
distance_matrix = atoms.get_all_distances(
|
|
37
|
+
mic=True
|
|
38
|
+
) # Use minimum image convention (MIC)
|
|
39
|
+
|
|
40
|
+
# 2. Calculate the bond matrix.
|
|
41
|
+
bond_matrix = np.zeros((num_atoms, num_atoms), dtype=int)
|
|
42
|
+
radii = [covalent_radii[atom.number] for atom in atoms] # Atomic numbers
|
|
43
|
+
|
|
44
|
+
# More efficient with neighborlist
|
|
45
|
+
neighbor_list = NeighborList(
|
|
46
|
+
cutoffs=[covalent_scale * r for r in radii], # Scaled cutoffs
|
|
47
|
+
skin=0.0, # No skin, we use the cutoffs directly
|
|
48
|
+
bothways=True, # Get both i->j and j->i
|
|
49
|
+
self_interaction=False, # No i->i connections
|
|
50
|
+
primitive=ase.neighborlist.NewPrimitiveNeighborList,
|
|
51
|
+
)
|
|
52
|
+
neighbor_list.update(atoms)
|
|
53
|
+
|
|
54
|
+
for i in range(num_atoms):
|
|
55
|
+
indices, offsets = neighbor_list.get_neighbors(i)
|
|
56
|
+
for j, _ in zip(indices, offsets, strict=False):
|
|
57
|
+
bond_matrix[i, j] = 1
|
|
58
|
+
|
|
59
|
+
# 3. Identify fragments.
|
|
60
|
+
visited = [False] * num_atoms
|
|
61
|
+
fragments = []
|
|
62
|
+
|
|
63
|
+
def dfs(atom_index: int, current_fragment: list[int]):
|
|
64
|
+
"""Depth-First Search to find connected components."""
|
|
65
|
+
visited[atom_index] = True
|
|
66
|
+
current_fragment.append(atom_index)
|
|
67
|
+
for neighbor_index in range(num_atoms):
|
|
68
|
+
if (
|
|
69
|
+
bond_matrix[atom_index, neighbor_index] == 1
|
|
70
|
+
and not visited[neighbor_index]
|
|
71
|
+
):
|
|
72
|
+
dfs(neighbor_index, current_fragment)
|
|
73
|
+
|
|
74
|
+
for i in range(num_atoms):
|
|
75
|
+
if not visited[i]:
|
|
76
|
+
new_fragment = []
|
|
77
|
+
dfs(i, new_fragment)
|
|
78
|
+
fragments.append(new_fragment)
|
|
79
|
+
|
|
80
|
+
# 4. Calculate Centroid Distances and *Corrected* Distances.
|
|
81
|
+
num_fragments = len(fragments)
|
|
82
|
+
centroid_distances = np.zeros((num_fragments, num_fragments))
|
|
83
|
+
corrected_distances = [] # Initialize with infinity
|
|
84
|
+
positions = atoms.get_positions()
|
|
85
|
+
atomic_numbers = atoms.get_atomic_numbers()
|
|
86
|
+
atomic_symbols = atoms.get_chemical_symbols()
|
|
87
|
+
|
|
88
|
+
if num_fragments > 1:
|
|
89
|
+
for i in range(num_fragments):
|
|
90
|
+
frag_i_positions = positions[fragments[i]]
|
|
91
|
+
centroid_i = np.mean(frag_i_positions, axis=0)
|
|
92
|
+
|
|
93
|
+
for j in range(i + 1, num_fragments): # Iterate only over i < j
|
|
94
|
+
frag_j_positions = positions[fragments[j]]
|
|
95
|
+
centroid_j = np.mean(frag_j_positions, axis=0)
|
|
96
|
+
centroid_dist = np.linalg.norm(centroid_i - centroid_j)
|
|
97
|
+
centroid_distances[i, j] = centroid_distances[j, i] = centroid_dist
|
|
98
|
+
|
|
99
|
+
# Find closest pair of atoms and calculate corrected distance
|
|
100
|
+
all_distances = cdist(frag_i_positions, frag_j_positions)
|
|
101
|
+
min_dist = np.min(all_distances)
|
|
102
|
+
min_index_i, min_index_j = np.unravel_index(
|
|
103
|
+
np.argmin(all_distances), all_distances.shape
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
atom_i_number = atomic_numbers[fragments[i][min_index_i]]
|
|
107
|
+
atom_j_number = atomic_numbers[fragments[j][min_index_j]]
|
|
108
|
+
atom_i_symbol = atomic_symbols[fragments[i][min_index_i]]
|
|
109
|
+
atom_j_symbol = atomic_symbols[fragments[j][min_index_j]]
|
|
110
|
+
covrad_sum = float(
|
|
111
|
+
covalent_radii[atom_i_number] + covalent_radii[atom_j_number]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
corrected_distances.append(
|
|
115
|
+
(
|
|
116
|
+
float(min_dist),
|
|
117
|
+
atom_i_symbol,
|
|
118
|
+
atom_j_symbol,
|
|
119
|
+
covrad_sum,
|
|
120
|
+
float(min_dist - covrad_sum * covalent_scale),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
distance_matrix,
|
|
126
|
+
bond_matrix,
|
|
127
|
+
fragments,
|
|
128
|
+
centroid_distances,
|
|
129
|
+
corrected_distances,
|
|
130
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# This is a separate module because building ira_mod [1] needs a Makefile and a
|
|
2
|
+
# PYTHONPATH export.. after that's cleared up this will probably be a dependency
|
|
3
|
+
# [1]: https://github.com/mammasmias/IterativeRotationsAssignments
|
|
4
|
+
import dataclasses
|
|
5
|
+
|
|
6
|
+
import ira_mod
|
|
7
|
+
import numpy as np
|
|
8
|
+
from collections import Counter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class IRAComp:
|
|
13
|
+
rot: np.array
|
|
14
|
+
trans: np.array
|
|
15
|
+
perm: np.array
|
|
16
|
+
hd: float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IncomparableStructuresError(ValueError):
|
|
20
|
+
"""Custom exception raised for incompatible atomistic structures."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _perform_ira_match(atm1, atm2, k_factor=2.8):
|
|
26
|
+
"""Performs IRA matching. Internal function."""
|
|
27
|
+
if len(atm1) != len(atm2):
|
|
28
|
+
errmsg = "Atomistic structures must have the same number of atoms."
|
|
29
|
+
raise IncomparableStructuresError(errmsg)
|
|
30
|
+
if not Counter(atm1.symbols) == Counter(atm2.symbols):
|
|
31
|
+
errmsg = "Atomistic structures must have the same atom types."
|
|
32
|
+
raise IncomparableStructuresError(errmsg)
|
|
33
|
+
|
|
34
|
+
ira = ira_mod.IRA()
|
|
35
|
+
return ira.match(
|
|
36
|
+
len(atm1),
|
|
37
|
+
atm1.get_atomic_numbers(),
|
|
38
|
+
atm1.get_positions(),
|
|
39
|
+
len(atm2),
|
|
40
|
+
atm2.get_atomic_numbers(),
|
|
41
|
+
atm2.get_positions(),
|
|
42
|
+
k_factor,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_ira_pair(atm1, atm2, hd_tol=1, k_factor=2.8):
|
|
47
|
+
"""Checks if two atomistic structures are an IRA pair."""
|
|
48
|
+
try:
|
|
49
|
+
_, _, _, hd = _perform_ira_match(atm1, atm2, k_factor)
|
|
50
|
+
return hd < hd_tol
|
|
51
|
+
except IncomparableStructuresError:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def do_ira(atm1, atm2, k_factor=2.8):
|
|
56
|
+
"""Performs IRA matching on two atomistic structures."""
|
|
57
|
+
rotation, translation, perm, hd = _perform_ira_match(atm1, atm2, k_factor)
|
|
58
|
+
return IRAComp(rot=rotation, trans=translation, perm=perm, hd=hd)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def calculate_rmsd(atm1, atm2, k_factor=2.8):
|
|
62
|
+
"""Calculates RMSD using do_ira."""
|
|
63
|
+
ira_comp = do_ira(atm1, atm2, k_factor)
|
|
64
|
+
atm2_coords_permuted = atm2.get_positions()[ira_comp.perm]
|
|
65
|
+
atm2_coords_transformed = (
|
|
66
|
+
np.dot(atm2_coords_permuted, ira_comp.rot) + ira_comp.trans
|
|
67
|
+
)
|
|
68
|
+
n_atoms = len(atm1.get_positions())
|
|
69
|
+
squared_distances = np.sum(
|
|
70
|
+
(atm1.get_positions() - atm2_coords_transformed) ** 2, axis=1
|
|
71
|
+
)
|
|
72
|
+
rmsd = np.sqrt(np.sum(squared_distances) / n_atoms)
|
|
73
|
+
return rmsd
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
+
import datetime
|
|
4
5
|
from collections import namedtuple
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
5
9
|
|
|
6
10
|
# namedtuple for storing NEB iteration data
|
|
7
11
|
nebiter = namedtuple("nebiter", ["iteration", "nebpath"])
|
|
@@ -45,3 +49,42 @@ Notes
|
|
|
45
49
|
The `nebpath` namedtuple is used within the `nebiter` namedtuple to store
|
|
46
50
|
detailed path information for each NEB iteration.
|
|
47
51
|
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class DimerOpt:
|
|
56
|
+
saddle: str = "dimer"
|
|
57
|
+
rot: str = "lbfgs"
|
|
58
|
+
trans: str = "lbfgs"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class SpinID:
|
|
63
|
+
mol_id: int
|
|
64
|
+
spin: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class MolGeom:
|
|
69
|
+
pos: np.array
|
|
70
|
+
energy: float
|
|
71
|
+
forces: np.array
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class SaddleMeasure:
|
|
76
|
+
pes_calls: int = 0
|
|
77
|
+
iter_steps: int = 0
|
|
78
|
+
tot_time: datetime.timedelta = datetime.timedelta(0).total_seconds()
|
|
79
|
+
saddle_energy: float = np.nan
|
|
80
|
+
saddle_fmax: float = np.nan
|
|
81
|
+
success: bool = False
|
|
82
|
+
method: str = "not run"
|
|
83
|
+
dimer_rot: str = "n/a"
|
|
84
|
+
dimer_trans: str = "n/a"
|
|
85
|
+
init_energy: float = np.nan
|
|
86
|
+
barrier: float = np.nan
|
|
87
|
+
mol_id: int = np.nan
|
|
88
|
+
spin: str = "unknown"
|
|
89
|
+
scf: float = np.nan
|
|
90
|
+
termination_status: str = "not set"
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import ase
|
|
5
|
+
import ase.io as aseio
|
|
6
|
+
import h5py
|
|
7
|
+
import numpy as np
|
|
8
|
+
from ase.calculators.calculator import Calculator, all_changes
|
|
9
|
+
from ase.calculators.nwchem import NWChem
|
|
10
|
+
from ase.io.trajectory import Trajectory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HDF5CalculatorDimerMidpoint(Calculator):
|
|
14
|
+
implemented_properties: typing.ClassVar[list[str]] = ["energy", "forces"]
|
|
15
|
+
|
|
16
|
+
def __init__(self, from_hdf5, natms, **kwargs):
|
|
17
|
+
Calculator.__init__(self, **kwargs)
|
|
18
|
+
# Reference to the HDF5 group or dataset
|
|
19
|
+
self.from_hdf5 = from_hdf5
|
|
20
|
+
self.natoms = natms
|
|
21
|
+
|
|
22
|
+
def calculate(
|
|
23
|
+
self, atoms=None, properties=["energy", "forces"], system_changes=all_changes
|
|
24
|
+
):
|
|
25
|
+
Calculator.calculate(self, atoms, properties, system_changes)
|
|
26
|
+
|
|
27
|
+
# Access energy and gradients directly from the referenced HDF5 object
|
|
28
|
+
energy = self.from_hdf5["energy"][0]
|
|
29
|
+
forces = self.from_hdf5["gradients"][: self.natoms * 3].reshape(-1, 3) * -1
|
|
30
|
+
|
|
31
|
+
# Store results
|
|
32
|
+
self.results["energy"] = energy
|
|
33
|
+
self.results["forces"] = forces
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HDF5Calculator(Calculator):
|
|
37
|
+
implemented_properties: typing.ClassVar[list[str]] = ["energy", "forces"]
|
|
38
|
+
|
|
39
|
+
def __init__(self, from_hdf5, **kwargs):
|
|
40
|
+
Calculator.__init__(self, **kwargs)
|
|
41
|
+
# Reference to the HDF5 group or dataset
|
|
42
|
+
self.from_hdf5 = from_hdf5
|
|
43
|
+
|
|
44
|
+
def calculate(
|
|
45
|
+
self, atoms=None, properties=["energy", "forces"], system_changes=all_changes
|
|
46
|
+
):
|
|
47
|
+
Calculator.calculate(self, atoms, properties, system_changes)
|
|
48
|
+
|
|
49
|
+
# Access energy and gradients directly from the referenced HDF5 object
|
|
50
|
+
energy = self.from_hdf5["energy"][0]
|
|
51
|
+
forces = self.from_hdf5["gradients"][:].reshape(-1, 3) * -1
|
|
52
|
+
|
|
53
|
+
# Store results
|
|
54
|
+
self.results["energy"] = energy
|
|
55
|
+
self.results["forces"] = forces
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_atoms_from_hdf5(template_atoms: ase.Atoms, hdf5_group: h5py.Group) -> ase.Atoms:
|
|
59
|
+
"""
|
|
60
|
+
Creates an ASE Atoms object from a template and an HDF5 group containing optimization data.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
template_atoms (ase.Atoms): The template ASE Atoms object (initial structure).
|
|
64
|
+
hdf5_group (h5py.Group): The HDF5 group containing 'energy', 'gradients', and 'positions' datasets.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
ase.Atoms: An ASE Atoms object with positions, energy, and forces from
|
|
68
|
+
the HDF5 group.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
atoms = template_atoms.copy()
|
|
72
|
+
calculator = HDF5Calculator(from_hdf5=hdf5_group)
|
|
73
|
+
atoms.calc = calculator
|
|
74
|
+
atoms.set_positions(hdf5_group["positions"][()].reshape(-1, 3))
|
|
75
|
+
return atoms
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_geom_traj_from_hdf5(
|
|
79
|
+
hdf5_file: str,
|
|
80
|
+
output_traj_file: str,
|
|
81
|
+
initial_structure_file: str,
|
|
82
|
+
outer_loop_group_name: str = "outer_loop",
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Creates an ASE trajectory file from an HDF5 file containing optimization
|
|
86
|
+
data. This only outputs the geometry steps after the initial rotations.
|
|
87
|
+
|
|
88
|
+
Generally this is what you want to see for checking the change in geometry
|
|
89
|
+
along the run. There are initial rotations and 2 additional calls (one in
|
|
90
|
+
the beginning and one at the end) which are not accounted for here.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
hdf5_file (str): Path to the HDF5 file.
|
|
94
|
+
output_traj_file (str): Path to the output trajectory file (e.g.,
|
|
95
|
+
'gprd_run.traj').
|
|
96
|
+
initial_structure_file (str): Path to the file containing the initial structure (e.g., 'pos.con').
|
|
97
|
+
outer_loop_group_name (str, optional): Name of the group containing
|
|
98
|
+
outer loop data. Defaults to "outer_loop".
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
f = h5py.File(hdf5_file, "r")
|
|
103
|
+
except FileNotFoundError:
|
|
104
|
+
print(f"Error: HDF5 file '{hdf5_file}' not found.")
|
|
105
|
+
return
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"An error occurred while opening HDF5 file: {e}")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
outer_loop_keys = [
|
|
111
|
+
str(x) for x in np.sort([int(x) for x in f[outer_loop_group_name].keys()])
|
|
112
|
+
]
|
|
113
|
+
print(f"Available outer loop keys: {outer_loop_keys}")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
init = aseio.read(initial_structure_file)
|
|
117
|
+
except FileNotFoundError:
|
|
118
|
+
print(f"Error: Initial structure file '{initial_structure_file}' not found.")
|
|
119
|
+
f.close()
|
|
120
|
+
return
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"An error occurred while reading initial structure file: {e}")
|
|
123
|
+
f.close()
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
traj = Trajectory(output_traj_file, "w")
|
|
127
|
+
|
|
128
|
+
for key in outer_loop_keys:
|
|
129
|
+
try:
|
|
130
|
+
# Create atoms object directly here
|
|
131
|
+
atoms = init.copy()
|
|
132
|
+
calculator = HDF5Calculator(from_hdf5=f[outer_loop_group_name][key])
|
|
133
|
+
atoms.calc = calculator
|
|
134
|
+
atoms.set_positions(
|
|
135
|
+
f[outer_loop_group_name][key]["positions"][:].reshape(-1, 3)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Trigger calculation of energy and forces
|
|
139
|
+
atoms.get_potential_energy()
|
|
140
|
+
|
|
141
|
+
traj.write(atoms)
|
|
142
|
+
except KeyError as e:
|
|
143
|
+
print(f"Skipping key {key} due to missing data: {e}")
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print(f"An error occurred while processing key {key}: {e}")
|
|
146
|
+
|
|
147
|
+
f.close()
|
|
148
|
+
traj.close()
|
|
149
|
+
print(f"Trajectory file '{output_traj_file}' created successfully.")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def create_nwchem_trajectory(
|
|
153
|
+
template_atoms: ase.Atoms,
|
|
154
|
+
hdf5_file: str,
|
|
155
|
+
output_traj_file: str,
|
|
156
|
+
mult=1,
|
|
157
|
+
outer_loop_group_name: str = "outer_loop",
|
|
158
|
+
):
|
|
159
|
+
"""
|
|
160
|
+
Creates an ASE trajectory file with NWChem energy and forces calculated for positions
|
|
161
|
+
taken from an HDF5 file.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
template_atoms (ase.Atoms): The template ASE Atoms object (initial structure).
|
|
165
|
+
hdf5_file (str): Path to the HDF5 file containing positions.
|
|
166
|
+
output_traj_file (str): Path to the output trajectory file (e.g., 'nwchem_run.traj').
|
|
167
|
+
outer_loop_group_name (str, optional): Name of the group containing outer loop data. Defaults to "outer_loop".
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
f = h5py.File(hdf5_file, "r")
|
|
172
|
+
except FileNotFoundError:
|
|
173
|
+
print(f"Error: HDF5 file '{hdf5_file}' not found.")
|
|
174
|
+
return
|
|
175
|
+
except Exception as e:
|
|
176
|
+
print(f"An error occurred while opening HDF5 file: {e}")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
outer_loop_keys = [
|
|
180
|
+
str(x) for x in np.sort([int(x) for x in f[outer_loop_group_name].keys()])
|
|
181
|
+
]
|
|
182
|
+
print(f"Available outer loop keys: {outer_loop_keys}")
|
|
183
|
+
|
|
184
|
+
traj = Trajectory(output_traj_file, "w")
|
|
185
|
+
|
|
186
|
+
nwchem_path = os.environ["NWCHEM_COMMAND"]
|
|
187
|
+
memory = "2 gb"
|
|
188
|
+
nwchem_kwargs = {
|
|
189
|
+
"command": f"{nwchem_path} PREFIX.nwi > PREFIX.nwo",
|
|
190
|
+
"memory": memory,
|
|
191
|
+
"scf": {
|
|
192
|
+
"nopen": mult - 1,
|
|
193
|
+
"thresh": 1e-8,
|
|
194
|
+
"maxiter": 200,
|
|
195
|
+
},
|
|
196
|
+
"basis": "3-21G",
|
|
197
|
+
"task": "gradient",
|
|
198
|
+
}
|
|
199
|
+
if mult == 2:
|
|
200
|
+
nwchem_kwargs["scf"]["uhf"] = None # switch to unrestricted calculation
|
|
201
|
+
|
|
202
|
+
for key in outer_loop_keys:
|
|
203
|
+
try:
|
|
204
|
+
# Create a copy of the template atoms
|
|
205
|
+
atoms = template_atoms.copy()
|
|
206
|
+
|
|
207
|
+
# Set positions from the HDF5 file
|
|
208
|
+
atoms.positions = f[outer_loop_group_name][key]["positions"][()].reshape(
|
|
209
|
+
-1, 3
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Assign calculator and calculate energy and forces
|
|
213
|
+
atoms.calc = NWChem(**nwchem_kwargs)
|
|
214
|
+
print(f"Calculating for {key}")
|
|
215
|
+
atoms.get_potential_energy()
|
|
216
|
+
|
|
217
|
+
# Write to trajectory
|
|
218
|
+
traj.write(atoms)
|
|
219
|
+
|
|
220
|
+
except KeyError as e:
|
|
221
|
+
print(f"Skipping key {key} due to missing data: {e}")
|
|
222
|
+
|
|
223
|
+
f.close()
|
|
224
|
+
traj.close()
|
|
225
|
+
print(f"NWChem trajectory file '{output_traj_file}' created successfully.")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def create_full_traj_from_hdf5(
|
|
229
|
+
hdf5_file: str,
|
|
230
|
+
output_traj_file: str,
|
|
231
|
+
initial_structure_file: str,
|
|
232
|
+
outer_loop_group_name: str = "outer_loop",
|
|
233
|
+
inner_loop_group_name: str = "initial_rotations",
|
|
234
|
+
):
|
|
235
|
+
"""
|
|
236
|
+
Creates an ASE trajectory file from an HDF5 file containing optimization
|
|
237
|
+
data. Includes **estimated points** for the initial rotations and the
|
|
238
|
+
endpoints. These are correct (correspond to the actual counts) but is
|
|
239
|
+
slightly convoluted, since the HDF5 contains both the midpoint and the
|
|
240
|
+
"forward dimer". Instead, the length of the inner rotations keys is the
|
|
241
|
+
number of (0 energy) points added to the trajectory. This again makes
|
|
242
|
+
intuitive sense, since we have the Elvl cutoff in the GPRD as well.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
hdf5_file (str): Path to the HDF5 file.
|
|
246
|
+
output_traj_file (str): Path to the output trajectory file (e.g.,
|
|
247
|
+
'gprd_run.traj').
|
|
248
|
+
initial_structure_file (str): Path to the file containing the initial structure (e.g., 'pos.con').
|
|
249
|
+
outer_loop_group_name (str, optional): Name of the group containing
|
|
250
|
+
outer loop data. Defaults to "outer_loop".
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
f = h5py.File(hdf5_file, "r")
|
|
255
|
+
except FileNotFoundError:
|
|
256
|
+
print(f"Error: HDF5 file '{hdf5_file}' not found.")
|
|
257
|
+
return
|
|
258
|
+
except Exception as e:
|
|
259
|
+
print(f"An error occurred while opening HDF5 file: {e}")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
outer_loop_keys = [
|
|
263
|
+
str(x) for x in np.sort([int(x) for x in f[outer_loop_group_name].keys()])
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
inner_loop_keys = [
|
|
267
|
+
str(x) for x in np.sort([int(x) for x in f[inner_loop_group_name].keys()])
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
print(f"Available innner loop keys: {inner_loop_keys}")
|
|
271
|
+
print(f"Available outer loop keys: {outer_loop_keys}")
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
init = aseio.read(initial_structure_file)
|
|
275
|
+
except FileNotFoundError:
|
|
276
|
+
print(f"Error: Initial structure file '{initial_structure_file}' not found.")
|
|
277
|
+
f.close()
|
|
278
|
+
return
|
|
279
|
+
except Exception as e:
|
|
280
|
+
print(f"An error occurred while reading initial structure file: {e}")
|
|
281
|
+
f.close()
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
traj = Trajectory(output_traj_file, "w")
|
|
285
|
+
|
|
286
|
+
# Generate the initial rotation stuff here
|
|
287
|
+
# Basically the number of keys, + 1
|
|
288
|
+
for idx, key in enumerate(inner_loop_keys):
|
|
289
|
+
atoms = init.copy()
|
|
290
|
+
iloop_dat = f[inner_loop_group_name][key]
|
|
291
|
+
calculator = HDF5CalculatorDimerMidpoint(from_hdf5=iloop_dat, natms=len(atoms))
|
|
292
|
+
atoms.calc = calculator
|
|
293
|
+
atoms.set_positions(iloop_dat["positions"][: len(atoms) * 3].reshape(-1, 3))
|
|
294
|
+
|
|
295
|
+
# Trigger calculation of energy and forces
|
|
296
|
+
atoms.get_potential_energy()
|
|
297
|
+
traj.write(atoms)
|
|
298
|
+
|
|
299
|
+
# Do it again for the first step
|
|
300
|
+
if idx == 0:
|
|
301
|
+
traj.write(atoms)
|
|
302
|
+
|
|
303
|
+
for idx, key in enumerate(outer_loop_keys):
|
|
304
|
+
try:
|
|
305
|
+
# Create atoms object directly here
|
|
306
|
+
atoms = init.copy()
|
|
307
|
+
calculator = HDF5Calculator(from_hdf5=f[outer_loop_group_name][key])
|
|
308
|
+
atoms.calc = calculator
|
|
309
|
+
atoms.set_positions(
|
|
310
|
+
f[outer_loop_group_name][key]["positions"][:].reshape(-1, 3)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Trigger calculation of energy and forces
|
|
314
|
+
atoms.get_potential_energy()
|
|
315
|
+
|
|
316
|
+
traj.write(atoms)
|
|
317
|
+
# Now for the final calculation done to finish the run
|
|
318
|
+
if idx == len(outer_loop_keys) - 1:
|
|
319
|
+
# Just write it one more time, same thing
|
|
320
|
+
traj.write(atoms)
|
|
321
|
+
|
|
322
|
+
except KeyError as e:
|
|
323
|
+
print(f"Skipping key {key} due to missing data: {e}")
|
|
324
|
+
except Exception as e:
|
|
325
|
+
print(f"An error occurred while processing key {key}: {e}")
|
|
326
|
+
|
|
327
|
+
f.close()
|
|
328
|
+
traj.close()
|
|
329
|
+
print(f"Trajectory file '{output_traj_file}' created successfully.")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def min_e_result(eresp: Path) -> dict:
|
|
8
|
+
"""Reads and parses the results.dat file.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
eresp: Path to the EON results directory.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
A dictionary containing the parsed data from results.dat, or None if the file
|
|
15
|
+
does not exist or the termination reason is not 0.
|
|
16
|
+
"""
|
|
17
|
+
respth = eresp / "results.dat"
|
|
18
|
+
if not respth.exists():
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
rdat = respth.read_text()
|
|
22
|
+
termination_reason = re.search(r"(\w+) termination_reason", rdat).group(1)
|
|
23
|
+
if termination_reason != "GOOD":
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
min_energy = float(re.search(r"(-?\d+\.\d+) potential_energy", rdat).group(1))
|
|
27
|
+
return min_energy
|