TCMU 0.17.0__py3-none-any.whl
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.
- tcmu/__init__.py +85 -0
- tcmu/analysis/__init__.py +0 -0
- tcmu/analysis/pyfrag.py +175 -0
- tcmu/analysis/task_specific/__init__.py +0 -0
- tcmu/analysis/task_specific/irc.py +96 -0
- tcmu/analysis/vdd/__init__.py +0 -0
- tcmu/analysis/vdd/charge.py +12 -0
- tcmu/analysis/vdd/manager.py +221 -0
- tcmu/analysis/vibration/__init__.py +0 -0
- tcmu/analysis/vibration/ts_vibration.py +149 -0
- tcmu/cache.py +188 -0
- tcmu/cite.py +326 -0
- tcmu/cli_scripts/__init__.py +0 -0
- tcmu/cli_scripts/cite.py +308 -0
- tcmu/cli_scripts/concatenate_irc.py +44 -0
- tcmu/cli_scripts/geo.py +101 -0
- tcmu/cli_scripts/job_script.py +57 -0
- tcmu/cli_scripts/read.py +34 -0
- tcmu/cli_scripts/resize_figures.py +58 -0
- tcmu/cli_scripts/tcparser.py +23 -0
- tcmu/cli_scripts/workflow.py +207 -0
- tcmu/connect.py +492 -0
- tcmu/constants.py +6 -0
- tcmu/data/__init__.py +0 -0
- tcmu/data/_atom_data_info/__init__.py +0 -0
- tcmu/data/_convert_to_json.py +8 -0
- tcmu/data/_read_BS_size.py +46 -0
- tcmu/data/atom.py +124 -0
- tcmu/data/basis_sets.py +85 -0
- tcmu/data/cosmo.py +99 -0
- tcmu/data/functionals.py +269 -0
- tcmu/data/molecules.py +27 -0
- tcmu/environment.py +28 -0
- tcmu/errors.py +79 -0
- tcmu/formula.py +111 -0
- tcmu/geometry.py +699 -0
- tcmu/job/__init__.py +0 -0
- tcmu/job/adf.py +1071 -0
- tcmu/job/ams.py +359 -0
- tcmu/job/crest.py +392 -0
- tcmu/job/dftb.py +82 -0
- tcmu/job/generic.py +502 -0
- tcmu/job/models.py +156 -0
- tcmu/job/nmr.py +95 -0
- tcmu/job/orca.py +289 -0
- tcmu/job/postscripts/__init__.py +0 -0
- tcmu/job/postscripts/clean_workdir.py +19 -0
- tcmu/job/postscripts/split_crest_xyz.py +15 -0
- tcmu/job/postscripts/write_converged_geoms.py +34 -0
- tcmu/job/workflow.py +481 -0
- tcmu/job/workflow3.py +441 -0
- tcmu/job/workflow_db.py +179 -0
- tcmu/job/workflow_status.py +23 -0
- tcmu/job/xtb.py +159 -0
- tcmu/log.py +527 -0
- tcmu/molecule.py +404 -0
- tcmu/pathfunc.py +214 -0
- tcmu/report/__init__.py +0 -0
- tcmu/report/_generate_font_widths.py +45 -0
- tcmu/report/character.py +97 -0
- tcmu/report/figure_resizer.py +174 -0
- tcmu/report/formatters/__init__.py +0 -0
- tcmu/report/formatters/generic.py +9 -0
- tcmu/report/formatters/xyz.py +124 -0
- tcmu/report/report.py +667 -0
- tcmu/results/__init__.py +0 -0
- tcmu/results/adf.py +453 -0
- tcmu/results/ams.py +690 -0
- tcmu/results/cache.py +79 -0
- tcmu/results/crest.py +330 -0
- tcmu/results/dftb.py +90 -0
- tcmu/results/orca.py +559 -0
- tcmu/results/read.py +186 -0
- tcmu/results/result.py +219 -0
- tcmu/results/xtb.py +441 -0
- tcmu/slurm.py +162 -0
- tcmu/spell_check.py +176 -0
- tcmu/timer.py +187 -0
- tcmu/typing_utilities.py +25 -0
- tcmu-0.17.0.dist-info/METADATA +111 -0
- tcmu-0.17.0.dist-info/RECORD +85 -0
- tcmu-0.17.0.dist-info/WHEEL +5 -0
- tcmu-0.17.0.dist-info/entry_points.txt +2 -0
- tcmu-0.17.0.dist-info/licenses/LICENSE +21 -0
- tcmu-0.17.0.dist-info/top_level.txt +1 -0
tcmu/__init__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Job imports
|
|
2
|
+
from tcmu.cache import cache, cache_file, timed_cache
|
|
3
|
+
from tcmu import log
|
|
4
|
+
from tcmu.analysis.pyfrag import PyFragResult, get_pyfrag_results
|
|
5
|
+
from tcmu.analysis.task_specific.irc import concatenate_irc_trajectories
|
|
6
|
+
from tcmu.analysis.vdd.charge import VDDCharge
|
|
7
|
+
|
|
8
|
+
# from tcmu.analysis.vdd.manager import VDDChargeManager, create_vdd_charge_manager # Don't load in vdd as it has an annoying pandas dependency that cannot be avoided upon importing
|
|
9
|
+
from tcmu.analysis.vibration.ts_vibration import avg_relative_bond_length_delta, determine_ts_reactioncoordinate, validate_transitionstate
|
|
10
|
+
from tcmu.cite import cite, _get_doi_data, _get_doi_data_from_title, _get_doi_data_from_query, _get_publisher_city, _get_journal_abbreviation
|
|
11
|
+
from tcmu.connect import Connection, Local, Server, ServerFile
|
|
12
|
+
from tcmu.data.functionals import categories, functional_name_from_path_safe_name, functionals, get_available_functionals, get_functional
|
|
13
|
+
from tcmu.environment import requires_optional_package
|
|
14
|
+
from tcmu.geometry import KabschTransform, MolTransform, Transform, apply_rotmat, get_rotmat, rotate, rotmat_to_angles, vector_align_rotmat
|
|
15
|
+
from tcmu.results.read import get_info, quick_status, read, get_timing
|
|
16
|
+
from tcmu.results.result import Result
|
|
17
|
+
from tcmu.job.workflow import WorkFlow
|
|
18
|
+
from tcmu.job import workflow_db, workflow_status
|
|
19
|
+
from tcmu.job.adf import ADFFragmentJob, ADFJob, DensfJob
|
|
20
|
+
from tcmu.job.ams import AMSJob
|
|
21
|
+
from tcmu.job.crest import CRESTJob, QCGJob
|
|
22
|
+
from tcmu.job.dftb import DFTBJob
|
|
23
|
+
from tcmu.job.nmr import NMRJob
|
|
24
|
+
from tcmu.job.orca import ORCAJob, GOATJob
|
|
25
|
+
from tcmu.job.xtb import XTBJob
|
|
26
|
+
from tcmu.molecule import from_string, guess_fragments, load, number_of_electrons, save, write_mol_to_amv_file, write_mol_to_xyz_file
|
|
27
|
+
# from tcmu.report.report import SI
|
|
28
|
+
from tcmu.timer import timer
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ADFFragmentJob",
|
|
32
|
+
"ADFJob",
|
|
33
|
+
"DensfJob",
|
|
34
|
+
"AMSJob",
|
|
35
|
+
"CRESTJob",
|
|
36
|
+
"QCGJob",
|
|
37
|
+
"DFTBJob",
|
|
38
|
+
"NMRJob",
|
|
39
|
+
"ORCAJob",
|
|
40
|
+
"GOATJob",
|
|
41
|
+
"XTBJob",
|
|
42
|
+
"log",
|
|
43
|
+
"from_string",
|
|
44
|
+
"guess_fragments",
|
|
45
|
+
"load",
|
|
46
|
+
"number_of_electrons",
|
|
47
|
+
"save",
|
|
48
|
+
"write_mol_to_amv_file",
|
|
49
|
+
"write_mol_to_xyz_file",
|
|
50
|
+
"get_info",
|
|
51
|
+
"quick_status",
|
|
52
|
+
"read",
|
|
53
|
+
"Result",
|
|
54
|
+
"timer",
|
|
55
|
+
"Connection",
|
|
56
|
+
"Local",
|
|
57
|
+
"Server",
|
|
58
|
+
"ServerFile",
|
|
59
|
+
"cache",
|
|
60
|
+
"cache_file",
|
|
61
|
+
"cite",
|
|
62
|
+
"requires_optional_package",
|
|
63
|
+
"Transform",
|
|
64
|
+
"KabschTransform",
|
|
65
|
+
"MolTransform",
|
|
66
|
+
"get_rotmat",
|
|
67
|
+
"rotmat_to_angles",
|
|
68
|
+
"apply_rotmat",
|
|
69
|
+
"rotate",
|
|
70
|
+
"vector_align_rotmat",
|
|
71
|
+
"PyFragResult",
|
|
72
|
+
"get_pyfrag_results",
|
|
73
|
+
"concatenate_irc_trajectories",
|
|
74
|
+
"VDDCharge",
|
|
75
|
+
"avg_relative_bond_length_delta",
|
|
76
|
+
"determine_ts_reactioncoordinate",
|
|
77
|
+
"validate_transitionstate",
|
|
78
|
+
"categories",
|
|
79
|
+
"functionals",
|
|
80
|
+
"functional_name_from_path_safe_name",
|
|
81
|
+
"get_functional",
|
|
82
|
+
"get_available_functionals",
|
|
83
|
+
# "SI",
|
|
84
|
+
"timed_cache",
|
|
85
|
+
]
|
|
File without changes
|
tcmu/analysis/pyfrag.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scm import plams
|
|
6
|
+
|
|
7
|
+
import tcmu
|
|
8
|
+
from tcmu.cache import cache
|
|
9
|
+
from tcmu.environment import requires_optional_package
|
|
10
|
+
from tcmu.geometry import parameter
|
|
11
|
+
from tcmu.pathfunc import match
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_pyfrag_results(path):
|
|
15
|
+
return PyFragResult(path)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PyFragResult:
|
|
19
|
+
def __init__(self, path, step_prefix: str = "Step."):
|
|
20
|
+
self.path = path
|
|
21
|
+
|
|
22
|
+
self._frag_results = {}
|
|
23
|
+
self._step_results = []
|
|
24
|
+
self._order = []
|
|
25
|
+
self._mask = []
|
|
26
|
+
self._properties = {}
|
|
27
|
+
|
|
28
|
+
self.step_prefix = step_prefix
|
|
29
|
+
|
|
30
|
+
self._load()
|
|
31
|
+
|
|
32
|
+
def _load(self):
|
|
33
|
+
for frag_path, info in tcmu.log.loadbar(match(self.path, "frag_{frag}").items()):
|
|
34
|
+
self._frag_results[info.frag] = tcmu.read(frag_path)
|
|
35
|
+
|
|
36
|
+
for step_path, info in tcmu.log.loadbar(match(self.path, self.step_prefix + "{step}", sort_by="step").items()):
|
|
37
|
+
self._step_results.append({})
|
|
38
|
+
for dir_name in os.listdir(step_path):
|
|
39
|
+
p = os.path.join(step_path, dir_name)
|
|
40
|
+
res = tcmu.read(p)
|
|
41
|
+
self._step_results[-1][dir_name] = res
|
|
42
|
+
|
|
43
|
+
self._order.append(int(info.step.removeprefix("0")))
|
|
44
|
+
self._mask.append(True)
|
|
45
|
+
|
|
46
|
+
self._mask = np.array(self._mask)
|
|
47
|
+
|
|
48
|
+
def get_property(self, key: str, calc: str = "complex"):
|
|
49
|
+
if key in self._properties:
|
|
50
|
+
return self._properties[key][self._order][self._mask[self._order]]
|
|
51
|
+
p = np.array([res[calc].properties.get_multi_key(key) for res in self._step_results])
|
|
52
|
+
return p[self._order][self._mask[self._order]]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def fragments(self):
|
|
56
|
+
return list(self._frag_results.keys())
|
|
57
|
+
|
|
58
|
+
def __len__(self):
|
|
59
|
+
return sum(self._mask)
|
|
60
|
+
|
|
61
|
+
def get_geometry(self, *args, **kwargs):
|
|
62
|
+
calc = kwargs.pop("calc", "complex")
|
|
63
|
+
g = np.array([parameter(res[calc].molecule.input, *args, **kwargs) for res in self._step_results])
|
|
64
|
+
return g[self._order][self._mask[self._order]]
|
|
65
|
+
|
|
66
|
+
def get_molecules(self, calc: str = "complex") -> List[plams.Molecule]:
|
|
67
|
+
mols = [res[calc].molecule.input for res in self._step_results]
|
|
68
|
+
return mols
|
|
69
|
+
return [mols[int(i)] for i in np.array(self._order)[self._mask[self._order]]]
|
|
70
|
+
|
|
71
|
+
def sort_by(self, val: Union[str, List], calc: str = "complex"):
|
|
72
|
+
if isinstance(val, str):
|
|
73
|
+
val = self.get_property(val, calc=calc)
|
|
74
|
+
self._order = np.argsort(val)
|
|
75
|
+
|
|
76
|
+
def set_mask(self, mask: list):
|
|
77
|
+
self._mask = mask
|
|
78
|
+
|
|
79
|
+
def set_coord(self, coord: list):
|
|
80
|
+
self._coord = coord
|
|
81
|
+
|
|
82
|
+
def coord(self):
|
|
83
|
+
return self._coord[self._order][self._mask[self._order]]
|
|
84
|
+
|
|
85
|
+
def set_property(self, name: str, vals):
|
|
86
|
+
self._properties[name] = vals
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def ts_idx(self):
|
|
90
|
+
energies = self.total_energy()
|
|
91
|
+
for i in range(len(self) - 2, 1, -1):
|
|
92
|
+
if energies[i - 1] < energies[i] and energies[i] < energies[i + 1]:
|
|
93
|
+
return i + 1
|
|
94
|
+
|
|
95
|
+
def total_energy(self):
|
|
96
|
+
return self.interaction_energy() + self.strain_energy()
|
|
97
|
+
|
|
98
|
+
def interaction_energy(self):
|
|
99
|
+
return self.get_property("energy.bond", "complex")
|
|
100
|
+
|
|
101
|
+
def strain_energy(self, fragment: str = ""):
|
|
102
|
+
if fragment is None:
|
|
103
|
+
t = 0
|
|
104
|
+
for frag in self.fragments:
|
|
105
|
+
t += self.strain_energy(frag)
|
|
106
|
+
return t
|
|
107
|
+
|
|
108
|
+
return self.get_property("energy.bond", f"frag_{fragment}") - self._frag_results[fragment].properties.energy.bond
|
|
109
|
+
|
|
110
|
+
@requires_optional_package("pyfmo")
|
|
111
|
+
def overlap(self, orb1, orb2, calc: str = "complex", absolute: bool = True):
|
|
112
|
+
S = np.array([orb.sfos[orb1] @ orb.sfos[orb2] for orb in self.orbs(calc)])
|
|
113
|
+
if absolute:
|
|
114
|
+
S = abs(S)
|
|
115
|
+
return S[self._order][self._mask[self._order]]
|
|
116
|
+
|
|
117
|
+
@requires_optional_package("pyfmo")
|
|
118
|
+
def orbital_energy_gap(self, orb1, orb2, calc: str = "complex"):
|
|
119
|
+
dE = np.array([abs(orb.sfos[orb1].energy - orb.sfos[orb2].energy) for orb in self.orbs(calc)])
|
|
120
|
+
return dE[self._order][self._mask[self._order]]
|
|
121
|
+
|
|
122
|
+
@requires_optional_package("pyfmo")
|
|
123
|
+
def sfo_coefficient(self, sfo, mo, calc: str = "complex"):
|
|
124
|
+
C = np.array([orb.sfos[sfo].coefficient(orb.mos[mo]) for orb in self.orbs(calc)])
|
|
125
|
+
return C[self._order][self._mask[self._order]]
|
|
126
|
+
|
|
127
|
+
@cache
|
|
128
|
+
@requires_optional_package("pyfmo")
|
|
129
|
+
def orbs(self, calc: str = "complex"):
|
|
130
|
+
import pyfmo
|
|
131
|
+
|
|
132
|
+
return [pyfmo.Orbitals(res[calc].files["adf.rkf"]) for res in self._step_results]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
import matplotlib.pyplot as plt
|
|
137
|
+
|
|
138
|
+
res_C = get_pyfrag_results("/Users/yumanhordijk/PhD/Projects/RadicalAdditionASMEDA/data/DFT/TS_C_O/PyFrag_OLYP_TZ2P")
|
|
139
|
+
res_X = get_pyfrag_results("/Users/yumanhordijk/PhD/Projects/RadicalAdditionASMEDA/data/DFT/TS_X_O/PyFrag_OLYP_TZ2P")
|
|
140
|
+
|
|
141
|
+
res_C.set_coord(res_C.get_geometry(0, 1))
|
|
142
|
+
res_X.set_coord(res_X.get_geometry(1, 2))
|
|
143
|
+
|
|
144
|
+
res_C.sort_by(res_C.coord())
|
|
145
|
+
res_X.sort_by(res_X.coord())
|
|
146
|
+
|
|
147
|
+
plt.figure()
|
|
148
|
+
plt.plot(res_C.coord(), res_C.overlap("Methyl(SOMO)_A", "Substrate(LUMO)_A"), label="C-addition")
|
|
149
|
+
plt.plot(res_X.coord(), res_X.overlap("Methyl(SOMO)_A", "Substrate(LUMO)_A"), label="X-addition")
|
|
150
|
+
plt.ylabel(r"$\langle SOMO | LUMO \rangle$")
|
|
151
|
+
plt.legend()
|
|
152
|
+
plt.xlim(3, 2.2)
|
|
153
|
+
|
|
154
|
+
plt.figure()
|
|
155
|
+
plt.plot(res_C.coord(), res_C.orbital_energy_gap("Methyl(SOMO)_A", "Substrate(LUMO)_A"), label="C-addition")
|
|
156
|
+
plt.plot(res_X.coord(), res_X.orbital_energy_gap("Methyl(SOMO)_A", "Substrate(LUMO)_A"), label="X-addition")
|
|
157
|
+
plt.ylabel(r"$|\epsilon_{SOMO} - \epsilon_{LUMO}|$")
|
|
158
|
+
plt.legend()
|
|
159
|
+
plt.xlim(3, 2.2)
|
|
160
|
+
|
|
161
|
+
plt.figure()
|
|
162
|
+
plt.plot(res_C.coord(), res_C.overlap("Methyl(LUMO)_B", "Substrate(7A_B)_B"), label="C-addition")
|
|
163
|
+
plt.plot(res_X.coord(), res_X.overlap("Methyl(LUMO)_B", "Substrate(7A_B)_B"), label="X-addition")
|
|
164
|
+
plt.ylabel(r"$\langle SUMO | HOMO \rangle$")
|
|
165
|
+
plt.legend()
|
|
166
|
+
plt.xlim(3, 2.2)
|
|
167
|
+
|
|
168
|
+
plt.figure()
|
|
169
|
+
plt.plot(res_C.coord(), res_C.orbital_energy_gap("Methyl(LUMO)_B", "Substrate(7A_B)_B"), label="C-addition")
|
|
170
|
+
plt.plot(res_X.coord(), res_X.orbital_energy_gap("Methyl(LUMO)_B", "Substrate(7A_B)_B"), label="X-addition")
|
|
171
|
+
plt.ylabel(r"$|\epsilon_{SUMO} - \epsilon_{HOMO}|$")
|
|
172
|
+
plt.legend()
|
|
173
|
+
plt.xlim(3, 2.2)
|
|
174
|
+
|
|
175
|
+
plt.show()
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import pathlib as pl
|
|
2
|
+
from typing import List, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scm.plams import Molecule
|
|
6
|
+
|
|
7
|
+
from tcmu.log import log
|
|
8
|
+
from tcmu.results.read import read
|
|
9
|
+
from tcmu.results.result import Result
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_result_objects(job_dirs: Union[List[str], List[pl.Path]]) -> List[Result]:
|
|
13
|
+
"""Creates a list of Result objects from a list of directories."""
|
|
14
|
+
return [read(pl.Path(file)) for file in job_dirs]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_converged_energies(res: Result) -> List[float]:
|
|
18
|
+
"""Returns a list of energies of the converged geometries."""
|
|
19
|
+
return [energy for converged, energy in zip(res.history.converged, res.history.energy) if converged] # type: ignore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_converged_molecules(res: Result) -> List[Molecule]:
|
|
23
|
+
"""Returns a list of molecules of the converged geometries."""
|
|
24
|
+
return [mol for converged, mol in zip(res.history.converged, res.history.molecule) if converged] # type: ignore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _concatenate_irc_trajectories_by_rmsd(irc_trajectories: List[List[Molecule]], energies: Union[List[List[float]], None]) -> Tuple[List[Molecule], List[float]]:
|
|
28
|
+
"""
|
|
29
|
+
Concatenates lists of molecules by comparing the RMSD values of the end and beginnings of the trajectory.
|
|
30
|
+
The entries that are closest to each other are used to concatenate the trajectories.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
irc_trajectories: A list of lists of Molecule objects representing the trajectories.
|
|
34
|
+
energies: A list of lists of float values representing the energies.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A tuple containing a list of Molecule objects and a list of energies.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If the RMSD values are not as expected.
|
|
41
|
+
"""
|
|
42
|
+
concatenated_mols: List[Molecule] = irc_trajectories[0][::-1]
|
|
43
|
+
concatenated_energies: List[float] = energies[0][::-1] if energies is not None else []
|
|
44
|
+
|
|
45
|
+
for traj_index in range(len(irc_trajectories) - 1):
|
|
46
|
+
# Calculate RMSD values of two connected trajectories to compare the connection points / molecules
|
|
47
|
+
rmsd_matrix = np.array([[Molecule.rmsd(irc_trajectories[traj_index][i], irc_trajectories[traj_index + 1][j]) for j in [0, -1]] for i in [0, -1]])
|
|
48
|
+
|
|
49
|
+
# Flatten the matrix and find the index of the minimum value
|
|
50
|
+
lowest_index = np.argmin(rmsd_matrix.flatten())
|
|
51
|
+
|
|
52
|
+
log(f"Lowest RMSD values: {rmsd_matrix.flatten()}", 10)
|
|
53
|
+
|
|
54
|
+
# Starting points are connected
|
|
55
|
+
if lowest_index == 0:
|
|
56
|
+
concatenated_mols += irc_trajectories[traj_index + 1][1:]
|
|
57
|
+
concatenated_energies += energies[traj_index + 1][1:] if energies is not None else []
|
|
58
|
+
# Ending points are connected
|
|
59
|
+
elif lowest_index == 1:
|
|
60
|
+
concatenated_mols += irc_trajectories[traj_index + 1][::-1]
|
|
61
|
+
concatenated_energies += energies[traj_index + 1][::-1] if energies is not None else []
|
|
62
|
+
# Something went wrong
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(f"The RMSD values are not as expected: {rmsd_matrix.flatten()} with {lowest_index=}.")
|
|
65
|
+
|
|
66
|
+
return concatenated_mols, concatenated_energies
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def concatenate_irc_trajectories(result_objects: List[Result], reverse: bool = False) -> Tuple[List[Molecule], List[float]]:
|
|
70
|
+
"""
|
|
71
|
+
Concatenates trajectories from irc calculations, often being forward and backward, through the RMSD values.
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
job_dirs: A list of directories containing the ams.rkf files.
|
|
75
|
+
user_log_level: The log level set by the user.
|
|
76
|
+
reverse: A boolean indicating whether to reverse the trajectory. Default is False.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A tuple containing a list of Molecule objects and a list of energies.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
Exception: If an exception is raised in the try block, it is caught and printed.
|
|
83
|
+
"""
|
|
84
|
+
traj_geometries: List[List[Molecule]] = [[] for _ in result_objects]
|
|
85
|
+
traj_energies: List[List[float]] = [[] for _ in result_objects]
|
|
86
|
+
|
|
87
|
+
for i, res_obj in enumerate(result_objects):
|
|
88
|
+
traj_geometries[i] = _get_converged_molecules(res_obj)
|
|
89
|
+
traj_energies[i] = _get_converged_energies(res_obj)
|
|
90
|
+
|
|
91
|
+
concatenated_mols, concatenated_energies = _concatenate_irc_trajectories_by_rmsd(traj_geometries, traj_energies)
|
|
92
|
+
|
|
93
|
+
if reverse:
|
|
94
|
+
concatenated_mols = concatenated_mols[::-1]
|
|
95
|
+
concatenated_energies = concatenated_energies[::-1]
|
|
96
|
+
return concatenated_mols, concatenated_energies
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class VDDCharge:
|
|
6
|
+
atom_index: int # index of the atom in the molecules
|
|
7
|
+
atom_symbol: str # symbol of the atom
|
|
8
|
+
charge: float # mili-electrons
|
|
9
|
+
frag_index: int # index of the fragment in the fragments list
|
|
10
|
+
|
|
11
|
+
def change_unit(self, ratio: float):
|
|
12
|
+
self.charge *= ratio
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.util import find_spec
|
|
4
|
+
|
|
5
|
+
from tcmu import environment, errors
|
|
6
|
+
|
|
7
|
+
if find_spec("pandas") is None:
|
|
8
|
+
raise errors.TCMUMissingOptionalPackageError("pandas")
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import pathlib as pl
|
|
12
|
+
from itertools import zip_longest
|
|
13
|
+
from typing import Dict, List, Literal, Optional, Sequence, Set, Union
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
from tcmu.analysis.vdd import charge
|
|
19
|
+
from tcmu.constants import VDD_UNITS
|
|
20
|
+
from tcmu.results import result
|
|
21
|
+
|
|
22
|
+
PRINT_FORMAT = {"me": "%+.0f", "e": "%+.3f"}
|
|
23
|
+
|
|
24
|
+
__all__ = ["VDDChargeManager", "create_vdd_charge_manager"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_vdd_charge_manager(results: result.Result) -> VDDChargeManager:
|
|
28
|
+
"""Create a VDDChargeManager from a Result object."""
|
|
29
|
+
vdd_charges: Dict[str, List[charge.VDDCharge]] = {}
|
|
30
|
+
atom_symbols = results.molecule.atom_symbols # type: ignore
|
|
31
|
+
frag_indices = results.molecule.frag_indices # type: ignore
|
|
32
|
+
calc_dir = pl.Path(results.files["root"]) # type: ignore
|
|
33
|
+
is_fragment_calculation = results.adf.used_regions # type: ignore
|
|
34
|
+
mol_charge = results.molecule.mol_charge # type: ignore
|
|
35
|
+
|
|
36
|
+
# Convert the VDD charges to VDDCharge objects
|
|
37
|
+
for irrep, charge_array in results.properties.vdd.items(): # type: ignore
|
|
38
|
+
irrep = "vdd" if irrep == "charges" else irrep
|
|
39
|
+
vdd_charges[irrep] = []
|
|
40
|
+
for atom_index, (frag_index, vdd_charge) in enumerate(zip(frag_indices, charge_array)):
|
|
41
|
+
atom_symbol = atom_symbols[atom_index]
|
|
42
|
+
vdd_charges[irrep].append(charge.VDDCharge(atom_index + 1, atom_symbol, vdd_charge, frag_index))
|
|
43
|
+
|
|
44
|
+
return VDDChargeManager(vdd_charges, is_fragment_calculation, calc_dir, mol_charge)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class VDDChargeManager:
|
|
48
|
+
"""Class to manage the VDD charges. It can be used to print the VDD charges in a nice table and write them to a text file or excel file."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, vdd_charges: Dict[str, List[charge.VDDCharge]], is_fragment_calculation: bool, calc_dir: pl.Path, mol_charge: int):
|
|
51
|
+
self.vdd_charges = vdd_charges
|
|
52
|
+
self.is_fragment_calculation = is_fragment_calculation
|
|
53
|
+
self.calc_dir = calc_dir
|
|
54
|
+
self.mol_charge = mol_charge
|
|
55
|
+
self.name = self.calc_dir.name if self.calc_dir is not None else ""
|
|
56
|
+
self.unit = "e" # unit of the VDD charges. Available units are "me" (mili-electrons) and "e" (electrons)
|
|
57
|
+
self.irreps: Set[str] = set(self.vdd_charges.keys())
|
|
58
|
+
self.change_unit("me")
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
"""Prints the VDD charges in a nice table. Checks if the calculation is a fragment calculation and prints the summed VDD charges if it is."""
|
|
62
|
+
individual_charges_table = self.get_vdd_charges_table()
|
|
63
|
+
summed_charges_table = self.get_summed_vdd_charges_table()
|
|
64
|
+
|
|
65
|
+
ret_str = f"{self.name}\nVDD charges (in unit {self.unit}):\n{individual_charges_table}\n\n"
|
|
66
|
+
if self.is_fragment_calculation:
|
|
67
|
+
ret_str += f"Summed VDD charges (in unit {self.unit}):\n{summed_charges_table}\n"
|
|
68
|
+
|
|
69
|
+
return ret_str
|
|
70
|
+
|
|
71
|
+
def charge_is_conserved(self) -> bool:
|
|
72
|
+
"""Check if the total charge of the molecule is conserved. The total charge is the sum of the VDD charges."""
|
|
73
|
+
tolerance = 1e-4 if self.unit == "e" else 1e-1
|
|
74
|
+
is_conserved = np.isclose(self.mol_charge, sum([charge.charge for charge in self.vdd_charges["vdd"]]), atol=tolerance)
|
|
75
|
+
return is_conserved # type: ignore since numpy _bool is not recognized as bool
|
|
76
|
+
|
|
77
|
+
def change_unit(self, new_unit: str) -> None:
|
|
78
|
+
"""Change the unit of the VDD charges. Available units are "me" (mili-electrons) and "e" (electrons)."""
|
|
79
|
+
if new_unit not in VDD_UNITS:
|
|
80
|
+
raise ValueError(f"Unit {new_unit} is not available. Choose from {VDD_UNITS.keys()}")
|
|
81
|
+
|
|
82
|
+
if new_unit == self.unit:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
ratio = VDD_UNITS[new_unit] / VDD_UNITS[self.unit]
|
|
86
|
+
[charge.change_unit(ratio) for charges in self.vdd_charges.values() for charge in charges]
|
|
87
|
+
self.unit = new_unit
|
|
88
|
+
|
|
89
|
+
def get_vdd_charges(self, unit: Literal["e", "me"] = "me") -> Dict[str, List[charge.VDDCharge]]:
|
|
90
|
+
"""Get the VDD charges in the specified unit ([me] or [e])."""
|
|
91
|
+
self.change_unit(unit)
|
|
92
|
+
return copy.deepcopy(self.vdd_charges)
|
|
93
|
+
|
|
94
|
+
def get_summed_vdd_charges(self, irreps: Optional[Sequence[str]] = None, unit: Literal["e", "me"] = "me") -> Dict[str, Dict[str, float]]:
|
|
95
|
+
"""Get the summed VDD charges per fragment for the specified unit ([me] or [e])."""
|
|
96
|
+
self.change_unit(unit)
|
|
97
|
+
irreps = irreps if irreps is not None else list(self.irreps)
|
|
98
|
+
summed_vdd_charges: Dict[str, Dict[str, float]] = {}
|
|
99
|
+
|
|
100
|
+
for irrep in irreps:
|
|
101
|
+
summed_vdd_charges[irrep] = {}
|
|
102
|
+
for vdd_charge in self.vdd_charges[irrep]:
|
|
103
|
+
frag_index = str(vdd_charge.frag_index)
|
|
104
|
+
summed_vdd_charges[irrep].setdefault(frag_index, 0.0)
|
|
105
|
+
summed_vdd_charges[irrep][frag_index] += vdd_charge.charge
|
|
106
|
+
|
|
107
|
+
return copy.deepcopy(summed_vdd_charges)
|
|
108
|
+
|
|
109
|
+
def get_vdd_charges_dataframe(self) -> pd.DataFrame:
|
|
110
|
+
"""Get the VDD charges as a pandas DataFrame in a specified unit ([me] or [e])."""
|
|
111
|
+
|
|
112
|
+
frag_indices = [charge.frag_index for charge in self.vdd_charges["vdd"]]
|
|
113
|
+
atom_symbols = [f"{charge.atom_index}{charge.atom_symbol}" for charge in self.vdd_charges["vdd"]]
|
|
114
|
+
charges = [[charge.charge for charge in charges] for _, charges in self.vdd_charges.items()]
|
|
115
|
+
headers = ["Frag", "Atom"] + [irrep for irrep, _ in self.vdd_charges.items()]
|
|
116
|
+
combined_table = list(zip_longest(frag_indices, atom_symbols, *charges, fillvalue=""))
|
|
117
|
+
df = pd.DataFrame(combined_table, columns=headers).rename(columns={"vdd": "Total"})
|
|
118
|
+
return df
|
|
119
|
+
|
|
120
|
+
def get_summed_vdd_charges_dataframe(self) -> pd.DataFrame:
|
|
121
|
+
"""Get the summed VDD charges as a pandas DataFrame in a specified unit ([me] or [e])."""
|
|
122
|
+
summed_data = self.get_summed_vdd_charges()
|
|
123
|
+
summed_data["Frag"] = {str(key): int(key) for key in summed_data["vdd"].keys()}
|
|
124
|
+
df = pd.DataFrame(summed_data).pipe(lambda df: df[df.columns.tolist()[-1:] + df.columns.tolist()[:-1]]) # move the "Frag" column to the front
|
|
125
|
+
return df.rename(columns={"vdd": "Total"})
|
|
126
|
+
|
|
127
|
+
def get_vdd_charges_table(self) -> str:
|
|
128
|
+
df = self.get_vdd_charges_dataframe()
|
|
129
|
+
return df.to_string(float_format=lambda x: PRINT_FORMAT[self.unit] % x, justify="center", index=False, col_space=6)
|
|
130
|
+
|
|
131
|
+
def get_summed_vdd_charges_table(self) -> str:
|
|
132
|
+
df = self.get_summed_vdd_charges_dataframe()
|
|
133
|
+
return df.to_string(float_format=lambda x: PRINT_FORMAT[self.unit] % x, justify="center", col_space=6, index=False)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def write_to_txt(output_dir: Union[str, pl.Path], managers: Union[VDDChargeManager, Sequence[VDDChargeManager]], unit: str = "me") -> None:
|
|
137
|
+
"""Write the VDD charges to a text file. It is a static method because multiple managers can be written to the same file."""
|
|
138
|
+
out_dir = pl.Path(output_dir) if not isinstance(output_dir, pl.Path) else output_dir
|
|
139
|
+
files = [out_dir / "VDD_charges_per_atom.txt", out_dir / "VDD_charges_per_fragment.txt"]
|
|
140
|
+
managers = managers if isinstance(managers, Sequence) else [managers]
|
|
141
|
+
|
|
142
|
+
# Print charges per atom and per fragment in seperate files
|
|
143
|
+
for i, file in enumerate(files):
|
|
144
|
+
with open(file, "w") as f:
|
|
145
|
+
f.write("VDD charges:\n")
|
|
146
|
+
for manager in managers:
|
|
147
|
+
f.write(f"{manager.name} (unit = {manager.unit})\n")
|
|
148
|
+
if i == 0:
|
|
149
|
+
f.write(manager.get_vdd_charges_table())
|
|
150
|
+
else:
|
|
151
|
+
f.write(manager.get_summed_vdd_charges_table()) if manager.is_fragment_calculation else f.write("No fragment calculation\n")
|
|
152
|
+
f.write("\n\n")
|
|
153
|
+
|
|
154
|
+
@environment.requires_optional_package("openpyxl")
|
|
155
|
+
def write_to_excel(self, output_file: Optional[Union[str, pl.Path]] = None) -> None:
|
|
156
|
+
"""Write the VDD charges to an excel file. Results are written to two sheets: "VDD charges" and "Summed VDD charges"."""
|
|
157
|
+
file = pl.Path(output_file) if output_file is not None else self.calc_dir / f"vdd_charges_{self.name}.xlsx"
|
|
158
|
+
file = file.with_suffix(".xlsx") if not file.suffix == ".xlsx" else file
|
|
159
|
+
|
|
160
|
+
df = self.get_vdd_charges_dataframe()
|
|
161
|
+
|
|
162
|
+
with pd.ExcelWriter(file) as writer:
|
|
163
|
+
df.to_excel(writer, sheet_name=f"VDD charges (in {self.unit})", index=False, float_format=PRINT_FORMAT[self.unit])
|
|
164
|
+
if self.is_fragment_calculation:
|
|
165
|
+
df_summed = self.get_summed_vdd_charges_dataframe()
|
|
166
|
+
df_summed.to_excel(writer, sheet_name=f"Summed VDD charges (in {self.unit})", index=False, float_format=PRINT_FORMAT[self.unit])
|
|
167
|
+
|
|
168
|
+
@environment.requires_optional_package("matplotlib")
|
|
169
|
+
def plot_vdd_charges_per_atom(self, output_file: Optional[Union[str, pl.Path]] = None, unit: str = "me") -> None:
|
|
170
|
+
"""Plot the VDD charges as a bar graph for each irrep."""
|
|
171
|
+
import matplotlib.pyplot as plt
|
|
172
|
+
|
|
173
|
+
file = pl.Path(output_file) if output_file is not None else self.calc_dir / f"vdd_charges_{self.name}.png"
|
|
174
|
+
file = file.with_suffix(".png") if not file.suffix == ".png" else file
|
|
175
|
+
self.change_unit(unit)
|
|
176
|
+
|
|
177
|
+
# Increase the global font size
|
|
178
|
+
plt.rcParams.update({"font.size": 14})
|
|
179
|
+
|
|
180
|
+
num_irreps = len(self.vdd_charges)
|
|
181
|
+
n_max_charges = max([len(charges) for charges in self.vdd_charges.values()])
|
|
182
|
+
_, axs = plt.subplots(num_irreps, 1, figsize=(n_max_charges * 1.15, 5 * num_irreps), sharey=True)
|
|
183
|
+
axs = [axs] if num_irreps == 1 else axs
|
|
184
|
+
|
|
185
|
+
# Initialize a variable to keep track of the most positive/negative values
|
|
186
|
+
adjusted_abs_max_values = []
|
|
187
|
+
|
|
188
|
+
counter = 1
|
|
189
|
+
for ax, (irrep, charges) in zip(axs, self.vdd_charges.items()):
|
|
190
|
+
atom_symbols = [f"{charge.atom_index}{charge.atom_symbol} ({charge.frag_index})" for charge in charges]
|
|
191
|
+
charge_values = [charge.charge for charge in charges]
|
|
192
|
+
|
|
193
|
+
bars = ax.bar(atom_symbols, charge_values, color="sandybrown", edgecolor="black")
|
|
194
|
+
if counter == len(axs):
|
|
195
|
+
ax.set_xlabel("Atom (#Fragment Number)")
|
|
196
|
+
ax.set_ylabel(f"Charge ({unit})")
|
|
197
|
+
ax.set_title(f"VDD Charges {self.name} - {irrep}")
|
|
198
|
+
ax.yaxis.grid(True) # Only display vertical grid lines
|
|
199
|
+
|
|
200
|
+
# Add the charge value on top of each bar. If the value is negative, place the text below the bar
|
|
201
|
+
for bar in bars:
|
|
202
|
+
yval = bar.get_height()
|
|
203
|
+
x_pos = bar.get_x() + bar.get_width() / 2
|
|
204
|
+
|
|
205
|
+
if yval <= 0:
|
|
206
|
+
ax.text(x_pos, yval + yval * 0.1, int(yval), va="top", ha="center")
|
|
207
|
+
else:
|
|
208
|
+
ax.text(x_pos, yval + yval * 0.1, int(yval), va="bottom", ha="center")
|
|
209
|
+
adjusted_abs_max_values.append(yval + yval * 0.1)
|
|
210
|
+
counter += 1
|
|
211
|
+
|
|
212
|
+
# Making sure the values do not extend outside the plot. Also determines the y-axis limits for all subplotss
|
|
213
|
+
min_value, max_value = min(adjusted_abs_max_values), max(adjusted_abs_max_values)
|
|
214
|
+
|
|
215
|
+
# Axes adjustments
|
|
216
|
+
# Set the y-axis limits for all subplots based on the most positive/negative values
|
|
217
|
+
for ax in axs:
|
|
218
|
+
ax.set_ylim([min_value - abs(min_value) * 0.2, max_value + abs(max_value) * 0.2])
|
|
219
|
+
|
|
220
|
+
# plt.tight_layout()
|
|
221
|
+
plt.savefig(file, dpi=300)
|
|
File without changes
|