| """Correlation-matrix and PCA-loadings plotting engine for the leveling-effect study (SI Figure S3). |
| |
| Draws the two figure types built by build_nb_leveling.py: per-nucleus inter-method Pearson |
| correlation matrices (log-scaled, with a scaled-RMSE side bar and a colour-scale histogram inset) |
| and PC1/PC2 method-loading panels (with a fitted parabola and adjustText-repelled labels). The |
| notebook supplies the family-colour map, nucleus display labels, and adjustText tuning as arguments. |
| """ |
| import numpy as np |
| import matplotlib.pyplot as plt |
| from adjustText import adjust_text |
|
|
| from leveling import log_r, parabola_curve |
|
|
|
|
| def _adjust_pca_labels(texts, ax, nuc, pca_adjust, pca_adjust_default): |
| params = pca_adjust.get(nuc, pca_adjust_default) |
| adjust_text(texts, ax=ax, max_move=None, min_arrow_len=0, |
| ensure_inside_axes=True, |
| arrowprops=dict(arrowstyle='-', color='0.55', lw=0.35), |
| **params) |
|
|
|
|
| def _family_legend(ax, families, family_colors, include_ref=True): |
| present = [] |
| for f in families: |
| if f == 'ref' and not include_ref: |
| continue |
| if f not in present: |
| present.append(f) |
| handles = [plt.Line2D([], [], marker='*' if f=='ref' else 'o', ls='', |
| mfc=family_colors.get(f,'#333333'), mec='k', |
| ms=12 if f=='ref' else 6.5, |
| label='CCSD(T)' if f=='ref' else f) |
| for f in present] |
| ax.legend(handles=handles, loc='best', fontsize=6.5, ncol=2, |
| frameon=True, framealpha=0.9) |
|
|
|
|
| def _pca_panel(ax, name, nuc, r, families, family_colors, nuc_display, pca_adjust, pca_adjust_default): |
| pc1, pc2 = r['pc1'], r['pc2'] |
| methods = r['methods'] |
| cx, cy = parabola_curve(r['par']) |
| |
| |
| xpad = 0.08*(pc1.max()-pc1.min()+1e-9) |
| ypad = 0.08*(pc2.max()-pc2.min()+1e-9) |
| xlim = (pc1.min()-xpad, pc1.max()+xpad) |
| ylim = (pc2.min()-ypad, pc2.max()+ypad) |
| m = (cx>=xlim[0])&(cx<=xlim[1])&(cy>=ylim[0])&(cy<=ylim[1]) |
| ax.plot(cx[m], cy[m], color='0.75', ls='--', lw=1.2, alpha=0.7, zorder=1) |
| texts = [] |
| for i, lab in enumerate(methods): |
| fam = families[i] |
| if fam == 'ref': |
| ax.scatter(pc1[i], pc2[i], s=75, marker='*', |
| c=family_colors['ref'], edgecolors='white', |
| linewidths=0.8, zorder=5) |
| else: |
| ax.scatter(pc1[i], pc2[i], s=11, |
| c=family_colors.get(fam, '#333333'), |
| edgecolors='black', linewidths=0.3, zorder=4) |
| texts.append(ax.text(pc1[i], pc2[i], lab, fontsize=3.8, zorder=6)) |
| ax.set_xlim(*xlim); ax.set_ylim(*ylim) |
| _adjust_pca_labels(texts, ax, nuc, pca_adjust, pca_adjust_default) |
| _family_legend(ax, families, family_colors, include_ref=True) |
| ev = r['ev'] |
| ax.set_title(f'{name} {nuc_display.get(nuc, nuc)} - ' |
| f'method loadings in PC1/PC2 space\n' |
| f'PC1 = {ev[0]*100:.2f}%, PC2 = {ev[1]*100:.2f}%, ' |
| f'parabola R² = {r["par"]["r2"]:.3f}', fontsize=9) |
| ax.set_xlabel('PC1 loading', fontsize=8) |
| ax.set_ylabel('PC2 loading', fontsize=8) |
| ax.tick_params(labelsize=7) |
| ax.axhline(0, color='gray', lw=0.4); ax.axvline(0, color='gray', lw=0.4) |
| ax.grid(True, ls=':', alpha=0.3) |
|
|
|
|
| def plot_pca_pair(name, nucs, res, fam_lookup, family_colors, nuc_display, pca_adjust, pca_adjust_default): |
| """PC1/PC2 method-loading panels, one nucleus per row, stacked in a single figure.""" |
| n = len(nucs) |
| fig, axes = plt.subplots(n, 1, figsize=(7.0, 5.2*n), squeeze=False) |
| for k, nuc in enumerate(nucs): |
| _pca_panel(axes[k][0], name, nuc, res[nuc], fam_lookup[nuc], |
| family_colors, nuc_display, pca_adjust, pca_adjust_default) |
| fig.tight_layout(h_pad=3.6) |
| return fig |
|
|
|
|
| def plot_corr_matrix(name, nucs, res, fam_lookup, vmax, ref_name, nuc_display): |
| """Per-nucleus inter-method correlation heatmap with a scaled-RMSE side bar. |
| |
| Layout: [ RMSE bar (viridis, bars grow leftward) | heatmap (inferno) ]. Methods are reordered so |
| the reference is first and the rest are sorted by ascending scaled RMSE vs the reference. The |
| heatmap is the lower triangle with the diagonal removed; its upper-right corner carries an inset |
| histogram of all pairwise log-correlations with the colour bar below it, showing two tick sets |
| (bottom = -log10(1-|r|) integers, top = the equivalent raw r values). All matrices share `vmax`. |
| """ |
| nm = len(res[nucs[0]]['methods']) |
| m = nm - 1 |
| hm_h, hm_w, bar_w = 0.135*nm + 1.9, 6.0, 1.5 |
| fig_w = bar_w + hm_w |
| fig_h = hm_h * len(nucs) + 0.4 |
| fig, axes = plt.subplots(len(nucs), 2, sharey='row', |
| gridspec_kw={'width_ratios': [bar_w, hm_w]}, |
| figsize=(fig_w, fig_h), |
| layout='constrained', squeeze=False) |
| fig.get_layout_engine().set(wspace=0.01) |
| cmap_corr = plt.cm.inferno.copy(); cmap_corr.set_bad('white') |
| cmap_rmse = plt.cm.viridis |
| fs = 4.3 if m > 24 else 6.0 |
| ticks_log = [t for t in [1, 2, 3, 4, 5] if t <= vmax + 0.01] |
| for k, nuc in enumerate(nucs): |
| axR = axes[k, 0]; ax = axes[k, 1] |
| r = res[nuc]; R = r['R']; methods = r['methods'] |
| rmse_dict = r['scaled_rmse'] |
| |
| ref_idx = methods.index(ref_name) |
| others = [i for i in range(nm) if i != ref_idx] |
| others_sorted = sorted(others, key=lambda i: rmse_dict[methods[i]]) |
| new_order = [ref_idx] + others_sorted |
| methods_s = [methods[i] for i in new_order] |
| R_s = R[np.ix_(new_order, new_order)] |
| rmse_s = np.array([rmse_dict[mth] for mth in methods_s]) |
| |
| sub = R_s[1:, :-1] |
| hide = np.triu(np.ones((m, m), dtype=bool), k=1) |
| lr = log_r(sub) |
| im = ax.imshow(np.ma.masked_array(lr, hide), cmap=cmap_corr, |
| vmin=0, vmax=vmax, aspect='auto') |
| ax.set_xticks(range(m)) |
| ax.set_xticklabels(methods_s[:-1], rotation=90, fontsize=fs) |
| ax.tick_params(left=False, labelleft=False) |
| if m <= 20: |
| for i in range(m): |
| for j in range(i+1): |
| ax.text(j, i, f'{sub[i,j]:.5f}', ha='center', va='center', |
| fontsize=3.4, |
| color='white' if lr[i,j] < vmax*0.5 else 'black') |
| ax.set_title(f'{name} {nuc_display.get(nuc, nuc)} - ' |
| f'inter-method Pearson correlation', fontsize=9) |
| |
| |
| |
| |
| |
| |
| all_lr = lr[~hide] |
| counts, edges = np.histogram(all_lr, bins=28, range=(0.0, vmax)) |
| centers = 0.5*(edges[:-1] + edges[1:]) |
| scale = 1.2 if name == 'NS372' else 1.0 |
| hist_w = 0.50 * scale |
| hist_h = 0.18 * scale |
| cb_h = 0.014 |
| gap = 0.012 |
| hist_top = 0.97 |
| x0 = 0.98 - hist_w |
| hist_y0 = hist_top - hist_h |
| cb_y0 = hist_y0 - gap - cb_h |
| hax = ax.inset_axes([x0, hist_y0, hist_w, hist_h]) |
| hax.set_facecolor('white') |
| hax.bar(centers, counts, width=(edges[1]-edges[0])*0.92, |
| color=cmap_corr(centers / vmax), edgecolor='black', linewidth=0.3) |
| hax.axvline(float(np.median(all_lr)), color='black', lw=1.6, ls='--') |
| hax.set_xlim(0, vmax) |
| hax.set_xticks(ticks_log) |
| hax.tick_params(axis='x', labelbottom=False, bottom=True, length=3) |
| hax.set_ylabel('count', fontsize=6, labelpad=1) |
| hax.tick_params(axis='y', labelsize=5) |
| hax.grid(axis='y', ls=':', alpha=0.4) |
| |
| cbax = ax.inset_axes([x0, cb_y0, hist_w, cb_h]) |
| cb = fig.colorbar(im, cax=cbax, orientation='horizontal') |
| cb.set_ticks(ticks_log) |
| cb.ax.set_xticklabels([f'{t}\n0.{"9"*t}' for t in ticks_log]) |
| cb.ax.tick_params(axis='x', labelsize=5.5, pad=1, |
| top=False, labeltop=False) |
| cb.set_label('-log10(1-|r|) / Pearson r', fontsize=6, labelpad=2) |
| |
| row_methods = methods_s[1:] |
| rmse_row = rmse_s[1:] |
| rmse_max = max(rmse_row.max(), 1e-6) |
| bar_colors = cmap_rmse(rmse_row / rmse_max) |
| axR.barh(range(m), rmse_row, color=bar_colors, |
| edgecolor='black', linewidth=0.3) |
| axR.yaxis.set_ticks_position('right') |
| axR.yaxis.set_label_position('right') |
| axR.set_yticks(range(m)) |
| axR.set_yticklabels(row_methods, fontsize=fs) |
| axR.set_xlim(rmse_max * 1.06, 0) |
| axR.set_xlabel(f'scaled RMSE (ppm)\nvs {ref_name}', fontsize=6.5) |
| axR.tick_params(axis='x', labelsize=5.5) |
| axR.tick_params(axis='y', length=0, pad=2) |
| axR.grid(axis='x', ls=':', alpha=0.4) |
| axR.set_ylim(m - 0.5, -0.5) |
| return fig |
|
|