File size: 4,003 Bytes
ef53368 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | """Figure 4 panel-drawing engines.
4A/4B share one scatter engine (predicted vs measured solvent-induced shift, implicit vs explicit
prediction column); 4C is a dumbbell plot of per-solvent fit RMSE vs experimental shift range. The
notebook supplies the data table and the styling dicts (highlight map, order, palette, markers /
colors) as arguments and calls `save_panel(...)` to draw and write the PNG.
"""
import matplotlib.pyplot as plt
import seaborn as sns
from adjustText import adjust_text
def draw_shift_scatter(ax, shift_df, pred_col, highlight_map, order, palette, markers, limits=(-0.25, 2.0)):
"""Figure 4A/4B: scatter predicted vs experimental solvent-induced shift, colored by solvent."""
df = shift_df[shift_df["solvent"].isin(highlight_map)].copy()
df["Solvent"] = df["solvent"].map(highlight_map)
sns.scatterplot(data=df, x="exp_diff", y=pred_col, hue="Solvent", style="Solvent",
hue_order=order, style_order=order, palette=palette,
markers=markers, s=60, alpha=0.6, edgecolor=None, ax=ax)
lo, hi = limits
ax.plot([lo, hi], [lo, hi], linestyle="--", alpha=0.35, linewidth=0.6, color="k") # ideal y = x
ax.set_xlim(lo, hi); ax.set_ylim(lo, hi); ax.set_box_aspect(1)
ax.set_xlabel(r"Experimental Solvent-Induced Shift (Δδ vs. CDCl3, $^{1}\mathrm{H}$ ppm)",
fontsize=13, labelpad=10)
ax.set_ylabel(r"Predicted Solvent-Induced Shift (Δδ vs. CDCl3, $^{1}\mathrm{H}$ ppm)",
fontsize=13, labelpad=10)
leg = ax.get_legend()
if leg is not None:
leg.set_title(None)
for side in ax.spines:
ax.spines[side].set_linewidth(0.6)
ax.tick_params(labelsize=10, length=1.5, width=0.6)
def save_panel(draw_fn, save_path, figsize=(6, 6)):
"""Create a figure/axes, call `draw_fn(ax)`, then save and show the panel as a PNG."""
sns.set_theme(style="ticks", context="paper")
fig, ax = plt.subplots(figsize=figsize)
draw_fn(ax)
fig.tight_layout()
fig.savefig(save_path, dpi=200, bbox_inches="tight")
plt.show()
def draw_rmse_dumbbell(ax, rows, labels, colors):
"""Figure 4C: dumbbell plot of implicit vs explicit solvent-correction RMSE per solvent."""
rows_sorted = sorted(rows, key=lambda r: r["range"])
xs = [r["range"] for r in rows_sorted]
for r in rows_sorted: # gray dumbbell connectors
ax.plot([r["range"], r["range"]], [r["implicit"], r["explicit"]],
color="#cccccc", lw=1.5, zorder=1)
ax.scatter(xs, [r["implicit"] for r in rows_sorted], s=60, marker="D",
color=colors["Implicit (PCM)"], alpha=0.85, edgecolors="none",
label="Implicit (PCM)", zorder=2)
ax.scatter(xs, [r["explicit"] for r in rows_sorted], s=60, marker="o",
color=colors["Explicit (Desmond)"], alpha=0.85, edgecolors="none",
label="Explicit (Desmond)", zorder=2)
# frame the data, then label each dumbbell at its midpoint (adjustText avoids overlaps)
xr = max(xs) - min(xs); yv = [r[k] for r in rows_sorted for k in ("implicit", "explicit")]
ax.set_xlim(min(xs) - 0.06 * xr, max(xs) + 0.10 * xr)
ax.set_ylim(min(yv) - 0.06 * (max(yv) - min(yv)), max(yv) + 0.10 * (max(yv) - min(yv)))
texts = [ax.text(r["range"], 0.5 * (r["implicit"] + r["explicit"]), labels[r["solvent"]],
fontsize=10, color="black", va="center", ha="left") for r in rows_sorted]
adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle="-", color="gray", lw=0.5, alpha=0.8))
ax.legend(loc="upper left", framealpha=0.9, fontsize=11)
ax.set_box_aspect(1)
ax.set_xlabel(r"Range of Solvent-Induced Shifts ($^{1}\mathrm{H}$ ppm)", fontsize=13, labelpad=10)
ax.set_ylabel(r"Predicted Solvent-Induced Shift RMSE ($^{1}\mathrm{H}$ ppm)", fontsize=13, labelpad=10)
for side in ax.spines:
ax.spines[side].set_linewidth(0.6)
ax.tick_params(labelsize=10, length=1.5, width=0.6)
|