
Nature Figure
- 4 installs
- 33.4k repo stars
- Updated August 4, 2026
- yuan1z0825/nature-skill
This is a copy of nature-figure by yuan1z0825 - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks during AI-assisted development.
About
nature-figure is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- nature-figure
- AI & Agent Building
- AI-coding skill
Nature Figure by the numbers
- 4 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yuan1z0825/nature-skill --skill nature-figureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 33.4k |
| Last updated | August 4, 2026 |
| Repository | yuan1z0825/nature-skill ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Nature Figure Making — Router
This skill is split into two layers:
- A static layer under
static/that holds versioned, reusable content fragments (the figure contract and default stance, plus a per-backend quick-start for Python and R). - A dynamic layer (this file plus
manifest.yaml) that detects the plotting backend and loads only the fragment needed for the current job. The large design, API, pattern, and QA material lives in on-demand references.
Do not try to apply the figure logic from memory or from this router. Always load fragments from disk as described below.
Routing protocol
Follow these five steps every time the skill is invoked.
1. Load the manifest and the core layer
Read manifest.yaml. It declares the backend axis, the allowed values, and the file paths each value maps to.
Also read every file listed under always_load (static/core/contract.md and static/core/stance.md). These hold the figure contract, the backend gate, the missing-runtime rule, the privacy rule, and the default operating stance that apply to every figure job.
2. Resolve the backend — a blocking gate
Backend selection blocks everything else. Decide the backend value only from an explicit user choice or a clearly language-specific input file/workflow:
python— matplotlib / seaborn.r— ggplot2 / patchwork / ComplexHeatmap.
If the user has not explicitly chosen, ask exactly one concise question — Python or R? — and stop. Do not default, guess, generate mock data, or write scripts before the answer. Only recommend a backend when the user explicitly asks you to choose; then use references/backend-selection.md, state the reason, and proceed. Once selected, the backend is exclusive for all drawing, previewing, exporting, and visual QA (see core/contract.md).
3. Load the matching backend fragment
After the backend is resolved, Read the mapped fragment (static/fragments/backend/python.md or static/fragments/backend/r.md). It carries the backend-only execution rule and the publication quick-start (rcParams/theme and export helper). Do not load the other backend's fragment.
4. Build the figure using the loaded material
Apply the loaded material in this order:
1. Figure contract (core/contract.md) — write the core conclusion, map the evidence chain, classify the archetype, set the journal/export contract, before any code. 2. Default stance (core/stance.md) — archetype-first composition, hero panel, restrained palette, statistics/integrity as part of the figure. 3. Backend fragment — the exclusive Python or R quick-start and execution rule.
The chart serves the scientific logic; aesthetic polish is subordinate to making the core conclusion clear, defensible, and reviewable.
5. Reach for references only when needed
The files under references/ are deep references, not defaults. Open them on demand per the references.on_demand table in the manifest — for example references/figure-contract.md to build the contract, references/api.md for the Python palette and helpers, references/r-workflow.md for R, references/design-theory.md for color/typography/export rationale, references/common-patterns.md and references/chart-types.md for layout/chart recipes, references/nature-2026-observations.md for real Nature page archetypes, references/qa-contract.md before final delivery, and references/tutorials.md / references/demos.md for worked examples.
Why this split
- The static layer is versioned and reviewable. The backend gate is now explicit in the manifest rather than buried in prose.
- The dynamic layer keeps each invocation cheap: only the selected backend's quick-start enters context, and the 2,600+ lines of reference depth load only when a step needs them.
- The router itself is short on purpose. Update fragments and references, not this file, when adding scope.
- This structure mirrors
nature-writing,nature-polishing,nature-reader, andnature-paper2ppt.
.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
data_comparison = {
'methods': [
r'DPO',
r'DA-DPO',
r'VIGIL (Ours)',
],
'colors': [
"#D88F8A",
"#8BCF8B",
"#0F4D92"
],
'results': {
'Qwen2.5-VL-7B\nPOPE$_{Adv}$': np.array([82.8, 84.2, 86.9]),
'LLaVA-OneVision-7B\nPOPE$_{Adv}$': np.array([82.8, 84.2, 86.9]),
'InternVL2.5-26B\nPOPE$_{Adv}$': np.array([85.5, 86.8, 89.4]),
'Qwen2.5-VL-72B\nPOPE$_{Adv}$': np.array([84.5, 87.4, 89.8]),
'Qwen2.5-VL-7B\nMathVista': np.array([48.0, 48.8, 49.5]),
'LLaVA-OneVision-7B\nMathVista': np.array([50.8, 51.5, 52.8]),
'InternVL2.5-26B\nMathVista': np.array([57.9, 58.8, 60.1]),
'Qwen2.5-VL-72B\nMathVista': np.array([54.1, 55.4, 56.6]),
'Qwen2.5-VL-7B\nMMBench': np.array([71.2, 72.0, 72.5]),
'LLaVA-OneVision-7B\nMMBench': np.array([72.5, 73.0, 73.8]),
'InternVL2.5-26B\nMMBench': np.array([79.5, 80.1, 81.3]),
'Qwen2.5-VL-72B\nMMBench': np.array([77.2, 77.8, 78.5]),
},
}
def _task_suffix(subtask_name):
"""Benchmark = part after the first newline (e.g. 'Qwen2.5-VL-7B\\nMathVista' -> 'MathVista')."""
return subtask_name.split('\n', 1)[-1] if '\n' in subtask_name else subtask_name
def plot_radar(data_comparison):
"""
Single radar chart. Each axis = one subtask; one curve per method.
Each benchmark (task suffix after \\n) has its own radii list: values are normalized
to display range 45-90 using that benchmark's min/max from its radii list, and
radius labels on each spoke show that benchmark's tick values.
"""
methods = data_comparison['methods']
colors = data_comparison['colors']
n_methods = len(methods)
benchmark_radii = {
'POPE$_{Adv}$': [75, 80, 85, 91],
'MathVista': [30, 40, 50, 61],
'MMBench': [40, 55, 70, 85],
}
# results is directly subtask_name -> value array
task_dict = data_comparison['results']
subtask_names = list(task_dict.keys())
value_arrays = list(task_dict.values())
fig = plt.figure(figsize=(12, 10))
ax = fig.add_subplot(111, projection='polar')
n_subtasks = len(subtask_names)
subtask_benchmarks = [_task_suffix(st) for st in subtask_names]
# Per-benchmark (r_min, r_max) from that benchmark's radii list
def limits_for_benchmark(bench):
radii = benchmark_radii.get(bench, [0, 100])
return (min(radii), max(radii))
# One polygon per method: normalize each spoke by its benchmark's radii range, then map to 45-90
angles = np.linspace(2 * np.pi, 0, n_subtasks, endpoint=False)
angles_closed = np.append(angles, angles[0])
for m in range(n_methods):
vals = np.array([v[m] for v in value_arrays], dtype=float)
mask = np.isnan(vals)
if np.any(mask):
vals = vals.copy()
fill = 0.0 if np.all(mask) else np.nanmean(vals)
vals[mask] = fill
# Normalize per spoke: benchmark's (r_min, r_max) -> display 45-90. One vertex per subtask (no interpolation).
normalized = np.zeros_like(vals)
for i, (v, bench) in enumerate(zip(vals, subtask_benchmarks)):
r_lo, r_hi = limits_for_benchmark(bench)
span = r_hi - r_lo
if span <= 0:
normalized[i] = 45 + 45 * 0.5
else:
n = np.clip((v - r_lo) / span, 0.0, 1.0)
normalized[i] = 45 + 45 * n
# Closed polygon: exact data at each angle, then back to first (no extra interpolation)
vals_closed = np.append(normalized, normalized[0])
ax.plot(angles_closed, vals_closed, color=colors[m], linewidth=2, label=methods[m])
ax.fill(angles_closed, vals_closed, color=colors[m], alpha=0.05)
# Mark actual vertices so it's clear each point is a real data value
ax.scatter(angles, normalized, color=colors[m], s=18, zorder=5, edgecolors='none')
ax.set_ylim(45, 90)
ax.set_theta_zero_location('N')
for spine in ax.spines.values():
spine.set_visible(False)
r_min_disp, r_max_disp = ax.get_ylim()
ax.grid(False)
# Outer boundary
ax.plot(angles_closed, np.full_like(angles_closed, r_max_disp), color='k', linewidth=0.8, zorder=4)
# Radial spokes: one per angle
for a in angles:
ax.plot([a, a], [r_min_disp, r_max_disp], color='gray', linewidth=0.5, zorder=4)
# Benchmark-specific contour polygons: one polygon per level index k (innermost=0, ...).
max_levels = max(len(benchmark_radii.get(b, [])) for b in subtask_benchmarks)
for k in range(max_levels):
display_radii = np.zeros(n_subtasks)
for i, bench in enumerate(subtask_benchmarks):
radii_list = benchmark_radii.get(bench, [])
if not radii_list:
display_radii[i] = 45 + 22.5
continue
contour_val = radii_list[k] if k < len(radii_list) else radii_list[-1]
r_lo, r_hi = limits_for_benchmark(bench)
span = r_hi - r_lo
if span <= 0:
display_radii[i] = 45 + 22.5
else:
frac = (contour_val - r_lo) / span
display_radii[i] = 45 + 45 * np.clip(frac, 0.0, 1.0)
contour_closed = np.append(display_radii, display_radii[0])
ax.plot(angles_closed, contour_closed, color='k', linewidth=0.6, zorder=4, label='_nolegend_')
ax.set_yticks([r_max_disp])
ax.set_yticklabels([])
ax.set_rlabel_position(0)
ax.set_xticks(angles)
ax.set_xticklabels([])
# Per-spoke radius labels: skip innermost tick to avoid clutter
for angle, bench in zip(angles, subtask_benchmarks):
radii_list = benchmark_radii.get(bench, [])
if len(radii_list) <= 1:
continue
r_lo, r_hi = limits_for_benchmark(bench)
span = r_hi - r_lo
for tick_val in radii_list[1:]: # skip innermost (smallest) number
if span <= 0:
display_r = 45 + 22.5
else:
frac = (tick_val - r_lo) / span
display_r = 45 + 45 * np.clip(frac, 0.0, 1.0)
lbl = f'{tick_val:.0f}' if tick_val == int(tick_val) else f'{tick_val:.1f}'
rot = np.degrees(angle)
ax.text(angle, display_r + 1, lbl, fontsize=12, ha='center', va='center',
rotation=rot, rotation_mode='anchor',
transform=ax.transData, clip_on=False)
# One label per subtask (angle)
for angle, label in zip(angles, subtask_names):
offset = 8 + 10 * np.abs(np.sin(angle))
label_r = r_max_disp + offset
ax.text(angle, label_r, label, fontsize=14, ha='center', va='center',
transform=ax.transData, clip_on=False, fontfamily='monospace')
ax.legend(loc='upper right', bbox_to_anchor=(1.40, 0.05), fontsize=15, frameon=False)
fig.tight_layout(pad=2)
os.makedirs('./figures/', exist_ok=True)
fig.savefig('./figures/comparison_radar.png', dpi=300, bbox_inches='tight')
plt.close(fig)
return
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
plot_radar(data_comparison)
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)
Backend: Python (matplotlib / seaborn)
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.
Python quick-start
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.
Going deeper
references/api.md— Python PALETTE, helper function signatures, validation rules.references/common-patterns.md— hero panels, legend-only axes, dark image plates, asymmetric layouts.references/chart-types.md— radar, 3D sphere, fill_between, scatter patterns.references/tutorials.md— end-to-end walkthroughs for bars, trends, heatmaps.references/demos.md— bundled figures4papers Python scripts and output previews.