Skip to content

Commit 4c53be2

Browse files
authored
Add files via upload
1 parent 5f088c7 commit 4c53be2

1 file changed

Lines changed: 197 additions & 52 deletions

File tree

multioptpy/Wrapper/autots.py

Lines changed: 197 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import os
32
import shutil
43
import numpy as np
@@ -277,6 +276,7 @@ def _run_step4_irc_and_opt(self, ts_final_files):
277276
# Get TS energy (as per Q2, this is set before IRC runs)
278277
ts_e = irc_instance.final_energy
279278
ts_bias_e = irc_instance.final_bias_energy
279+
ts_g = self._extract_free_energy(irc_instance)
280280

281281
# Get IRC endpoint paths
282282
endpoint_paths = irc_instance.irc_terminal_struct_paths
@@ -327,6 +327,7 @@ def _run_step4_irc_and_opt(self, ts_final_files):
327327
"path": final_opt_path,
328328
"e": opt_instance.final_energy,
329329
"bias_e": opt_instance.final_bias_energy,
330+
"g": self._extract_free_energy(opt_instance),
330331
"label": f"Endpoint_{j+1}"
331332
})
332333

@@ -345,7 +346,7 @@ def _run_step4_irc_and_opt(self, ts_final_files):
345346
os.makedirs(result_dir, exist_ok=True)
346347

347348
e_profile = {
348-
"TS": {"e": ts_e, "bias_e": ts_bias_e, "path": ts_path},
349+
"TS": {"e": ts_e, "bias_e": ts_bias_e, "g": ts_g, "path": ts_path},
349350
}
350351
if len(endpoint_results) >= 1:
351352
e_profile["End1"] = endpoint_results[0]
@@ -372,103 +373,247 @@ def _run_step4_irc_and_opt(self, ts_final_files):
372373

373374
print("\n--- STEP 4: IRC & EQ OPTIMIZATION COMPLETE ---")
374375

376+
@staticmethod
377+
def _extract_free_energy(instance):
378+
"""Return G_tot (Hartree, float) from an optimizer instance, or None.
379+
380+
Looks up ``instance.thermochemistry_results["G_tot"][0]``. Returns
381+
``None`` silently when vibrational analysis was not performed or when
382+
the key is absent, so callers can treat ``None`` as "not available".
383+
"""
384+
try:
385+
thermo = getattr(instance, "thermochemistry_results", None)
386+
if thermo is None:
387+
return None
388+
return thermo["G_tot"][0]
389+
except (KeyError, TypeError, IndexError):
390+
return None
391+
375392
def _create_energy_profile_plot(self, e_profile, output_path, title_name):
376-
"""Generates an energy profile plot using matplotlib."""
393+
"""Generate an energy profile plot, optionally with a free-energy panel.
394+
395+
When at least one structure in *e_profile* carries a ``"g"`` key with a
396+
non-None value the figure is split into two vertically stacked subplots:
397+
398+
* **Top panel** – electronic / bias energy profile (existing behaviour).
399+
* **Bottom panel** – Gibbs free energy profile (``G_tot`` from
400+
``thermochemistry_results``).
401+
402+
Structures whose ``"g"`` value is ``None`` are shown as gaps on the
403+
free-energy panel with an ``"N/A"`` annotation so the reader knows the
404+
data was not available rather than zero.
405+
406+
When *no* structure has a ``"g"`` value the plot falls back to the
407+
original single-panel layout.
408+
"""
377409
if not MATPLOTLIB_AVAILABLE:
378410
print(f" Skipping plot generation: matplotlib not installed.")
379411
return
380-
412+
381413
try:
382414
labels = []
383415
energies = []
384416
biased_energies = []
385-
386-
417+
free_energies = [] # None where not available
418+
387419
if "End1" in e_profile:
388420
labels.append("End1")
389421
energies.append(e_profile["End1"]["e"])
390422
biased_energies.append(e_profile["End1"]["bias_e"])
391-
423+
free_energies.append(e_profile["End1"].get("g"))
424+
392425
labels.append("TS")
393426
energies.append(e_profile["TS"]["e"])
394427
biased_energies.append(e_profile["TS"]["bias_e"])
395-
428+
free_energies.append(e_profile["TS"].get("g"))
429+
396430
if "End2" in e_profile:
397431
labels.append("End2")
398432
energies.append(e_profile["End2"]["e"])
399433
biased_energies.append(e_profile["End2"]["bias_e"])
400-
401-
434+
free_energies.append(e_profile["End2"].get("g"))
435+
402436
if not energies:
403437
print(" Warning: No energies found to plot.")
404438
return
405-
406-
# Convert to relative kcal/mol
439+
440+
# Determine whether any free-energy data is present
441+
has_free_energy = any(g is not None for g in free_energies)
442+
443+
# --- Convert electronic/bias energies to relative kcal/mol ---
407444
min_e = min(energies)
408445
min_bias_e = min(biased_energies)
409-
410446
rel_energies = (np.array(energies) - min_e) * 627.509
411447
rel_biased_energies = (np.array(biased_energies) - min_bias_e) * 627.509
412-
413-
x = list(range(len(labels)))
414-
plt.figure(figsize=(8, 6))
415-
416-
# Plot both energies as requested in Q1
417-
plt.plot(x, rel_energies, 'o-', c='blue', label='Energy (E_final)')
418-
plt.plot(x, rel_biased_energies, 'o--', c='red', label='Bias Energy (E_bias_final)')
419-
420-
plt.xticks(x, labels)
421-
plt.ylabel("Relative Energy (kcal/mol)")
422-
plt.title(f"Reaction Profile for {title_name}")
423-
plt.legend()
424-
plt.grid(axis='y', linestyle='--', alpha=0.7)
425-
plt.savefig(output_path, dpi=300)
448+
x = list(range(len(labels)))
449+
450+
if has_free_energy:
451+
# Two-panel figure: electronic energy (top) + free energy (bottom)
452+
fig, (ax_e, ax_g) = plt.subplots(
453+
2, 1, figsize=(8, 10),
454+
sharex=True,
455+
gridspec_kw={"hspace": 0.35},
456+
)
457+
458+
# --- Top panel: electronic / bias energies ---
459+
ax_e.plot(x, rel_energies, "o-", c="blue", label="Energy (E_final)")
460+
ax_e.plot(x, rel_biased_energies, "o--", c="red", label="Bias Energy (E_bias_final)")
461+
ax_e.set_xticks(x)
462+
ax_e.set_xticklabels(labels)
463+
ax_e.set_ylabel("Relative Energy (kcal/mol)")
464+
ax_e.set_title(f"Electronic Energy Profile – {title_name}")
465+
ax_e.legend()
466+
ax_e.grid(axis="y", linestyle="--", alpha=0.7)
467+
468+
# --- Bottom panel: Gibbs free energy ---
469+
# Build arrays; use NaN for missing points so matplotlib breaks
470+
# the line but still plots the available segments.
471+
g_values_ha = [g if g is not None else float("nan") for g in free_energies]
472+
g_arr = np.array(g_values_ha, dtype=float)
473+
474+
# Compute reference using the minimum of available (non-NaN) values
475+
valid_g = g_arr[~np.isnan(g_arr)]
476+
if valid_g.size > 0:
477+
min_g = valid_g.min()
478+
rel_g = (g_arr - min_g) * 627.509
479+
else:
480+
rel_g = g_arr # All NaN – panel will be empty
481+
482+
ax_g.plot(x, rel_g, "o-", c="green", label="Free Energy (G_tot)")
483+
484+
# Annotate N/A for unavailable points
485+
for xi, (g_val, lbl) in enumerate(zip(free_energies, labels)):
486+
if g_val is None:
487+
ax_g.annotate(
488+
"N/A",
489+
xy=(xi, 0),
490+
xytext=(xi, 0),
491+
ha="center",
492+
va="bottom",
493+
color="gray",
494+
fontsize=9,
495+
)
496+
print(
497+
f" Warning: Free energy not available for '{lbl}' "
498+
f"(vibrational analysis may not have been performed)."
499+
)
500+
501+
ax_g.set_xticks(x)
502+
ax_g.set_xticklabels(labels)
503+
ax_g.set_ylabel("Relative Free Energy (kcal/mol)")
504+
ax_g.set_title(f"Gibbs Free Energy Profile – {title_name}")
505+
ax_g.legend()
506+
ax_g.grid(axis="y", linestyle="--", alpha=0.7)
507+
508+
else:
509+
# Original single-panel layout (no free-energy data at all)
510+
fig, ax_e = plt.subplots(figsize=(8, 6))
511+
ax_e.plot(x, rel_energies, "o-", c="blue", label="Energy (E_final)")
512+
ax_e.plot(x, rel_biased_energies, "o--", c="red", label="Bias Energy (E_bias_final)")
513+
ax_e.set_xticks(x)
514+
ax_e.set_xticklabels(labels)
515+
ax_e.set_ylabel("Relative Energy (kcal/mol)")
516+
ax_e.set_title(f"Reaction Profile for {title_name}")
517+
ax_e.legend()
518+
ax_e.grid(axis="y", linestyle="--", alpha=0.7)
519+
520+
plt.savefig(output_path, dpi=300, bbox_inches="tight")
426521
plt.close()
427522
print(f" Generated energy plot: {output_path}")
428-
523+
429524
except Exception as e:
430525
print(f" Warning: Failed to generate energy plot: {e}")
431526

432527
def _write_energy_profile_text(self, e_profile, output_path, title_name):
433-
"""Writes the final energies to a text file."""
528+
"""Write final energies to a text file.
529+
530+
The existing electronic-energy table is preserved unchanged. When at
531+
least one structure carries a ``"g"`` key a dedicated free-energy
532+
section is appended after the existing content, including:
533+
534+
* A table of absolute ``G_tot`` values (Hartree).
535+
* Activation free energies (ΔG‡) and reaction free energy (ΔG_rxn)
536+
in kcal/mol, or ``N/A`` where the data is missing.
537+
"""
538+
KCAL = 627.509 # Hartree → kcal/mol
539+
434540
try:
435-
with open(output_path, 'w') as f:
541+
with open(output_path, "w") as f:
542+
# ----------------------------------------------------------------
543+
# Existing electronic-energy section (unchanged)
544+
# ----------------------------------------------------------------
436545
f.write(f"# Energy Profile for {title_name}\n")
437546
f.write("# All energies in Hartree\n")
438547
f.write("# -------------------------------------------------------------------\n")
439-
f.write(f"Structure, File_Path, Final_Energy, Final_Bias_Energy\n")
440-
441-
442-
ts_total = e_profile['TS']['bias_e']
548+
f.write("Structure, File_Path, Final_Energy, Final_Bias_Energy\n")
549+
550+
ts_total = e_profile["TS"]["bias_e"]
443551
f.write(f"TS, {e_profile['TS']['path']}, {e_profile['TS']['e']:.12f}, {e_profile['TS']['bias_e']:.12f}\n")
444552

445553
end1_total = None
446554
if "End1" in e_profile:
447-
end1_total = e_profile['End1']['bias_e']
555+
end1_total = e_profile["End1"]["bias_e"]
448556
f.write(f"Endpoint_1, {e_profile['End1']['path']}, {e_profile['End1']['e']:.12f}, {e_profile['End1']['bias_e']:.12f}\n")
449-
557+
450558
end2_total = None
451559
if "End2" in e_profile:
452-
end2_total = e_profile['End2']['bias_e']
560+
end2_total = e_profile["End2"]["bias_e"]
453561
f.write(f"Endpoint_2, {e_profile['End2']['path']}, {e_profile['End2']['e']:.12f}, {e_profile['End2']['bias_e']:.12f}\n")
454-
562+
455563
f.write("# -------------------------------------------------------------------\n")
456-
# Calculate and write barriers and reaction energies if possible
457564
if end1_total is not None:
458-
barrier_1 = (ts_total - end1_total) * 627.509
565+
barrier_1 = (ts_total - end1_total) * KCAL
459566
f.write(f"Activation Energy (End1 -> TS): {barrier_1: .2f} kcal/mol\n")
460-
461567
if end2_total is not None:
462-
barrier_2 = (ts_total - end2_total) * 627.509
568+
barrier_2 = (ts_total - end2_total) * KCAL
463569
f.write(f"Activation Energy (End2 -> TS): {barrier_2: .2f} kcal/mol\n")
464-
465570
if end1_total is not None and end2_total is not None:
466-
reaction_e = (end2_total - end1_total) * 627.509
571+
reaction_e = (end2_total - end1_total) * KCAL
467572
f.write(f"Reaction Energy (End1 -> End2): {reaction_e: .2f} kcal/mol\n")
468-
elif end1_total is not None:
469-
pass
470-
471-
573+
574+
# ----------------------------------------------------------------
575+
# Free-energy section – appended when any G_tot data is present
576+
# ----------------------------------------------------------------
577+
ts_g = e_profile["TS"].get("g")
578+
end1_g = e_profile["End1"].get("g") if "End1" in e_profile else None
579+
end2_g = e_profile["End2"].get("g") if "End2" in e_profile else None
580+
581+
if any(v is not None for v in [ts_g, end1_g, end2_g]):
582+
f.write("\n")
583+
f.write("# ===================================================================\n")
584+
f.write("# FREE ENERGY SECTION (G_tot from thermochemistry analysis)\n")
585+
f.write("# ===================================================================\n")
586+
f.write("# All values in Hartree unless noted otherwise.\n")
587+
f.write("# 'N/A' indicates vibrational analysis was not performed for that\n")
588+
f.write("# structure; results in that row should be interpreted with caution.\n")
589+
f.write("# -------------------------------------------------------------------\n")
590+
f.write("Structure, G_tot [Hartree]\n")
591+
592+
def _fmt_g(val):
593+
return f"{val:.12f}" if val is not None else "N/A"
594+
595+
f.write(f"TS, {_fmt_g(ts_g)}\n")
596+
if "End1" in e_profile:
597+
f.write(f"Endpoint_1, {_fmt_g(end1_g)}\n")
598+
if "End2" in e_profile:
599+
f.write(f"Endpoint_2, {_fmt_g(end2_g)}\n")
600+
601+
f.write("# -------------------------------------------------------------------\n")
602+
603+
# ΔG‡ and ΔG_rxn – only when both operands are available
604+
def _delta_kcal(a, b):
605+
"""Return (a - b) * KCAL as formatted string, or 'N/A'."""
606+
if a is None or b is None:
607+
return "N/A"
608+
return f"{(a - b) * KCAL: .2f} kcal/mol"
609+
610+
if "End1" in e_profile:
611+
f.write(f"Activation Free Energy (End1 -> TS): {_delta_kcal(ts_g, end1_g)}\n")
612+
if "End2" in e_profile:
613+
f.write(f"Activation Free Energy (End2 -> TS): {_delta_kcal(ts_g, end2_g)}\n")
614+
if "End1" in e_profile and "End2" in e_profile:
615+
f.write(f"Reaction Free Energy (End1 -> End2): {_delta_kcal(end2_g, end1_g)}\n")
616+
472617
print(f" Generated energy text file: {output_path}")
473618
except Exception as e:
474619
print(f" Warning: Failed to write energy text file: {e}")
@@ -1167,6 +1312,7 @@ def _run_step4(self, settings, input_data, run_index=0):
11671312

11681313
ts_e = irc_instance.final_energy
11691314
ts_bias_e = irc_instance.final_bias_energy
1315+
ts_g = self._extract_free_energy(irc_instance)
11701316
endpoint_paths = irc_instance.irc_terminal_struct_paths
11711317

11721318
if not endpoint_paths or len(endpoint_paths) != 2:
@@ -1203,6 +1349,7 @@ def _run_step4(self, settings, input_data, run_index=0):
12031349
"path": final_opt_path,
12041350
"e": opt_instance.final_energy,
12051351
"bias_e": opt_instance.final_bias_energy,
1352+
"g": self._extract_free_energy(opt_instance),
12061353
})
12071354

12081355
if not endpoint_results:
@@ -1213,7 +1360,7 @@ def _run_step4(self, settings, input_data, run_index=0):
12131360
result_dir = f"{ts_name_base}_Step4_Profile"
12141361
os.makedirs(result_dir, exist_ok=True)
12151362

1216-
e_profile = {"TS": {"e": ts_e, "bias_e": ts_bias_e, "path": ts_path}}
1363+
e_profile = {"TS": {"e": ts_e, "bias_e": ts_bias_e, "g": ts_g, "path": ts_path}}
12171364
if len(endpoint_results) >= 1: e_profile["End1"] = endpoint_results[0]
12181365
if len(endpoint_results) >= 2: e_profile["End2"] = endpoint_results[1]
12191366

@@ -1234,6 +1381,4 @@ def _run_step4(self, settings, input_data, run_index=0):
12341381
# --- FIX: Store relative path ---
12351382
profile_dirs.append(result_dir)
12361383

1237-
return {"profile_dirs": profile_dirs}
1238-
1239-
1384+
return {"profile_dirs": profile_dirs}

0 commit comments

Comments
 (0)