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.
Files changed (32) hide show
  1. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/.gitignore +1 -0
  2. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/PKG-INFO +7 -6
  3. chemparseplot-0.0.3/_version.py +34 -0
  4. chemparseplot-0.0.3/chemparseplot/analyze/dist.py +130 -0
  5. chemparseplot-0.0.3/chemparseplot/analyze/use_ira.py +73 -0
  6. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/basetypes.py +43 -0
  7. chemparseplot-0.0.3/chemparseplot/parse/eon/gprd.py +329 -0
  8. chemparseplot-0.0.3/chemparseplot/parse/eon/minimization.py +27 -0
  9. chemparseplot-0.0.3/chemparseplot/parse/eon/saddle_search.py +341 -0
  10. chemparseplot-0.0.3/chemparseplot/parse/sella/saddle_search.py +234 -0
  11. chemparseplot-0.0.3/chemparseplot/plot/__init__.py +9 -0
  12. chemparseplot-0.0.3/chemparseplot/plot/_aids.py +9 -0
  13. chemparseplot-0.0.3/chemparseplot/plot/geomscan.py +37 -0
  14. chemparseplot-0.0.3/chemparseplot/plot/structs.py +89 -0
  15. chemparseplot-0.0.3/chemparseplot/util.py +103 -0
  16. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/pyproject.toml +13 -12
  17. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/tests/parse/orca/test_interp.py +7 -6
  18. chemparseplot-0.0.2/_version.py +0 -16
  19. chemparseplot-0.0.2/chemparseplot/plot/__init__.py +0 -3
  20. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/LICENSE +0 -0
  21. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/__init__.py +0 -0
  22. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/__init__.py +0 -0
  23. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/converter.py +0 -0
  24. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/orca/__init__.py +0 -0
  25. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/orca/geomscan.py +0 -0
  26. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/orca/neb/interp.py +0 -0
  27. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/parse/patterns.py +0 -0
  28. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/chemparseplot/units.py +0 -0
  29. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/readme.md +0 -0
  30. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/tests/parse/orca/test_geomscan.py +0 -0
  31. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/tests/parse/test_converter.py +0 -0
  32. {chemparseplot-0.0.2 → chemparseplot-0.0.3}/tests/parse/test_patterns.py +0 -0
@@ -1,3 +1,4 @@
1
+ apidocs/*
1
2
  ### Generated by gibo (https://github.com/simonwhitaker/gibo)
2
3
  ### https://raw.github.com/github/gitignore/4488915eec0b3a45b5c63ead28f286819c0917de/Python.gitignore
3
4
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: chemparseplot
3
- Version: 0.0.2
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.0.0; extra == 'doc'
26
- Requires-Dist: myst-parser>=2.0.0; extra == 'doc'
27
- Requires-Dist: sphinx-autodoc2>=0.5.0; extra == 'doc'
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.0; extra == 'doc'
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