
Nature Figure
- 18 installs
- 45 repo stars
- Updated June 23, 2026
- yuanyuanma03/academic-research-skills
Produces submission-grade Nature-family manuscript figures in Python or R, exporting journal-ready SVG, PDF, or TIFF.
About
Creates, revises, and audits multi-panel scientific manuscript figures using matplotlib/seaborn or ggplot2/patchwork with journal-ready exports. A researcher uses it to build high-impact-journal figures after choosing Python or R.
- Python (matplotlib) or R (ggplot2) backends
- Journal-ready SVG/PDF/TIFF export and QA
Nature Figure by the numbers
- 18 all-time installs (skills.sh)
- Ranked #1,278 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yuanyuanma03/academic-research-skills --skill nature-figureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 45 |
| Last updated | June 23, 2026 |
| Repository | yuanyuanma03/academic-research-skills ↗ |
What it does
Produces submission-grade Nature-family manuscript figures in Python or R, exporting journal-ready SVG, PDF, or TIFF.
Files
Nature Figure Making Skill
A guide for producing publication-quality scientific figures as a visual argument, not as isolated pretty plots. Every figure starts from a claim, an evidence hierarchy, and a review-risk check before code or aesthetics.
The older Python/matplotlib rules in this skill remain valid. The skill now also supports R, especially ggplot2 + patchwork + ComplexHeatmap + ggrepel + svglite/cairo_pdf + ragg. If the user provides a private plotting template collection, use it only as an internal adaptation source and do not reveal its path, filenames, or provenance in user-facing output.
Color policy: prefer unified method families across all panels over maximal hue separation. For dense Nature Machine Intelligence-style figure pages, use the low-saturation NMI pastel family described in references/api.md and reserve green/red mainly for gains, drops, and other directional cues.
First move: figure contract before plotting
Before generating or editing code, establish the contract below.
Backend selection is a blocking gate. If the user has not explicitly chosen Python or R in the current request or provided a clearly language-specific input file/workflow, ask one concise question: Python or R? Then stop and wait for the user's answer. Do not generate mock data, write scripts, create figures, or choose Python/R by default. This overrides general autonomy/default-execution behavior for figure tasks.
The selected backend is exclusive for all figure generation. Once Python or R is selected, every plotting script, preview image, SVG/PDF/TIFF/PNG export, QA render, and visual workaround must be produced by that same backend. Do not use Python to draw a preview for an R figure, and do not use R to draw a preview for a Python figure, even if the selected runtime or packages are missing locally. The non-selected language may only be used for non-visual file inspection or data conversion when it does not open a graphics device, import plotting libraries, create image/vector files, or change the final visual appearance.
Missing runtime/package rule. After the backend is selected, check the selected runtime early (Rscript/R for R; Python and required plotting packages for Python). If the selected runtime or required packages are unavailable, stop before rendering and report the exact blocker. You may provide a selected-backend script and installation commands, or ask permission to install dependencies, but you must not fall back to the other language to make a substitute figure.
Only recommend a backend when the user explicitly asks you to choose or recommend one. In that case, use references/backend-selection.md, state the reason, and then proceed with the recommended backend.
1. Core conclusion: write the one-sentence claim the figure must defend. 2. Evidence chain: map each planned panel to the claim, and drop panels that do not carry a unique piece of evidence. 3. Archetype: classify the figure as quantitative grid, schematic-led composite, image plate + quant, or asymmetric mixed-modality figure. 4. Backend: use the selected Python or R track exclusively for all figure drawing, previewing, exporting, and visual QA. Do not cross-render with the other language. 5. Journal/export contract: set final dimensions, editable text, source data, statistics, image-integrity notes, and export formats before styling.
The highest-priority rule is: the chart serves the scientific logic. Aesthetic polish, template matching, and complex layout are subordinate to making the core conclusion clear, defensible, and reviewable.
User-facing privacy rule
Do not disclose private local paths, private filenames, chat-attachment names, internal reference filenames, template identifiers, or the provenance of private working materials in user-facing replies, generated code comments, figure legends, reports, or manuscript text. Use generic descriptions such as "the provided R template collection", "a private working draft", or "the internal figure contract". Only reveal an exact path or source file when the user explicitly asks for that audit trail.
Python quick-start
Python-only execution rule. When the user has selected Python, do all figure drawing, previewing, exporting, and visual QA in Python. Do not call R/ggplot2, ComplexHeatmap, patchwork, or any R graphics device to create a temporary preview, fallback export, or layout approximation. If Python or required Python plotting packages are missing, stop before rendering and report the missing dependency. You may still write the Python script, provide pip/environment install commands, or ask permission to install dependencies, but do not cross-render the figure in R.
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rcParams.update({
"font.family": "sans-serif",
"font.sans-serif": ["Arial", "Helvetica", "DejaVu Sans", "sans-serif"],
"svg.fonttype": "none", # editable text in SVG
"pdf.fonttype": 42, # editable TrueType text in PDF
"font.size": 7, # use 15-24 only for large slide-sized panels
"axes.spines.right": False,
"axes.spines.top": False,
"axes.linewidth": 0.8,
"legend.frameon": False,
})
def save_pub_py(fig, filename, dpi=600):
fig.savefig(f"{filename}.svg", bbox_inches="tight")
fig.savefig(f"{filename}.pdf", bbox_inches="tight")
fig.savefig(f"{filename}.tiff", dpi=dpi, bbox_inches="tight")Use text.usetex = True only when LaTeX is installed and math-rich labels are required.
R quick-start
library(ggplot2)
library(patchwork)
theme_set(
theme_classic(base_size = 6.5, base_family = "Arial") +
theme(
axis.line = element_line(linewidth = 0.35, colour = "black"),
axis.ticks = element_line(linewidth = 0.35, colour = "black"),
legend.title = element_text(size = 6.2),
legend.text = element_text(size = 5.8),
strip.text = element_text(size = 6.2, face = "bold"),
plot.title = element_text(size = 7, face = "bold"),
panel.grid = element_blank()
)
)
save_pub_r <- function(plot, filename, width_mm = 183, height_mm = 120, dpi = 600) {
w <- width_mm / 25.4
h <- height_mm / 25.4
svglite::svglite(paste0(filename, ".svg"), width = w, height = h)
print(plot)
dev.off()
grDevices::cairo_pdf(paste0(filename, ".pdf"), width = w, height = h, family = "Arial")
print(plot)
dev.off()
ragg::agg_tiff(paste0(filename, ".tiff"), width = w, height = h, units = "in", res = dpi)
print(plot)
dev.off()
}Default operating stance
- Start by classifying the requested figure into one of four archetypes:
quantitative grid, schematic-led composite, image plate + quant, or asymmetric mixed-modality figure.
- Prefer one hero panel plus subordinate evidence panels over filling the canvas with equal-sized subplots.
- If the user asks for a single chart, still identify its role in the manuscript claim:
discovery, mechanism, validation, comparison, robustness, or clinical/biological relevance.
- Keep the background white for plots and diagrams; switch to black only for microscopy / volume-rendering image plates.
- Prefer direct labels over legends when categories are spatially fixed or the legend would force unnecessary eye travel.
- Keep one restrained palette per figure: usually one neutral family, one signal family, and one accent family.
- Treat statistics,
n, error-bar definitions, source-data traceability, and image-integrity notes as part of the figure,
not as optional caption cleanup.
- When the user asks for broad
Naturestyle rather than ML/NMI-specific style, readreferences/nature-2026-observations.mdbefore choosing layout. - When the user references
figures4papersor the olderscientific-figure-makingskill,
treat this skill as the successor and open references/demos.md for bundled Python demo scripts.
When to load this skill
- Python or R figures for papers, slides, or reports targeting Nature, Science, Cell, NeurIPS, ICLR, or similar venues.
- Requests involving grouped bars, trend lines, heatmaps, radar plots, multi-panel grids, or PDF/SVG/high-DPI output.
- Any mention of "Nature style", "publication figure", "paper figure", "SCI figure", "figures4papers", "scientific-figure-making", "R plotting template", or "high-quality scientific plot".
- Requests to improve a figure's logic, aesthetics, panel layout, figure legend, export quality, or journal-readiness.
When NOT to load
- Plotly, Altair, Bokeh, or other interactive/web-first plotting.
- EDA-only plots without a publication target.
- Primary workflow is 3D, GIS, or non-scientific illustration tooling.
- Illustrator / Figma–first layout.
Related files
| File | Open when |
|---|---|
| references/figure-contract.md | Need to convert a user request into core conclusion, evidence hierarchy, panel map, and review-risk checks |
| references/backend-selection.md | User has not chosen Python/R, asks for a recommendation, or a mixed Python/R workflow is possible |
| references/r-workflow.md | User chooses R or provides R scripts/templates/data |
| references/r-template-index.md | Need to adapt a user-provided or private R template collection without exposing source paths |
| references/qa-contract.md | Before final delivery, revision package, microscopy/blot figure, or journal-specific audit |
| references/design-theory.md | Typography, color theory, layout rationale, export policy |
| references/api.md | Python PALETTE, helper function signatures, validation rules |
| references/common-patterns.md | Python layout patterns: hero panels, legend-only axes, dark image plates, asymmetric layouts |
| references/nature-2026-observations.md | Real Nature page archetypes: schematic-led composites, dark image plates, clinical triptychs, asymmetric hero layouts |
| references/tutorials.md | End-to-end walkthroughs: bars, trends, heatmaps |
| references/chart-types.md | Radar, 3D sphere, fill_between, scatter patterns |
| references/demos.md | Bundled figures4papers Python scripts and output previews for concrete pattern adaptation |
.DS_Store
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec as gridspec
from matplotlib import patheffects as path_effects
data_brute_force_math = {
'methods': [
r'DeepSeek R1 Distill Qwen 1.5B',
r'DeepSeek R1 Distill Qwen 14B',
r'DeepSeek R1 Distill Llama 70B',
r'deepseek-chat (Deepseek-V3)',
r'deepseek-reasoner (Deepseek-R1)',
r'gemini-2.5-flash-preview-04-17',
r'OpenAI o3'
],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#FFF6CC', '#3775BA'],
'prompts': ['CoT Prompt', 'Math Prompt', 'Hint Prompt', 'Math + Hint'],
'subtypes': [r'$\bf{Only}$ $\bf{Model}$ brute force',
r'$\bf{Only}$ $\bf{Human}$ brute force',
r'$\bf{Neither}$ brute force',
r'$\bf{Both}$ brute force'],
'hatch_styles': ['/', '\\', '', 'x'],
'result': {
'CoT Prompt': np.array([[26.8, 3.6, 60.0, 9.6],
[27.6, 3.2, 59.2, 10.0],
[24.4, 4.0, 62.4, 9.2],
[31.2, 3.6, 55.6, 9.6],
[14.0, 7.2, 72.8, 6.0],
[16.9, 5.9, 70.0, 7.2],
[9.5, 7.5, 79.4, 3.5]]) / 100,
'Math Prompt': np.array([[27.2, 4.8, 59.6, 8.4],
[25.6, 4.8, 61.2, 8.4],
[24.4, 4.8, 62.4, 8.4],
[28.4, 3.2, 58.4, 10.0],
[10.0, 5.6, 76.7, 7.6],
[12.6, 5.2, 74.8, 7.4],
[4.2, 7.9, 85.7, 2.1]]) / 100,
'Hint Prompt': np.array([[29.6, 4.4, 57.2, 8.8],
[27.6, 2.8, 59.2, 10.4],
[20.4, 5.2, 66.4, 8.0],
[28.0, 3.2, 58.8, 10.0],
[14.0, 6.0, 72.8, 7.2],
[12.4, 5.1, 74.4, 8.1],
[6.8, 6.8, 83.2, 3.1]]) / 100,
'Math + Hint': np.array([[26.4, 4.0, 60.4, 9.2],
[27.6, 3.2, 59.2, 10.0],
[24.0, 4.8, 62.8, 8.4],
[25.2, 2.8, 61.6, 10.4],
[8.0, 6.8, 78.8, 6.4],
[10.0, 6.1, 76.4, 7.4],
[3.7, 6.4, 86.7, 3.2]]) / 100,
},
}
data_brute_force_logic = {
'methods': [
r'DeepSeek R1 Distill Qwen 1.5B',
r'DeepSeek R1 Distill Qwen 14B',
r'DeepSeek R1 Distill Llama 70B',
r'deepseek-chat (Deepseek-V3)',
r'deepseek-reasoner (Deepseek-R1)',
r'gemini-2.5-flash-preview-04-17',
r'OpenAI o3'
],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#FFF6CC', '#3775BA'],
'prompts': ['CoT Prompt', 'Math Prompt', 'Hint Prompt', 'Math + Hint'],
'subtypes': [r'$\bf{Only}$ $\bf{Model}$ brute force',
r'$\bf{Only}$ $\bf{Human}$ brute force',
r'$\bf{Neither}$ brute force',
r'$\bf{Both}$ brute force'],
'hatch_styles': ['/', '\\', '', 'x'],
'result': {
'CoT Prompt': np.array([[18.0, 5.2, 72.0, 4.8],
[30.8, 3.2, 59.2, 6.8],
[23.2, 2.8, 66.8, 7.2],
[32.4, 3.6, 57.6, 6.4],
[13.7, 6.8, 76.7, 2.8],
[15.7, 3.2, 75.6, 5.5],
[15.9, 6.5, 75.6, 2.0]]) / 100,
'Math Prompt': np.array([[20.0, 4.4, 70.0, 5.6],
[31.2, 4.0, 58.8, 6.0],
[24.4, 4.0, 65.6, 6.0],
[33.6, 3.6, 56.4, 6.4],
[12.4, 7.6, 77.6, 2.4],
[15.2, 5.5, 75.7, 3.7],
[10.5, 6.2, 80.9, 2.4]]) / 100,
'Hint Prompt': np.array([[15.6, 4.8, 74.4, 5.2],
[30.4, 3.2, 59.6, 6.8],
[25.2, 2.8, 64.8, 7.2],
[27.6, 2.8, 62.4, 7.2],
[9.6, 6.0, 80.7, 3.6],
[14.3, 4.5, 75.8, 5.4],
[6.9, 5.9, 84.8, 2.5]]) / 100,
'Math + Hint': np.array([[20.0, 3.2, 70.0, 6.8],
[32.0, 4.8, 58.0, 5.2],
[19.6, 4.0, 70.4, 6.0],
[26.0, 4.0, 64.0, 6.0],
[8.8, 5.6, 81.5, 4.0],
[13.0, 5.3, 76.8, 4.8],
[5.1, 7.4, 86.5, 0.9]]) / 100,
},
}
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(52, 12))
gs = gridspec.GridSpec(2, 5)
for prompt_idx, prompt_name in enumerate(data_brute_force_math['prompts']):
ax = fig.add_subplot(gs[prompt_idx])
num_methods = len(data_brute_force_math['methods'])
bars = ax.bar(
np.arange(num_methods),
data_brute_force_math['result'][prompt_name][:, 0],
color=data_brute_force_math['colors'],
label=data_brute_force_math['methods'],
hatch=data_brute_force_math['hatch_styles'][0],
edgecolor='black',
linewidth=2,
)
for bar in bars:
height = bar.get_height()
text = ax.text(
bar.get_x() + bar.get_width() / 2,
height / 2,
f'{height:.3f}',
ha='center',
va='center',
color='#FFD700',
fontsize=20,
path_effects=[
path_effects.Stroke(linewidth=4, foreground='black'),
path_effects.Normal()
]
)
for subtype_idx in range(1, len(data_brute_force_math['subtypes'])):
ax.bar(
np.arange(num_methods),
data_brute_force_math['result'][prompt_name][:, subtype_idx],
color=data_brute_force_math['colors'],
label=data_brute_force_math['methods'],
hatch=data_brute_force_math['hatch_styles'][subtype_idx],
bottom=np.cumsum(data_brute_force_math['result'][prompt_name], axis=1)[:, subtype_idx - 1],
edgecolor='black',
linewidth=2,
alpha=0.8,
)
ax.set_title(data_brute_force_math['prompts'][prompt_idx], fontsize=36, pad=36)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1.01])
ax.set_xticks([])
ax = fig.add_subplot(gs[4])
bar = ax.bar(
np.arange(num_methods),
np.ones_like(np.arange(num_methods)),
color=data_brute_force_math['colors'],
label=data_brute_force_math['methods'],
hatch='',
edgecolor='black',
linewidth=3,
)
handles, labels = ax.get_legend_handles_labels()
for b in bar:
b.remove()
ax.legend(handles, labels, fontsize=30, loc='center', frameon=False)
ax.set_axis_off()
for prompt_idx, prompt_name in enumerate(data_brute_force_logic['prompts']):
ax = fig.add_subplot(gs[prompt_idx + 5])
num_methods = len(data_brute_force_logic['methods'])
bars = ax.bar(
np.arange(num_methods),
data_brute_force_logic['result'][prompt_name][:, 0],
color=data_brute_force_logic['colors'],
label=data_brute_force_logic['methods'],
hatch=data_brute_force_logic['hatch_styles'][0],
edgecolor='black',
linewidth=2,
)
for bar in bars:
height = bar.get_height()
text = ax.text(
bar.get_x() + bar.get_width() / 2,
height / 2,
f'{height:.3f}',
ha='center',
va='center',
color='#FFD700',
fontsize=20,
path_effects=[
path_effects.Stroke(linewidth=4, foreground='black'),
path_effects.Normal()
]
)
for subtype_idx in range(1, len(data_brute_force_logic['subtypes'])):
ax.bar(
np.arange(num_methods),
data_brute_force_logic['result'][prompt_name][:, subtype_idx],
color=data_brute_force_logic['colors'],
label=data_brute_force_logic['methods'],
hatch=data_brute_force_logic['hatch_styles'][subtype_idx],
bottom=np.cumsum(data_brute_force_logic['result'][prompt_name], axis=1)[:, subtype_idx - 1],
edgecolor='black',
linewidth=2,
alpha=0.8,
)
ax.set_title(data_brute_force_logic['prompts'][prompt_idx], fontsize=36, pad=36)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1.01])
ax.set_xticks([])
ax = fig.add_subplot(gs[9])
num_subtypes = len(data_brute_force_math['subtypes'])
bar = ax.bar(
np.arange(num_subtypes),
np.ones_like(np.arange(num_subtypes)),
color='white',
label=data_brute_force_math['subtypes'],
hatch=data_brute_force_math['hatch_styles'],
edgecolor='black',
linewidth=3,
)
handles, labels = ax.get_legend_handles_labels()
for b in bar:
b.remove()
ax.legend(handles, labels, fontsize=30, loc='center', frameon=False)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/brute_force.png', dpi=300)
plt.close(fig)
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec as gridspec
data_math_by_category = {
'methods': [
r'DeepSeek R1 Distill Qwen 1.5B',
r'DeepSeek R1 Distill Qwen 14B',
r'DeepSeek R1 Distill Llama 70B',
r'deepseek-chat (Deepseek-V3)',
r'deepseek-reasoner (Deepseek-R1)',
r'gemini-2.5-flash-preview-04-17',
r'OpenAI o3'
],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#FFF6CC', '#3775BA'],
'subtypes': ['Standard',
'Nonstandard',
'Heuristic'],
'result': {
'Standard': np.array([0.253623188, 0.5, 0.5, 0.594202899, 0.688405797, 0.710144928, 0.789855073]),
'Geometry': np.array([0.125, 0.416666667, 0.375, 0.291666667, 0.375, 0.541666667, 0.666666667]),
'Number Theory': np.array([0.441176471, 0.558823529, 0.529411765, 0.676470588, 0.823529412, 0.735294118, 0.852941177]),
'Combinatorics': np.array([0.25, 0.416666667, 0.416666667, 0.625, 0.666666667, 0.625, 0.75]),
'Algebra': np.array([0.196428571, 0.535714286, 0.571428571, 0.660714286, 0.75, 0.803571429, 0.821428571]),
'Nonstandard': np.array([0.103448276, 0.482758621, 0.431034483, 0.620689655, 0.74137931, 0.672413793, 0.827586207]),
'Logic': np.array([0.034482759, 0.413793103, 0.448275862, 0.517241379, 0.620689655, 0.517241379, 0.75862069]),
'Special Number': np.array([0.172413793, 0.551724138, 0.413793103, 0.724137931, 0.862068966, 0.827586207, 0.896551724]),
'Heuristic': np.array([0, 0.260869565, 0.173913044, 0.413043478, 0.673913044, 0.47826087, 0.804347826]),
'Pattern': np.array([0, 0.214285714, 0.178571429, 0.357142857, 0.642857143, 0.428571429, 0.75]),
'Arithmetic': np.array([0, 0.333333333, 0.166666667, 0.5, 0.722222222, 0.555555556, 0.888888889]),
},
}
data_logic_by_category = {
'methods': [
r'DeepSeek R1 Distill Qwen 1.5B',
r'DeepSeek R1 Distill Qwen 14B',
r'DeepSeek R1 Distill Llama 70B',
r'deepseek-chat (Deepseek-V3)',
r'deepseek-reasoner (Deepseek-R1)',
r'gemini-2.5-flash-preview-04-17',
r'OpenAI o3'
],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#FFF6CC', '#3775BA'],
'subtypes': ['Simple/large',
'Complex/small',
'Math-like',
'Heuristic'],
'result': {
'Simple/large': np.array([0.042105263, 0.178947368, 0.189473684, 0.389473684, 0.410526316, 0.515789474, 0.694736842]),
'0D': np.array([0.068965517, 0.172413793, 0.206896552, 0.379310345, 0.448275862, 0.517241379, 0.689655172]),
'1D': np.array([0, 0.230769231, 0.230769231, 0.615384615, 0.538461539, 0.538461539, 0.692307692]),
'2D': np.array([0, 0.045454545, 0.090909091, 0.272727273, 0.181818182, 0.363636364, 0.454545455]),
'Number': np.array([0.058823529, 0.470588235, 0.411764706, 0.588235294, 0.823529412, 0.941176471, 1]),
'Clusters': np.array([0, 0, 0, 0.125, 0, 0.25, 0.875]),
'Tree': np.array([0.166666667, 0, 0, 0.166666667, 0.166666667, 0.166666667, 0.5]),
'Complex/small': np.array([0, 0.433333333, 0.4, 0.466666667, 0.566666667, 0.833333333, 0.866666667]),
'Liars': np.array([0, 0.411764706, 0.411764706, 0.411764706, 0.705882353, 0.882352941, 0.882352941]),
'Communication': np.array([0, 0.5, 0, 0.25, 0, 0.5, 0.5]),
'Compound': np.array([0, 0.444444444, 0.555555556, 0.666666667, 0.555555556, 0.888888889, 1]),
'Math-like': np.array([0.085714286, 0.328571429, 0.371428571, 0.514285714, 0.542857143, 0.542857143, 0.714285714]),
'Algorithm': np.array([0.078947368, 0.315789474, 0.368421053, 0.473684211, 0.447368421, 0.473684211, 0.710526316]),
'Math': np.array([0.09375, 0.34375, 0.375, 0.5625, 0.65625, 0.625, 0.71875]),
'Heuristic': np.array([0, 0.12195122, 0.097560976, 0.317073171, 0.365853659, 0.268292683, 0.658536585]),
'Pattern': np.array([0, 0.153846154, 0.115384615, 0.230769231, 0.346153846, 0.307692308, 0.576923077]),
'Linguistic': np.array([0, 0.066666667, 0.066666667, 0.466666667, 0.4, 0.2, 0.8]), },
}
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(36, 12))
gs = gridspec.GridSpec(2, 4)
for subtype_idx, subtype_name in enumerate(data_math_by_category['subtypes']):
ax = fig.add_subplot(gs[subtype_idx])
num_methods = len(data_math_by_category['methods'])
ax.bar(
np.arange(num_methods),
data_math_by_category['result'][subtype_name],
color=data_math_by_category['colors'],
label=data_math_by_category['methods'],
)
ax.set_title(data_math_by_category['subtypes'][subtype_idx], fontsize=36, pad=36)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1])
ax.set_xticks([])
ax = fig.add_subplot(gs[3])
bar = ax.bar(
np.arange(num_methods),
np.ones_like(np.arange(num_methods)),
color=data_math_by_category['colors'],
label=data_math_by_category['methods'],
hatch='',
)
handles, labels = ax.get_legend_handles_labels()
for b in bar:
b.remove()
ax.legend(handles, labels, fontsize=28, loc='center', frameon=False)
ax.set_axis_off()
for subtype_idx, subtype_name in enumerate(data_logic_by_category['subtypes']):
ax = fig.add_subplot(gs[4 + subtype_idx])
num_methods = len(data_logic_by_category['methods'])
ax.bar(
np.arange(num_methods),
data_logic_by_category['result'][subtype_name],
color=data_logic_by_category['colors'],
label=data_logic_by_category['methods'],
)
ax.set_title(data_logic_by_category['subtypes'][subtype_idx], fontsize=36, pad=36)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1])
ax.set_xticks([])
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/correctness_by_category.png', dpi=300)
plt.close(fig)
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec as gridspec
data_math_by_category = {
'methods': [
r'DeepSeek R1 Distill Qwen 1.5B',
r'DeepSeek R1 Distill Qwen 14B',
r'DeepSeek R1 Distill Llama 70B',
r'deepseek-chat (Deepseek-V3)',
r'deepseek-reasoner (Deepseek-R1)',
r'gemini-2.5-flash-preview-04-17',
r'OpenAI o3'
],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#FFF6CC', '#3775BA'],
'subtypes': ['Geometry', 'Number Theory', 'Combinatorics', 'Algebra',
'Logic', 'Special Number', 'Pattern', 'Arithmetic'],
'result': {
'Standard': np.array([0.253623188, 0.5, 0.5, 0.594202899, 0.688405797, 0.710144928, 0.789855073]),
'Geometry': np.array([0.125, 0.416666667, 0.375, 0.291666667, 0.375, 0.541666667, 0.666666667]),
'Number Theory': np.array([0.441176471, 0.558823529, 0.529411765, 0.676470588, 0.823529412, 0.735294118, 0.852941177]),
'Combinatorics': np.array([0.25, 0.416666667, 0.416666667, 0.625, 0.666666667, 0.625, 0.75]),
'Algebra': np.array([0.196428571, 0.535714286, 0.571428571, 0.660714286, 0.75, 0.803571429, 0.821428571]),
'Nonstandard': np.array([0.103448276, 0.482758621, 0.431034483, 0.620689655, 0.74137931, 0.672413793, 0.827586207]),
'Logic': np.array([0.034482759, 0.413793103, 0.448275862, 0.517241379, 0.620689655, 0.517241379, 0.75862069]),
'Special Number': np.array([0.172413793, 0.551724138, 0.413793103, 0.724137931, 0.862068966, 0.827586207, 0.896551724]),
'Heuristic': np.array([0, 0.260869565, 0.173913044, 0.413043478, 0.673913044, 0.47826087, 0.804347826]),
'Pattern': np.array([0, 0.214285714, 0.178571429, 0.357142857, 0.642857143, 0.428571429, 0.75]),
'Arithmetic': np.array([0, 0.333333333, 0.166666667, 0.5, 0.722222222, 0.555555556, 0.888888889]),
},
}
data_logic_by_category = {
'methods': [
r'DeepSeek R1 Distill Qwen 1.5B',
r'DeepSeek R1 Distill Qwen 14B',
r'DeepSeek R1 Distill Llama 70B',
r'deepseek-chat (Deepseek-V3)',
r'deepseek-reasoner (Deepseek-R1)',
r'gemini-2.5-flash-preview-04-17',
r'OpenAI o3'
],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#FFF6CC', '#3775BA'],
'subtypes': ['0D', '1D', '2D', 'Number', 'Clusters', 'Tree', 'Liars',
'Communication', 'Compound', 'Algorithm', 'Math', 'Pattern', 'Linguistic'],
'result': {
'Simple/large': np.array([0.042105263, 0.178947368, 0.189473684, 0.389473684, 0.410526316, 0.515789474, 0.694736842]),
'0D': np.array([0.068965517, 0.172413793, 0.206896552, 0.379310345, 0.448275862, 0.517241379, 0.689655172]),
'1D': np.array([0, 0.230769231, 0.230769231, 0.615384615, 0.538461539, 0.538461539, 0.692307692]),
'2D': np.array([0, 0.045454545, 0.090909091, 0.272727273, 0.181818182, 0.363636364, 0.454545455]),
'Number': np.array([0.058823529, 0.470588235, 0.411764706, 0.588235294, 0.823529412, 0.941176471, 1]),
'Clusters': np.array([0, 0, 0, 0.125, 0, 0.25, 0.875]),
'Tree': np.array([0.166666667, 0, 0, 0.166666667, 0.166666667, 0.166666667, 0.5]),
'Complex/small': np.array([0, 0.433333333, 0.4, 0.466666667, 0.566666667, 0.833333333, 0.866666667]),
'Liars': np.array([0, 0.411764706, 0.411764706, 0.411764706, 0.705882353, 0.882352941, 0.882352941]),
'Communication': np.array([0, 0.5, 0, 0.25, 0, 0.5, 0.5]),
'Compound': np.array([0, 0.444444444, 0.555555556, 0.666666667, 0.555555556, 0.888888889, 1]),
'Math-like': np.array([0.085714286, 0.328571429, 0.371428571, 0.514285714, 0.542857143, 0.542857143, 0.714285714]),
'Algorithm': np.array([0.078947368, 0.315789474, 0.368421053, 0.473684211, 0.447368421, 0.473684211, 0.710526316]),
'Math': np.array([0.09375, 0.34375, 0.375, 0.5625, 0.65625, 0.625, 0.71875]),
'Heuristic': np.array([0, 0.12195122, 0.097560976, 0.317073171, 0.365853659, 0.268292683, 0.658536585]),
'Pattern': np.array([0, 0.153846154, 0.115384615, 0.230769231, 0.346153846, 0.307692308, 0.576923077]),
'Linguistic': np.array([0, 0.066666667, 0.066666667, 0.466666667, 0.4, 0.2, 0.8]),
},
}
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(96, 12))
gs = gridspec.GridSpec(2, 13)
for subtype_idx, subtype_name in enumerate(data_math_by_category['subtypes']):
ax = fig.add_subplot(gs[subtype_idx])
num_methods = len(data_math_by_category['methods'])
ax.bar(
np.arange(num_methods),
data_math_by_category['result'][subtype_name],
color=data_math_by_category['colors'],
label=data_math_by_category['methods'],
)
ax.set_title(data_math_by_category['subtypes'][subtype_idx], fontsize=36, pad=36)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1])
ax.set_xticks([])
ax = fig.add_subplot(gs[11:12])
bar = ax.bar(
np.arange(num_methods),
np.ones_like(np.arange(num_methods)),
color=data_math_by_category['colors'],
label=data_math_by_category['methods'],
hatch='',
)
handles, labels = ax.get_legend_handles_labels()
for b in bar:
b.remove()
ax.legend(handles, labels, fontsize=28, loc='center', frameon=False)
ax.set_axis_off()
for subtype_idx, subtype_name in enumerate(data_logic_by_category['subtypes']):
ax = fig.add_subplot(gs[13 + subtype_idx])
num_methods = len(data_logic_by_category['methods'])
ax.bar(
np.arange(num_methods),
data_logic_by_category['result'][subtype_name],
color=data_logic_by_category['colors'],
label=data_logic_by_category['methods'],
)
ax.set_title(data_logic_by_category['subtypes'][subtype_idx], fontsize=36, pad=36)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1])
ax.set_xticks([])
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/correctness_by_subcategory.png', dpi=300)
plt.close(fig)
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec as gridspec
data_rewriting_math = {
'methods': [r'DeepSeek R1 Distill Llama 70B',
r'deepseek-reasoner (Deepseek-R1)',
r'OpenAI o3'],
'colors': ['#8BCF8B', '#E9A6A1', '#3775BA'],
'hatch_styles': ['', '|', '\\', '/', '-'],
'fig1': ['Before rewriting', 'After rewriting'],
'fig2': [r'correct $\rightarrow$ incorrect', r'incorrect $\rightarrow$ correct', 'same result'],
'result': {
'Before rewriting': np.array([7, 15, 17]) / 30,
'After rewriting': np.array([10, 19, 22]) / 30,
r'correct $\rightarrow$ incorrect': np.array([0, 2, 1]) / 30,
r'incorrect $\rightarrow$ correct': np.array([3, 6, 6]) / 30,
'same result': np.array([27, 22, 23]) / 30,
},
}
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(24, 12))
gs = gridspec.GridSpec(2, 2)
num_methods = len(data_rewriting_math['methods'])
ax = fig.add_subplot(gs[0])
width = 0.3
for category_idx, category in enumerate(data_rewriting_math['fig1']):
ax.bar(np.arange(num_methods) + width * category_idx * 1.1,
data_rewriting_math['result'][category],
width=width,
label=category,
color=data_rewriting_math['colors'],
edgecolor='black',
linewidth=2,
hatch=data_rewriting_math['hatch_styles'][category_idx])
ax.set_title('Correctness', fontsize=36, pad=0)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1.01])
ax.set_xticks([])
ax = fig.add_subplot(gs[1])
width = 0.25
for category_idx, category in enumerate(data_rewriting_math['fig2']):
ax.bar(np.arange(num_methods) + width * category_idx * 1.1,
data_rewriting_math['result'][category],
width=width,
label=category,
color=data_rewriting_math['colors'],
edgecolor='black',
linewidth=2,
hatch=data_rewriting_math['hatch_styles'][category_idx + 2])
ax.set_title('Change in Result', fontsize=36, pad=0)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1.01])
ax.set_xticks([])
ax = fig.add_subplot(gs[2])
bar = ax.bar(
np.arange(num_methods),
np.ones_like(np.arange(num_methods)),
color=data_rewriting_math['colors'],
edgecolor='black',
linewidth=2,
label=data_rewriting_math['methods'],
hatch='',
)
handles, labels = ax.get_legend_handles_labels()
for b in bar:
b.remove()
ax.legend(handles, labels, fontsize=30, loc='center', frameon=False)
ax.set_axis_off()
ax = fig.add_subplot(gs[3])
subtypes = data_rewriting_math['fig1'] + data_rewriting_math['fig2']
bar = ax.bar(
np.arange(len(subtypes)),
np.ones_like(np.arange(len(subtypes))),
color='white',
edgecolor='black',
linewidth=2,
label=subtypes,
hatch=data_rewriting_math['hatch_styles'],
)
handles, labels = ax.get_legend_handles_labels()
for b in bar:
b.remove()
ax.legend(handles, labels, fontsize=30, loc='center', frameon=False)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/rewriting.png', dpi=300)
plt.close(fig)
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec as gridspec
data_math_correcting_llm = {
'methods': [r'DeepSeek R1 Distill Qwen 1.5B',
r'DeepSeek R1 Distill Qwen 14B',
r'DeepSeek R1 Distill Llama 70B',
r'deepseek-chat (Deepseek-V3)',
r'deepseek-reasoner (Deepseek-R1)',
r'OpenAI o3'],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#3775BA'],
'subtypes': [r'Fault denial$\downarrow$',
r'Error misattribution$\downarrow$',
r'Degenerate repetition or stuck$\downarrow$',
r'Flawed correction$\downarrow$',
r'Valid correction$\uparrow$'],
'result': {
r'Fault denial$\downarrow$': np.array([1, 0, 0, 1, 0, 0]) / 14 ,
r'Error misattribution$\downarrow$': np.array([4, 4, 3, 1, 0, 1]) / 14 ,
r'Degenerate repetition or stuck$\downarrow$': np.array([9, 2, 4, 0, 0, 0]) / 14 ,
r'Flawed correction$\downarrow$': np.array([0, 1, 0, 1, 3, 2]) / 14 ,
r'Valid correction$\uparrow$': np.array([0, 7, 7, 12, 11, 11]) / 14 ,
},
}
data_math_correcting_human = {
'methods': [r'DeepSeek R1 Distill Qwen 1.5B',
r'DeepSeek R1 Distill Qwen 14B',
r'DeepSeek R1 Distill Llama 70B',
r'deepseek-chat (Deepseek-V3)',
r'deepseek-reasoner (Deepseek-R1)',
r'OpenAI o3'],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#3775BA'],
'subtypes': [r'False confession$\downarrow$',
r'Degenerate repetition or stuck$\downarrow$',
r'Justified denial$\uparrow$'],
'result': {
r'False confession$\downarrow$': np.array([8, 10, 10, 13, 14, 12]) / 14 ,
r'Degenerate repetition or stuck$\downarrow$': np.array([5, 3, 2, 0, 0, 0]) / 14 ,
r'Justified denial$\uparrow$': np.array([0, 1, 2, 1, 0, 0]) / 14 ,
},
}
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(36, 12))
gs = gridspec.GridSpec(2, 5)
for subtype_idx, subtype_name in enumerate(data_math_correcting_llm['subtypes']):
ax = fig.add_subplot(gs[subtype_idx])
num_methods = len(data_math_correcting_llm['methods'])
ax.bar(
np.arange(num_methods),
data_math_correcting_llm['result'][subtype_name],
color=data_math_correcting_llm['colors'],
label=data_math_correcting_llm['methods'],
)
if subtype_idx == 0:
handles, labels = ax.get_legend_handles_labels()
ax.set_title(data_math_correcting_llm['subtypes'][subtype_idx], fontsize=30, pad=36)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1])
ax.set_xticks([])
for subtype_idx, subtype_name in enumerate(data_math_correcting_human['subtypes']):
ax = fig.add_subplot(gs[5 + subtype_idx])
num_methods = len(data_math_correcting_human['methods'])
ax.bar(
np.arange(num_methods),
data_math_correcting_human['result'][subtype_name],
color=data_math_correcting_human['colors'],
label=data_math_correcting_human['methods'],
)
ax.set_title(data_math_correcting_human['subtypes'][subtype_idx], fontsize=30, pad=36)
ax.set_ylabel('Probability', fontsize=30, labelpad=12)
ax.set_ylim([0, 1])
ax.set_xticks([])
ax = fig.add_subplot(gs[8:])
ax.legend(handles, labels, fontsize=30, loc='center', frameon=False)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/selfcorrection_math.png', dpi=300)
plt.close(fig)
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec as gridspec
data_ablation = {
'methods': [
r'CellSpliceNet',
r'No Expression',
r'No Structure',
r'No ROI',
r'No Sequence',
],
'colors': ['#0F4D92', '#B4E6B4', '#AFE6E6', '#FFE080', '#D3D3D3'],
'result': np.array([0.88, 0.84, 0.82, 0.81, 0.74]),
}
def is_dark(color_in_hex, threshold=128):
color = color_in_hex.lstrip('#')
r = int(color[0:2], 16)
g = int(color[2:4], 16)
b = int(color[4:6], 16)
luminance = 0.299*r + 0.587*g + 0.114*b
return luminance < threshold
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(13, 13))
ax = fig.add_subplot(1, 1, 1)
num_methods = len(data_ablation['methods'])
bars = ax.bar(
np.arange(num_methods),
data_ablation['result'],
color=data_ablation['colors'],
label=data_ablation['methods'],
)
for i, (bar, value) in enumerate(zip(bars, data_ablation['result'])):
textcolor = 'white' if is_dark(data_ablation['colors'][i]) else 'black'
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() - 0.08,
f'{value:.2f}', ha='center', va='bottom', fontsize=32, color=textcolor)
# Add horizontal reference line at the first bar
baseline = data_ablation['result'][0] # 0.88
ax.axhline(y=baseline, color=data_ablation['colors'][0], linestyle='--', linewidth=4, alpha=0.7)
# Add arrows and reduction values for bars 2-5 (skip the first bar)
for i in range(1, num_methods):
bar = bars[i]
current_value = data_ablation['result'][i]
reduction = baseline - current_value
# Position for the arrow (right side of the bar)
x_pos = bar.get_x() + bar.get_width()
# Draw arrow from baseline down to bar top (top to bottom)
ax.annotate('', xy=(x_pos, current_value), xytext=(x_pos, baseline),
arrowprops=dict(arrowstyle='->', color='red', lw=4))
# Add reduction text near the top (at baseline level)
ax.text(x_pos - 0.3, baseline + 0.005, r'$-$'+f'{reduction:.2f}',
ha='left', va='bottom', fontsize=24, color='red')
ax.set_ylabel('Spearman correlation', fontsize=54, labelpad=12)
ymax = np.max(data_ablation['result'][:])
ax.set_ylim([0.0, ymax + 0.5])
ax.set_yticks([0.0, 0.25, 0.50, 0.75, 1.0])
ax.tick_params(axis='y', labelsize=36, length=10, width=2)
ax.set_xticks([])
ax.legend(bbox_to_anchor=(0.50, 1.08), loc='upper left', fontsize=36, frameon=False)
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/ablation.png', dpi=300)
plt.close(fig)
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec as gridspec
data_ablation = {
'methods': [
r'CellSpliceNet',
r'ViT',
r'SpliceFinder',
r'Pangolin',
r'SpliceTransformer',
r'SpliceAI',
r'ESM2',
],
'colors': ['#0F4D92', "#F09F97", "#F1B3AC", "#EFBEB8", "#F0CDC8", "#F3D9D8", '#FCEEED'],
'metrics': [r'Spearman correlation', r'Pearson correlation', r'R$^2$ score'],
'result': {
# r'Spearman correlation': np.array([0.88, 0.81, 0.80, 0.79, 0.77, 0.71, 0.606]),
# r'Pearson correlation': np.array([0.88, 0.81, 0.80, 0.79, 0.77, 0.72, 0.613]),
# r'R$^2$ score': np.array([0.77, 0.66, 0.64, 0.62, 0.59, 0.52, 0.369]),
r'Spearman correlation': np.array([
[0.88, 0.88, 0.88], # TODO: update!
[0.81, 0.81, 0.81], # TODO: update!
[0.806, 0.793, 0.794],
[0.792, 0.785, 0.788],
[0.765, 0.765, 0.767],
[0.714, 0.716, 0.706],
[0.598, 0.625, 0.594],
]),
r'Pearson correlation': np.array([
[0.88, 0.88, 0.88], # TODO: update!
[0.81, 0.81, 0.81], # TODO: update!
[0.812, 0.798, 0.801],
[0.792, 0.785, 0.788],
[0.765, 0.765, 0.766],
[0.722, 0.722, 0.714],
[0.604, 0.631, 0.605],
]),
r'R$^2$ score': np.array([
[0.77, 0.77, 0.77], # TODO: update!
[0.66, 0.66, 0.66], # TODO: update!
[0.658, 0.632, 0.642],
[0.633, 0.615, 0.627],
[0.585, 0.585, 0.586],
[0.518, 0.520, 0.507],
[0.359, 0.384, 0.364],
]),
}
}
def is_dark(color_in_hex, threshold=128):
color = color_in_hex.lstrip('#')
r = int(color[0:2], 16)
g = int(color[2:4], 16)
b = int(color[4:6], 16)
luminance = 0.299*r + 0.587*g + 0.114*b
return luminance < threshold
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(45, 12))
gs = gridspec.GridSpec(1, 3)
for metric_idx, metric_name in enumerate(data_ablation['metrics']):
ax = fig.add_subplot(gs[metric_idx])
num_methods = len(data_ablation['methods'])
bars = ax.bar(
np.arange(num_methods),
data_ablation['result'][metric_name].mean(axis=1),
yerr=data_ablation['result'][metric_name].std(axis=1),
error_kw={
'elinewidth': 2, # thickness of vertical error bar line
'capthick': 2, # thickness of caps
'capsize': 15 # length of caps
},
color=data_ablation['colors'],
label=data_ablation['methods'],
)
for i, (bar, value) in enumerate(zip(bars, data_ablation['result'][metric_name].mean(axis=1))):
textcolor = 'white' if is_dark(data_ablation['colors'][i]) else 'black'
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() - 0.10,
f'{value:.2f}', ha='center', va='bottom', fontsize=32, color=textcolor)
ax.set_ylabel(metric_name, fontsize=54, labelpad=12)
ymax = np.max(data_ablation['result'][metric_name])
ax.set_ylim([0.0, ymax + 0.5])
ax.set_xticks([])
ax.set_yticks([0.00, 0.25, 0.50, 0.75, 1.00])
ax.tick_params(axis='y', labelsize=36, length=10, width=2)
ax.legend(bbox_to_anchor=(0.02, 1.08), loc='upper left', fontsize=38, frameon=False, ncols=2, columnspacing=0.6)
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/comparison.png', dpi=300)
plt.close(fig)
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.distance import pdist, squareform
# Generate Swiss Roll data
def generate_swiss_roll_2d(n_samples=80, noise=0.1):
t = 1.5 * np.pi * (1 + 2 * np.random.rand(n_samples))
x = t * np.cos(t)
z = t * np.sin(t)
# Add some noise
x += noise * np.random.randn(n_samples)
z += noise * np.random.randn(n_samples)
return x, z, t
# Compute diffusion matrix (transition probabilities)
def compute_diffusion_matrix(x, z, t, sigma=1.0):
# Order points by manifold parameter t for better matrix visualization
sorted_indices = np.argsort(t)
x_sorted = x[sorted_indices]
z_sorted = z[sorted_indices]
t_sorted = t[sorted_indices]
# Compute pairwise distances in ambient space
points = np.column_stack([x_sorted, z_sorted])
distances = squareform(pdist(points))
# Compute manifold distances (along parameter t)
t_distances = np.abs(t_sorted[:, None] - t_sorted[None, :])
# Combine spatial and manifold distances (emphasize manifold structure)
combined_distances = distances + 0.5 * t_distances
# Gaussian kernel for transition probabilities
P = np.exp(-combined_distances**2 / (2 * sigma**2))
# Make matrix sparser by thresholding small values
P[P < 0.01] = 0
# Normalize rows to make it a proper transition matrix
row_sums = P.sum(axis=1)
row_sums[row_sums == 0] = 1 # Avoid division by zero
P = P / row_sums[:, None]
return P, sorted_indices
if __name__ == '__main__':
# Create two subplots
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))
# Generate swiss roll point cloud (smaller for visualization clarity)
x, z, t = generate_swiss_roll_2d(n_samples=500, noise=0.5)
# Compute diffusion matrix
P, sorted_indices = compute_diffusion_matrix(x, z, t, sigma=2)
# Left plot: Diffusion Matrix
im = ax1.imshow(P, cmap='Reds', aspect='equal', origin='upper')
ax1.axis('off')
ax1.set_facecolor('white')
# Right plot: Swiss Roll with probability-weighted connections
# Use original (unsorted) coordinates for the swiss roll plot
x_orig, z_orig, t_orig = x, z, t
# Draw line segments between points with opacity = transition probability
threshold = 0.02 # Only draw lines above this probability threshold
for i in range(len(x_orig)):
for j in range(i+1, len(x_orig)):
# Find corresponding indices in sorted matrix
orig_i_in_sorted = np.where(sorted_indices == i)[0][0]
orig_j_in_sorted = np.where(sorted_indices == j)[0][0]
# Get transition probability from matrix
prob = max(P[orig_i_in_sorted, orig_j_in_sorted], P[orig_j_in_sorted, orig_i_in_sorted])
if prob > threshold:
ax2.plot([x_orig[i], x_orig[j]], [z_orig[i], z_orig[j]],
color='black', linewidth=2, alpha=prob*2, zorder=1)
# Plot the swiss roll points on top
scatter = ax2.scatter(x_orig, z_orig, c=t_orig, cmap='viridis', s=100,
alpha=0.5, edgecolors='white', linewidth=1, zorder=2)
# Styling for swiss roll plot
ax2.set_aspect('equal')
ax2.axis('off')
ax2.set_facecolor('white')
# Clean overall styling
fig.patch.set_facecolor('white')
plt.subplots_adjust(left=0, right=1, top=1, bottom=0, wspace=0.02)
plt.savefig('figures/diffusion_swiss_roll.png', dpi=300, bbox_inches='tight',
facecolor='white', edgecolor='none', pad_inches=1)
import os
import numpy as np
from matplotlib import pyplot as plt
data_comparison_Ablation = {
'methods': [r'OT-CFM', r'SB-CFM', r'SF2M', r'Cflows (w/o growth + energy)', r'Cflows (w/o growth)', r'Cflows'],
'colors': ['#AADCA9', '#8BCF8B', '#E9A6A1', '#B8C9E5', '#7097CA', '#3775BA'],
'metrics': [r'RMSE$\downarrow$', r'MAE$\downarrow$', r'PCC$\uparrow$', r'SCC$\uparrow$'],
'mean': {
r'RMSE$\downarrow$': np.array([0.94, 1.05, 0.99, 0.89, 0.75, 0.62]),
r'MAE$\downarrow$': np.array([0.75, 0.85, 0.78, 0.70, 0.59, 0.48]),
r'PCC$\uparrow$': np.array([0.53, 0.49, 0.55, 0.58, 0.65, 0.72]),
r'SCC$\uparrow$': np.array([0.50, 0.47, 0.52, 0.55, 0.68, 0.70]),
},
'std': {
r'RMSE$\downarrow$': np.array([0.08, 0.09, 0.09, 0.07, 0.06, 0.05]),
r'MAE$\downarrow$': np.array([0.07, 0.08, 0.07, 0.06, 0.05, 0.04]),
r'PCC$\uparrow$': np.array([0.04, 0.02, 0.01, 0.03, 0.02, 0.02]),
r'SCC$\uparrow$': np.array([0.03, 0.02, 0.03, 0.03, 0.03, 0.01]),
},
}
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(35, 7))
num_methods = len(data_comparison_Ablation['methods'])
for metric_idx, metric_name in enumerate(data_comparison_Ablation['metrics']):
ax = fig.add_subplot(1, 5, metric_idx + 1)
ax.bar(
np.arange(num_methods),
data_comparison_Ablation['mean'][metric_name],
yerr=data_comparison_Ablation['std'][metric_name],
capsize=8,
error_kw={'capthick': 2},
color=data_comparison_Ablation['colors'],
label=data_comparison_Ablation['methods'],
)
if metric_idx == 0:
handles, labels = ax.get_legend_handles_labels()
ax.set_xticks([])
ax.set_ylabel(data_comparison_Ablation['metrics'][metric_idx], fontsize=36, labelpad=12)
ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))
ax = fig.add_subplot(1, 5, 5)
ax.legend(handles, labels, fontsize=30, loc='lower left', frameon=False)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/figX_comparison_Ablation.png', dpi=300)
fig.savefig('./figures/figX_comparison_Ablation.pdf', dpi=300)
plt.close(fig)
import os
import numpy as np
from matplotlib import pyplot as plt
data_comparison_GeneRegulatory = {
'methods': [r'OCE', r'PC', r'mTE', r'mMI', r'NRI', r'DCRNN', r'GTS', r'NIR', r'GC (ours)'],
'colors': ['#D0A3A3', '#EFE7B1', '#F4C2C2', '#D7C4E2', '#E5C09F', '#A8C6C2', '#B7D3B0', '#F5B5A0', '#3775BA'],
'metric': 'Graph Edit Distance',
'datasets': [
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (100, 137)',
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (150, 329)',
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (200, 507)',
],
'mean': {
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (100, 137)':
np.array([138.6, 140.4, 126.4, 51.2, 72.1, 158.14, 215.4, 62.7, 51.2]),
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (150, 329)':
np.array([293.4, 317.2, 261.0, 99.8, 106.6, 303.79, 347.2, 86.3, 109.0]),
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (200, 507)':
np.array([449.8, 495.6, 397.4, 162.8, 219.8, 508.25, 481.8, 159.2, 158.8]),
},
'std': {
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (100, 137)':
np.array([3.5, 3.9, 2.4, 3.3, 6.2, 8.6, 13.8, 3.2, 3.3]),
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (150, 329)':
np.array([2.9, 3.7, 2.2, 4.0, 5.4, 12.4, 19.3, 2.8, 6.4]),
r'($|\mathcal{V}|$, $|\mathcal{E}|$) = (200, 507)':
np.array([1.1, 6.5, 8.8, 6.2, 13.4, 23.6, 7.0, 11.6, 12.6]),
},
}
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(36, 6))
num_methods = len(data_comparison_GeneRegulatory['methods'])
for dataset_idx, dataset_name in enumerate(data_comparison_GeneRegulatory['datasets']):
ax = fig.add_subplot(1, 4, dataset_idx + 1)
ax.bar(
np.arange(num_methods),
data_comparison_GeneRegulatory['mean'][dataset_name],
yerr=data_comparison_GeneRegulatory['std'][dataset_name],
capsize=8,
error_kw={'capthick': 2},
color=data_comparison_GeneRegulatory['colors'],
label=data_comparison_GeneRegulatory['methods'],
)
if dataset_idx == 0:
handles, labels = ax.get_legend_handles_labels()
ax.set_xticks([])
ax.set_ylabel(data_comparison_GeneRegulatory['metric'], fontsize=36, labelpad=12)
ax.set_xlabel(dataset_name, fontsize=36, labelpad=12)
ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))
ax = fig.add_subplot(1, 4, 4)
ax.legend(handles, labels, fontsize=30, loc='lower left', ncols=2, frameon=False)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/fig2_comparison_GeneRegulatory.png', dpi=300)
fig.savefig('./figures/fig2_comparison_GeneRegulatory.pdf', dpi=300)
plt.close(fig)
import os
import numpy as np
from matplotlib import pyplot as plt
data_comparison_Trajectory = {
'methods': [r'TrajectoryNet', r'OT-CFM', r'SB-CFM', r'BEMIOflow (ours)'],
'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#3775BA'],
'metrics': ['PHATE Space RMSE', 'Gene Space RMSE', 'Interpolation EMD'],
'datasets': ['Bifurcation', 'Cycle', 'Unidirectional'],
'mean': {
'PHATE Space RMSE': {
'Bifurcation': np.array([6.16, 2.98, 2.83, 2.60]) * 1e-3,
'Cycle': np.array([2.49, 3.79, 1.58, 0.777]) * 1e-3,
'Unidirectional': np.array([8.08, 5.33, 5.67, 3.40]) * 1e-3,
},
'Gene Space RMSE': {
'Bifurcation': np.array([0.150, 0.0842, 0.0835, 0.0773]),
'Cycle': np.array([0.151, 0.209, 0.141, 0.118]),
'Unidirectional': np.array([0.118, 0.0960, 0.0978, 0.0853]),
},
'Interpolation EMD': {
'Bifurcation': np.array([1.07, 0.495, 0.516, 0.465]) * 1e-2,
'Cycle': np.array([0.710, 0.818, 0.526, 0.465]) * 1e-2,
'Unidirectional': np.array([1.50, 1.32, 1.28, 0.935]) * 1e-2,
},
},
}
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
fig = plt.figure(figsize=(36, 6))
num_methods = len(data_comparison_Trajectory['methods'])
for metric_idx, metric_name in enumerate(data_comparison_Trajectory['metrics']):
ax = fig.add_subplot(1, 4, metric_idx + 1)
xtick_list = []
for dataset_idx, dataset_name in enumerate(data_comparison_Trajectory['datasets']):
ax.bar(
np.arange(num_methods) + dataset_idx * (num_methods + 1),
data_comparison_Trajectory['mean'][metric_name][dataset_name],
color=data_comparison_Trajectory['colors'],
label=data_comparison_Trajectory['methods'],
)
xtick_list.append(np.mean(np.arange(num_methods)) + dataset_idx * (num_methods + 1))
if dataset_idx == 0:
handles, labels = ax.get_legend_handles_labels()
ax.set_xticks(xtick_list)
ax.set_xticklabels(data_comparison_Trajectory['datasets'])
ax.set_ylabel(data_comparison_Trajectory['metrics'][metric_idx], fontsize=36, labelpad=12)
ax.set_xlabel('Dataset', fontsize=36, labelpad=12)
ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))
ax = fig.add_subplot(1, 4, 4)
ax.legend(handles, labels, fontsize=30, loc='lower left', frameon=False)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/fig2_comparison_Trajectory.png', dpi=300)
fig.savefig('./figures/fig2_comparison_Trajectory.pdf', dpi=300)
plt.close(fig)
import os
import numpy as np
import matplotlib.pyplot as plt
EPSILON = 1e-6
def sample_points_in_ball(num_points, theta_range=2*np.pi):
r = np.sqrt(np.random.uniform(0, 0.95, num_points))
theta = np.random.uniform(np.pi/2 - theta_range/2, np.pi/2 + theta_range/2, num_points)
x = r * np.cos(theta)
y = r * np.sin(theta)
return np.stack([x, y], axis=1)
def plot_ball_with_points(ax, pts, facecolor):
num_points_grid = 512
xs = np.linspace(-1, 1, num_points_grid)
ys = np.linspace(-1, 1, num_points_grid)
x, y = np.meshgrid(xs, ys)
r2 = x**2 + y**2
mask = r2 <= 1.0
z = np.zeros_like(x)
z[mask] = np.sqrt(1.0 - r2[mask])
nx, ny, nz = x.copy(), y.copy(), z.copy()
norm = np.sqrt(nx**2 + ny**2 + nz**2) + EPSILON
nx, ny, nz = nx / norm, ny / norm, nz / norm
# light from top left.
light_dir = np.array([-0.5, 0.5, 0.8])
light_dir /= np.linalg.norm(light_dir)
intensity = np.maximum(0.0, nx*light_dir[0] + ny*light_dir[1] + nz*light_dir[2])
shade = np.clip(-0.5 + 2.0*intensity, 0, 1)
img = np.ones((num_points_grid, num_points_grid))
img[mask] = shade[mask]
ax.imshow(img, cmap='gray', origin='lower', extent=[-1, 1, -1, 1], vmin=0, vmax=1, alpha=0.3)
ax.set_xlim([-2, 2])
ax.set_ylim([-2, 2])
ax.set_axis_off()
# add points on sphere.
ax.scatter(pts[:, 0], pts[:, 1], s=80, facecolor=facecolor, edgecolor='black', alpha=0.8)
for i in range(pts.shape[0]):
ax.plot([pts[i, 0], 0], [pts[i, 1], 0],
linestyle="--", color="black", linewidth=1, alpha=0.8)
return ax
if __name__ == "__main__":
save_path = './figures/idea.png'
num_points = 16
np.random.seed(1)
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'sans-serif'
fig = plt.figure(figsize=(18, 6))
ax = fig.add_subplot(1, 3, 1)
pts = sample_points_in_ball(num_points=num_points)
plot_ball_with_points(ax, pts, facecolor='#cde5f8')
ax = fig.add_subplot(1, 3, 2)
pts = sample_points_in_ball(num_points=num_points, theta_range=np.pi/4)
plot_ball_with_points(ax, pts, facecolor='#6a98cb')
ax = fig.add_subplot(1, 3, 3)
pts = sample_points_in_ball(num_points=num_points)
plot_ball_with_points(ax, pts, facecolor='#6a98cb')
os.makedirs(os.path.dirname(save_path), exist_ok=True)
fig.tight_layout(pad=2)
fig.savefig(save_path, dpi=300)
import os
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import proj3d
import matplotlib.cm as cm
EPSILON = 1e-6
def pairwise_sqdist(X: np.ndarray) -> np.ndarray:
X2 = np.sum(X**2, axis=1, keepdims=True)
D2 = X2 + X2.T - 2 * (X @ X.T)
return np.maximum(D2, 0.0)
def safe_scale(V, s=0.6, eps=EPSILON):
n = np.linalg.norm(V, axis=1, keepdims=True)
return V / (eps + n) / s
def nice_axes(ax, L):
y_scale = 1
z_scale = 1.2
ax.quiver(0, 0, 0, 0, -L, 0, color="black", linewidth=2, arrow_length_ratio=0.1)
ax.quiver(0, 0, 0, y_scale*L, 0, 0, color="black", linewidth=2, arrow_length_ratio=0.1)
ax.quiver(0, 0, 0, 0, 0, z_scale*L, color="black", linewidth=2, arrow_length_ratio=0.1)
ax.text(0, -L*1.2, 0, "x", color="black", fontsize=36)
ax.text(y_scale*L*1.05, -0.2, 0, "y", color="black", fontsize=36)
ax.text(-0.2, 0, z_scale*L*1.05, "z", color="black", fontsize=36)
ax.grid(False)
ax.xaxis.pane.set_visible(False)
ax.yaxis.pane.set_visible(False)
ax.zaxis.pane.set_visible(False)
ax.xaxis.line.set_color((1, 1, 1, 0))
ax.yaxis.line.set_color((1, 1, 1, 0))
ax.zaxis.line.set_color((1, 1, 1, 0))
ax.set_xticks([])
ax.set_yticks([])
ax.set_zticks([])
return ax
def _to3d_xy(xy):
x, y = xy[:, 0], xy[:, 1]
z = np.sqrt(np.clip(1.0 - x*x - y*y, 0.0, 1.0))
P = np.stack([x, y, z], axis=1)
# normalize (robust against tiny numerical drift)
P /= np.linalg.norm(P, axis=1, keepdims=True) + EPSILON
return P
def _slerp_arc(p, q, n=200):
p = p / (np.linalg.norm(p) + EPSILON)
q = q / (np.linalg.norm(q) + EPSILON)
dot = np.clip(np.dot(p, q), -1.0, 1.0)
theta = np.arccos(dot)
if theta < EPSILON: # nearly identical points
return np.repeat(p[None, :], n, axis=0)
# great-circle via SLERP
t = np.linspace(0.0, 1.0, n)
s = np.sin
arc = (s((1-t)*theta)[:,None]*p + s(t*theta)[:,None]*q) / (s(theta) + EPSILON)
# normalize for safety
arc /= np.linalg.norm(arc, axis=1, keepdims=True) + EPSILON
return arc
def draw_geodesic(ax, a2d, b2d, linestyle='-', draw_arrow=True, alpha=0.8,
num_points_grid=300, color='blue', lw=2.0, arrow_scale=30, shorten=0.04):
A3, B3 = _to3d_xy(np.array([a2d])), _to3d_xy(np.array([b2d]))
arc = _slerp_arc(A3[0], B3[0], n=num_points_grid)
x_full, y_full = arc[:, 0], arc[:, 1]
if draw_arrow:
k0 = int(2 * shorten * num_points_grid)
k1 = num_points_grid - k0
else:
k0 = int(shorten * num_points_grid)
k1 = num_points_grid - k0
x, y = x_full[k0:k1], y_full[k0:k1]
ax.plot(x, y, color=color, lw=lw, solid_capstyle='round', alpha=alpha, linestyle=linestyle)
if draw_arrow:
# Add arrowheads at both ends
k0 = int(shorten * num_points_grid)
k1 = num_points_grid - k0
x, y = x_full[k0:k1], y_full[k0:k1]
arrow1 = FancyArrowPatch(
(x[1], y[1]), (x[0], y[0]),
arrowstyle='-|>', color=color,
mutation_scale=arrow_scale, lw=0
)
arrow2 = FancyArrowPatch(
(x[-2], y[-2]), (x[-1], y[-1]),
arrowstyle='-|>', color=color,
mutation_scale=arrow_scale, lw=0
)
ax.add_patch(arrow1)
ax.add_patch(arrow2)
return ax
class Arrow3D(FancyArrowPatch):
def __init__(self, xs, ys, zs, *args, **kwargs):
super().__init__((0, 0), (0, 0), *args, **kwargs)
self._verts3d = xs, ys, zs
def do_3d_projection(self, renderer=None):
xs3d, ys3d, zs3d = self._verts3d
xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.get_proj())
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
return np.min(zs)
def plot_decorrelation(ax):
num_points_grid = 512
num_points_on_ellipsoid = 18
image_scale = 2
xs = np.linspace(-2, 2, num_points_grid)
ys = np.linspace(-2, 2, num_points_grid)
x, y = np.meshgrid(xs, ys)
r2 = x**2 + y**2
mask_s = r2 <= 1.0
z_s = np.zeros_like(x)
z_s[mask_s] = np.sqrt(1.0 - r2[mask_s])
nx, ny, nz = x.copy(), y.copy(), z_s.copy()
nrm = np.sqrt(nx**2 + ny**2 + nz**2) + EPSILON
nx, ny, nz = nx/nrm, ny/nrm, nz/nrm
light_dir = np.array([-0.5, -0.5, 0.8])
light_dir /= np.linalg.norm(light_dir)
intensity = np.maximum(0.0, nx*light_dir[0] + ny*light_dir[1] + nz*light_dir[2])
img_s = np.ones_like(x)
img_s[mask_s] = np.clip(0.2 + 0.9*intensity[mask_s], 0, 1)
ax.imshow(img_s, cmap='gray',
extent=[-image_scale, image_scale, -image_scale, image_scale],
vmin=0, vmax=1, alpha=1)
a, b, c = 1.50, 0.60, 0.40
theta = np.deg2rad(30)
ct, st = np.cos(theta), np.sin(theta)
xL = ct*x + st*y
yL = -st*x + ct*y
vL = (xL/a)**2 + (yL/b)**2
mask_e = vL <= 1.0
zL = np.zeros_like(xL)
zL[mask_e] = c * np.sqrt(1.0 - vL[mask_e])
nxL = xL/(a*a)
nyL = yL/(b*b)
nzL = np.zeros_like(zL)
nzL[mask_e] = zL[mask_e]/(c*c)
nrm = np.sqrt(nxL**2 + nyL**2 + nzL**2) + EPSILON
nxL, nyL, nzL = nxL/nrm, nyL/nrm, nzL/nrm
nxE = ct*nxL - st*nyL
nyE = st*nxL + ct*nyL
nzE = nzL
light_dir = np.array([0.5, 0.5, -0.8])
light_dir /= np.linalg.norm(light_dir)
intensity_e = np.maximum(0.0, nxE*light_dir[0] + nyE*light_dir[1] + nzE*light_dir[2])
img_e = np.full_like(x, np.nan, dtype=float)
img_e[mask_e] = np.clip(0.5 + 0.9*intensity_e[mask_e], 0, 1)
cmap = cm.Blues.copy()
cmap.set_bad(color="white")
ax.imshow(img_e, cmap=cmap, origin='lower',
extent=[-image_scale, image_scale, -image_scale, image_scale],
vmin=0, vmax=1, alpha=0.4)
# Pick points on ellipsoid uniformly.
phi = np.linspace(0, 2*np.pi, num_points_on_ellipsoid, endpoint=False)
# Ellipsoid rim in local coords.
xL_rim = a*np.cos(phi)
yL_rim = b*np.sin(phi)
zL_rim = np.zeros_like(phi)
# rotate back to world
xw = ct*xL_rim - st*yL_rim
yw = st*xL_rim + ct*yL_rim
zw = zL_rim
Pellip = np.stack([xw, yw, zw], axis=1)
# matching sphere rim points: same world direction in xy, unit radius, z=0
rxy = np.sqrt(xw**2 + yw**2) + EPSILON
Psphere = np.stack([xw/rxy, yw/rxy, np.zeros_like(rxy)], axis=1)
ax.scatter(Pellip[:, 0], Pellip[:, 1], s=80, color='#0c2458', alpha=0.5)
ax.scatter(Psphere[:, 0], Psphere[:, 1], s=80, facecolors='#b64342', alpha=0.5, linewidths=2)
for p0, p1 in zip(Pellip, Psphere):
ax.annotate("", xy=(p1[0], p1[1]), xytext=(p0[0], p0[1]),
arrowprops=dict(arrowstyle="->", color="#b64342", lw=3, mutation_scale=20))
ax.set_xlim([-1.6, 1.6])
ax.set_ylim([-2, 1.6])
ax.set_axis_off()
arrow_cov = Line2D([], [], color="#b64342", alpha=0.8,
marker=r'$\rightarrow$', linestyle="None", markersize=35, label="Decorrelation")
ax.legend(handles=[arrow_cov], frameon=False, loc="lower center", fontsize=24, bbox_to_anchor=(0.5, 0.1))
return ax
def plot_orthogonalization(ax):
num_points_grid = 512
xs = np.linspace(-1, 1, num_points_grid)
ys = np.linspace(-1, 1, num_points_grid)
x, y = np.meshgrid(xs, ys)
r2 = x**2 + y**2
mask = r2 <= 1.0
z = np.zeros_like(x)
z[mask] = np.sqrt(1.0 - r2[mask])
nx, ny, nz = x.copy(), y.copy(), z.copy()
norm = np.sqrt(nx**2 + ny**2 + nz**2) + EPSILON
nx, ny, nz = nx / norm, ny / norm, nz / norm
# light from top left.
light_dir = np.array([-0.5, 0.5, 0.8])
light_dir /= np.linalg.norm(light_dir)
intensity = np.maximum(0.0, nx*light_dir[0] + ny*light_dir[1] + nz*light_dir[2])
ambient = 0.3
shade = np.clip(ambient + 0.9*intensity, 0, 1)
img = np.ones((num_points_grid, num_points_grid))
img[mask] = shade[mask]
ax.imshow(img, cmap='gray', origin='lower', extent=[-1, 1, -1, 1], vmin=0, vmax=1, alpha=0.5)
ax.set_ylim([-2.1, 1.5])
ax.set_axis_off()
# add 3 fixed blue points on sphere
pts = np.array([
[-0.2, 0.6],
[0.9, 0.0],
[-0.75, -0.4],
])
ax.scatter(pts[:, 0], pts[:, 1], s=80, color='#0c2458', alpha=0.5)
ax = draw_geodesic(ax, pts[0], pts[1], color='#b64342', lw=4)
ax = draw_geodesic(ax, pts[0], pts[2], color='#b64342', lw=4)
ax = draw_geodesic(ax, pts[1], pts[2], linestyle='--', draw_arrow=False, color='#42949e', lw=4, alpha=0.5)
ax = draw_geodesic(ax, pts[0], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)
ax = draw_geodesic(ax, pts[1], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)
ax = draw_geodesic(ax, pts[2], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)
ax.text(0.45, 0.4, "acute angle,\ndisperse", color="#b64342", fontsize=24, ha="center", va="center",
bbox=dict(facecolor="white", alpha=1, edgecolor="none", boxstyle="round,pad=0.2"))
ax.text(-0.6, 0.15, "acute angle,\ndisperse", color="#b64342", fontsize=24, ha="center", va="center",
bbox=dict(facecolor="white", alpha=1, edgecolor="none", boxstyle="round,pad=0.2"))
ax.text(0.15, -0.36, "obtuse angle,\ndo nothing", color="#42949e", fontsize=24, ha="center", va="center",
bbox=dict(facecolor="white", alpha=1, edgecolor="none", boxstyle="round,pad=0.2"))
arrow_disp = Line2D([], [], color="#b64342", alpha=0.8,
marker=r'$\leftarrow\rightarrow$', linestyle="None", markersize=50, label="Orthogonalization")
ax.legend(handles=[arrow_disp], frameon=False, loc="lower center", fontsize=24, bbox_to_anchor=(0.5, 0.15))
return ax
def plot_l2_repel(ax):
tau = 0.5
Z = np.array([
[ 3.0, -3.0, 0.0],
[ 1.0, -3.0, 0.0],
[ -3.0, -2.0, -1.0],
[ -2.0, -1.0, 1.0],
[ -2.0, -1.0, 4.0],
[ 0.0, 2.0, 2.0],
[ 4.0, 0.0, 2.0],
[ 4.0, -2.0, 0.0],
])
num_points, d = Z.shape
D2 = pairwise_sqdist(Z) / d
W = np.exp(-D2 / tau)
G_disp = np.zeros_like(Z)
for i in range(num_points):
diff = Z[i] - Z
G_disp[i] = (2.0 / tau) * (W[i][:, None] * diff).sum(axis=0)
A_disp = safe_scale(G_disp)
Vn = -Z / (np.linalg.norm(Z, axis=1, keepdims=True) + EPSILON)
ax.view_init(elev=30, azim=-60)
ax = nice_axes(ax, 6)
ax.set_xlim([-3, 5])
ax.set_ylim([-3, 5])
ax.set_zlim([-6, 4])
ax.scatter(Z[:, 0], Z[:, 1], Z[:, 2], s=80, color='#0c2458', alpha=0.5)
for i in range(num_points):
p0 = Z[i]
p1 = Z[i] + A_disp[i] * 1.25
arrow = Arrow3D([p0[0], p1[0]], [p0[1], p1[1]], [p0[2], p1[2]],
mutation_scale=16, lw=4, arrowstyle='->', color='#b64342', alpha=0.8)
ax.add_artist(arrow)
for i in range(num_points):
p0 = Z[i]
p1 = Z[i] + Vn[i] * 1.2
arrow = Arrow3D([p0[0], p1[0]], [p0[1], p1[1]], [p0[2], p1[2]],
mutation_scale=10, lw=3, arrowstyle='->', color='#9a4d8e', alpha=0.8)
ax.add_artist(arrow)
for i in range(num_points):
ax.plot([Z[i, 0], 0], [Z[i, 1], 0], [Z[i, 2], 0],
linestyle="--", color="black", linewidth=1, alpha=0.8)
arrow_disp = Line2D([], [], color="#b64342", alpha=0.8,
marker=r'$\rightarrow$', linestyle="None", markersize=25,
label=r"${\ell_2}$-repel")
arrow_norm = Line2D([0, 1], [0, 0], color="#9a4d8e", alpha=0.8,
marker=r'$\rightarrow$', linestyle="None", markersize=25,
label="norm regularization")
ax.legend(handles=[arrow_disp, arrow_norm], frameon=False, loc="lower center",
fontsize=24, bbox_to_anchor=(0.5, 0))
ax.text(0.8, 0.0, 8.0, "pairwise dispersion", color="#b64342", fontsize=24, ha="left", va="center",
bbox=dict(facecolor="white", alpha=1, edgecolor="none", boxstyle="round,pad=0.2"))
ax.text(0.8, 0.0, 6.5, "norm reduction", color="#9a4d8e", fontsize=24, ha="left", va="center",
bbox=dict(facecolor="white", alpha=1, edgecolor="none", boxstyle="round,pad=0.2"))
return ax
def plot_angular_spread(ax):
num_points_grid = 512
xs = np.linspace(-1, 1, num_points_grid)
ys = np.linspace(-1, 1, num_points_grid)
x, y = np.meshgrid(xs, ys)
r2 = x**2 + y**2
mask = r2 <= 1.0
z = np.zeros_like(x)
z[mask] = np.sqrt(1.0 - r2[mask])
nx, ny, nz = x.copy(), y.copy(), z.copy()
norm = np.sqrt(nx**2 + ny**2 + nz**2) + EPSILON
nx, ny, nz = nx / norm, ny / norm, nz / norm
# light from top left.
light_dir = np.array([-0.5, 0.5, 0.8])
light_dir /= np.linalg.norm(light_dir)
intensity = np.maximum(0.0, nx*light_dir[0] + ny*light_dir[1] + nz*light_dir[2])
ambient = 0.3
shade = np.clip(ambient + 0.9*intensity, 0, 1)
img = np.ones((num_points_grid, num_points_grid))
img[mask] = shade[mask]
ax.imshow(img, cmap='gray', origin='lower', extent=[-1, 1, -1, 1], vmin=0, vmax=1, alpha=0.5)
ax.set_ylim([-2.1, 1.5])
ax.set_axis_off()
# add 3 fixed blue points on sphere
pts = np.array([
[-0.2, 0.6],
[0.9, 0.0],
[-0.75, -0.4],
])
ax.scatter(pts[:, 0], pts[:, 1], s=80, color='#0c2458', alpha=0.5)
ax = draw_geodesic(ax, pts[0], pts[1], color='#b64342', lw=4)
ax = draw_geodesic(ax, pts[0], pts[2], color='#b64342', lw=4)
ax = draw_geodesic(ax, pts[1], pts[2], color='#b64342', lw=4)
ax = draw_geodesic(ax, pts[0], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)
ax = draw_geodesic(ax, pts[1], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)
ax = draw_geodesic(ax, pts[2], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)
ax.text(0.45, 0.4, "disperse", color="#b64342", fontsize=24, ha="center", va="center",
bbox=dict(facecolor="white", alpha=1, edgecolor="none", boxstyle="round,pad=0.2"))
ax.text(-0.6, 0.15, "disperse", color="#b64342", fontsize=24, ha="center", va="center",
bbox=dict(facecolor="white", alpha=1, edgecolor="none", boxstyle="round,pad=0.2"))
ax.text(0.15, -0.36, "disperse", color="#b64342", fontsize=24, ha="center", va="center",
bbox=dict(facecolor="white", alpha=1, edgecolor="none", boxstyle="round,pad=0.2"))
arrow_disp = Line2D([], [], color="#b64342", alpha=0.8,
marker=r'$\leftarrow\rightarrow$', linestyle="None", markersize=50, label="Dispersion loss")
ax.legend(handles=[arrow_disp], frameon=False, loc="lower center", fontsize=24, bbox_to_anchor=(0.5, 0.145))
return ax
if __name__ == "__main__":
save_path = './figures/illustration.png'
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'sans-serif'
fig = plt.figure(figsize=(24, 8))
ax = fig.add_subplot(1, 4, 1)
plot_angular_spread(ax)
ax = fig.add_subplot(1, 4, 2)
plot_decorrelation(ax)
ax = fig.add_subplot(1, 4, 3, projection="3d")
plot_l2_repel(ax)
ax = fig.add_subplot(1, 4, 4)
plot_orthogonalization(ax)
os.makedirs(os.path.dirname(save_path), exist_ok=True)
fig.tight_layout(pad=2)
fig.savefig(save_path, dpi=300)
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.ticker import LinearLocator, FormatStrFormatter
def load_freq_prior_data(data_dir):
datasets = {}
for name in sorted(os.listdir(data_dir)):
if not name.endswith(".npz"):
continue
path = os.path.join(data_dir, name)
data = np.load(path)
datasets[name] = {key: data[key] for key in data.files}
return datasets
def plot_dataset(ax, title, sample1_arr, sample2_arr, max_freq_radius,
line_colors, line_styles, legend_labels):
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_linewidth(2)
ax.spines['bottom'].set_linewidth(2)
ax.plot(np.arange(len(sample1_arr))[:max_freq_radius],
sample1_arr[:max_freq_radius],
color=line_colors[0], linestyle=line_styles[0], linewidth=2.5)
ax.plot(np.arange(len(sample2_arr))[:max_freq_radius],
sample2_arr[:max_freq_radius],
color=line_colors[1], linestyle=line_styles[1], linewidth=2.5)
ax.set_title(title, fontfamily='monospace')
ax.set_xlabel('Frequency Radius')
ax.set_xticks(np.arange(max_freq_radius)[::4])
ax.set_ylabel('Mean Amplitude')
ax.set_ylim(bottom=0)
ax.yaxis.set_major_locator(LinearLocator(4))
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f'))
ax.legend(legend_labels, frameon=False, fontsize=14)
if __name__ == "__main__":
data_dir = os.path.join(os.path.dirname(__file__), "data")
datasets = load_freq_prior_data(data_dir)
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 16
fig = plt.figure(figsize=(20, 7))
gs = fig.add_gridspec(2, 5, width_ratios=[1, 1, 0, 1, 1])
# Part 1. Plot the individual datasets.
max_freq_radius = 17
line_colors = ['#2A9D8F', '#E76F51']
line_styles = ['-', '--']
subplot_positions = [gs[0, 0], gs[0, 1], gs[1, 0], gs[1, 1]]
per_dataset_names = ['Kvasir', 'CVC-ClinicDB', 'CVC-ColonDB', 'ETIS']
max_name_len = max(len(name) for name in per_dataset_names)
for dataset_idx, dataset_name in enumerate(per_dataset_names):
data = datasets[dataset_name.lower() + '_freq_prior.npz']
sample1_arr = data['data1']
sample2_arr = data['data2']
ax = fig.add_subplot(subplot_positions[dataset_idx])
plot_dataset(ax, dataset_name, sample1_arr, sample2_arr,
max_freq_radius, line_colors, line_styles,
[r'Random subset 1 $(N=50)$', r'Random subset 2 $(N=50)$'])
# Part 2. Plot the mixed dataset across two columns.
data = datasets['mixed_freq_prior.npz']
sample1_arr = data['data1']
sample2_arr = data['data2']
ax = fig.add_subplot(gs[0, 3:5])
plot_dataset(ax, '', sample1_arr, sample2_arr, max_freq_radius, line_colors, line_styles,
[r'Random subset 1 $(N=500)$', r'Random subset 2 $(N=500)$'])
ax.set_title('Mixuture of all four datasets', fontfamily='helvetica')
# Part 3. Plot per-dataset mean/std curves in a single panel.
ax = fig.add_subplot(gs[1, 3])
per_dataset_colors = ['#4C72B0', '#C2A5CF', '#7B3294', '#8C564B']
curve_means = []
for dataset_idx, dataset_name in enumerate(['Kvasir', 'CVC-ClinicDB', 'CVC-ColonDB', 'ETIS']):
data = datasets[dataset_name.lower() + '_freq_prior.npz']
sample1_arr = data['data1'][:max_freq_radius]
sample2_arr = data['data2'][:max_freq_radius]
stacked = np.stack([sample1_arr, sample2_arr], axis=0)
mean = stacked.mean(axis=0)
std = stacked.std(axis=0)
x_vals = np.arange(len(mean))
color = per_dataset_colors[dataset_idx]
padded_name = dataset_name.ljust(max_name_len)
ax.plot(x_vals, mean, color=color, linewidth=1.5,
label=f'{padded_name}' + r' $(N=100)$')
ax.fill_between(x_vals, mean - std, mean + std, color=color, alpha=0.2)
curve_means.append(mean)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_linewidth(2)
ax.spines['bottom'].set_linewidth(2)
ax.axvline(2, color='0.6', linestyle='--', linewidth=1.2, label=r'Radius$=2$')
x_left = ax.get_xlim()[0]
for mean, color in zip(curve_means, per_dataset_colors):
ax.hlines(mean[2], xmin=x_left, xmax=2, color=color, linestyle='--', linewidth=1.0)
ax.set_xlabel('Frequency Radius')
ax.set_xticks(np.arange(max_freq_radius)[::4])
ax.set_xlim(left=x_left, right=max_freq_radius)
ax.set_ylabel('Mean Amplitude')
ax.set_ylim(bottom=0, top=3)
ax.yaxis.set_major_locator(LinearLocator(4))
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f'))
ax.legend(frameon=False, ncol=1, prop={'family': 'monospace', 'size': 12})
# Part 4. Plot foreground/background curves in a single panel.
data = datasets['foreground_background.npz']
polyp_arr = data['data1'][:max_freq_radius]
background_arr = data['data2'][:max_freq_radius]
x_vals = np.arange(len(polyp_arr))
polyp_color = '#1F3A93'
background_color = '#16A085'
ax = fig.add_subplot(gs[1, 4])
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_linewidth(2)
ax.spines['bottom'].set_linewidth(2)
fb_names = ['Polyp', 'Background']
fb_max_len = max(len(name) for name in fb_names)
ax.plot(x_vals, polyp_arr, color=polyp_color, linewidth=2.5,
label=f'{fb_names[0].ljust(fb_max_len)}' + r' $(N=500)$')
ax.plot(x_vals, background_arr, color=background_color, linewidth=2.5,
linestyle=':', label=f'{fb_names[1].ljust(fb_max_len)}' + r' $(N=500)$')
ax.axvline(2, color='0.6', linestyle='--', linewidth=1.2, label=r'Radius$=2$')
x_left = ax.get_xlim()[0]
ax.hlines(polyp_arr[2], xmin=x_left, xmax=2, color=polyp_color, linestyle='--', linewidth=1.2)
ax.hlines(background_arr[2], xmin=x_left, xmax=2, color=background_color, linestyle='--', linewidth=1.2)
ax.set_xlabel('Frequency Radius')
ax.set_xticks(np.arange(max_freq_radius)[::4])
ax.set_xlim(left=x_left, right=max_freq_radius)
ax.set_ylabel('Mean Amplitude')
ax.set_ylim(bottom=0)
ax.yaxis.set_major_locator(LinearLocator(4))
ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f'))
ax.legend(frameon=False, prop={'family': 'monospace', 'size': 12})
fig.tight_layout(pad=1)
os.makedirs('figures', exist_ok=True)
fig.savefig('figures/freq_prior.png', dpi=300)import os
import numpy as np
from matplotlib import pyplot as plt
from raw_data import data_comparison_IEDB, data_ablation_IEDB, data_comparison_Cancer, data_ablation_Cancer
def decode_ablation(data_dict):
binary_list = data_dict['ablations']
component_str = data_dict['components']
decoded_list = []
for binary_code in binary_list:
assert len(binary_code) == len(component_str)
decoded_str = []
for i, c in enumerate(binary_code):
if c == '1':
decoded_str.append(component_str[i])
decoded_list.append(' + '.join(decoded_str))
return decoded_list
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 3
plt.rcParams['svg.fonttype'] = 'none'
fig = plt.figure(figsize=(28, 6))
ax = fig.add_subplot(1, 4, 1)
ax.bar(range(len(data_comparison_IEDB['mean'])),
data_comparison_IEDB['mean'][:, 0],
yerr=data_comparison_IEDB['std'][:, 0],
capsize=5,
color=data_comparison_IEDB['colors'],
label=data_comparison_IEDB['methods'])
handles, labels = ax.get_legend_handles_labels()
ax.set_xticks([])
ax.set_ylim([0.5, 0.9])
ax.set_ylabel(data_comparison_IEDB['metrics'][0], fontsize=32)
ax = fig.add_subplot(1, 4, 2)
ax.bar(range(len(data_comparison_IEDB['mean'])),
data_comparison_IEDB['mean'][:, 1],
yerr=data_comparison_IEDB['std'][:, 1],
capsize=5,
color=data_comparison_IEDB['colors'])
ax.set_xticks([])
ax.set_ylim([0.15, 0.75])
ax.set_ylabel(data_comparison_IEDB['metrics'][1], fontsize=32)
ax = fig.add_subplot(1, 4, 3)
ax.bar(range(len(data_comparison_IEDB['mean'])),
data_comparison_IEDB['mean'][:, 2],
yerr=data_comparison_IEDB['std'][:, 2],
capsize=5,
color=data_comparison_IEDB['colors'])
ax.set_xticks([])
ax.set_ylim([0.18, 0.55])
ax.set_ylabel(data_comparison_IEDB['metrics'][2], fontsize=32)
ax = fig.add_subplot(1, 4, 4)
ax.legend(handles, labels)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/bars_comparison_IEDB.png', dpi=600)
plt.close(fig)
fig = plt.figure(figsize=(24, 8))
ax = fig.add_subplot(1, 3, 1)
ax.barh(range(len(data_ablation_IEDB['mean'][:, 0])),
data_ablation_IEDB['mean'][:, 0],
xerr=data_ablation_IEDB['std'][:, 0],
color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in np.linspace(0.2, 1.0, 12)],
ecolor='k',
capsize=5,
)
ax.set_yticks(range(len(data_ablation_IEDB['ablations'])))
ax.set_yticklabels(decode_ablation(data_ablation_IEDB))
ax.set_xlim([0.75, 0.9])
ax.set_xticks([0.75, 0.8, 0.85, 0.9])
ax.set_xticklabels([0.75, 0.8, 0.85, 0.9])
ax.set_xlabel(data_ablation_IEDB['metrics'][0], fontsize=32)
ax = fig.add_subplot(1, 3, 2)
ax.barh(range(len(data_ablation_IEDB['mean'][:, 1])),
data_ablation_IEDB['mean'][:, 1],
xerr=data_ablation_IEDB['std'][:, 1],
color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in np.linspace(0.2, 1.0, 12)],
ecolor='k',
capsize=5,
)
ax.set_yticks([])
ax.set_xlim([0.4, 0.72])
ax.set_xticks([0.4, 0.5, 0.6, 0.7])
ax.set_xticklabels([0.4, 0.5, 0.6, 0.7])
ax.set_xlabel(data_ablation_IEDB['metrics'][1], fontsize=32)
ax = fig.add_subplot(1, 3, 3)
ax.barh(range(len(data_ablation_IEDB['mean'][:, 2])),
data_ablation_IEDB['mean'][:, 2],
xerr=data_ablation_IEDB['std'][:, 2],
color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in np.linspace(0.2, 1.0, 12)],
ecolor='k',
capsize=5,
)
ax.set_yticks([])
ax.set_xlim([0.4, 0.55])
ax.set_xticks([0.4, 0.45, 0.5, 0.55])
ax.set_xticklabels([0.4, 0.45, 0.5, 0.55])
ax.set_xlabel(data_ablation_IEDB['metrics'][2], fontsize=32)
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/bars_ablation_IEDB.png', dpi=600)
plt.close(fig)
fig = plt.figure(figsize=(28, 6))
ax = fig.add_subplot(1, 4, 1)
ax.bar(range(len(data_comparison_Cancer['mean'])),
data_comparison_Cancer['mean'][:, 0],
yerr=data_comparison_Cancer['std'][:, 0],
capsize=5,
color=data_comparison_Cancer['colors'],
label=data_comparison_Cancer['methods'])
handles, labels = ax.get_legend_handles_labels()
ax.set_xticks([])
ax.set_ylim([0.5, 0.82])
ax.set_ylabel(data_comparison_Cancer['metrics'][0], fontsize=32)
ax = fig.add_subplot(1, 4, 2)
ax.bar(range(len(data_comparison_Cancer['mean'])),
data_comparison_Cancer['mean'][:, 1],
yerr=data_comparison_Cancer['std'][:, 1],
capsize=5,
color=data_comparison_Cancer['colors'])
ax.set_xticks([])
ax.set_ylim([0.16, 0.52])
ax.set_ylabel(data_comparison_Cancer['metrics'][1], fontsize=32)
ax = fig.add_subplot(1, 4, 3)
ax.bar(range(len(data_comparison_Cancer['mean'])),
data_comparison_Cancer['mean'][:, 2],
yerr=data_comparison_Cancer['std'][:, 2],
capsize=5,
color=data_comparison_Cancer['colors'])
ax.set_xticks([])
ax.set_ylim([0.14, 0.44])
ax.set_ylabel(data_comparison_Cancer['metrics'][2], fontsize=32)
ax = fig.add_subplot(1, 4, 4)
ax.legend(handles, labels)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/bars_comparison_Cancer.png', dpi=600)
plt.close(fig)
fig = plt.figure(figsize=(28, 6))
items_shown = [6, 4, 0] # transfer + contrastive, transfer, none
ax = fig.add_subplot(1, 4, 1)
ax.bar(range(len(items_shown)),
data_ablation_Cancer['mean'][:, 0][items_shown],
yerr=data_ablation_Cancer['std'][:, 0][items_shown],
capsize=5,
color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in [1.0, 0.7, 0.4]],
label=['ImmunoStruct', 'No Contrastive Learning',
'No Contrastive Learning &\nNo Transfer Learning'])
handles, labels = ax.get_legend_handles_labels()
ax.set_xticks([])
ax.set_ylim([0.68, 0.80])
ax.set_ylabel(data_ablation_Cancer['metrics'][0], fontsize=32)
ax = fig.add_subplot(1, 4, 2)
ax.bar(range(len(items_shown)),
data_ablation_Cancer['mean'][:, 1][items_shown],
yerr=data_ablation_Cancer['std'][:, 1][items_shown],
capsize=5,
color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in [1.0, 0.7, 0.4]])
ax.set_xticks([])
ax.set_ylim([0.30, 0.52])
ax.set_ylabel(data_ablation_Cancer['metrics'][1], fontsize=32)
ax = fig.add_subplot(1, 4, 3)
ax.bar(range(len(items_shown)),
data_ablation_Cancer['mean'][:, 2][items_shown],
yerr=data_ablation_Cancer['std'][:, 2][items_shown],
capsize=5,
color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in [1.0, 0.7, 0.4]])
ax.set_xticks([])
ax.set_ylim([0.29, 0.43])
ax.set_ylabel(data_ablation_Cancer['metrics'][2], fontsize=32)
ax = fig.add_subplot(1, 4, 4)
ax.legend(handles, labels)
ax.set_axis_off()
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/bars_ablation_Cancer.png', dpi=600)
plt.close(fig)
import numpy as np
data_comparison_IEDB = {
'methods': [r'Prime-2.1', r'NetMHCpan', r'MHCnuggets', r'MHCflurry', r'DeepNeo',
r'BigMHC-EL', r'BigMHC-IM', r'BigMHC$_\text{retrained}$', r'ImmunoStruct (ours)'],
'colors': ['#CFCECE', '#F4EEAC', '#FBDFE2', '#D9B9D4', '#DAA87C', '#DDF3DE', '#AADCA9', '#8BCF8B', '#3775BA'],
'metrics': ['AUROC', 'AUPRC', 'Mean PPVn'],
'mean': np.array([
[0.538, 0.212, 0.207],
[0.537, 0.214, 0.220],
[0.546, 0.246, 0.233],
[0.577, 0.260, 0.273],
[0.767, 0.438, 0.411],
[0.588, 0.269, 0.290],
[0.684, 0.319, 0.333],
[0.793, 0.462, 0.445],
[0.882, 0.696, 0.514],
]),
'std': np.array([
[0.012, 0.031, 0.028 / np.sqrt(5)],
[0.027, 0.020, 0.018 / np.sqrt(5)],
[0.023, 0.019, 0.031 / np.sqrt(5)],
[0.021, 0.026, 0.028 / np.sqrt(5)],
[0.032, 0.024, 0.017 / np.sqrt(5)],
[0.018, 0.016, 0.024 / np.sqrt(5)],
[0.028, 0.048, 0.034 / np.sqrt(5)],
[0.013, 0.027, 0.009 / np.sqrt(5)],
[0.005, 0.020, 0.021 / np.sqrt(5)],
]),
}
data_ablation_IEDB = {
'ablations': ['10000', '01000', '11000', '01100', '11100', '11110',
'10001', '01001', '11001', '01101', '11101', '11111'],
'components': ['Structure', 'Sequence', 'Bchems', 'MMA', 'Transfer Learning'],
'metrics': ['AUROC', 'AUPRC', 'Mean PPVn'],
'mean': np.array([
[0.775, 0.442, 0.433],
[0.842, 0.553, 0.430],
[0.840, 0.547, 0.444],
[0.842, 0.554, 0.419],
[0.840, 0.547, 0.440],
[0.860, 0.615, 0.462],
[0.805, 0.497, 0.414],
[0.845, 0.568, 0.427],
[0.844, 0.569, 0.440],
[0.844, 0.561, 0.442],
[0.840, 0.554, 0.441],
[0.882, 0.696, 0.514],
]),
'std': np.array([
[0.012, 0.022, 0.020 / np.sqrt(5)],
[0.006, 0.014, 0.009 / np.sqrt(5)],
[0.007, 0.024, 0.028 / np.sqrt(5)],
[0.006, 0.020, 0.017 / np.sqrt(5)],
[0.006, 0.018, 0.032 / np.sqrt(5)],
[0.013, 0.045, 0.015 / np.sqrt(5)],
[0.006, 0.022, 0.027 / np.sqrt(5)],
[0.007, 0.024, 0.017 / np.sqrt(5)],
[0.005, 0.014, 0.035 / np.sqrt(5)],
[0.006, 0.017, 0.024 / np.sqrt(5)],
[0.006, 0.028, 0.029 / np.sqrt(5)],
[0.005, 0.020, 0.021 / np.sqrt(5)],
]),
}
data_comparison_Cancer = {
'methods': [r'Prime-2.1', r'NetMHCpan', r'MHCnuggets', r'MHCflurry', r'DeepNeo',
r'BigMHC-EL', r'BigMHC-IM', r'BigMHC$_\text{retrained}$', r'NeoaPred',
r'ImmunoStruct (ours)'],
'colors': ['#CFCECE', '#F4EEAC', '#FBDFE2', '#D9B9D4', '#DAA87C', '#DDF3DE', '#AADCA9', '#8BCF8B', "#92E3F9", '#3775BA'],
'metrics': ['AUROC', 'AUPRC', 'Mean PPVn'],
'mean': np.array([
[0.645, 0.259, 0.295],
[0.559, 0.210, 0.211],
[0.530, 0.210, 0.175],
[0.658, 0.304, 0.329],
[0.535, 0.261, 0.222],
[0.632, 0.248, 0.245],
[0.771, 0.373, 0.357],
[0.682, 0.310, 0.325],
[0.556, 0.267, 0.292],
[0.771, 0.433, 0.364],
]),
'std': np.array([
[0.026, 0.023, 0.029 / np.sqrt(5)],
[0.049, 0.037, 0.045 / np.sqrt(5)],
[0.014, 0.017, 0.023 / np.sqrt(5)],
[0.015, 0.032, 0.030 / np.sqrt(5)],
[0.016, 0.053, 0.070 / np.sqrt(5)],
[0.034, 0.039, 0.042 / np.sqrt(5)],
[0.040, 0.062, 0.036 / np.sqrt(5)],
[0.012, 0.020, 0.030 / np.sqrt(5)],
[0.016, 0.022, 0.081 / np.sqrt(5)],
[0.024, 0.069, 0.127 / np.sqrt(5)],
]),
}
data_ablation_Cancer = {
'coeffs': [[False, 0], [False, 0.001], [False, 0.01], [False, 0.1],
[True, 0], [True, 0.001], [True, 0.01], [True, 0.1]],
'metrics': ['AUROC', 'AUPRC', 'Mean PPVn'],
'mean': np.array([
[0.723, 0.391, 0.358],
[0.712, 0.370, 0.331],
[0.727, 0.405, 0.387],
[0.700, 0.413, 0.354],
[0.756, 0.426, 0.362],
[0.762, 0.418, 0.351],
[0.771, 0.433, 0.365],
[0.725, 0.406, 0.348],
]),
'std': np.array([
[0.027, 0.068, 0.096 / np.sqrt(5)],
[0.040, 0.077, 0.114 / np.sqrt(5)],
[0.037, 0.080, 0.111 / np.sqrt(5)],
[0.036, 0.081, 0.109 / np.sqrt(5)],
[0.031, 0.068, 0.103 / np.sqrt(5)],
[0.024, 0.064, 0.151 / np.sqrt(5)],
[0.024, 0.069, 0.127 / np.sqrt(5)],
[0.035, 0.079, 0.129 / np.sqrt(5)],
]),
}
import os
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
DATA = {
'clinical_stage': [
'Benchmark\nEvaluation', 'Expert\nEvaluation', 'Retrospective\nClinical Validation',
'Prospective\nPilot Study', 'Full\nClinical Trial',
],
'pub_by_category':
{
'Clinical Workflow': {
'Screening or Diagnosis': [10, 7, 10, 3, 0],
'Report Generation': [2, 3, 3, 0, 0],
'Treatment Planning or\nRecommendation': [2, 3, 3, 2, 0],
},
'Patient Support': {
'Patient Question Answering': [5, 13, 1, 1, 0],
'After Visit or Discharge\nSummary Generation': [0, 2, 0, 0, 0],
'Consultation or Interview': [0, 1, 1, 0, 0],
'Patient Education\nMaterial Generation': [1, 6, 0, 0, 0],
'Physician Recommendation': [0, 1, 0, 0, 0],
},
'Education and Training': {
'Exam Taking': [19, 5, 0, 0, 0],
'Medical Education and\nLearning Support': [3, 5, 0, 0, 0],
}
}
}
def plot_heatmap(fig_name: str):
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 16
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 2
os.makedirs(os.path.dirname(fig_name), exist_ok=True)
fig = plt.figure(figsize=(14, 10))
ax = fig.add_subplot(1, 1, 1)
category_count_arr, category_arr = [], []
subcategory_arr = []
value_arr = []
for category in DATA['pub_by_category']:
subcategories = DATA['pub_by_category'][category].keys()
category_count_arr.append(len(subcategories))
category_arr.append(category)
for subcategory in subcategories:
value = DATA['pub_by_category'][category][subcategory]
subcategory_arr.append(subcategory)
value_arr.append(value)
value_arr = np.stack(value_arr, axis=0)
for loc, item in enumerate(subcategory_arr):
item += '\n' + rf'($n={value_arr[loc, :].sum()}$)'
subcategory_arr[loc] = item
stage_arr = DATA['clinical_stage']
for loc, item in enumerate(stage_arr):
item += '\n' + rf'($n={value_arr[:, loc].sum()}$)'
stage_arr[loc] = item
hm = sns.heatmap(value_arr, annot=True, vmin=0, vmax=20, fmt='d', cmap='Reds',
linewidths=1, linecolor='white', ax=ax, cbar=True)
cbar = hm.collections[0].colorbar
cbar.set_ticks([0, 5, 10, 15, 20])
cbar.set_ticklabels([0, 5, 10, 15, 20])
ax.set_yticks(np.arange(len(subcategory_arr)) + 0.5)
ax.set_yticklabels(subcategory_arr, rotation=0)
ax.set_xticks(np.arange(len(stage_arr)) + 0.5)
ax.set_xticklabels(stage_arr, rotation=0)
fig.tight_layout(pad=2)
fig.savefig(fig_name, dpi=300)
return
if __name__ == '__main__':
plot_heatmap('./figures/composition_heatmap.png')import os
import numpy as np
from matplotlib import pyplot as plt
from datetime import datetime
from dateutil.relativedelta import relativedelta
DATA = {
'names': ['Methodological Contribution (Text-only)', 'Evaluation / Application (Text-only)',
'Methodological Contribution (Multimodal)', 'Evaluation / Application (Multimodal)'],
'pub_by_month': np.array(
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 2, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2],
[0, 0, 4, 0, 0, 6, 1, 3, 2, 0, 5, 0, 4, 0, 9, 1, 3, 4, 7, 6, 7, 6, 6, 1, 6, 4, 6, 2, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 2, 1, 0, 2, 3, 0, 1, 2, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0]]
),
# NOTE: Use `*` to move the label up in the annotation. Each `*` moves it up a bit.
'dates_llm': {
'2022-11': 'ChatGPT\n(GPT-3.5)',
'2023-02': 'Bard',
'2023-02': 'LlaMA 1',
'2023-03': 'GPT-4*',
'2025-08': 'GPT-5',
'2023-07': 'LlaMA 2',
'2024-04': 'LlaMA 3',
'2025-04': 'LlaMA 4',
'2023-12': 'Gemini 1.0',
'2024-02': 'Gemini 1.5',
'2024-12': 'Gemini 2.0',
'2025-06': 'Gemini 2.5*',
},
'dates_vlm': {
'2023-02': 'BLIP-2',
'2023-07': 'LlaVA 1.0',
'2023-9': 'GPT-4v',
'2023-10': 'LlaVA 1.5*',
'2023-12': 'Gemini 1.0',
'2024-02': 'Gemini 1.5',
'2024-12': 'Gemini 2.0',
'2025-06': 'Gemini 2.5',
}
}
def month_year_list(start_year, start_month, n_months):
start = datetime(start_year, start_month, 1)
out = []
for i in range(n_months):
d = start + relativedelta(months=i)
out.append(d.strftime('%Y-%m'))
return out
def mark_events(ax, time_arr, y_curve, events, dy=0.1):
x_idx = {t: i for i, t in enumerate(time_arr)}
y0, y1 = ax.get_ylim()
prev_date = None
for date, label in events.items():
if prev_date is None:
prev_date = date
if date in x_idx:
i = x_idx[date]
x, y = i, y_curve[i]
ax.annotate(
label.replace('*', ''),
xy=(x, y),
xytext=(x, y + (1 + 0.8 * np.uint8(label.count('*'))) * dy * (y1 - y0)),
ha='center',
va='bottom',
fontsize=11,
arrowprops=dict(arrowstyle='-|>', lw=1.3, color='black',
shrinkA=0, shrinkB=0, mutation_scale=15)
)
return
def plot_curve(fig_name: str):
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 15
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 2
colors = ["#9BC8FA", "#ffa8a6", "#13457E", "#850c0a"]
os.makedirs(os.path.dirname(fig_name), exist_ok=True)
fig = plt.figure(figsize=(14, 8))
num_months = DATA['pub_by_month'].shape[1]
ax = fig.add_subplot(2, 1, 1)
time_arr = month_year_list(start_year=2022, start_month=11, n_months=num_months)
ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][1, :]), np.cumsum(DATA['pub_by_month'][1, :]), color=colors[1],
label=DATA['names'][1])
ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][0, :]), np.cumsum(DATA['pub_by_month'][0, :]), color=colors[0],
label=DATA['names'][0])
ax.plot(time_arr, np.cumsum(DATA['pub_by_month'][0, :]), lw=3, c=colors[2])
ax.plot(time_arr, np.cumsum(DATA['pub_by_month'][1, :]), lw=3, c=colors[3])
mark_events(ax, time_arr, np.cumsum(DATA['pub_by_month'][1, :]), DATA['dates_llm'])
ax.legend(frameon=False)
ax.set_xticks(time_arr[2::6])
ax.set_ylim([0, 105])
ax.set_ylabel('Cumulative\nPublication Count\n(Text-only)')
ax = fig.add_subplot(2, 1, 2)
time_arr = month_year_list(start_year=2022, start_month=11, n_months=num_months)
ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][3, :]), np.cumsum(DATA['pub_by_month'][3, :]), color=colors[1],
hatch='\\\\\\', edgecolor='black', label=DATA['names'][3])
ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][3, :]), np.cumsum(DATA['pub_by_month'][3, :]), color=colors[1],
facecolor='none', edgecolor='white', linewidth=2) # To visually "erase" the border.
ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][2, :]), np.cumsum(DATA['pub_by_month'][2, :]), color=colors[0],
hatch='///', edgecolor='black', label=DATA['names'][2])
ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][2, :]), np.cumsum(DATA['pub_by_month'][2, :]), color=colors[0],
facecolor='none', edgecolor='white', linewidth=2) # To visually "erase" the border.
ax.plot(time_arr, np.cumsum(DATA['pub_by_month'][2, :]), lw=3, c=colors[2])
ax.plot(time_arr, np.cumsum(DATA['pub_by_month'][3, :]), lw=3, c=colors[3])
ax.legend(frameon=False)
mark_events(ax, time_arr, np.cumsum(DATA['pub_by_month'][3, :]), DATA['dates_vlm'])
ax.set_xticks(time_arr[2::6])
ax.set_ylim([0, 24])
ax.set_ylabel('Cumulative\nPublication Count\n(Multimodal)')
fig.tight_layout(pad=2)
fig.savefig(fig_name, dpi=300)
return
if __name__ == '__main__':
plot_curve('./figures/trend_by_month.png')import os
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
summary_label = r'\textit{Improvement}'
options = ['VAE', 'DDPM', 'LDM', 'FM',
'DiffAb', 'IgLM', 'NOS-C', 'NOS-D',
'OAE + gradient ascent',
'OAE + MCMC',
'OAE + hill climbing',
'OAE + stochastic hill climbing',
r'$\texttt{RNAGenScape}$ \textbf{(ours)}',
summary_label]
colors = ["#cdcdcd", "#767676", "#4d4d4d", "#272727",
"#c4ece7", "#ecc4c4", "#ecc4e7", "#ea84dd",
"#d5e29b", "#bdd35c", "#9fbc1d", "#8ead03",
"#0f4d92"]
xlabel = np.arange(len(options))
xticks = []
results_inference = [0.13, 0.91, 0.74, 5.82, 41.04, 157.57, 0.99, 0.96,
0.50, 10.93, 81.52, 99.66, 0.57]
results_openvaccine_delta_pos = [-0.23, -0.33, -0.07, -0.34, 0.06, 0.24, 0.09, 0.18,
-0.01, -0.11, 0.40, -0.12, 0.54]
results_openvaccine_pct_pos = [42.1, 33.8, 47.5, 32.5, 55.0, 63.1, 54.6, 58.3,
51.0, 45.1, 69.2, 43.5, 77.5]
results_openvaccine_delta_neg = [-0.23, -0.33, -0.07, -0.34, 0.11, 0.01, -2.25, -0.29,
-0.19, -0.19, -0.29, -0.15, -2.81]
results_openvaccine_pct_neg = [57.9, 66.2, 52.5, 67.5, 44.0, 49.6, 90.6, 65.4,
61.3, 57.7, 64.2, 60.0, 97.9]
results_zebrafish_delta_pos = [-0.01, 0.29, -0.95, 0.32, -0.21, -0.57, 0.03, 0.31,
-1.21, -0.20, 0.76, 0.32, 0.77]
results_zebrafish_pct_pos = [49.4, 58.2, 22.5, 59.8, 36.3, 32., 51.1, 57.9,
18.0, 44.3, 74.4, 60.3, 75.0]
results_zebrafish_delta_neg = [-0.01, 0.29, -0.95, 0.32, -0.21, -0.83, -1.07, 0.38,
-0.33, -0.37, -0.87, -0.80, -1.29]
results_zebrafish_pct_neg = [50.6, 41.8, 77.5, 40.2, 60.8, 74.3, 80.8, 40.2,
66.5, 60.1, 75.2, 73.7, 85.1]
results_ribosome_delta_pos = [-0.24, -0.11, -0.24, -0.10, -0.05, 0.63, 0.08, -0.09,
0.43, -0.19, 0.53, 0.19, 0.63]
results_ribosome_pct_pos = [41.7, 45.9, 41.8, 46.4, 45.0, 80.5, 53.6, 46.5,
73.6, 43.0, 83.0, 60.4, 81.4]
results_ribosome_delta_neg = [-0.24, -0.11, -0.24, -0.10, -0.04, -0.51, 0.10, -0.10,
-0.05, -0.10, -0.06, -0.05, -0.58]
results_ribosome_pct_neg = [58.3, 54.1, 58.2, 53.6, 54.2, 65.5, 46.0, 53.6,
55.7, 54.2, 56.1, 55.2, 67.8]
if __name__ == '__main__':
PLOT_DE_NOVO_SPEED = False
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 16
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 2
fig = plt.figure(figsize=(9, 5))
ax = fig.add_subplot(1, 1, 1)
if PLOT_DE_NOVO_SPEED:
ax.bar(np.arange(len(options)), 1 / np.array(results_inference), color=colors, label=options)
ax.hlines(y=1 / results_inference[-1], xmin=-1, xmax=len(options), color=colors[-1], linestyle='--')
ax.set_xlim([-1, len(options)])
ax.legend(loc='upper right', frameon=False, fontsize=12)
ax.set_xticks([])
ax.set_ylabel('Inference Throughput ' + r'$\uparrow$' +'\n(samples / ms)', fontsize=18)
# Add braces.
ymin, ymax = ax.get_ylim()
ax.set_ylim(ymin - 1, ymax)
len_de_novo = 4
mid_de_novo = 0 + (len_de_novo - 0) / 2
start_opt = len_de_novo + 1
end_opt = len(options) - 1
mid_opt = (start_opt + end_opt) / 2
ax.text(mid_de_novo, ymin - 0.3,
r'$\underbrace{\rule{5cm}{0pt}}_{\textrm{\textit{de\ novo}\ generative\ models}}$',
ha='center', va='top', fontsize=14)
ax.text(mid_opt, ymin - 0.3,
r'$\underbrace{\rule{9.2cm}{0pt}}_{\textrm{property\ optimization\ methods}}$',
ha='center', va='top', fontsize=14)
ax.spines['bottom'].set_position(('data', 0))
ax.xaxis.set_ticks_position('bottom')
ax.spines['bottom'].set_linewidth(2)
ax.spines['left'].set_bounds(0, ymax)
else:
# speed_values = 1 / np.array(results_inference[4:])
# options_speed = options[4:-1]
# ax.bar(np.arange(len(options_speed)), speed_values, color=colors[4:], label=options_speed)
speed_values = 1 / np.array(results_inference[4:8] + [results_inference[-1]])
options_speed = options[4:8] + [options[-2]]
ax.bar(np.arange(len(options_speed)), speed_values, color=colors[4:8] + [colors[-1]], label=options_speed)
ax.set_xlim([-1, len(options_speed)])
ax.legend(loc='upper left', frameon=False, fontsize=16, ncol=1)
ax.set_xticks([])
ax.set_ylabel('Inference Throughput ' + r'$\uparrow$' +'\n(samples / ms)', fontsize=18)
ax.set_yscale('log')
ymin, ymax = ax.get_ylim()
ax.set_ylim(ymin, ymax * 20)
# Add numbers on top of bars
for i, val in enumerate(speed_values):
ax.text(i, val * 1.1, f'{val:.3f}', ha='center', va='bottom', fontsize=16)
fig.tight_layout(pad=1)
os.makedirs('./figures', exist_ok=True)
fig.savefig('./figures/results_comparison_speed.png', dpi=300)
plt.close(fig)
fig = plt.figure(figsize=(20, 9))
ax = fig.add_subplot(1, 2, 1)
improvements = np.stack([results_openvaccine_delta_pos, results_openvaccine_delta_neg,
results_zebrafish_delta_pos, results_zebrafish_delta_neg,
results_ribosome_delta_pos, results_ribosome_delta_neg], axis=1)
base_improvements = improvements[:-1]
best_improvements = []
for j in range(base_improvements.shape[1]):
col = base_improvements[:, j]
best_improvements.append(col.max() if j % 2 == 0 else col.min())
best_improvements = np.array(best_improvements)
denom = np.where(best_improvements == 0, np.nan, best_improvements)
improvement_over_best = 100 * (improvements[-1] - best_improvements) / denom
improvements = np.vstack([improvements, improvement_over_best])
improvements_display = improvements.copy()
improvements_display[-1, :] = np.nan
n_rows, n_cols = improvements.shape
vmin, vmax = improvements[:-1].min(0), improvements[:-1].max(0)
cmap_red, cmap_blue = plt.cm.Reds, plt.cm.Blues_r
for j in range(n_cols):
cmap = cmap_red if j % 2 == 0 else cmap_blue
cmap = cmap.copy()
cmap.set_bad(color="white")
norm = mpl.colors.Normalize(vmin=vmin[j] if j % 2 == 1 else 0, vmax=vmax[j] if j % 2 == 0 else 0)
ax.imshow(improvements_display[:, j:j+1], cmap=cmap, norm=norm,
aspect="auto", extent=[j - 0.5, j + 0.5, 0, n_rows], origin="lower")
for (i, j), val in np.ndenumerate(improvements):
if i == n_rows - 1:
color = "forestgreen" if val >= 0 else "darkred"
label = f"{val:+.1f} \\%"
fontsize = 16
else:
cmap = cmap_red if j % 2 == 0 else cmap_blue
norm = mpl.colors.Normalize(vmin=vmin[j] if j % 2 == 1 else 0, vmax=vmax[j] if j % 2 == 0 else 0)
r, g, b, _ = cmap(norm(val))
lum = 0.299 * r + 0.587 * g + 0.114 * b
color = "white" if lum < 0.5 else "black"
label = f"{val:.2f}"
fontsize = 14
ax.text(j, i + 0.5, label, ha="center", va="center", fontsize=fontsize, color=color)
ax.set_title('Median change in property', fontsize=32, pad=24)
ax.set_xlim(-0.5, n_cols - 0.5)
ax.set_xticks(np.arange(n_cols))
ax.set_xticklabels([r'$\texttt{OpenVaccine}~(+)$', r'$\texttt{OpenVaccine}~(-)$',
r'$\texttt{Zebrafish}~(+)$', r'$\texttt{Zebrafish}~(-)$',
r'$\texttt{Ribosome}~(+)$', r'$\texttt{Ribosome}~(-)$'],
fontsize=20, rotation=30)
ax.tick_params(axis='x', which='both', bottom=False, top=False, length=0)
ax.set_yticks(np.arange(n_rows) + 0.5)
ax.set_yticklabels(options, rotation=0, fontsize=20, ha='right')
for tick in ax.get_yticklabels():
if r'$\texttt{RNAGenScape}$' in tick.get_text():
tick.set_fontsize(24)
ax.set_frame_on(False)
ax.invert_yaxis()
# n_rows, n_cols = improvements.shape
# rect = patches.Rectangle((-0.5, n_rows - 1), n_cols, 1, fill=False, edgecolor='black', linewidth=3)
# rect.set_clip_on(False)
# ax.add_patch(rect)
ax = fig.add_subplot(1, 2, 2)
percentages = np.stack([results_openvaccine_pct_pos, results_openvaccine_pct_neg,
results_zebrafish_pct_pos, results_zebrafish_pct_neg,
results_ribosome_pct_pos, results_ribosome_pct_neg], axis=1)
base_percentages = percentages[:-1]
best_percentages = base_percentages.max(0)
pct_improvement_over_best = 100 * (percentages[-1] - best_percentages) / best_percentages
percentages = np.vstack([percentages, pct_improvement_over_best])
percentages_display = percentages.copy()
percentages_display[-1, :] = np.nan
n_rows, n_cols = percentages.shape
vmin, vmax = percentages[:-1].min(0), percentages[:-1].max(0)
cmap_red = plt.cm.Reds.copy()
cmap_red.set_bad(color="white")
for j in range(n_cols):
norm = mpl.colors.Normalize(vmin=max(50, vmin[j]), vmax=vmax[j])
ax.imshow(percentages_display[:, j:j+1], cmap=cmap_red, norm=norm,
aspect="auto", extent=[j - 0.5, j + 0.5, 0, n_rows], origin="lower")
for (i, j), val in np.ndenumerate(percentages):
if i == n_rows - 1:
color = "forestgreen" if val >= 0 else "darkred"
label = f"{val:+.1f} \\%"
fontsize = 16
else:
norm = mpl.colors.Normalize(vmin=max(50, vmin[j]), vmax=vmax[j])
r, g, b, _ = cmap_red(norm(val))
lum = 0.299 * r + 0.587 * g + 0.114 * b
color = "white" if lum < 0.5 else "black"
label = f"{val:.1f} \\%"
fontsize = 14
ax.text(j, i + 0.5, label, ha="center", va="center",
fontsize=fontsize, color=color)
ax.set_title('Success rate', fontsize=32, pad=24)
ax.set_xlim(-0.5, n_cols - 0.5)
ax.set_xticks(np.arange(n_cols))
ax.set_xticklabels([r'$\texttt{OpenVaccine}~(+)$', r'$\texttt{OpenVaccine}~(-)$',
r'$\texttt{Zebrafish}~(+)$', r'$\texttt{Zebrafish}~(-)$',
r'$\texttt{Ribosome}~(+)$', r'$\texttt{Ribosome}~(-)$'],
fontsize=20, rotation=30)
ax.tick_params(axis='x', which='both', bottom=False, top=False, length=0)
ax.set_yticks([])
# ax.set_yticks(np.arange(n_rows) + 0.5)
# ax.set_yticklabels(options, fontsize=16, ha='right')
ax.set_frame_on(False)
ax.invert_yaxis()
# rect = patches.Rectangle((-0.5, n_rows - 1), n_cols, 1,
# fill=False, edgecolor='black', linewidth=3)
# rect.set_clip_on(False)
# ax.add_patch(rect)
fig.tight_layout(pad=2)
os.makedirs('./figures', exist_ok=True)
fig.savefig('./figures/results_comparison_optimization.png', dpi=300)
plt.close(fig)
import os
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
def function(x, y):
z = 0.6 * np.exp(-((x - 1)**2 + (y + 1)**2))
z += 0.5 * np.exp(-((x - 1)**2 + (y - 4)**2))
z += 0.3 * np.exp(-((x - 2)**2 + (y - 2)**2))
z += 0.2 * np.exp(-((x + 3)**2 + (y + 1)**2))
z += 0.3 * np.exp(-((x + 1)**2 + (y + 1)**2))
z -= 0.1 * np.exp(-((x + 1)**2 + (y - 2)**2))
z += 0.3 * np.exp(-((x + 2)**2 + (y - 2)**2))
z += 0.3 * np.exp(-((x + 2)**2 + (y - 1)**2))
return z
x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
X, Y = np.meshgrid(x, y)
Z = function(X, Y)
peak_i, peak_j = np.unravel_index(np.argmax(Z), Z.shape)
x_peak, y_peak = X[peak_i, peak_j], Y[peak_i, peak_j]
rng = np.random.default_rng(42)
num_patches = 30
r = 0.3
r_forbid = 0.9
pad = r + 0.1
centers = []
attempts = 0
while len(centers) < num_patches and attempts < 5000:
cx = rng.uniform(x.min()+pad, x.max()-pad)
cy = rng.uniform(y.min()+pad, y.max()-pad)
if (cx - x_peak)**2 + (cy - y_peak)**2 < r_forbid**2:
attempts += 1
continue
ok = True
for px, py in centers:
if (cx - px)**2 + (cy - py)**2 < (1.6*r)**2:
ok = False
break
if ok:
centers.append((cx, cy))
attempts += 1
mask = np.zeros_like(Z, dtype=bool)
for cx, cy in centers:
mask |= (X - cx)**2 + (Y - cy)**2 <= r**2
cmap = LinearSegmentedColormap.from_list(
"softgreen", ["#e9f5ec", "#d9f0e1", "#c9e5d3", "#a9cbb8", "#7f9e8a", "#4f5c4f"], N=256
)
norm = plt.Normalize(Z.min(), Z.max())
facecolors = cmap(norm(Z))
facecolors_with_gray = facecolors.copy()
facecolors_with_gray[mask] = [0.7, 0.7, 0.7, 0.5]
fig = plt.figure(figsize=(14, 6))
for i, (fc, title) in enumerate([(facecolors, "Smooth Manifold"),
(facecolors_with_gray, "Manifold with Gray Patches")], 1):
ax = fig.add_subplot(1, 2, i, projection="3d")
ax.plot_surface(
X, Y, Z,
facecolors=fc,
rstride=4, cstride=4,
linewidth=0.05, edgecolor="k",
antialiased=True, shade=False, alpha=0.95
)
ax.set_title(title, fontsize=14)
ax.set_xticks([]); ax.set_yticks([]); ax.set_zticks([])
for a in (ax.xaxis, ax.yaxis, ax.zaxis):
a.pane.set_visible(False)
a.line.set_color((0,0,0,0))
ax.set_box_aspect([1, 1, 0.5])
ax.view_init(elev=20, azim=50)
fig.tight_layout(pad=2)
os.makedirs("./figures", exist_ok=True)
fig.savefig("./figures/manifold_holes.png", dpi=300)
import os
import numpy as np
import matplotlib.pyplot as plt
def function(x, y):
z = 0.6 * np.exp(-((x - 1)**2 + (y + 1)**2))
z += 0.5 * np.exp(-((x - 1)**2 + (y - 4)**2))
z += 0.3 * np.exp(-((x - 2)**2 + (y - 2)**2))
z += 0.2 * np.exp(-((x + 3)**2 + (y + 1)**2))
z += 0.3 * np.exp(-((x + 1)**2 + (y + 1)**2))
z -= 0.1 * np.exp(-((x + 1)**2 + (y - 2)**2))
z += 0.3 * np.exp(-((x + 2)**2 + (y - 2)**2))
z += 0.3 * np.exp(-((x + 2)**2 + (y - 1)**2))
return z
if __name__ == '__main__':
# Generate coordinates
x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
x, y = np.meshgrid(x, y)
# Define a multi-well "energy" function (inverted to form valleys)
z = function(x, y)
# Set up plot
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(1, 1, 1, projection='3d')
# Plot the surface with smooth shading
ax.plot_surface(
x, y, z,
cmap='coolwarm',
edgecolor='none',
linewidth=0,
antialiased=True,
alpha=0.95,
)
# # Plot descent path
# path_x = np.linspace(-2.5, 1, 100)
# path_y = np.linspace(2.5, -1, 100)
# path_z = function(path_x, path_y)
# ax.plot(path_x, path_y, path_z, color='red', linestyle='--', linewidth=3)
# Aesthetics
ax.set_xticks([])
ax.set_yticks([])
ax.set_zticks([])
ax.xaxis.pane.set_visible(False)
ax.yaxis.pane.set_visible(False)
ax.zaxis.pane.set_visible(False)
ax.xaxis.line.set_color((0.0, 0.0, 0.0, 0.0))
ax.yaxis.line.set_color((0.0, 0.0, 0.0, 0.0))
ax.zaxis.line.set_color((0.0, 0.0, 0.0, 0.0))
ax.set_box_aspect([1, 1, 0.5])
ax.view_init(elev=20, azim=50)
fig.tight_layout(pad=2)
os.makedirs('./figures', exist_ok=True)
fig.savefig('./figures/manifold.png')
import os
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
# results_increase = {
# r'Median property change': [0.1125, 0.44046, 0.46006, 0.45625, 0.46510],
# r'Percentage improved $\uparrow$': [55.15, 70.11, 71.02, 71.46, 71.52],
# r'Latent space distance $\downarrow$': [0.11868, 0.11877, 0.11961, 0.11964, 0.11999],
# }
# results_decrease = {
# r'Median property change': [-1.1246, -1.5761, -1.62873, -1.65457, -1.65872],
# r'Percentage improved $\uparrow$': [79.39, 86.31, 87.08, 87.24, 87.01],
# r'Latent space distance $\downarrow$': [0.16017, 0.12758, 0.12141, 0.12024, 0.12015],
# }
results_increase = {
r'Median change in property': [0.292, 0.5047563, 0.57401, 0.55921, 0.5471513271],
r'Success rate $\uparrow$': [67.6, 77.3, 79.9, 79.4, 79.6],
}
results_decrease = {
r'Median change in property': [-0.90106, -1.27954, -1.3083, -1.2785, -1.29],
r'Success rate $\uparrow$': [75.9, 84.7, 84.9, 84.7, 85.1],
}
if __name__ == '__main__':
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 15
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.linewidth'] = 2
x_label = 'Optimization step'
x_values = [1, 5, 10, 20, 40]
keys = ['Median change in property', 'Success rate']
fig = plt.figure(figsize=(4.5 * len(keys), 4))
for fig_idx, y_key in enumerate(keys):
ax = fig.add_subplot(1, len(keys), fig_idx + 1)
for key in results_increase.keys():
if y_key in key:
y_values_increase = results_increase[key]
y_label_increase = key
for key in results_decrease.keys():
if y_key in key:
y_values_decrease = results_decrease[key]
y_label_decrease = key
#0F4D92
ax.plot(x_values,
y_values_increase,
linestyle='-', linewidth=3,
marker='o', markersize=8,
label='increase property',
alpha=0.8,
color="#ea84dd")
ax.plot(x_values,
y_values_decrease,
linestyle='-', linewidth=3,
marker='o', markersize=8,
label='decrease property',
alpha=0.8,
color="#0f4d92")
if fig_idx == 1:
ax.set_ylim([0, 100])
ax.set_xticks(x_values)
ax.set_xlabel(x_label, fontsize=16)
ax.set_ylabel(y_label_increase, fontsize=16)
ax.yaxis.set_major_locator(MaxNLocator(nbins=5))
if fig_idx == len(keys) - 1:
ax.legend(loc='lower right', frameon=False)
fig.tight_layout(pad=1)
os.makedirs('./figures', exist_ok=True)
fig.savefig('./figures/results_sweep.png', dpi=300)
import os
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec as gridspec
from matplotlib.collections import LineCollection
from matplotlib.colors import to_rgba
from matplotlib.lines import Line2D
data_posttraining = {
'methods': [
'DPO',
'DA-DPO',
'VIGIL (Ours)',
],
'colors': [
"#D88F8A",
"#8BCF8B",
"#0F4D92"
],
'steps': [0, 200, 400, 600, 800],
'results': np.array([
[22.0, 25.5, 28.2, 29.5, 30.2],
[22.0, 33.5, 38.2, 39.8, 40.5],
[22.0, 52.5, 56.8, 57.9, 58.5],
]),
}
def plot_curves(data_posttraining):
methods = data_posttraining['methods']
colors = data_posttraining['colors']
fig = plt.figure(figsize=(9, 8))
ax = fig.add_subplot(1, 1, 1)
y_ticks = [0, 20, 40, 60]
x = np.asarray(data_posttraining['steps'])
results = data_posttraining['results'] # shape (n_methods, n_steps)
x_pos = np.arange(len(x))
ax.axhline(y=results[0][0], color='black', alpha=0.3, linewidth=4, linestyle='--')
for m, (method, color) in enumerate(zip(methods, colors)):
y = results[m]
# Segments with alpha increasing left to right
pts = np.column_stack([x_pos, y])
segments = np.stack([pts[:-1], pts[1:]], axis=1)
n_seg = len(segments)
alphas = np.linspace(0.3, 0.9, n_seg)
rgb = np.array(to_rgba(color))
seg_colors = [(*rgb[:3], a) for a in alphas]
lc = LineCollection(segments, colors=seg_colors, linewidths=3, capstyle='round')
ax.add_collection(lc)
ax.plot(x_pos, y, color=color, linewidth=0, marker='o', markersize=10, label='_nolegend_')
# Legend with line + marker for each method
handles = [ Line2D([0], [0], color='black', linestyle='--', linewidth=4, alpha=0.3, label='SFT only')]
for method, color in zip(methods, colors):
handles.append(Line2D([0], [0], color=color, linewidth=3, marker='o', markersize=10, label=method))
ax.legend(handles=handles, fontsize=20, loc='lower right', ncols=2, frameon=False)
ax.set_xlabel('Post-training steps', fontsize=28, fontfamily='helvetica', labelpad=12)
ax.set_xticks(x_pos)
ax.set_xticklabels([str(b) for b in x])
ax.set_ylabel('Performance on highly\nvision-dependent tasks' + r'$\uparrow$', fontsize=28, fontfamily='helvetica', labelpad=12)
ax.set_yticks(y_ticks)
ax.set_yticklabels(y_ticks)
ax.tick_params(labelsize=20, length=8, width=1.5)
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/comparison_posttraining.png', dpi=300)
plt.close(fig)
return
if __name__ == '__main__':
plt.rcParams['font.family'] = 'helvetica'
plt.rcParams['font.size'] = 24
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.linewidth'] = 3
plot_curves(data_posttraining)
{
"skill_name": "nature-figure",
"evals": [
{
"id": "backend-exclusivity-r-missing-runtime",
"prompt": "Use R to remake the provided ecological heatmap plus taxonomy-flow figure in Nature style with simulated data. Assume R/Rscript is not installed locally.",
"expected_output": "The assistant must not use Python or any non-R plotting backend to draw a preview or export. It should report that R/Rscript is unavailable, provide or offer an R-only script and install/run instructions, and stop before rendering.",
"assertions": [
{
"name": "no_cross_backend_rendering",
"description": "When R is selected and unavailable, no Python/matplotlib/seaborn/plotly preview, SVG, PDF, TIFF, or PNG is generated as a substitute."
},
{
"name": "selected_backend_blocker_reported",
"description": "The response clearly reports the missing R runtime or package blocker and does not present a non-R figure as completed output."
}
],
"files": []
},
{
"id": "backend-exclusivity-python-missing-package",
"prompt": "Use Python to make a Nature-style multi-panel heatmap and flow figure with simulated data. Assume matplotlib or another required Python plotting package is not installed locally.",
"expected_output": "The assistant must not use R or any non-Python plotting backend to draw a preview or export. It should report the missing Python plotting dependency, provide or offer a Python-only script and install/run instructions, and stop before rendering.",
"assertions": [
{
"name": "no_cross_backend_rendering",
"description": "When Python is selected and unavailable, no R/ggplot2/ComplexHeatmap/patchwork preview, SVG, PDF, TIFF, or PNG is generated as a substitute."
},
{
"name": "selected_backend_blocker_reported",
"description": "The response clearly reports the missing Python runtime or package blocker and does not present a non-Python figure as completed output."
}
],
"files": []
}
]
}