MagNET / analysis /code /fig3_plots.py
ekwan16's picture
Add files using upload-large-folder tool
ef53368 verified
Raw
History Blame Contribute Delete
8.2 kB
"""Plotting engines for Figure 3 panels 3A, 3B, and 3D (delta-22 solvent-correction figures).
Each panel's notebook (fig3a_pcm_benefit.ipynb, fig3b_shifts.ipynb, fig3d_solvent_corrections.ipynb)
supplies the data table and styling dicts and calls the corresponding function here.
"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns
from matplotlib.ticker import MaxNLocator
def plot_pcm_benefit_with_arrows(benefit_df, combos, colors, save_path,
xlimits=(-20, 35), figsize=(6, 6)):
"""Figure 3A: implicit-solvent (PCM) benefit by method, chloroform vs benzene."""
solvents = ["chloroform", "benzene"]
labels = [m for (m, _b, _g) in combos]
arrow_label = "__arrow__"
ordered_labels = [arrow_label] + labels # blank spacer row at the top holds the arrows
rows = []
for method, basis, geom in combos:
for solvent in solvents:
sub = benefit_df[(benefit_df["sap_nmr_method"] == method) &
(benefit_df["sap_basis"] == basis) &
(benefit_df["sap_geometry_type"] == geom) &
(benefit_df["solvent"] == solvent)]
for v in sub["percent_benefit"]:
rows.append({"Label": method, "Solvent": solvent, "PCM_Benefit": float(v)})
long_df = pd.DataFrame(rows)
long_df = pd.concat([long_df, pd.DataFrame([{"Label": arrow_label, "Solvent": solvents[0],
"PCM_Benefit": float(np.mean(xlimits))}])],
ignore_index=True)
long_df["Label"] = pd.Categorical(long_df["Label"], categories=ordered_labels, ordered=True)
sns.set_theme(context="paper", style="white")
fig, ax = plt.subplots(figsize=figsize)
sns.boxplot(data=long_df, x="PCM_Benefit", y="Label", order=ordered_labels,
hue="Solvent", hue_order=solvents, palette=colors, dodge=False, width=0.3,
fliersize=0, linewidth=0.5, whiskerprops=dict(alpha=0.5, linewidth=0.8),
capprops=dict(alpha=0.5, linewidth=1), medianprops=dict(alpha=0.9, linewidth=0.4),
boxprops=dict(alpha=0.9), ax=ax, orient="h")
# blank the spacer row's tick label
ticks = ax.get_yticks()
ax.yaxis.set_major_locator(mticker.FixedLocator(ticks))
ax.yaxis.set_major_formatter(mticker.FixedFormatter(
["" if t.get_text() == arrow_label else t.get_text() for t in ax.get_yticklabels()]))
leg = ax.get_legend()
if leg is not None:
leg.set_title("")
for txt in leg.get_texts():
txt.set_text(txt.get_text().capitalize())
leg.set_frame_on(True); leg.get_frame().set_alpha(0.95)
for t in leg.get_texts():
t.set_fontsize(13)
leg.set_bbox_to_anchor((0.8, 1.02)); leg._loc = 9
# arrows on the blank spacer row
y_spacer = ordered_labels.index(arrow_label)
span = xlimits[1] - xlimits[0]
offset = 0.02 * span
def box_arrow(text, x, ha, facecolor):
ax.text(x, y_spacer, text, ha=ha, va="center", fontsize=11, fontweight="bold",
color="white", clip_on=False, zorder=10,
bbox=dict(boxstyle=f"{'larrow' if ha=='right' else 'rarrow'},pad=0.35",
fc=facecolor, ec="none", alpha=0.85))
box_arrow("Improves Acc.", +offset, "left", "#708B75")
box_arrow("Decreases Acc.", -offset, "right", "#C3A29E")
ax.axvline(0, color="black", lw=0.2, alpha=0.4, zorder=0)
ax.set_xlim(*xlimits)
ax.margins(x=0.01)
ax.set_xlabel("Benefit of PCM (%)", fontsize=13)
ax.set_ylabel("")
sns.despine(ax=ax, left=False, bottom=False)
for spine in ax.spines.values():
spine.set_linewidth(0.6)
ax.tick_params(axis="both", which="major", length=1.5, width=0.6, labelsize=10)
fig.tight_layout()
fig.savefig(save_path, dpi=200, bbox_inches="tight")
plt.show()
def plot_shift_vs_pcm(shift_df, save_path, xlimits=(-0.2, 0.2), ylimits=(-1.6, 0.3), figsize=(8, 4)):
"""Figure 3B: measured vs PCM-predicted solvent-induced shift differences."""
sns.set_theme(style="white", context="paper")
fig, axes = plt.subplots(1, 2, figsize=figsize)
# left: experimental solvent-induced shifts (teal) -- large and uncorrelated
sns.scatterplot(data=shift_df, x="exp_diff_x", y="exp_diff_y", ax=axes[0],
color="#44AA99", s=15, alpha=1.0, edgecolor=None)
# right: PCM solvent corrections (dark red) -- small and linearly correlated. Same axis limits
# as the left panel, so the tiny PCM predictions are visible against the real shifts.
pcm = shift_df.dropna(subset=["pcm_diff_x", "pcm_diff_y"])
sns.scatterplot(data=pcm, x="pcm_diff_x", y="pcm_diff_y", ax=axes[1],
color="#A72608", s=15, alpha=0.5, edgecolor=None)
for ax in axes:
ax.set_xlim(*xlimits); ax.set_ylim(*ylimits)
ax.xaxis.set_major_locator(MaxNLocator(nbins=6))
ax.yaxis.set_major_locator(MaxNLocator(nbins=8))
ax.tick_params(axis="both", which="major", direction="out", length=1.5, width=0.6, labelsize=10)
axes[0].set_xlabel(r"$\delta_{MeOD} - \delta_{CDCl_3}$ ($^{1}\mathrm{H}$ ppm)", fontsize=13)
axes[0].set_ylabel(r"$\delta_{benzene} - \delta_{CDCl_3}$ ($^{1}\mathrm{H}$ ppm)", fontsize=13)
axes[1].set_xlabel(r"$\Delta\sigma_{MeOH} - \Delta\sigma_{CHCl_3}$ ($^{1}\mathrm{H}$ ppm)", fontsize=13)
axes[1].set_ylabel(r"$\Delta\sigma_{benzene} - \Delta\sigma_{CHCl_3}$ ($^{1}\mathrm{H}$ ppm)", fontsize=13)
plt.tight_layout(rect=[0, 0, 1, 0.95])
sns.despine()
for ax in axes:
for spine in ax.spines.values():
spine.set_linewidth(0.6)
fig.savefig(save_path, dpi=200, bbox_inches="tight")
plt.show()
def plot_solvent_correction_boxplot(results_df, formula_labels, solvent_order, colors,
solvent_labels, save_path, box_widths=0.70, figsize=(8, 6)):
"""Figure 3D: implicit vs explicit solvent corrections, one box per solvent."""
labels = list(formula_labels.values()) # implicit, implicit+vib, explicit, explicit+vib
palette = dict(zip(labels, colors))
df = results_df[results_df["formula"].isin(formula_labels)].copy()
df["Model"] = df["formula"].map(formula_labels)
df = df[df["solvent"].isin(solvent_order)].copy()
df["solvent"] = pd.Categorical(df["solvent"], categories=solvent_order, ordered=True)
xpos = {s: float(i) for i, s in enumerate(solvent_order)}
df["xpos"] = df["solvent"].map(xpos).astype(float)
sns.set_theme(context="paper", style="white")
fig, ax = plt.subplots(figsize=figsize)
sns.boxplot(data=df, x="xpos", y="test_RMSE", hue="Model", hue_order=labels, palette=palette,
width=box_widths, showfliers=False, linewidth=0.5,
whiskerprops=dict(alpha=0.5, linewidth=0.8), capprops=dict(alpha=0.5, linewidth=1),
medianprops=dict(alpha=0.9, linewidth=0.4), boxprops=dict(alpha=0.9), ax=ax)
# center each solvent's tick under its "Explicit Solvent" box
per_box = box_widths / len(labels)
offset = (labels.index("Explicit Solvent") - (len(labels) - 1) / 2) * per_box
ax.set_xticks([xpos[s] + offset for s in solvent_order])
ax.set_xticklabels([solvent_labels.get(s, s) for s in solvent_order], rotation=45, fontsize=8, ha="right")
ax.set_ylabel(r"Accuracy vs. Benchmark (RMSE, $^{1}\mathrm{H}$ ppm)", fontsize=13)
ax.set_xlabel("")
ax.set_ylim(bottom=0)
ax.set_title("Accuracy Improves Broadly with Explicit Solvation", fontsize=14, fontweight="bold")
leg = ax.get_legend()
if leg is not None:
leg.set_title("")
for t in leg.get_texts():
t.set_fontsize(13)
ax.yaxis.set_major_locator(MaxNLocator(nbins=6))
ax.tick_params(axis="both", which="major", direction="out", length=1.5, width=0.6, labelsize=10)
ax.grid(False)
sns.despine(ax=ax)
for spine in ax.spines.values():
spine.set_linewidth(0.6)
fig.tight_layout()
fig.savefig(save_path, dpi=200, bbox_inches="tight")
plt.show()