nebcert 1.2.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.
- nebcert/__init__.py +28 -0
- nebcert/citation.py +22 -0
- nebcert/cli.py +323 -0
- nebcert/core/__init__.py +22 -0
- nebcert/core/neb_profile.py +240 -0
- nebcert/core/scoring.py +113 -0
- nebcert/core/ts_frequency.py +95 -0
- nebcert/core/tst_kinetics.py +140 -0
- nebcert/core/tunneling.py +195 -0
- nebcert/parsers/__init__.py +15 -0
- nebcert/parsers/gaussian_irc.py +60 -0
- nebcert/parsers/generic_mep_csv.py +52 -0
- nebcert/parsers/orca_neb.py +121 -0
- nebcert/parsers/vasp_neb.py +58 -0
- nebcert/reporters/__init__.py +13 -0
- nebcert/reporters/html_report.py +309 -0
- nebcert/reporters/manuscript_prep.py +126 -0
- nebcert/reporters/plot_generator.py +153 -0
- nebcert-1.2.0.dist-info/METADATA +180 -0
- nebcert-1.2.0.dist-info/RECORD +24 -0
- nebcert-1.2.0.dist-info/WHEEL +5 -0
- nebcert-1.2.0.dist-info/entry_points.txt +2 -0
- nebcert-1.2.0.dist-info/licenses/LICENSE +21 -0
- nebcert-1.2.0.dist-info/top_level.txt +1 -0
nebcert/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NEBCert: quality checks for NEB reaction paths, transition states and
|
|
3
|
+
tunnelling-corrected rate constants.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "1.2.0"
|
|
7
|
+
__author__ = "Andres Monreal-Hernández"
|
|
8
|
+
__license__ = "MIT"
|
|
9
|
+
|
|
10
|
+
from nebcert.core.neb_profile import calculate_neb_profile_analysis, NEBProfileResult
|
|
11
|
+
from nebcert.core.ts_frequency import verify_ts_frequency_and_irc, TSFrequencyResult
|
|
12
|
+
from nebcert.core.tst_kinetics import calculate_eyring_tst_rates, TSTKineticsResult
|
|
13
|
+
from nebcert.core.tunneling import calculate_quantum_tunneling_corrections, TunnelingResult
|
|
14
|
+
from nebcert.core.scoring import assess_reaction_pathway_quality, ReactionPathwayReport
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"calculate_neb_profile_analysis",
|
|
19
|
+
"NEBProfileResult",
|
|
20
|
+
"verify_ts_frequency_and_irc",
|
|
21
|
+
"TSFrequencyResult",
|
|
22
|
+
"calculate_eyring_tst_rates",
|
|
23
|
+
"TSTKineticsResult",
|
|
24
|
+
"calculate_quantum_tunneling_corrections",
|
|
25
|
+
"TunnelingResult",
|
|
26
|
+
"assess_reaction_pathway_quality",
|
|
27
|
+
"ReactionPathwayReport"
|
|
28
|
+
]
|
nebcert/citation.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Citation strings for NEBCert (concept DOI: resolves to the latest version on Zenodo).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from nebcert import __version__
|
|
6
|
+
|
|
7
|
+
CONCEPT_DOI = "10.5281/zenodo.22217586"
|
|
8
|
+
TITLE = "NEBCert: quality checks for NEB paths, transition states and tunnelling-corrected rate constants"
|
|
9
|
+
|
|
10
|
+
BIBTEX = (
|
|
11
|
+
"@software{monreal2026nebcert,\n"
|
|
12
|
+
" author = {Monreal-Hern{\\'a}ndez, Andr{\\'e}s},\n"
|
|
13
|
+
f" title = {{{{{TITLE}}}}},\n"
|
|
14
|
+
" year = {2026},\n"
|
|
15
|
+
f" version = {{{__version__}}},\n"
|
|
16
|
+
" publisher = {Zenodo},\n"
|
|
17
|
+
f" doi = {{{CONCEPT_DOI}}},\n"
|
|
18
|
+
" url = {https://github.com/sircalch/nebcert}\n"
|
|
19
|
+
"}\n"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
APA = f"Monreal-Hernández, A. (2026). {TITLE} (v{__version__}). Zenodo. https://doi.org/{CONCEPT_DOI}"
|
nebcert/cli.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command Line Interface (CLI) for NEBCert.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from nebcert import __version__
|
|
11
|
+
from nebcert.citation import APA, BIBTEX
|
|
12
|
+
from nebcert.parsers.vasp_neb import parse_vasp_neb_dat
|
|
13
|
+
from nebcert.parsers.orca_neb import parse_orca_neb_output
|
|
14
|
+
from nebcert.parsers.gaussian_irc import parse_gaussian_irc_output
|
|
15
|
+
from nebcert.parsers.generic_mep_csv import parse_mep_csv
|
|
16
|
+
|
|
17
|
+
from nebcert.core.neb_profile import calculate_neb_profile_analysis
|
|
18
|
+
from nebcert.core.ts_frequency import verify_ts_frequency_and_irc
|
|
19
|
+
from nebcert.core.tst_kinetics import calculate_eyring_tst_rates
|
|
20
|
+
from nebcert.core.tunneling import calculate_quantum_tunneling_corrections
|
|
21
|
+
from nebcert.core.scoring import assess_reaction_pathway_quality
|
|
22
|
+
|
|
23
|
+
from nebcert.reporters.plot_generator import generate_nebcert_figures
|
|
24
|
+
from nebcert.reporters.manuscript_prep import generate_nebcert_manuscript_assets
|
|
25
|
+
from nebcert.reporters.html_report import generate_nebcert_html_report
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_banner():
|
|
29
|
+
banner = rf"""
|
|
30
|
+
_ _ ______ _____ _____ _
|
|
31
|
+
| \ | | ____| _ \ / ____| | |
|
|
32
|
+
| \| | |__ | |_) | | ___ _ __| |_
|
|
33
|
+
| . ` | __| | _ <| | / _ \ '__| __|
|
|
34
|
+
| |\ | |____| |_) | |___| __/ | | |_
|
|
35
|
+
|_| \_|______|____/ \_____\___|_| \__| v{__version__}
|
|
36
|
+
|
|
37
|
+
Transition State, NEB Reaction Pathways & Quantum Tunneling Toolkit
|
|
38
|
+
Monreal-Hernández et al., 2026
|
|
39
|
+
"""
|
|
40
|
+
print(banner)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_demo(output_dir: str = "nebcert_demo_output"):
|
|
44
|
+
"""
|
|
45
|
+
Executes a benchmark demonstration evaluating a hydrogen atom abstraction reaction:
|
|
46
|
+
CH4 + OH* -> CH3* + H2O with 9-image NEB band, 1st-order saddle point TS (nu_imag = -1250 cm^-1),
|
|
47
|
+
Eyring TST kinetics, and asymmetric Eckart quantum tunneling.
|
|
48
|
+
"""
|
|
49
|
+
print(f"\n[NEBCert] Running demonstration benchmark on Reaction Pathway (CH4 + OH* -> CH3* + H2O)...")
|
|
50
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
metadata = {
|
|
53
|
+
"reaction": "CH4 + OH* -> [TS]* -> CH3* + H2O (Hydrogen Abstraction)",
|
|
54
|
+
"functional": "wB97X-D3 / def2-TZVP",
|
|
55
|
+
"software": "SYNTHETIC DEMO DATA (ORCA-like CI-NEB / NumFreq; not a real calculation)"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# 1. 9-image NEB reaction energy profile (eV)
|
|
59
|
+
# Reactant = 0.0, Peak TS (img 4) = 0.225 eV (5.19 kcal/mol), Product (img 8) = -0.620 eV (-14.30 kcal/mol)
|
|
60
|
+
s_coords = [0.0, 0.35, 0.70, 1.05, 1.40, 1.75, 2.10, 2.45, 2.80]
|
|
61
|
+
energies_rel_ev = [0.000, 0.045, 0.120, 0.195, 0.225, 0.110, -0.220, -0.510, -0.620]
|
|
62
|
+
tangent_forces = [0.00, 0.02, 0.03, 0.02, 0.01, 0.03, 0.02, 0.02, 0.00]
|
|
63
|
+
|
|
64
|
+
print(" -> Analyzing NEB Minimum Energy Path, fitting cubic spline, and auditing CI-NEB forces...")
|
|
65
|
+
neb_res = calculate_neb_profile_analysis(
|
|
66
|
+
energies_ev=energies_rel_ev,
|
|
67
|
+
coordinates_s_ang=s_coords,
|
|
68
|
+
tangent_forces_ev_ang=tangent_forces
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# 2. Transition state imaginary frequency
|
|
72
|
+
print(" -> Verifying 1st-order saddle point vibrational frequencies (nu_imag = -1250.0 cm^-1)...")
|
|
73
|
+
sample_freqs = [-1250.0, 120.0, 250.0, 480.0, 850.0, 1100.0, 1450.0, 2900.0, 3100.0, 3650.0]
|
|
74
|
+
ts_res = verify_ts_frequency_and_irc(
|
|
75
|
+
frequencies_cm1=sample_freqs,
|
|
76
|
+
min_significant_freq_cm1=50.0,
|
|
77
|
+
irc_confirmed=True
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# 3. Eyring TST rate constants
|
|
81
|
+
print(" -> Computing Eyring-Polanyi TST rate constants k(T) and Arrhenius parameters...")
|
|
82
|
+
tst_res = calculate_eyring_tst_rates(
|
|
83
|
+
e_activation_ev=neb_res.e_forward_barrier_ev
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# 4. Quantum tunneling corrections (Eckart & Wigner)
|
|
87
|
+
print(" -> Computing asymmetric Eckart quantum tunneling transmission factor kappa(T)...")
|
|
88
|
+
tun_res = calculate_quantum_tunneling_corrections(
|
|
89
|
+
imaginary_freq_cm1=1250.0,
|
|
90
|
+
e_forward_barrier_ev=neb_res.e_forward_barrier_ev,
|
|
91
|
+
e_reverse_barrier_ev=neb_res.e_reverse_barrier_ev
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
report = assess_reaction_pathway_quality(
|
|
95
|
+
metadata=metadata,
|
|
96
|
+
neb_res=neb_res,
|
|
97
|
+
ts_freq_res=ts_res,
|
|
98
|
+
tst_res=tst_res,
|
|
99
|
+
tunneling_res=tun_res
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
print(" -> Generating publication-ready vector figures (NEB MEP Profile, Arrhenius Plot, Tunneling Curve)...")
|
|
103
|
+
generate_nebcert_figures(report, output_dir)
|
|
104
|
+
|
|
105
|
+
print(" -> Drafting manuscript Methods text snippet, summary LaTeX tables, and BibTeX citations...")
|
|
106
|
+
assets = generate_nebcert_manuscript_assets(report, output_dir)
|
|
107
|
+
|
|
108
|
+
with open(assets["methods_text"], "r", encoding="utf-8") as f:
|
|
109
|
+
methods_txt = f.read()
|
|
110
|
+
with open(assets["citation_bib"], "r", encoding="utf-8") as f:
|
|
111
|
+
bib_txt = f.read()
|
|
112
|
+
|
|
113
|
+
html_p = os.path.join(output_dir, "report.html")
|
|
114
|
+
print(f" -> Writing interactive report to {html_p}...")
|
|
115
|
+
generate_nebcert_html_report(report, html_p, methods_text=methods_txt, citation_bib=bib_txt)
|
|
116
|
+
|
|
117
|
+
print("\n" + "="*70)
|
|
118
|
+
print(f" [RESULT] Overall status: {report.overall_status}")
|
|
119
|
+
print(f" [SCORE] {report.validation_score}")
|
|
120
|
+
print("="*70)
|
|
121
|
+
print(f" * Reaction Target : {report.metadata['reaction']}")
|
|
122
|
+
print(f" * Activation Barrier: E_a^fwd = {report.neb_profile.e_forward_barrier_kcal_mol:.2f} kcal/mol ({report.neb_profile.e_forward_barrier_ev:.3f} eV) | Delta E_rxn = {report.neb_profile.delta_e_reaction_kcal_mol:.2f} kcal/mol")
|
|
123
|
+
print(f" * Transition State : {report.ts_frequency.imaginary_frequency_cm1:.1f} cm^-1 | Status: {report.ts_frequency.status}")
|
|
124
|
+
print(f" * Eyring TST Rate : k(298 K) = {report.tst_kinetics.k_298_s_minus_1:.2e} s^-1 (Arrhenius A = {report.tst_kinetics.arrhenius_pre_exponential_a_s_minus_1:.2e} s^-1, E_a = {report.tst_kinetics.arrhenius_e_activation_kcal_mol:.2f} kcal/mol)")
|
|
125
|
+
print(f" * Quantum Tunneling: kappa_Eckart(298 K) = {report.tunneling.kappa_eckart_298:.2f}")
|
|
126
|
+
print("="*70)
|
|
127
|
+
print(f"\nAll outputs successfully saved to: {os.path.abspath(output_dir)}/")
|
|
128
|
+
print(f"Open {os.path.abspath(html_p)} in your browser to inspect the full report.\n")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def run_assess(args):
|
|
132
|
+
"""
|
|
133
|
+
Evaluates user-provided MEP/NEB files or frequencies.
|
|
134
|
+
"""
|
|
135
|
+
output_dir = args.output
|
|
136
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
137
|
+
|
|
138
|
+
neb_res = None
|
|
139
|
+
ts_res = None
|
|
140
|
+
tst_res = None
|
|
141
|
+
tun_res = None
|
|
142
|
+
|
|
143
|
+
# 1. Parse NEB / MEP input
|
|
144
|
+
orca_neb = None
|
|
145
|
+
if args.input_neb:
|
|
146
|
+
print(f"\n[NEBCert] Parsing NEB data from: {args.input_neb}...")
|
|
147
|
+
lower = args.input_neb.lower()
|
|
148
|
+
if lower.endswith(".dat"):
|
|
149
|
+
try:
|
|
150
|
+
m_data = parse_vasp_neb_dat(args.input_neb)
|
|
151
|
+
except Exception:
|
|
152
|
+
m_data = parse_mep_csv(args.input_neb)
|
|
153
|
+
elif lower.endswith((".out", ".log")):
|
|
154
|
+
m_data = parse_orca_neb_output(args.input_neb)
|
|
155
|
+
if not m_data["energies_ev"]:
|
|
156
|
+
print("[Error] No ORCA NEB 'PATH SUMMARY' table found in the file.", file=sys.stderr)
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
if m_data["n_images_without_energy"]:
|
|
159
|
+
print(f"[Error] {m_data['n_images_without_energy']} of {m_data['n_images']} images in the last "
|
|
160
|
+
f"'PATH SUMMARY' table have no energy (E = 0): the image calculations failed or never ran; see the ORCA errors above the table.",
|
|
161
|
+
file=sys.stderr)
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
orca_neb = m_data
|
|
164
|
+
print(f" -> ORCA NEB: {m_data['n_images']} images, NEB converged: {m_data['neb_converged']}, "
|
|
165
|
+
f"climbing image: {m_data['ci_index']}")
|
|
166
|
+
if m_data.get("ts"):
|
|
167
|
+
print(f" -> NEB-TS transition state: E_a = {m_data['ts']['barrier_fwd_kcal']:.2f} kcal/mol, "
|
|
168
|
+
f"TS optimisation converged: {m_data['ts']['converged']}")
|
|
169
|
+
else:
|
|
170
|
+
m_data = parse_mep_csv(args.input_neb)
|
|
171
|
+
|
|
172
|
+
ts_data = orca_neb.get("ts") if orca_neb else None
|
|
173
|
+
neb_res = calculate_neb_profile_analysis(
|
|
174
|
+
energies_ev=m_data["energies_ev"],
|
|
175
|
+
coordinates_s_ang=m_data.get("coordinates_s_ang"),
|
|
176
|
+
tangent_forces_ev_ang=m_data.get("tangent_forces"),
|
|
177
|
+
band_converged=orca_neb["neb_converged"] if orca_neb else None,
|
|
178
|
+
ts_energy_rel_ev=ts_data["barrier_fwd_ev"] if ts_data else None,
|
|
179
|
+
ts_optimisation_converged=ts_data["converged"] if ts_data else None,
|
|
180
|
+
climbing_image_index=orca_neb["ci_index"] if orca_neb else None
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# 2. Parse / evaluate TS frequencies
|
|
184
|
+
if args.frequencies:
|
|
185
|
+
freq_list = [float(x) for x in args.frequencies.split(",")]
|
|
186
|
+
ts_res = verify_ts_frequency_and_irc(
|
|
187
|
+
frequencies_cm1=freq_list,
|
|
188
|
+
irc_confirmed=args.irc
|
|
189
|
+
)
|
|
190
|
+
elif orca_neb and orca_neb.get("frequencies"):
|
|
191
|
+
ts_res = verify_ts_frequency_and_irc(
|
|
192
|
+
frequencies_cm1=orca_neb["frequencies"],
|
|
193
|
+
irc_confirmed=args.irc
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# 3. Calculate kinetics and tunneling
|
|
197
|
+
# Electronic barriers (forward and reverse) for the Eckart barrier; the band already holds the
|
|
198
|
+
# optimised NEB-TS energy when there is one.
|
|
199
|
+
ea_val = None
|
|
200
|
+
ea_rev = neb_res.e_reverse_barrier_ev if neb_res else None
|
|
201
|
+
if neb_res:
|
|
202
|
+
ea_val = neb_res.e_forward_barrier_ev
|
|
203
|
+
elif args.barrier_ev:
|
|
204
|
+
ea_val = float(args.barrier_ev)
|
|
205
|
+
elif args.barrier_kcal:
|
|
206
|
+
ea_val = float(args.barrier_kcal) / 23.06054887
|
|
207
|
+
|
|
208
|
+
if args.gibbs_barrier_kcal is not None:
|
|
209
|
+
tst_res = calculate_eyring_tst_rates(e_activation_ev=float(args.gibbs_barrier_kcal) / 23.06054887,
|
|
210
|
+
barrier_type="gibbs")
|
|
211
|
+
elif ea_val is not None:
|
|
212
|
+
tst_res = calculate_eyring_tst_rates(e_activation_ev=ea_val, barrier_type="electronic")
|
|
213
|
+
if ea_val is not None and ts_res and ts_res.imaginary_frequency_cm1:
|
|
214
|
+
tun_res = calculate_quantum_tunneling_corrections(
|
|
215
|
+
imaginary_freq_cm1=ts_res.imaginary_frequency_cm1,
|
|
216
|
+
e_forward_barrier_ev=ea_val,
|
|
217
|
+
e_reverse_barrier_ev=ea_rev
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
meta = {
|
|
221
|
+
"reaction": args.reaction or "the reaction",
|
|
222
|
+
"functional": args.functional or "an unstated level of theory",
|
|
223
|
+
"software": args.software or ("ORCA" if orca_neb else "an unstated program")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
report = assess_reaction_pathway_quality(
|
|
227
|
+
metadata=meta,
|
|
228
|
+
neb_res=neb_res,
|
|
229
|
+
ts_freq_res=ts_res,
|
|
230
|
+
tst_res=tst_res,
|
|
231
|
+
tunneling_res=tun_res
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
print(" -> Generating publication figures...")
|
|
235
|
+
generate_nebcert_figures(report, output_dir)
|
|
236
|
+
|
|
237
|
+
print(" -> Generating manuscript text, LaTeX summary table, and BibTeX citations...")
|
|
238
|
+
assets = generate_nebcert_manuscript_assets(report, output_dir)
|
|
239
|
+
|
|
240
|
+
with open(assets["methods_text"], "r", encoding="utf-8") as f:
|
|
241
|
+
methods_txt = f.read()
|
|
242
|
+
with open(assets["citation_bib"], "r", encoding="utf-8") as f:
|
|
243
|
+
bib_txt = f.read()
|
|
244
|
+
|
|
245
|
+
html_p = os.path.join(output_dir, "report.html")
|
|
246
|
+
print(f" -> Writing HTML quality report to {html_p}...")
|
|
247
|
+
generate_nebcert_html_report(report, html_p, methods_text=methods_txt, citation_bib=bib_txt)
|
|
248
|
+
|
|
249
|
+
print("\n" + "="*70)
|
|
250
|
+
print(f" [RESULT] Overall status: {report.overall_status}")
|
|
251
|
+
print(f" [SCORE] {report.validation_score}")
|
|
252
|
+
print("="*70)
|
|
253
|
+
if report.neb_profile:
|
|
254
|
+
print(f" * Barrier (E_a^fwd): {report.neb_profile.e_forward_barrier_kcal_mol:.2f} kcal/mol ({report.neb_profile.e_forward_barrier_ev:.3f} eV)")
|
|
255
|
+
if report.tst_kinetics:
|
|
256
|
+
print(f" * TST Rate (298 K) : k = {report.tst_kinetics.k_298_s_minus_1:.2e} s^-1")
|
|
257
|
+
print("="*70)
|
|
258
|
+
print(f"\nReport ready at: {os.path.abspath(html_p)}\n")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def print_citation():
|
|
262
|
+
print()
|
|
263
|
+
print("If you use NEBCert in your publications, please cite:")
|
|
264
|
+
print()
|
|
265
|
+
print("APA Style:")
|
|
266
|
+
print(APA)
|
|
267
|
+
print()
|
|
268
|
+
print("BibTeX:")
|
|
269
|
+
print(BIBTEX)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def main():
|
|
273
|
+
parser = argparse.ArgumentParser(
|
|
274
|
+
prog="nebcert",
|
|
275
|
+
description="NEBCert: Transition State, NEB Reaction Pathways, Quantum Tunneling, and TST Kinetics Certification."
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument("-v", "--version", action="version", version=f"nebcert {__version__}")
|
|
278
|
+
|
|
279
|
+
subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
|
|
280
|
+
|
|
281
|
+
# Assess command
|
|
282
|
+
assess_parser = subparsers.add_parser("assess", help="Assess NEB pathway, TS frequencies, and reaction kinetics")
|
|
283
|
+
assess_parser.add_argument("-i", "--input-neb", default=None, help="Path to NEB file: VASP neb.dat, ORCA NEB/NEB-CI/NEB-TS output (.out/.log), or CSV/TSV table of image energies")
|
|
284
|
+
assess_parser.add_argument("--frequencies", default=None, help="Comma-separated vibrational frequencies in cm^-1 (e.g. '-1250,150,300,800')")
|
|
285
|
+
assess_parser.add_argument("--barrier-ev", type=float, default=None, help="Activation energy in eV (if not using NEB profile)")
|
|
286
|
+
assess_parser.add_argument("--barrier-kcal", type=float, default=None, help="Activation energy in kcal/mol")
|
|
287
|
+
assess_parser.add_argument("--gibbs-barrier-kcal", type=float, default=None, help="Gibbs free energy of activation (kcal/mol) for the Eyring rate constant; without it the rate uses the electronic barrier and is flagged")
|
|
288
|
+
assess_parser.add_argument("--irc", action="store_const", const=True, default=None, help="Declare that an IRC connected the intended minima (not checked by NEBCert); omit when no IRC was run")
|
|
289
|
+
assess_parser.add_argument("-o", "--output", default="nebcert_output", help="Directory for output report (default: nebcert_output)")
|
|
290
|
+
assess_parser.add_argument("--reaction", default=None, help="Reaction description (e.g. 'CH4 + OH -> CH3 + H2O')")
|
|
291
|
+
assess_parser.add_argument("--functional", default=None, help="DFT functional / level of theory")
|
|
292
|
+
assess_parser.add_argument("--software", default=None, help="Software code (e.g. 'VASP', 'ORCA', 'Gaussian')")
|
|
293
|
+
|
|
294
|
+
# Demo command
|
|
295
|
+
demo_parser = subparsers.add_parser("demo", help="Run benchmark demonstration (H-abstraction 9-image NEB + TS + TST + Tunneling)")
|
|
296
|
+
demo_parser.add_argument("-o", "--output", default="nebcert_demo_output", help="Output directory (default: nebcert_demo_output)")
|
|
297
|
+
|
|
298
|
+
# Cite command
|
|
299
|
+
subparsers.add_parser("cite", help="Display BibTeX and APA citation details")
|
|
300
|
+
|
|
301
|
+
if len(sys.argv) == 1:
|
|
302
|
+
print_banner()
|
|
303
|
+
parser.print_help()
|
|
304
|
+
sys.exit(0)
|
|
305
|
+
|
|
306
|
+
args = parser.parse_args()
|
|
307
|
+
|
|
308
|
+
if args.command == "assess":
|
|
309
|
+
print_banner()
|
|
310
|
+
run_assess(args)
|
|
311
|
+
elif args.command == "demo":
|
|
312
|
+
print_banner()
|
|
313
|
+
run_demo(args.output)
|
|
314
|
+
elif args.command == "cite":
|
|
315
|
+
print_banner()
|
|
316
|
+
print_citation()
|
|
317
|
+
else:
|
|
318
|
+
parser.print_help()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
main()
|
|
323
|
+
|
nebcert/core/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core kinetics, NEB spline analysis, and quantum tunneling engines for NEBCert.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from nebcert.core.neb_profile import calculate_neb_profile_analysis, NEBProfileResult
|
|
6
|
+
from nebcert.core.ts_frequency import verify_ts_frequency_and_irc, TSFrequencyResult
|
|
7
|
+
from nebcert.core.tst_kinetics import calculate_eyring_tst_rates, TSTKineticsResult
|
|
8
|
+
from nebcert.core.tunneling import calculate_quantum_tunneling_corrections, TunnelingResult
|
|
9
|
+
from nebcert.core.scoring import assess_reaction_pathway_quality, ReactionPathwayReport
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"calculate_neb_profile_analysis",
|
|
13
|
+
"NEBProfileResult",
|
|
14
|
+
"verify_ts_frequency_and_irc",
|
|
15
|
+
"TSFrequencyResult",
|
|
16
|
+
"calculate_eyring_tst_rates",
|
|
17
|
+
"TSTKineticsResult",
|
|
18
|
+
"calculate_quantum_tunneling_corrections",
|
|
19
|
+
"TunnelingResult",
|
|
20
|
+
"assess_reaction_pathway_quality",
|
|
21
|
+
"ReactionPathwayReport"
|
|
22
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nudged Elastic Band (NEB / CI-NEB) profile analysis, spline interpolation, and MEP force audit.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.interpolate import CubicSpline
|
|
9
|
+
|
|
10
|
+
EV_TO_KCAL_MOL = 23.06054887
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class NEBImagePoint:
|
|
15
|
+
image_index: int
|
|
16
|
+
reaction_coordinate_s_ang: float
|
|
17
|
+
energy_ev: float
|
|
18
|
+
relative_energy_ev: float
|
|
19
|
+
relative_energy_kcal_mol: float
|
|
20
|
+
tangent_force_ev_ang: Optional[float]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class NEBProfileResult:
|
|
25
|
+
n_images: int
|
|
26
|
+
total_path_length_ang: float
|
|
27
|
+
ts_image_index: int
|
|
28
|
+
ts_position_s_ang: float
|
|
29
|
+
e_forward_barrier_ev: float
|
|
30
|
+
e_forward_barrier_kcal_mol: float
|
|
31
|
+
e_reverse_barrier_ev: float
|
|
32
|
+
e_reverse_barrier_kcal_mol: float
|
|
33
|
+
delta_e_reaction_ev: float
|
|
34
|
+
delta_e_reaction_kcal_mol: float
|
|
35
|
+
max_tangent_force_ev_ang: Optional[float]
|
|
36
|
+
is_climbing_image_converged: bool
|
|
37
|
+
has_intermediate_minimum: bool
|
|
38
|
+
interpolated_s: List[float]
|
|
39
|
+
interpolated_e_rel_ev: List[float]
|
|
40
|
+
image_points: List[NEBImagePoint]
|
|
41
|
+
status: str # 'PASS', 'WARNING', 'FAIL'
|
|
42
|
+
diagnostic_message: str
|
|
43
|
+
e_forward_barrier_spline_ev: Optional[float] = None
|
|
44
|
+
barrier_source: str = "spline"
|
|
45
|
+
band_converged: Optional[bool] = None
|
|
46
|
+
ts_optimisation_converged: Optional[bool] = None
|
|
47
|
+
deepest_intermediate_well_kcal_mol: float = 0.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def calculate_neb_profile_analysis(
|
|
51
|
+
energies_ev: List[float],
|
|
52
|
+
coordinates_s_ang: Optional[List[float]] = None,
|
|
53
|
+
tangent_forces_ev_ang: Optional[List[float]] = None,
|
|
54
|
+
force_convergence_threshold: float = 0.05, # eV/Å
|
|
55
|
+
warning_force_threshold: float = 0.10,
|
|
56
|
+
band_converged: Optional[bool] = None,
|
|
57
|
+
ts_energy_rel_ev: Optional[float] = None,
|
|
58
|
+
ts_optimisation_converged: Optional[bool] = None,
|
|
59
|
+
min_well_depth_kcal_mol: float = 0.5,
|
|
60
|
+
min_path_length_ang: float = 0.05,
|
|
61
|
+
climbing_image_index: Optional[int] = None
|
|
62
|
+
) -> NEBProfileResult:
|
|
63
|
+
"""
|
|
64
|
+
Analyzes NEB minimum energy path, fits cubic spline interpolation, calculates
|
|
65
|
+
forward and reverse activation barriers, and audits force convergence and continuity.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
energies_ev : list of float
|
|
70
|
+
Total DFT energies for all images along the band (eV).
|
|
71
|
+
coordinates_s_ang : list of float, optional
|
|
72
|
+
Cumulative reaction coordinates s (Å). If None, uniform spacing 0..1 is used.
|
|
73
|
+
tangent_forces_ev_ang : list of float, optional
|
|
74
|
+
Parallel / residual forces along band (eV/Å).
|
|
75
|
+
force_convergence_threshold : float, default 0.05 eV/Å
|
|
76
|
+
Limit on the force at the image nearest the barrier.
|
|
77
|
+
warning_force_threshold : float, default 0.10 eV/Å
|
|
78
|
+
Unused; kept for backward compatibility.
|
|
79
|
+
band_converged : bool, optional
|
|
80
|
+
Convergence flag of the band as printed by the program (ORCA: "THE NEB OPTIMIZATION HAS
|
|
81
|
+
CONVERGED"). False gives FAIL: an unconverged band does not locate the barrier.
|
|
82
|
+
ts_energy_rel_ev : float, optional
|
|
83
|
+
Energy of an optimised transition state (e.g. ORCA NEB-TS) relative to image 0. When given it
|
|
84
|
+
replaces the spline maximum as the forward barrier; the spline value is kept for comparison.
|
|
85
|
+
ts_optimisation_converged : bool, optional
|
|
86
|
+
False gives FAIL (the TS energy is then not a saddle-point energy). When True, the force check on the
|
|
87
|
+
band is skipped: NEB-TS converges the band only loosely by design and the TS optimisation refines it.
|
|
88
|
+
climbing_image_index : int, optional
|
|
89
|
+
Index of the climbing image. When the band converged and no optimised TS is given, the climbing-image
|
|
90
|
+
energy is the barrier (a converged climbing image sits at the saddle point; a spline through a coarse
|
|
91
|
+
band does not).
|
|
92
|
+
min_well_depth_kcal_mol : float, default 0.5
|
|
93
|
+
An interior image lower than both neighbours by more than this is reported as an intermediate
|
|
94
|
+
minimum; shallower dips are treated as noise of the band.
|
|
95
|
+
min_path_length_ang : float, default 0.05
|
|
96
|
+
When coordinates_s_ang are distances along the path (Å), a shorter path means that both end points
|
|
97
|
+
are the same structure (e.g. one end point slid into the other during pre-optimisation): FAIL.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
result : NEBProfileResult
|
|
102
|
+
"""
|
|
103
|
+
e_arr = np.asarray(energies_ev, dtype=float)
|
|
104
|
+
n_img = len(e_arr)
|
|
105
|
+
if n_img < 3:
|
|
106
|
+
raise ValueError("NEB reaction path requires at least 3 images (Reactant, TS, Product).")
|
|
107
|
+
|
|
108
|
+
if coordinates_s_ang is not None and len(coordinates_s_ang) == n_img:
|
|
109
|
+
s_arr = np.asarray(coordinates_s_ang, dtype=float)
|
|
110
|
+
else:
|
|
111
|
+
s_arr = np.linspace(0.0, float(n_img - 1), n_img)
|
|
112
|
+
|
|
113
|
+
# Reference energies relative to Reactant (image 0)
|
|
114
|
+
e_rel = e_arr - e_arr[0]
|
|
115
|
+
total_length = float(s_arr[-1] - s_arr[0])
|
|
116
|
+
# Distances printed with few decimals can repeat (e.g. a band whose end points coincide); the spline
|
|
117
|
+
# then needs a strictly increasing abscissa, so it falls back to the image index.
|
|
118
|
+
if not np.all(np.diff(s_arr) > 0):
|
|
119
|
+
s_arr = np.linspace(0.0, float(n_img - 1), n_img)
|
|
120
|
+
|
|
121
|
+
# Fit natural cubic spline
|
|
122
|
+
spline = CubicSpline(s_arr, e_rel, bc_type='natural')
|
|
123
|
+
s_fine = np.linspace(s_arr[0], s_arr[-1], 300)
|
|
124
|
+
e_fine = spline(s_fine)
|
|
125
|
+
|
|
126
|
+
# Find peak (Transition State candidate)
|
|
127
|
+
ts_fine_idx = int(np.argmax(e_fine))
|
|
128
|
+
ts_s_pos = float(s_fine[ts_fine_idx])
|
|
129
|
+
ts_peak_e_rel = float(e_fine[ts_fine_idx])
|
|
130
|
+
|
|
131
|
+
# Nearest discrete image to TS peak
|
|
132
|
+
ts_img_idx = int(np.argmin(np.abs(s_arr - ts_s_pos)))
|
|
133
|
+
|
|
134
|
+
spline_barrier_ev = max(0.0, ts_peak_e_rel)
|
|
135
|
+
barrier_source = "spline"
|
|
136
|
+
if ts_energy_rel_ev is not None:
|
|
137
|
+
ts_peak_e_rel = float(ts_energy_rel_ev)
|
|
138
|
+
barrier_source = "optimised TS"
|
|
139
|
+
elif climbing_image_index is not None and band_converged and 0 < climbing_image_index < n_img - 1:
|
|
140
|
+
ts_peak_e_rel = float(e_rel[climbing_image_index])
|
|
141
|
+
barrier_source = "climbing image"
|
|
142
|
+
e_fwd_barrier_ev = max(0.0, ts_peak_e_rel)
|
|
143
|
+
e_fwd_barrier_kcal = float(e_fwd_barrier_ev * EV_TO_KCAL_MOL)
|
|
144
|
+
|
|
145
|
+
e_product_rel_ev = float(e_rel[-1])
|
|
146
|
+
e_rev_barrier_ev = max(0.0, ts_peak_e_rel - e_product_rel_ev)
|
|
147
|
+
e_rev_barrier_kcal = float(e_rev_barrier_ev * EV_TO_KCAL_MOL)
|
|
148
|
+
|
|
149
|
+
delta_e_rxn_ev = e_product_rel_ev
|
|
150
|
+
delta_e_rxn_kcal = float(delta_e_rxn_ev * EV_TO_KCAL_MOL)
|
|
151
|
+
|
|
152
|
+
# Check for intermediate minima (valleys along MEP)
|
|
153
|
+
# Check if there is an internal local minimum between 0 and -1
|
|
154
|
+
well = 0.0
|
|
155
|
+
for i in range(1, n_img - 1):
|
|
156
|
+
if e_rel[i] < e_rel[i-1] and e_rel[i] < e_rel[i+1]:
|
|
157
|
+
# depth below the lower of the two barriers that bound it
|
|
158
|
+
depth = min(np.max(e_rel[:i]), np.max(e_rel[i+1:])) - e_rel[i]
|
|
159
|
+
well = max(well, float(depth * EV_TO_KCAL_MOL))
|
|
160
|
+
has_interm_min = well > min_well_depth_kcal_mol
|
|
161
|
+
|
|
162
|
+
# Force audit
|
|
163
|
+
max_f = None
|
|
164
|
+
ts_f = None
|
|
165
|
+
is_f_conv = True
|
|
166
|
+
if tangent_forces_ev_ang is not None and len(tangent_forces_ev_ang) == n_img:
|
|
167
|
+
# Check force specifically at climbing image
|
|
168
|
+
f_arr = np.abs(np.asarray(tangent_forces_ev_ang, dtype=float))
|
|
169
|
+
max_f = float(np.max(f_arr))
|
|
170
|
+
ts_f = float(f_arr[ts_img_idx])
|
|
171
|
+
if ts_f > force_convergence_threshold and ts_optimisation_converged is not True:
|
|
172
|
+
is_f_conv = False
|
|
173
|
+
|
|
174
|
+
# Image points
|
|
175
|
+
img_points = []
|
|
176
|
+
for i in range(n_img):
|
|
177
|
+
f_val = float(tangent_forces_ev_ang[i]) if tangent_forces_ev_ang is not None and i < len(tangent_forces_ev_ang) else None
|
|
178
|
+
img_points.append(NEBImagePoint(
|
|
179
|
+
image_index=i,
|
|
180
|
+
reaction_coordinate_s_ang=float(s_arr[i]),
|
|
181
|
+
energy_ev=float(e_arr[i]),
|
|
182
|
+
relative_energy_ev=float(e_rel[i]),
|
|
183
|
+
relative_energy_kcal_mol=float(e_rel[i] * EV_TO_KCAL_MOL),
|
|
184
|
+
tangent_force_ev_ang=f_val
|
|
185
|
+
))
|
|
186
|
+
|
|
187
|
+
# Decision logic (first matching condition wins)
|
|
188
|
+
bar = f"E_a^fwd = {e_fwd_barrier_kcal:.2f} kcal/mol ({barrier_source})"
|
|
189
|
+
if coordinates_s_ang is not None and total_length < min_path_length_ang:
|
|
190
|
+
status = "FAIL"
|
|
191
|
+
diag = (f"The path is {total_length:.4f} Å long: both end points are the same structure, so the band "
|
|
192
|
+
f"describes no reaction. Check the end-point optimisations.")
|
|
193
|
+
elif band_converged is False:
|
|
194
|
+
status = "FAIL"
|
|
195
|
+
diag = f"The band did not converge; the highest image does not locate the barrier ({bar})."
|
|
196
|
+
elif ts_optimisation_converged is False:
|
|
197
|
+
status = "FAIL"
|
|
198
|
+
diag = f"The transition-state optimisation did not converge ({bar})."
|
|
199
|
+
elif e_fwd_barrier_ev < 1e-4 and abs(delta_e_rxn_ev) < 1e-4:
|
|
200
|
+
status = "FAIL"
|
|
201
|
+
diag = "Flat energy profile (barrier and reaction energy < 1e-4 eV). Check the end points."
|
|
202
|
+
elif not is_f_conv:
|
|
203
|
+
status = "WARNING"
|
|
204
|
+
diag = (f"Force at the image nearest the barrier is {ts_f:.3f} eV/Å > {force_convergence_threshold:.2f} "
|
|
205
|
+
f"eV/Å ({bar}).")
|
|
206
|
+
elif has_interm_min:
|
|
207
|
+
status = "WARNING"
|
|
208
|
+
diag = (f"Intermediate minimum {well:.2f} kcal/mol deep along the band: the path has more than one "
|
|
209
|
+
f"step, which should be located separately ({bar}).")
|
|
210
|
+
else:
|
|
211
|
+
status = "PASS"
|
|
212
|
+
conv = " Band convergence not reported." if band_converged is None else ""
|
|
213
|
+
diag = (f"No problem found in the band: {bar}, Delta E_rxn = {delta_e_rxn_kcal:.2f} kcal/mol, "
|
|
214
|
+
f"no intermediate minimum deeper than {min_well_depth_kcal_mol:.1f} kcal/mol.{conv}")
|
|
215
|
+
|
|
216
|
+
return NEBProfileResult(
|
|
217
|
+
n_images=n_img,
|
|
218
|
+
total_path_length_ang=total_length,
|
|
219
|
+
ts_image_index=ts_img_idx,
|
|
220
|
+
ts_position_s_ang=ts_s_pos,
|
|
221
|
+
e_forward_barrier_ev=e_fwd_barrier_ev,
|
|
222
|
+
e_forward_barrier_kcal_mol=e_fwd_barrier_kcal,
|
|
223
|
+
e_reverse_barrier_ev=e_rev_barrier_ev,
|
|
224
|
+
e_reverse_barrier_kcal_mol=e_rev_barrier_kcal,
|
|
225
|
+
delta_e_reaction_ev=delta_e_rxn_ev,
|
|
226
|
+
delta_e_reaction_kcal_mol=delta_e_rxn_kcal,
|
|
227
|
+
max_tangent_force_ev_ang=max_f,
|
|
228
|
+
is_climbing_image_converged=is_f_conv,
|
|
229
|
+
has_intermediate_minimum=has_interm_min,
|
|
230
|
+
interpolated_s=s_fine.tolist(),
|
|
231
|
+
interpolated_e_rel_ev=e_fine.tolist(),
|
|
232
|
+
image_points=img_points,
|
|
233
|
+
status=status,
|
|
234
|
+
diagnostic_message=diag,
|
|
235
|
+
e_forward_barrier_spline_ev=float(spline_barrier_ev),
|
|
236
|
+
barrier_source=barrier_source,
|
|
237
|
+
band_converged=band_converged,
|
|
238
|
+
ts_optimisation_converged=ts_optimisation_converged,
|
|
239
|
+
deepest_intermediate_well_kcal_mol=well
|
|
240
|
+
)
|