Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'Error misattribution$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'Degenerate repetition or stuck$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'Flawed correction$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'Valid correction$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

],\n 'result': {\n r'Fault denial$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([1, 0, 0, 1, 0, 0]) / 14 ,\n r'Error misattribution$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([4, 4, 3, 1, 0, 1]) / 14 ,\n r'Degenerate repetition or stuck$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([9, 2, 4, 0, 0, 0]) / 14 ,\n r'Flawed correction$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0, 1, 0, 1, 3, 2]) / 14 ,\n r'Valid correction$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0, 7, 7, 12, 11, 11]) / 14 ,\n },\n}\n\ndata_math_correcting_human = {\n 'methods': [r'DeepSeek R1 Distill Qwen 1.5B',\n r'DeepSeek R1 Distill Qwen 14B',\n r'DeepSeek R1 Distill Llama 70B',\n r'deepseek-chat (Deepseek-V3)',\n r'deepseek-reasoner (Deepseek-R1)',\n r'OpenAI o3'],\n 'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#F6CFCB', '#E9A6A1', '#3775BA'],\n 'subtypes': [r'False confession$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'Degenerate repetition or stuck$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'Justified denial$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

],\n 'result': {\n r'False confession$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([8, 10, 10, 13, 14, 12]) / 14 ,\n r'Degenerate repetition or stuck$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([5, 3, 2, 0, 0, 0]) / 14 ,\n r'Justified denial$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0, 1, 2, 1, 0, 0]) / 14 ,\n },\n}\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n fig = plt.figure(figsize=(36, 12))\n\n gs = gridspec.GridSpec(2, 5)\n\n for subtype_idx, subtype_name in enumerate(data_math_correcting_llm['subtypes']):\n ax = fig.add_subplot(gs[subtype_idx])\n num_methods = len(data_math_correcting_llm['methods'])\n ax.bar(\n np.arange(num_methods),\n data_math_correcting_llm['result'][subtype_name],\n color=data_math_correcting_llm['colors'],\n label=data_math_correcting_llm['methods'],\n )\n if subtype_idx == 0:\n handles, labels = ax.get_legend_handles_labels()\n\n ax.set_title(data_math_correcting_llm['subtypes'][subtype_idx], fontsize=30, pad=36)\n ax.set_ylabel('Probability', fontsize=30, labelpad=12)\n ax.set_ylim([0, 1])\n ax.set_xticks([])\n\n for subtype_idx, subtype_name in enumerate(data_math_correcting_human['subtypes']):\n ax = fig.add_subplot(gs[5 + subtype_idx])\n num_methods = len(data_math_correcting_human['methods'])\n ax.bar(\n np.arange(num_methods),\n data_math_correcting_human['result'][subtype_name],\n color=data_math_correcting_human['colors'],\n label=data_math_correcting_human['methods'],\n )\n\n ax.set_title(data_math_correcting_human['subtypes'][subtype_idx], fontsize=30, pad=36)\n ax.set_ylabel('Probability', fontsize=30, labelpad=12)\n ax.set_ylim([0, 1])\n ax.set_xticks([])\n\n ax = fig.add_subplot(gs[8:])\n ax.legend(handles, labels, fontsize=30, loc='center', frameon=False)\n ax.set_axis_off()\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/selfcorrection_math.png', dpi=300)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4015,"content_sha256":"7656335018d67ecaaae362e63669f22c709e51644681dc801f298356fb7fef7c"},{"filename":"assets/figures4papers/figure_CellSpliceNet/plot_ablation.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom matplotlib import gridspec as gridspec\n\n\ndata_ablation = {\n 'methods': [\n r'CellSpliceNet',\n r'No Expression',\n r'No Structure',\n r'No ROI',\n r'No Sequence',\n ],\n 'colors': ['#0F4D92', '#B4E6B4', '#AFE6E6', '#FFE080', '#D3D3D3'],\n 'result': np.array([0.88, 0.84, 0.82, 0.81, 0.74]),\n}\n\ndef is_dark(color_in_hex, threshold=128):\n color = color_in_hex.lstrip('#')\n r = int(color[0:2], 16)\n g = int(color[2:4], 16)\n b = int(color[4:6], 16)\n\n luminance = 0.299*r + 0.587*g + 0.114*b\n return luminance \u003c threshold\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n fig = plt.figure(figsize=(13, 13))\n\n ax = fig.add_subplot(1, 1, 1)\n num_methods = len(data_ablation['methods'])\n bars = ax.bar(\n np.arange(num_methods),\n data_ablation['result'],\n color=data_ablation['colors'],\n label=data_ablation['methods'],\n )\n\n for i, (bar, value) in enumerate(zip(bars, data_ablation['result'])):\n textcolor = 'white' if is_dark(data_ablation['colors'][i]) else 'black'\n ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() - 0.08,\n f'{value:.2f}', ha='center', va='bottom', fontsize=32, color=textcolor)\n\n # Add horizontal reference line at the first bar\n baseline = data_ablation['result'][0] # 0.88\n ax.axhline(y=baseline, color=data_ablation['colors'][0], linestyle='--', linewidth=4, alpha=0.7)\n\n # Add arrows and reduction values for bars 2-5 (skip the first bar)\n for i in range(1, num_methods):\n bar = bars[i]\n current_value = data_ablation['result'][i]\n reduction = baseline - current_value\n\n # Position for the arrow (right side of the bar)\n x_pos = bar.get_x() + bar.get_width()\n\n # Draw arrow from baseline down to bar top (top to bottom)\n ax.annotate('', xy=(x_pos, current_value), xytext=(x_pos, baseline),\n arrowprops=dict(arrowstyle='->', color='red', lw=4))\n\n # Add reduction text near the top (at baseline level)\n ax.text(x_pos - 0.3, baseline + 0.005, r'$-

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

+f'{reduction:.2f}',\n ha='left', va='bottom', fontsize=24, color='red')\n\n ax.set_ylabel('Spearman correlation', fontsize=54, labelpad=12)\n ymax = np.max(data_ablation['result'][:])\n ax.set_ylim([0.0, ymax + 0.5])\n ax.set_yticks([0.0, 0.25, 0.50, 0.75, 1.0])\n ax.tick_params(axis='y', labelsize=36, length=10, width=2)\n ax.set_xticks([])\n\n ax.legend(bbox_to_anchor=(0.50, 1.08), loc='upper left', fontsize=36, frameon=False)\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/ablation.png', dpi=300)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2964,"content_sha256":"0d0959102afe26249199a60fb67d5c8bd282a6fdb9c7e024c51a9c42a6a58c7f"},{"filename":"assets/figures4papers/figure_CellSpliceNet/plot_comparison_cross_species.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom matplotlib import gridspec as gridspec\n\n\ndata_comparison = {\n 'methods': [\n r'CellSpliceNet',\n r'Pangolin',\n r'SpliceTransformer',\n r'SpliceAI',\n r'SpliceFinder',\n r'ViT',\n r'AlphaGenome',\n r'ESM2',\n ],\n 'colors': ['#0F4D92', '#D4685F', '#DA7B73', '#DF8E87', '#E5A19B', '#EBB4AF', '#F1C7C3', '#F6DAD8', '#FCEEED'],\n 'metrics': [r'Spearman correlation', r'Pearson correlation', r'R$^2$ score'],\n 'result_worm': {\n r'Spearman correlation': np.array([\n [0.909, 0.872, 0.906],\n [0.813, 0.825, 0.832],\n [0.786, 0.799, 0.815],\n [0.751, 0.715, 0.753],\n [0.725, 0.715, 0.752],\n [0.689, 0.722, 0.705],\n [0.637, 0.637, 0.636],\n [0.642, 0.649, 0.596],\n ]),\n r'Pearson correlation': np.array([\n [0.929, 0.863, 0.918],\n [0.846, 0.867, 0.870],\n [0.827, 0.844, 0.862],\n [0.751, 0.708, 0.768],\n [0.728, 0.721, 0.761],\n [0.694, 0.723, 0.713],\n [0.646, 0.648, 0.645],\n [0.616, 0.635, 0.599],\n ]),\n r'R$^2$ score': np.array([\n [0.863, 0.745, 0.843],\n [0.634, 0.672, 0.689],\n [0.604, 0.609, 0.714],\n [0.542, 0.483, 0.532],\n [0.354, 0.359, 0.440],\n [0.375, 0.415, 0.347],\n [0.399, 0.401, 0.401],\n [0.353, 0.387, 0.348],\n ]),\n },\n 'result_human': {\n r'Spearman correlation': np.array([\n [0.512, 0.475, 0.461],\n [0.030, -0.002, -0.007],\n [-0.002, -0.016, -0.015],\n [0.318, 0.299, 0.319],\n [0.083, -0.011, 0.060],\n [0.056, 0.034, 0.050],\n [0.306, 0.297, 0.296],\n [0.325, 0.308, 0.316],\n ]),\n r'Pearson correlation': np.array([\n [0.479, 0.484, 0.449],\n [0.036, -0.002, -0.002],\n [0.004, -0.017, 0.007],\n [0.344, 0.327, 0.348],\n [0.110, -0.013, 0.060],\n [0.096, 0.080, 0.043],\n [0.342, 0.331, 0.330],\n [0.355, 0.340, 0.343],\n ]),\n r'R$^2$ score': np.array([\n [0.229, 0.235, 0.201],\n [-0.013, -0.037, -0.052],\n [-0.005, -0.004, -0.004],\n [0.110, 0.100, 0.114],\n [0.003, -0.002, -0.003],\n [0.000, -0.003, -0.001],\n [0.099, 0.103, 0.095],\n [0.123, 0.108, 0.114],\n ]),\n }\n}\n\ndef is_dark(color_in_hex, threshold=128):\n color = color_in_hex.lstrip('#')\n r = int(color[0:2], 16)\n g = int(color[2:4], 16)\n b = int(color[4:6], 16)\n\n luminance = 0.299*r + 0.587*g + 0.114*b\n return luminance \u003c threshold\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n fig = plt.figure(figsize=(45, 12))\n\n gs = gridspec.GridSpec(1, 3)\n\n for metric_idx, metric_name in enumerate(data_comparison['metrics']):\n ax = fig.add_subplot(gs[metric_idx])\n\n num_methods = len(data_comparison['methods'])\n bars = ax.bar(\n np.arange(num_methods),\n data_comparison['result_worm'][metric_name].mean(axis=1),\n yerr=data_comparison['result_worm'][metric_name].std(axis=1),\n error_kw={\n 'elinewidth': 2, # thickness of vertical error bar line\n 'capthick': 2, # thickness of caps\n 'capsize': 15 # length of caps\n },\n color=data_comparison['colors'],\n label=data_comparison['methods'],\n )\n\n for i, (bar, value, value_std) in enumerate(zip(bars, data_comparison['result_worm'][metric_name].mean(axis=1), data_comparison['result_worm'][metric_name].std(axis=1))):\n ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + value_std + 0.02,\n f'{value:.2f}', ha='center', va='bottom', fontsize=32, color='black')\n\n ax.set_ylabel(metric_name, fontsize=54, labelpad=12)\n ymax = np.max(data_comparison['result_worm'][metric_name])\n ax.set_ylim([0.0, ymax + 0.5])\n ax.set_xticks([])\n ax.set_yticks([0.00, 0.25, 0.50, 0.75, 1.00])\n ax.tick_params(axis='y', labelsize=36, length=10, width=2)\n\n ax.legend(bbox_to_anchor=(0.02, 1.08), loc='upper left', fontsize=36, frameon=False, ncols=2, columnspacing=0.6)\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/comparison_worm.png', dpi=300)\n plt.close(fig)\n\n\n fig = plt.figure(figsize=(45, 12))\n\n gs = gridspec.GridSpec(1, 3)\n\n for metric_idx, metric_name in enumerate(data_comparison['metrics']):\n ax = fig.add_subplot(gs[metric_idx])\n\n num_methods = len(data_comparison['methods'])\n bars = ax.bar(\n np.arange(num_methods),\n data_comparison['result_human'][metric_name].mean(axis=1),\n yerr=data_comparison['result_human'][metric_name].std(axis=1),\n error_kw={\n 'elinewidth': 2, # thickness of vertical error bar line\n 'capthick': 2, # thickness of caps\n 'capsize': 15 # length of caps\n },\n color=data_comparison['colors'],\n label=data_comparison['methods'],\n )\n\n for i, (bar, value, value_std) in enumerate(zip(bars, data_comparison['result_human'][metric_name].mean(axis=1), data_comparison['result_human'][metric_name].std(axis=1))):\n ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + value_std + 0.02,\n f'{value:.2f}', ha='center', va='bottom', fontsize=32, color='black')\n\n ax.set_ylabel(metric_name, fontsize=54, labelpad=12)\n ymax = np.max(data_comparison['result_human'][metric_name])\n ax.set_ylim([-0.08, ymax + 0.5])\n ax.set_xticks([])\n ax.set_yticks([-0.00, 0.25, 0.50, 0.75, 1.00])\n ax.tick_params(axis='y', labelsize=36, length=10, width=2)\n\n ax.legend(bbox_to_anchor=(0.02, 1.08), loc='upper left', fontsize=36, frameon=False, ncols=2, columnspacing=0.6)\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/comparison_human.png', dpi=300)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6532,"content_sha256":"9061cd3476a2923e557e34585803da8e0df31a01f7cb5dd0f468b83458eeae33"},{"filename":"assets/figures4papers/figure_CellSpliceNet/plot_comparison.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom matplotlib import gridspec as gridspec\n\n\ndata_ablation = {\n 'methods': [\n r'CellSpliceNet',\n r'ViT',\n r'SpliceFinder',\n r'Pangolin',\n r'SpliceTransformer',\n r'SpliceAI',\n r'ESM2',\n ],\n 'colors': ['#0F4D92', \"#F09F97\", \"#F1B3AC\", \"#EFBEB8\", \"#F0CDC8\", \"#F3D9D8\", '#FCEEED'],\n 'metrics': [r'Spearman correlation', r'Pearson correlation', r'R$^2$ score'],\n 'result': {\n # r'Spearman correlation': np.array([0.88, 0.81, 0.80, 0.79, 0.77, 0.71, 0.606]),\n # r'Pearson correlation': np.array([0.88, 0.81, 0.80, 0.79, 0.77, 0.72, 0.613]),\n # r'R$^2$ score': np.array([0.77, 0.66, 0.64, 0.62, 0.59, 0.52, 0.369]),\n r'Spearman correlation': np.array([\n [0.88, 0.88, 0.88], # TODO: update!\n [0.81, 0.81, 0.81], # TODO: update!\n [0.806, 0.793, 0.794],\n [0.792, 0.785, 0.788],\n [0.765, 0.765, 0.767],\n [0.714, 0.716, 0.706],\n [0.598, 0.625, 0.594],\n ]),\n r'Pearson correlation': np.array([\n [0.88, 0.88, 0.88], # TODO: update!\n [0.81, 0.81, 0.81], # TODO: update!\n [0.812, 0.798, 0.801],\n [0.792, 0.785, 0.788],\n [0.765, 0.765, 0.766],\n [0.722, 0.722, 0.714],\n [0.604, 0.631, 0.605],\n ]),\n r'R$^2$ score': np.array([\n [0.77, 0.77, 0.77], # TODO: update!\n [0.66, 0.66, 0.66], # TODO: update!\n [0.658, 0.632, 0.642],\n [0.633, 0.615, 0.627],\n [0.585, 0.585, 0.586],\n [0.518, 0.520, 0.507],\n [0.359, 0.384, 0.364],\n ]),\n }\n}\n\ndef is_dark(color_in_hex, threshold=128):\n color = color_in_hex.lstrip('#')\n r = int(color[0:2], 16)\n g = int(color[2:4], 16)\n b = int(color[4:6], 16)\n\n luminance = 0.299*r + 0.587*g + 0.114*b\n return luminance \u003c threshold\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n fig = plt.figure(figsize=(45, 12))\n\n gs = gridspec.GridSpec(1, 3)\n\n for metric_idx, metric_name in enumerate(data_ablation['metrics']):\n ax = fig.add_subplot(gs[metric_idx])\n\n num_methods = len(data_ablation['methods'])\n bars = ax.bar(\n np.arange(num_methods),\n data_ablation['result'][metric_name].mean(axis=1),\n yerr=data_ablation['result'][metric_name].std(axis=1),\n error_kw={\n 'elinewidth': 2, # thickness of vertical error bar line\n 'capthick': 2, # thickness of caps\n 'capsize': 15 # length of caps\n },\n color=data_ablation['colors'],\n label=data_ablation['methods'],\n )\n\n for i, (bar, value) in enumerate(zip(bars, data_ablation['result'][metric_name].mean(axis=1))):\n textcolor = 'white' if is_dark(data_ablation['colors'][i]) else 'black'\n ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() - 0.10,\n f'{value:.2f}', ha='center', va='bottom', fontsize=32, color=textcolor)\n\n ax.set_ylabel(metric_name, fontsize=54, labelpad=12)\n ymax = np.max(data_ablation['result'][metric_name])\n ax.set_ylim([0.0, ymax + 0.5])\n ax.set_xticks([])\n ax.set_yticks([0.00, 0.25, 0.50, 0.75, 1.00])\n ax.tick_params(axis='y', labelsize=36, length=10, width=2)\n\n ax.legend(bbox_to_anchor=(0.02, 1.08), loc='upper left', fontsize=38, frameon=False, ncols=2, columnspacing=0.6)\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/comparison.png', dpi=300)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3909,"content_sha256":"4e888489587ca7d345893715cc8f34a9b92cc347ffe5b20291093df1c2f23bd6"},{"filename":"assets/figures4papers/figure_Cflows/diffusion_swiss_roll.py","content":"import numpy as np\nimport matplotlib.pyplot as plt\nfrom scipy.spatial.distance import pdist, squareform\n\n# Generate Swiss Roll data\ndef generate_swiss_roll_2d(n_samples=80, noise=0.1):\n t = 1.5 * np.pi * (1 + 2 * np.random.rand(n_samples))\n x = t * np.cos(t)\n z = t * np.sin(t)\n\n # Add some noise\n x += noise * np.random.randn(n_samples)\n z += noise * np.random.randn(n_samples)\n\n return x, z, t\n\n# Compute diffusion matrix (transition probabilities)\ndef compute_diffusion_matrix(x, z, t, sigma=1.0):\n # Order points by manifold parameter t for better matrix visualization\n sorted_indices = np.argsort(t)\n x_sorted = x[sorted_indices]\n z_sorted = z[sorted_indices]\n t_sorted = t[sorted_indices]\n\n # Compute pairwise distances in ambient space\n points = np.column_stack([x_sorted, z_sorted])\n distances = squareform(pdist(points))\n\n # Compute manifold distances (along parameter t)\n t_distances = np.abs(t_sorted[:, None] - t_sorted[None, :])\n\n # Combine spatial and manifold distances (emphasize manifold structure)\n combined_distances = distances + 0.5 * t_distances\n\n # Gaussian kernel for transition probabilities\n P = np.exp(-combined_distances**2 / (2 * sigma**2))\n\n # Make matrix sparser by thresholding small values\n P[P \u003c 0.01] = 0\n\n # Normalize rows to make it a proper transition matrix\n row_sums = P.sum(axis=1)\n row_sums[row_sums == 0] = 1 # Avoid division by zero\n P = P / row_sums[:, None]\n\n return P, sorted_indices\n\nif __name__ == '__main__':\n # Create two subplots\n fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))\n\n # Generate swiss roll point cloud (smaller for visualization clarity)\n x, z, t = generate_swiss_roll_2d(n_samples=500, noise=0.5)\n\n # Compute diffusion matrix\n P, sorted_indices = compute_diffusion_matrix(x, z, t, sigma=2)\n\n # Left plot: Diffusion Matrix\n im = ax1.imshow(P, cmap='Reds', aspect='equal', origin='upper')\n ax1.axis('off')\n ax1.set_facecolor('white')\n\n # Right plot: Swiss Roll with probability-weighted connections\n # Use original (unsorted) coordinates for the swiss roll plot\n x_orig, z_orig, t_orig = x, z, t\n\n # Draw line segments between points with opacity = transition probability\n threshold = 0.02 # Only draw lines above this probability threshold\n\n for i in range(len(x_orig)):\n for j in range(i+1, len(x_orig)):\n # Find corresponding indices in sorted matrix\n orig_i_in_sorted = np.where(sorted_indices == i)[0][0]\n orig_j_in_sorted = np.where(sorted_indices == j)[0][0]\n\n # Get transition probability from matrix\n prob = max(P[orig_i_in_sorted, orig_j_in_sorted], P[orig_j_in_sorted, orig_i_in_sorted])\n\n if prob > threshold:\n ax2.plot([x_orig[i], x_orig[j]], [z_orig[i], z_orig[j]],\n color='black', linewidth=2, alpha=prob*2, zorder=1)\n\n # Plot the swiss roll points on top\n scatter = ax2.scatter(x_orig, z_orig, c=t_orig, cmap='viridis', s=100,\n alpha=0.5, edgecolors='white', linewidth=1, zorder=2)\n\n # Styling for swiss roll plot\n ax2.set_aspect('equal')\n ax2.axis('off')\n ax2.set_facecolor('white')\n\n # Clean overall styling\n fig.patch.set_facecolor('white')\n plt.subplots_adjust(left=0, right=1, top=1, bottom=0, wspace=0.02)\n\n plt.savefig('figures/diffusion_swiss_roll.png', dpi=300, bbox_inches='tight',\n facecolor='white', edgecolor='none', pad_inches=1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3545,"content_sha256":"2ecd52231b6f095d8471b94bcae58d6c844f14cc8494c6c1636887711cabe592"},{"filename":"assets/figures4papers/figure_Cflows/plot_comparison_Ablation.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\n\n\ndata_comparison_Ablation = {\n 'methods': [r'OT-CFM', r'SB-CFM', r'SF2M', r'Cflows (w/o growth + energy)', r'Cflows (w/o growth)', r'Cflows'],\n 'colors': ['#AADCA9', '#8BCF8B', '#E9A6A1', '#B8C9E5', '#7097CA', '#3775BA'],\n 'metrics': [r'RMSE$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'MAE$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'PCC$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'SCC$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

],\n 'mean': {\n r'RMSE$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0.94, 1.05, 0.99, 0.89, 0.75, 0.62]),\n r'MAE$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0.75, 0.85, 0.78, 0.70, 0.59, 0.48]),\n r'PCC$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0.53, 0.49, 0.55, 0.58, 0.65, 0.72]),\n r'SCC$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0.50, 0.47, 0.52, 0.55, 0.68, 0.70]),\n },\n 'std': {\n r'RMSE$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0.08, 0.09, 0.09, 0.07, 0.06, 0.05]),\n r'MAE$\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0.07, 0.08, 0.07, 0.06, 0.05, 0.04]),\n r'PCC$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0.04, 0.02, 0.01, 0.03, 0.02, 0.02]),\n r'SCC$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([0.03, 0.02, 0.03, 0.03, 0.03, 0.01]),\n },\n}\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n fig = plt.figure(figsize=(35, 7))\n\n num_methods = len(data_comparison_Ablation['methods'])\n for metric_idx, metric_name in enumerate(data_comparison_Ablation['metrics']):\n ax = fig.add_subplot(1, 5, metric_idx + 1)\n\n ax.bar(\n np.arange(num_methods),\n data_comparison_Ablation['mean'][metric_name],\n yerr=data_comparison_Ablation['std'][metric_name],\n capsize=8,\n error_kw={'capthick': 2},\n color=data_comparison_Ablation['colors'],\n label=data_comparison_Ablation['methods'],\n )\n\n if metric_idx == 0:\n handles, labels = ax.get_legend_handles_labels()\n\n ax.set_xticks([])\n ax.set_ylabel(data_comparison_Ablation['metrics'][metric_idx], fontsize=36, labelpad=12)\n ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))\n\n ax = fig.add_subplot(1, 5, 5)\n ax.legend(handles, labels, fontsize=30, loc='lower left', frameon=False)\n ax.set_axis_off()\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/figX_comparison_Ablation.png', dpi=300)\n fig.savefig('./figures/figX_comparison_Ablation.pdf', dpi=300)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2506,"content_sha256":"d5ab173adbf5be9486e8325f9904123fe7430c0c937e1eebd88dee38049cd787"},{"filename":"assets/figures4papers/figure_Cflows/plot_comparison_GeneRegulatory.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\n\n\ndata_comparison_GeneRegulatory = {\n 'methods': [r'OCE', r'PC', r'mTE', r'mMI', r'NRI', r'DCRNN', r'GTS', r'NIR', r'GC (ours)'],\n 'colors': ['#D0A3A3', '#EFE7B1', '#F4C2C2', '#D7C4E2', '#E5C09F', '#A8C6C2', '#B7D3B0', '#F5B5A0', '#3775BA'],\n 'metric': 'Graph Edit Distance',\n 'datasets': [\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (100, 137)',\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (150, 329)',\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (200, 507)',\n ],\n 'mean': {\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (100, 137)':\n np.array([138.6, 140.4, 126.4, 51.2, 72.1, 158.14, 215.4, 62.7, 51.2]),\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (150, 329)':\n np.array([293.4, 317.2, 261.0, 99.8, 106.6, 303.79, 347.2, 86.3, 109.0]),\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (200, 507)':\n np.array([449.8, 495.6, 397.4, 162.8, 219.8, 508.25, 481.8, 159.2, 158.8]),\n },\n 'std': {\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (100, 137)':\n np.array([3.5, 3.9, 2.4, 3.3, 6.2, 8.6, 13.8, 3.2, 3.3]),\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (150, 329)':\n np.array([2.9, 3.7, 2.2, 4.0, 5.4, 12.4, 19.3, 2.8, 6.4]),\n r'($|\\mathcal{V}|$, $|\\mathcal{E}|$) = (200, 507)':\n np.array([1.1, 6.5, 8.8, 6.2, 13.4, 23.6, 7.0, 11.6, 12.6]),\n },\n}\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n fig = plt.figure(figsize=(36, 6))\n\n num_methods = len(data_comparison_GeneRegulatory['methods'])\n for dataset_idx, dataset_name in enumerate(data_comparison_GeneRegulatory['datasets']):\n ax = fig.add_subplot(1, 4, dataset_idx + 1)\n\n ax.bar(\n np.arange(num_methods),\n data_comparison_GeneRegulatory['mean'][dataset_name],\n yerr=data_comparison_GeneRegulatory['std'][dataset_name],\n capsize=8,\n error_kw={'capthick': 2},\n color=data_comparison_GeneRegulatory['colors'],\n label=data_comparison_GeneRegulatory['methods'],\n )\n\n if dataset_idx == 0:\n handles, labels = ax.get_legend_handles_labels()\n\n ax.set_xticks([])\n ax.set_ylabel(data_comparison_GeneRegulatory['metric'], fontsize=36, labelpad=12)\n ax.set_xlabel(dataset_name, fontsize=36, labelpad=12)\n ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))\n\n ax = fig.add_subplot(1, 4, 4)\n ax.legend(handles, labels, fontsize=30, loc='lower left', ncols=2, frameon=False)\n ax.set_axis_off()\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/fig2_comparison_GeneRegulatory.png', dpi=300)\n fig.savefig('./figures/fig2_comparison_GeneRegulatory.pdf', dpi=300)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3022,"content_sha256":"8659356aa5b92a7969dd8e46144c4c68fe97850cb58c2da6a5b011b7daf88129"},{"filename":"assets/figures4papers/figure_Cflows/plot_comparison_Trajectory.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\n\n\ndata_comparison_Trajectory = {\n 'methods': [r'TrajectoryNet', r'OT-CFM', r'SB-CFM', r'BEMIOflow (ours)'],\n 'colors': ['#DDF3DE', '#AADCA9', '#8BCF8B', '#3775BA'],\n 'metrics': ['PHATE Space RMSE', 'Gene Space RMSE', 'Interpolation EMD'],\n 'datasets': ['Bifurcation', 'Cycle', 'Unidirectional'],\n 'mean': {\n 'PHATE Space RMSE': {\n 'Bifurcation': np.array([6.16, 2.98, 2.83, 2.60]) * 1e-3,\n 'Cycle': np.array([2.49, 3.79, 1.58, 0.777]) * 1e-3,\n 'Unidirectional': np.array([8.08, 5.33, 5.67, 3.40]) * 1e-3,\n },\n 'Gene Space RMSE': {\n 'Bifurcation': np.array([0.150, 0.0842, 0.0835, 0.0773]),\n 'Cycle': np.array([0.151, 0.209, 0.141, 0.118]),\n 'Unidirectional': np.array([0.118, 0.0960, 0.0978, 0.0853]),\n },\n 'Interpolation EMD': {\n 'Bifurcation': np.array([1.07, 0.495, 0.516, 0.465]) * 1e-2,\n 'Cycle': np.array([0.710, 0.818, 0.526, 0.465]) * 1e-2,\n 'Unidirectional': np.array([1.50, 1.32, 1.28, 0.935]) * 1e-2,\n },\n },\n}\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n fig = plt.figure(figsize=(36, 6))\n\n num_methods = len(data_comparison_Trajectory['methods'])\n for metric_idx, metric_name in enumerate(data_comparison_Trajectory['metrics']):\n ax = fig.add_subplot(1, 4, metric_idx + 1)\n xtick_list = []\n\n for dataset_idx, dataset_name in enumerate(data_comparison_Trajectory['datasets']):\n\n ax.bar(\n np.arange(num_methods) + dataset_idx * (num_methods + 1),\n data_comparison_Trajectory['mean'][metric_name][dataset_name],\n color=data_comparison_Trajectory['colors'],\n label=data_comparison_Trajectory['methods'],\n )\n\n xtick_list.append(np.mean(np.arange(num_methods)) + dataset_idx * (num_methods + 1))\n\n if dataset_idx == 0:\n handles, labels = ax.get_legend_handles_labels()\n\n ax.set_xticks(xtick_list)\n ax.set_xticklabels(data_comparison_Trajectory['datasets'])\n ax.set_ylabel(data_comparison_Trajectory['metrics'][metric_idx], fontsize=36, labelpad=12)\n ax.set_xlabel('Dataset', fontsize=36, labelpad=12)\n ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))\n\n ax = fig.add_subplot(1, 4, 4)\n ax.legend(handles, labels, fontsize=30, loc='lower left', frameon=False)\n ax.set_axis_off()\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/fig2_comparison_Trajectory.png', dpi=300)\n fig.savefig('./figures/fig2_comparison_Trajectory.pdf', dpi=300)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2941,"content_sha256":"bc154c3e99fd4895ceefefeef81298055a8cd42e5795547e1fe1632edabe5b69"},{"filename":"assets/figures4papers/figure_Dispersion/plot_idea.py","content":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n\nEPSILON = 1e-6\n\ndef sample_points_in_ball(num_points, theta_range=2*np.pi):\n r = np.sqrt(np.random.uniform(0, 0.95, num_points))\n theta = np.random.uniform(np.pi/2 - theta_range/2, np.pi/2 + theta_range/2, num_points)\n x = r * np.cos(theta)\n y = r * np.sin(theta)\n return np.stack([x, y], axis=1)\n\ndef plot_ball_with_points(ax, pts, facecolor):\n num_points_grid = 512\n\n xs = np.linspace(-1, 1, num_points_grid)\n ys = np.linspace(-1, 1, num_points_grid)\n x, y = np.meshgrid(xs, ys)\n r2 = x**2 + y**2\n mask = r2 \u003c= 1.0\n z = np.zeros_like(x)\n z[mask] = np.sqrt(1.0 - r2[mask])\n\n nx, ny, nz = x.copy(), y.copy(), z.copy()\n norm = np.sqrt(nx**2 + ny**2 + nz**2) + EPSILON\n nx, ny, nz = nx / norm, ny / norm, nz / norm\n\n # light from top left.\n light_dir = np.array([-0.5, 0.5, 0.8])\n light_dir /= np.linalg.norm(light_dir)\n intensity = np.maximum(0.0, nx*light_dir[0] + ny*light_dir[1] + nz*light_dir[2])\n shade = np.clip(-0.5 + 2.0*intensity, 0, 1)\n\n img = np.ones((num_points_grid, num_points_grid))\n img[mask] = shade[mask]\n\n ax.imshow(img, cmap='gray', origin='lower', extent=[-1, 1, -1, 1], vmin=0, vmax=1, alpha=0.3)\n ax.set_xlim([-2, 2])\n ax.set_ylim([-2, 2])\n ax.set_axis_off()\n\n # add points on sphere.\n ax.scatter(pts[:, 0], pts[:, 1], s=80, facecolor=facecolor, edgecolor='black', alpha=0.8)\n for i in range(pts.shape[0]):\n ax.plot([pts[i, 0], 0], [pts[i, 1], 0],\n linestyle=\"--\", color=\"black\", linewidth=1, alpha=0.8)\n\n return ax\n\n\nif __name__ == \"__main__\":\n save_path = './figures/idea.png'\n num_points = 16\n\n np.random.seed(1)\n plt.rcParams['text.usetex'] = True\n plt.rcParams['font.family'] = 'sans-serif'\n fig = plt.figure(figsize=(18, 6))\n\n ax = fig.add_subplot(1, 3, 1)\n pts = sample_points_in_ball(num_points=num_points)\n plot_ball_with_points(ax, pts, facecolor='#cde5f8')\n\n ax = fig.add_subplot(1, 3, 2)\n pts = sample_points_in_ball(num_points=num_points, theta_range=np.pi/4)\n plot_ball_with_points(ax, pts, facecolor='#6a98cb')\n\n ax = fig.add_subplot(1, 3, 3)\n pts = sample_points_in_ball(num_points=num_points)\n plot_ball_with_points(ax, pts, facecolor='#6a98cb')\n\n os.makedirs(os.path.dirname(save_path), exist_ok=True)\n fig.tight_layout(pad=2)\n fig.savefig(save_path, dpi=300)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2432,"content_sha256":"c540a368c5a60b90fec950da8c5085d2ebfa47c367e3270fa3fb38176cf8134e"},{"filename":"assets/figures4papers/figure_Dispersion/plot_illustration.py","content":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom matplotlib.lines import Line2D\nfrom matplotlib.patches import FancyArrowPatch\nfrom mpl_toolkits.mplot3d import proj3d\nimport matplotlib.cm as cm\n\n\nEPSILON = 1e-6\n\ndef pairwise_sqdist(X: np.ndarray) -> np.ndarray:\n X2 = np.sum(X**2, axis=1, keepdims=True)\n D2 = X2 + X2.T - 2 * (X @ X.T)\n return np.maximum(D2, 0.0)\n\ndef safe_scale(V, s=0.6, eps=EPSILON):\n n = np.linalg.norm(V, axis=1, keepdims=True)\n return V / (eps + n) / s\n\ndef nice_axes(ax, L):\n y_scale = 1\n z_scale = 1.2\n ax.quiver(0, 0, 0, 0, -L, 0, color=\"black\", linewidth=2, arrow_length_ratio=0.1)\n ax.quiver(0, 0, 0, y_scale*L, 0, 0, color=\"black\", linewidth=2, arrow_length_ratio=0.1)\n ax.quiver(0, 0, 0, 0, 0, z_scale*L, color=\"black\", linewidth=2, arrow_length_ratio=0.1)\n ax.text(0, -L*1.2, 0, \"x\", color=\"black\", fontsize=36)\n ax.text(y_scale*L*1.05, -0.2, 0, \"y\", color=\"black\", fontsize=36)\n ax.text(-0.2, 0, z_scale*L*1.05, \"z\", color=\"black\", fontsize=36)\n ax.grid(False)\n ax.xaxis.pane.set_visible(False)\n ax.yaxis.pane.set_visible(False)\n ax.zaxis.pane.set_visible(False)\n ax.xaxis.line.set_color((1, 1, 1, 0))\n ax.yaxis.line.set_color((1, 1, 1, 0))\n ax.zaxis.line.set_color((1, 1, 1, 0))\n ax.set_xticks([])\n ax.set_yticks([])\n ax.set_zticks([])\n return ax\n\ndef _to3d_xy(xy):\n x, y = xy[:, 0], xy[:, 1]\n z = np.sqrt(np.clip(1.0 - x*x - y*y, 0.0, 1.0))\n P = np.stack([x, y, z], axis=1)\n # normalize (robust against tiny numerical drift)\n P /= np.linalg.norm(P, axis=1, keepdims=True) + EPSILON\n return P\n\ndef _slerp_arc(p, q, n=200):\n p = p / (np.linalg.norm(p) + EPSILON)\n q = q / (np.linalg.norm(q) + EPSILON)\n dot = np.clip(np.dot(p, q), -1.0, 1.0)\n theta = np.arccos(dot)\n if theta \u003c EPSILON: # nearly identical points\n return np.repeat(p[None, :], n, axis=0)\n # great-circle via SLERP\n t = np.linspace(0.0, 1.0, n)\n s = np.sin\n arc = (s((1-t)*theta)[:,None]*p + s(t*theta)[:,None]*q) / (s(theta) + EPSILON)\n # normalize for safety\n arc /= np.linalg.norm(arc, axis=1, keepdims=True) + EPSILON\n return arc\n\ndef draw_geodesic(ax, a2d, b2d, linestyle='-', draw_arrow=True, alpha=0.8,\n num_points_grid=300, color='blue', lw=2.0, arrow_scale=30, shorten=0.04):\n A3, B3 = _to3d_xy(np.array([a2d])), _to3d_xy(np.array([b2d]))\n arc = _slerp_arc(A3[0], B3[0], n=num_points_grid)\n x_full, y_full = arc[:, 0], arc[:, 1]\n\n if draw_arrow:\n k0 = int(2 * shorten * num_points_grid)\n k1 = num_points_grid - k0\n else:\n k0 = int(shorten * num_points_grid)\n k1 = num_points_grid - k0\n x, y = x_full[k0:k1], y_full[k0:k1]\n ax.plot(x, y, color=color, lw=lw, solid_capstyle='round', alpha=alpha, linestyle=linestyle)\n\n if draw_arrow:\n # Add arrowheads at both ends\n k0 = int(shorten * num_points_grid)\n k1 = num_points_grid - k0\n x, y = x_full[k0:k1], y_full[k0:k1]\n arrow1 = FancyArrowPatch(\n (x[1], y[1]), (x[0], y[0]),\n arrowstyle='-|>', color=color,\n mutation_scale=arrow_scale, lw=0\n )\n arrow2 = FancyArrowPatch(\n (x[-2], y[-2]), (x[-1], y[-1]),\n arrowstyle='-|>', color=color,\n mutation_scale=arrow_scale, lw=0\n )\n ax.add_patch(arrow1)\n ax.add_patch(arrow2)\n return ax\n\nclass Arrow3D(FancyArrowPatch):\n def __init__(self, xs, ys, zs, *args, **kwargs):\n super().__init__((0, 0), (0, 0), *args, **kwargs)\n self._verts3d = xs, ys, zs\n\n def do_3d_projection(self, renderer=None):\n xs3d, ys3d, zs3d = self._verts3d\n xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.get_proj())\n self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))\n return np.min(zs)\n\ndef plot_decorrelation(ax):\n num_points_grid = 512\n num_points_on_ellipsoid = 18\n image_scale = 2\n\n xs = np.linspace(-2, 2, num_points_grid)\n ys = np.linspace(-2, 2, num_points_grid)\n x, y = np.meshgrid(xs, ys)\n\n r2 = x**2 + y**2\n mask_s = r2 \u003c= 1.0\n z_s = np.zeros_like(x)\n z_s[mask_s] = np.sqrt(1.0 - r2[mask_s])\n\n nx, ny, nz = x.copy(), y.copy(), z_s.copy()\n nrm = np.sqrt(nx**2 + ny**2 + nz**2) + EPSILON\n nx, ny, nz = nx/nrm, ny/nrm, nz/nrm\n\n light_dir = np.array([-0.5, -0.5, 0.8])\n light_dir /= np.linalg.norm(light_dir)\n intensity = np.maximum(0.0, nx*light_dir[0] + ny*light_dir[1] + nz*light_dir[2])\n\n img_s = np.ones_like(x)\n img_s[mask_s] = np.clip(0.2 + 0.9*intensity[mask_s], 0, 1)\n ax.imshow(img_s, cmap='gray',\n extent=[-image_scale, image_scale, -image_scale, image_scale],\n vmin=0, vmax=1, alpha=1)\n\n a, b, c = 1.50, 0.60, 0.40\n theta = np.deg2rad(30)\n ct, st = np.cos(theta), np.sin(theta)\n\n xL = ct*x + st*y\n yL = -st*x + ct*y\n vL = (xL/a)**2 + (yL/b)**2\n mask_e = vL \u003c= 1.0\n zL = np.zeros_like(xL)\n zL[mask_e] = c * np.sqrt(1.0 - vL[mask_e])\n\n nxL = xL/(a*a)\n nyL = yL/(b*b)\n nzL = np.zeros_like(zL)\n nzL[mask_e] = zL[mask_e]/(c*c)\n nrm = np.sqrt(nxL**2 + nyL**2 + nzL**2) + EPSILON\n nxL, nyL, nzL = nxL/nrm, nyL/nrm, nzL/nrm\n nxE = ct*nxL - st*nyL\n nyE = st*nxL + ct*nyL\n nzE = nzL\n\n light_dir = np.array([0.5, 0.5, -0.8])\n light_dir /= np.linalg.norm(light_dir)\n intensity_e = np.maximum(0.0, nxE*light_dir[0] + nyE*light_dir[1] + nzE*light_dir[2])\n img_e = np.full_like(x, np.nan, dtype=float)\n img_e[mask_e] = np.clip(0.5 + 0.9*intensity_e[mask_e], 0, 1)\n cmap = cm.Blues.copy()\n cmap.set_bad(color=\"white\")\n ax.imshow(img_e, cmap=cmap, origin='lower',\n extent=[-image_scale, image_scale, -image_scale, image_scale],\n vmin=0, vmax=1, alpha=0.4)\n\n # Pick points on ellipsoid uniformly.\n phi = np.linspace(0, 2*np.pi, num_points_on_ellipsoid, endpoint=False)\n\n # Ellipsoid rim in local coords.\n xL_rim = a*np.cos(phi)\n yL_rim = b*np.sin(phi)\n zL_rim = np.zeros_like(phi)\n\n # rotate back to world\n xw = ct*xL_rim - st*yL_rim\n yw = st*xL_rim + ct*yL_rim\n zw = zL_rim\n Pellip = np.stack([xw, yw, zw], axis=1)\n\n # matching sphere rim points: same world direction in xy, unit radius, z=0\n rxy = np.sqrt(xw**2 + yw**2) + EPSILON\n Psphere = np.stack([xw/rxy, yw/rxy, np.zeros_like(rxy)], axis=1)\n\n ax.scatter(Pellip[:, 0], Pellip[:, 1], s=80, color='#0c2458', alpha=0.5)\n ax.scatter(Psphere[:, 0], Psphere[:, 1], s=80, facecolors='#b64342', alpha=0.5, linewidths=2)\n\n for p0, p1 in zip(Pellip, Psphere):\n ax.annotate(\"\", xy=(p1[0], p1[1]), xytext=(p0[0], p0[1]),\n arrowprops=dict(arrowstyle=\"->\", color=\"#b64342\", lw=3, mutation_scale=20))\n\n ax.set_xlim([-1.6, 1.6])\n ax.set_ylim([-2, 1.6])\n ax.set_axis_off()\n\n arrow_cov = Line2D([], [], color=\"#b64342\", alpha=0.8,\n marker=r'$\\rightarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, linestyle=\"None\", markersize=35, label=\"Decorrelation\")\n ax.legend(handles=[arrow_cov], frameon=False, loc=\"lower center\", fontsize=24, bbox_to_anchor=(0.5, 0.1))\n return ax\n\ndef plot_orthogonalization(ax):\n num_points_grid = 512\n\n xs = np.linspace(-1, 1, num_points_grid)\n ys = np.linspace(-1, 1, num_points_grid)\n x, y = np.meshgrid(xs, ys)\n r2 = x**2 + y**2\n mask = r2 \u003c= 1.0\n z = np.zeros_like(x)\n z[mask] = np.sqrt(1.0 - r2[mask])\n\n nx, ny, nz = x.copy(), y.copy(), z.copy()\n norm = np.sqrt(nx**2 + ny**2 + nz**2) + EPSILON\n nx, ny, nz = nx / norm, ny / norm, nz / norm\n\n # light from top left.\n light_dir = np.array([-0.5, 0.5, 0.8])\n light_dir /= np.linalg.norm(light_dir)\n intensity = np.maximum(0.0, nx*light_dir[0] + ny*light_dir[1] + nz*light_dir[2])\n ambient = 0.3\n shade = np.clip(ambient + 0.9*intensity, 0, 1)\n\n img = np.ones((num_points_grid, num_points_grid))\n img[mask] = shade[mask]\n\n ax.imshow(img, cmap='gray', origin='lower', extent=[-1, 1, -1, 1], vmin=0, vmax=1, alpha=0.5)\n ax.set_ylim([-2.1, 1.5])\n ax.set_axis_off()\n\n # add 3 fixed blue points on sphere\n pts = np.array([\n [-0.2, 0.6],\n [0.9, 0.0],\n [-0.75, -0.4],\n ])\n ax.scatter(pts[:, 0], pts[:, 1], s=80, color='#0c2458', alpha=0.5)\n ax = draw_geodesic(ax, pts[0], pts[1], color='#b64342', lw=4)\n ax = draw_geodesic(ax, pts[0], pts[2], color='#b64342', lw=4)\n ax = draw_geodesic(ax, pts[1], pts[2], linestyle='--', draw_arrow=False, color='#42949e', lw=4, alpha=0.5)\n ax = draw_geodesic(ax, pts[0], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)\n ax = draw_geodesic(ax, pts[1], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)\n ax = draw_geodesic(ax, pts[2], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)\n\n ax.text(0.45, 0.4, \"acute angle,\\ndisperse\", color=\"#b64342\", fontsize=24, ha=\"center\", va=\"center\",\n bbox=dict(facecolor=\"white\", alpha=1, edgecolor=\"none\", boxstyle=\"round,pad=0.2\"))\n ax.text(-0.6, 0.15, \"acute angle,\\ndisperse\", color=\"#b64342\", fontsize=24, ha=\"center\", va=\"center\",\n bbox=dict(facecolor=\"white\", alpha=1, edgecolor=\"none\", boxstyle=\"round,pad=0.2\"))\n ax.text(0.15, -0.36, \"obtuse angle,\\ndo nothing\", color=\"#42949e\", fontsize=24, ha=\"center\", va=\"center\",\n bbox=dict(facecolor=\"white\", alpha=1, edgecolor=\"none\", boxstyle=\"round,pad=0.2\"))\n\n arrow_disp = Line2D([], [], color=\"#b64342\", alpha=0.8,\n marker=r'$\\leftarrow\\rightarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, linestyle=\"None\", markersize=50, label=\"Orthogonalization\")\n ax.legend(handles=[arrow_disp], frameon=False, loc=\"lower center\", fontsize=24, bbox_to_anchor=(0.5, 0.15))\n return ax\n\ndef plot_l2_repel(ax):\n tau = 0.5\n\n Z = np.array([\n [ 3.0, -3.0, 0.0],\n [ 1.0, -3.0, 0.0],\n [ -3.0, -2.0, -1.0],\n [ -2.0, -1.0, 1.0],\n [ -2.0, -1.0, 4.0],\n [ 0.0, 2.0, 2.0],\n [ 4.0, 0.0, 2.0],\n [ 4.0, -2.0, 0.0],\n ])\n\n num_points, d = Z.shape\n\n D2 = pairwise_sqdist(Z) / d\n W = np.exp(-D2 / tau)\n\n G_disp = np.zeros_like(Z)\n for i in range(num_points):\n diff = Z[i] - Z\n G_disp[i] = (2.0 / tau) * (W[i][:, None] * diff).sum(axis=0)\n\n A_disp = safe_scale(G_disp)\n Vn = -Z / (np.linalg.norm(Z, axis=1, keepdims=True) + EPSILON)\n\n ax.view_init(elev=30, azim=-60)\n ax = nice_axes(ax, 6)\n ax.set_xlim([-3, 5])\n ax.set_ylim([-3, 5])\n ax.set_zlim([-6, 4])\n ax.scatter(Z[:, 0], Z[:, 1], Z[:, 2], s=80, color='#0c2458', alpha=0.5)\n\n for i in range(num_points):\n p0 = Z[i]\n p1 = Z[i] + A_disp[i] * 1.25\n arrow = Arrow3D([p0[0], p1[0]], [p0[1], p1[1]], [p0[2], p1[2]],\n mutation_scale=16, lw=4, arrowstyle='->', color='#b64342', alpha=0.8)\n ax.add_artist(arrow)\n\n for i in range(num_points):\n p0 = Z[i]\n p1 = Z[i] + Vn[i] * 1.2\n arrow = Arrow3D([p0[0], p1[0]], [p0[1], p1[1]], [p0[2], p1[2]],\n mutation_scale=10, lw=3, arrowstyle='->', color='#9a4d8e', alpha=0.8)\n ax.add_artist(arrow)\n\n for i in range(num_points):\n ax.plot([Z[i, 0], 0], [Z[i, 1], 0], [Z[i, 2], 0],\n linestyle=\"--\", color=\"black\", linewidth=1, alpha=0.8)\n\n arrow_disp = Line2D([], [], color=\"#b64342\", alpha=0.8,\n marker=r'$\\rightarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, linestyle=\"None\", markersize=25,\n label=r\"${\\ell_2}$-repel\")\n arrow_norm = Line2D([0, 1], [0, 0], color=\"#9a4d8e\", alpha=0.8,\n marker=r'$\\rightarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, linestyle=\"None\", markersize=25,\n label=\"norm regularization\")\n ax.legend(handles=[arrow_disp, arrow_norm], frameon=False, loc=\"lower center\",\n fontsize=24, bbox_to_anchor=(0.5, 0))\n\n ax.text(0.8, 0.0, 8.0, \"pairwise dispersion\", color=\"#b64342\", fontsize=24, ha=\"left\", va=\"center\",\n bbox=dict(facecolor=\"white\", alpha=1, edgecolor=\"none\", boxstyle=\"round,pad=0.2\"))\n ax.text(0.8, 0.0, 6.5, \"norm reduction\", color=\"#9a4d8e\", fontsize=24, ha=\"left\", va=\"center\",\n bbox=dict(facecolor=\"white\", alpha=1, edgecolor=\"none\", boxstyle=\"round,pad=0.2\"))\n\n return ax\n\n\ndef plot_angular_spread(ax):\n num_points_grid = 512\n\n xs = np.linspace(-1, 1, num_points_grid)\n ys = np.linspace(-1, 1, num_points_grid)\n x, y = np.meshgrid(xs, ys)\n r2 = x**2 + y**2\n mask = r2 \u003c= 1.0\n z = np.zeros_like(x)\n z[mask] = np.sqrt(1.0 - r2[mask])\n\n nx, ny, nz = x.copy(), y.copy(), z.copy()\n norm = np.sqrt(nx**2 + ny**2 + nz**2) + EPSILON\n nx, ny, nz = nx / norm, ny / norm, nz / norm\n\n # light from top left.\n light_dir = np.array([-0.5, 0.5, 0.8])\n light_dir /= np.linalg.norm(light_dir)\n intensity = np.maximum(0.0, nx*light_dir[0] + ny*light_dir[1] + nz*light_dir[2])\n ambient = 0.3\n shade = np.clip(ambient + 0.9*intensity, 0, 1)\n\n img = np.ones((num_points_grid, num_points_grid))\n img[mask] = shade[mask]\n\n ax.imshow(img, cmap='gray', origin='lower', extent=[-1, 1, -1, 1], vmin=0, vmax=1, alpha=0.5)\n ax.set_ylim([-2.1, 1.5])\n ax.set_axis_off()\n\n # add 3 fixed blue points on sphere\n pts = np.array([\n [-0.2, 0.6],\n [0.9, 0.0],\n [-0.75, -0.4],\n ])\n ax.scatter(pts[:, 0], pts[:, 1], s=80, color='#0c2458', alpha=0.5)\n ax = draw_geodesic(ax, pts[0], pts[1], color='#b64342', lw=4)\n ax = draw_geodesic(ax, pts[0], pts[2], color='#b64342', lw=4)\n ax = draw_geodesic(ax, pts[1], pts[2], color='#b64342', lw=4)\n ax = draw_geodesic(ax, pts[0], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)\n ax = draw_geodesic(ax, pts[1], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)\n ax = draw_geodesic(ax, pts[2], [0, 0], linestyle='--', draw_arrow=False, color='black', lw=1, alpha=0.8, shorten=0)\n\n ax.text(0.45, 0.4, \"disperse\", color=\"#b64342\", fontsize=24, ha=\"center\", va=\"center\",\n bbox=dict(facecolor=\"white\", alpha=1, edgecolor=\"none\", boxstyle=\"round,pad=0.2\"))\n ax.text(-0.6, 0.15, \"disperse\", color=\"#b64342\", fontsize=24, ha=\"center\", va=\"center\",\n bbox=dict(facecolor=\"white\", alpha=1, edgecolor=\"none\", boxstyle=\"round,pad=0.2\"))\n ax.text(0.15, -0.36, \"disperse\", color=\"#b64342\", fontsize=24, ha=\"center\", va=\"center\",\n bbox=dict(facecolor=\"white\", alpha=1, edgecolor=\"none\", boxstyle=\"round,pad=0.2\"))\n\n arrow_disp = Line2D([], [], color=\"#b64342\", alpha=0.8,\n marker=r'$\\leftarrow\\rightarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, linestyle=\"None\", markersize=50, label=\"Dispersion loss\")\n ax.legend(handles=[arrow_disp], frameon=False, loc=\"lower center\", fontsize=24, bbox_to_anchor=(0.5, 0.145))\n return ax\n\n\nif __name__ == \"__main__\":\n save_path = './figures/illustration.png'\n plt.rcParams['text.usetex'] = True\n plt.rcParams['font.family'] = 'sans-serif'\n fig = plt.figure(figsize=(24, 8))\n\n ax = fig.add_subplot(1, 4, 1)\n plot_angular_spread(ax)\n\n ax = fig.add_subplot(1, 4, 2)\n plot_decorrelation(ax)\n\n ax = fig.add_subplot(1, 4, 3, projection=\"3d\")\n plot_l2_repel(ax)\n\n ax = fig.add_subplot(1, 4, 4)\n plot_orthogonalization(ax)\n\n os.makedirs(os.path.dirname(save_path), exist_ok=True)\n fig.tight_layout(pad=2)\n fig.savefig(save_path, dpi=300)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15472,"content_sha256":"7f9c451d336f718a2c4287130a45ec4597b9fad43746de314a018655c639ec98"},{"filename":"assets/figures4papers/figure_FPGM/plot_freq_prior.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom matplotlib.ticker import LinearLocator, FormatStrFormatter\n\n\ndef load_freq_prior_data(data_dir):\n datasets = {}\n for name in sorted(os.listdir(data_dir)):\n if not name.endswith(\".npz\"):\n continue\n path = os.path.join(data_dir, name)\n data = np.load(path)\n datasets[name] = {key: data[key] for key in data.files}\n return datasets\n\n\ndef plot_dataset(ax, title, sample1_arr, sample2_arr, max_freq_radius,\n line_colors, line_styles, legend_labels):\n ax.spines['top'].set_visible(False)\n ax.spines['right'].set_visible(False)\n ax.spines['left'].set_linewidth(2)\n ax.spines['bottom'].set_linewidth(2)\n ax.plot(np.arange(len(sample1_arr))[:max_freq_radius],\n sample1_arr[:max_freq_radius],\n color=line_colors[0], linestyle=line_styles[0], linewidth=2.5)\n ax.plot(np.arange(len(sample2_arr))[:max_freq_radius],\n sample2_arr[:max_freq_radius],\n color=line_colors[1], linestyle=line_styles[1], linewidth=2.5)\n ax.set_title(title, fontfamily='monospace')\n ax.set_xlabel('Frequency Radius')\n ax.set_xticks(np.arange(max_freq_radius)[::4])\n ax.set_ylabel('Mean Amplitude')\n ax.set_ylim(bottom=0)\n ax.yaxis.set_major_locator(LinearLocator(4))\n ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f'))\n ax.legend(legend_labels, frameon=False, fontsize=14)\n\n\nif __name__ == \"__main__\":\n data_dir = os.path.join(os.path.dirname(__file__), \"data\")\n datasets = load_freq_prior_data(data_dir)\n\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 16\n fig = plt.figure(figsize=(20, 7))\n gs = fig.add_gridspec(2, 5, width_ratios=[1, 1, 0, 1, 1])\n\n # Part 1. Plot the individual datasets.\n max_freq_radius = 17\n line_colors = ['#2A9D8F', '#E76F51']\n line_styles = ['-', '--']\n subplot_positions = [gs[0, 0], gs[0, 1], gs[1, 0], gs[1, 1]]\n\n per_dataset_names = ['Kvasir', 'CVC-ClinicDB', 'CVC-ColonDB', 'ETIS']\n max_name_len = max(len(name) for name in per_dataset_names)\n for dataset_idx, dataset_name in enumerate(per_dataset_names):\n data = datasets[dataset_name.lower() + '_freq_prior.npz']\n sample1_arr = data['data1']\n sample2_arr = data['data2']\n\n ax = fig.add_subplot(subplot_positions[dataset_idx])\n plot_dataset(ax, dataset_name, sample1_arr, sample2_arr,\n max_freq_radius, line_colors, line_styles,\n [r'Random subset 1 $(N=50)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'Random subset 2 $(N=50)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

])\n\n # Part 2. Plot the mixed dataset across two columns.\n data = datasets['mixed_freq_prior.npz']\n sample1_arr = data['data1']\n sample2_arr = data['data2']\n ax = fig.add_subplot(gs[0, 3:5])\n plot_dataset(ax, '', sample1_arr, sample2_arr, max_freq_radius, line_colors, line_styles,\n [r'Random subset 1 $(N=500)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'Random subset 2 $(N=500)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

])\n ax.set_title('Mixuture of all four datasets', fontfamily='helvetica')\n\n # Part 3. Plot per-dataset mean/std curves in a single panel.\n ax = fig.add_subplot(gs[1, 3])\n per_dataset_colors = ['#4C72B0', '#C2A5CF', '#7B3294', '#8C564B']\n curve_means = []\n for dataset_idx, dataset_name in enumerate(['Kvasir', 'CVC-ClinicDB', 'CVC-ColonDB', 'ETIS']):\n data = datasets[dataset_name.lower() + '_freq_prior.npz']\n sample1_arr = data['data1'][:max_freq_radius]\n sample2_arr = data['data2'][:max_freq_radius]\n stacked = np.stack([sample1_arr, sample2_arr], axis=0)\n mean = stacked.mean(axis=0)\n std = stacked.std(axis=0)\n x_vals = np.arange(len(mean))\n color = per_dataset_colors[dataset_idx]\n padded_name = dataset_name.ljust(max_name_len)\n ax.plot(x_vals, mean, color=color, linewidth=1.5,\n label=f'{padded_name}' + r' $(N=100)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

)\n ax.fill_between(x_vals, mean - std, mean + std, color=color, alpha=0.2)\n curve_means.append(mean)\n\n ax.spines['top'].set_visible(False)\n ax.spines['right'].set_visible(False)\n ax.spines['left'].set_linewidth(2)\n ax.spines['bottom'].set_linewidth(2)\n ax.axvline(2, color='0.6', linestyle='--', linewidth=1.2, label=r'Radius$=2

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

)\n x_left = ax.get_xlim()[0]\n for mean, color in zip(curve_means, per_dataset_colors):\n ax.hlines(mean[2], xmin=x_left, xmax=2, color=color, linestyle='--', linewidth=1.0)\n ax.set_xlabel('Frequency Radius')\n ax.set_xticks(np.arange(max_freq_radius)[::4])\n ax.set_xlim(left=x_left, right=max_freq_radius)\n ax.set_ylabel('Mean Amplitude')\n ax.set_ylim(bottom=0, top=3)\n ax.yaxis.set_major_locator(LinearLocator(4))\n ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f'))\n ax.legend(frameon=False, ncol=1, prop={'family': 'monospace', 'size': 12})\n\n # Part 4. Plot foreground/background curves in a single panel.\n data = datasets['foreground_background.npz']\n polyp_arr = data['data1'][:max_freq_radius]\n background_arr = data['data2'][:max_freq_radius]\n x_vals = np.arange(len(polyp_arr))\n polyp_color = '#1F3A93'\n background_color = '#16A085'\n\n ax = fig.add_subplot(gs[1, 4])\n ax.spines['top'].set_visible(False)\n ax.spines['right'].set_visible(False)\n ax.spines['left'].set_linewidth(2)\n ax.spines['bottom'].set_linewidth(2)\n fb_names = ['Polyp', 'Background']\n fb_max_len = max(len(name) for name in fb_names)\n ax.plot(x_vals, polyp_arr, color=polyp_color, linewidth=2.5,\n label=f'{fb_names[0].ljust(fb_max_len)}' + r' $(N=500)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

)\n ax.plot(x_vals, background_arr, color=background_color, linewidth=2.5,\n linestyle=':', label=f'{fb_names[1].ljust(fb_max_len)}' + r' $(N=500)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

)\n ax.axvline(2, color='0.6', linestyle='--', linewidth=1.2, label=r'Radius$=2

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

)\n x_left = ax.get_xlim()[0]\n ax.hlines(polyp_arr[2], xmin=x_left, xmax=2, color=polyp_color, linestyle='--', linewidth=1.2)\n ax.hlines(background_arr[2], xmin=x_left, xmax=2, color=background_color, linestyle='--', linewidth=1.2)\n ax.set_xlabel('Frequency Radius')\n ax.set_xticks(np.arange(max_freq_radius)[::4])\n ax.set_xlim(left=x_left, right=max_freq_radius)\n ax.set_ylabel('Mean Amplitude')\n ax.set_ylim(bottom=0)\n ax.yaxis.set_major_locator(LinearLocator(4))\n ax.yaxis.set_major_formatter(FormatStrFormatter('%.1f'))\n ax.legend(frameon=False, prop={'family': 'monospace', 'size': 12})\n\n fig.tight_layout(pad=1)\n os.makedirs('figures', exist_ok=True)\n fig.savefig('figures/freq_prior.png', dpi=300)","content_type":"text/x-python; charset=utf-8","language":"python","size":6578,"content_sha256":"d9c5a53ff6c76e9545ea0d65292841765e849d1076afc6a92f2127041740d8bf"},{"filename":"assets/figures4papers/figure_ImmunoStruct/plot_bars.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom raw_data import data_comparison_IEDB, data_ablation_IEDB, data_comparison_Cancer, data_ablation_Cancer\n\n\ndef decode_ablation(data_dict):\n binary_list = data_dict['ablations']\n component_str = data_dict['components']\n decoded_list = []\n for binary_code in binary_list:\n assert len(binary_code) == len(component_str)\n decoded_str = []\n for i, c in enumerate(binary_code):\n if c == '1':\n decoded_str.append(component_str[i])\n decoded_list.append(' + '.join(decoded_str))\n return decoded_list\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n plt.rcParams['svg.fonttype'] = 'none'\n\n fig = plt.figure(figsize=(28, 6))\n\n ax = fig.add_subplot(1, 4, 1)\n ax.bar(range(len(data_comparison_IEDB['mean'])),\n data_comparison_IEDB['mean'][:, 0],\n yerr=data_comparison_IEDB['std'][:, 0],\n capsize=5,\n color=data_comparison_IEDB['colors'],\n label=data_comparison_IEDB['methods'])\n handles, labels = ax.get_legend_handles_labels()\n ax.set_xticks([])\n ax.set_ylim([0.5, 0.9])\n ax.set_ylabel(data_comparison_IEDB['metrics'][0], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 2)\n ax.bar(range(len(data_comparison_IEDB['mean'])),\n data_comparison_IEDB['mean'][:, 1],\n yerr=data_comparison_IEDB['std'][:, 1],\n capsize=5,\n color=data_comparison_IEDB['colors'])\n ax.set_xticks([])\n ax.set_ylim([0.15, 0.75])\n ax.set_ylabel(data_comparison_IEDB['metrics'][1], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 3)\n ax.bar(range(len(data_comparison_IEDB['mean'])),\n data_comparison_IEDB['mean'][:, 2],\n yerr=data_comparison_IEDB['std'][:, 2],\n capsize=5,\n color=data_comparison_IEDB['colors'])\n ax.set_xticks([])\n ax.set_ylim([0.18, 0.55])\n ax.set_ylabel(data_comparison_IEDB['metrics'][2], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 4)\n ax.legend(handles, labels)\n ax.set_axis_off()\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/bars_comparison_IEDB.png', dpi=600)\n plt.close(fig)\n\n\n fig = plt.figure(figsize=(24, 8))\n\n ax = fig.add_subplot(1, 3, 1)\n ax.barh(range(len(data_ablation_IEDB['mean'][:, 0])),\n data_ablation_IEDB['mean'][:, 0],\n xerr=data_ablation_IEDB['std'][:, 0],\n color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in np.linspace(0.2, 1.0, 12)],\n ecolor='k',\n capsize=5,\n )\n\n ax.set_yticks(range(len(data_ablation_IEDB['ablations'])))\n ax.set_yticklabels(decode_ablation(data_ablation_IEDB))\n ax.set_xlim([0.75, 0.9])\n ax.set_xticks([0.75, 0.8, 0.85, 0.9])\n ax.set_xticklabels([0.75, 0.8, 0.85, 0.9])\n ax.set_xlabel(data_ablation_IEDB['metrics'][0], fontsize=32)\n\n ax = fig.add_subplot(1, 3, 2)\n ax.barh(range(len(data_ablation_IEDB['mean'][:, 1])),\n data_ablation_IEDB['mean'][:, 1],\n xerr=data_ablation_IEDB['std'][:, 1],\n color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in np.linspace(0.2, 1.0, 12)],\n ecolor='k',\n capsize=5,\n )\n\n ax.set_yticks([])\n ax.set_xlim([0.4, 0.72])\n ax.set_xticks([0.4, 0.5, 0.6, 0.7])\n ax.set_xticklabels([0.4, 0.5, 0.6, 0.7])\n ax.set_xlabel(data_ablation_IEDB['metrics'][1], fontsize=32)\n\n ax = fig.add_subplot(1, 3, 3)\n ax.barh(range(len(data_ablation_IEDB['mean'][:, 2])),\n data_ablation_IEDB['mean'][:, 2],\n xerr=data_ablation_IEDB['std'][:, 2],\n color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in np.linspace(0.2, 1.0, 12)],\n ecolor='k',\n capsize=5,\n )\n\n ax.set_yticks([])\n ax.set_xlim([0.4, 0.55])\n ax.set_xticks([0.4, 0.45, 0.5, 0.55])\n ax.set_xticklabels([0.4, 0.45, 0.5, 0.55])\n ax.set_xlabel(data_ablation_IEDB['metrics'][2], fontsize=32)\n\n fig.tight_layout(pad=2)\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/bars_ablation_IEDB.png', dpi=600)\n plt.close(fig)\n\n\n fig = plt.figure(figsize=(28, 6))\n\n ax = fig.add_subplot(1, 4, 1)\n ax.bar(range(len(data_comparison_Cancer['mean'])),\n data_comparison_Cancer['mean'][:, 0],\n yerr=data_comparison_Cancer['std'][:, 0],\n capsize=5,\n color=data_comparison_Cancer['colors'],\n label=data_comparison_Cancer['methods'])\n handles, labels = ax.get_legend_handles_labels()\n ax.set_xticks([])\n ax.set_ylim([0.5, 0.82])\n ax.set_ylabel(data_comparison_Cancer['metrics'][0], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 2)\n ax.bar(range(len(data_comparison_Cancer['mean'])),\n data_comparison_Cancer['mean'][:, 1],\n yerr=data_comparison_Cancer['std'][:, 1],\n capsize=5,\n color=data_comparison_Cancer['colors'])\n ax.set_xticks([])\n ax.set_ylim([0.16, 0.52])\n ax.set_ylabel(data_comparison_Cancer['metrics'][1], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 3)\n ax.bar(range(len(data_comparison_Cancer['mean'])),\n data_comparison_Cancer['mean'][:, 2],\n yerr=data_comparison_Cancer['std'][:, 2],\n capsize=5,\n color=data_comparison_Cancer['colors'])\n ax.set_xticks([])\n ax.set_ylim([0.14, 0.44])\n ax.set_ylabel(data_comparison_Cancer['metrics'][2], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 4)\n ax.legend(handles, labels)\n ax.set_axis_off()\n\n fig.tight_layout(pad=2)\n\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/bars_comparison_Cancer.png', dpi=600)\n plt.close(fig)\n\n fig = plt.figure(figsize=(28, 6))\n\n items_shown = [6, 4, 0] # transfer + contrastive, transfer, none\n\n ax = fig.add_subplot(1, 4, 1)\n ax.bar(range(len(items_shown)),\n data_ablation_Cancer['mean'][:, 0][items_shown],\n yerr=data_ablation_Cancer['std'][:, 0][items_shown],\n capsize=5,\n color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in [1.0, 0.7, 0.4]],\n label=['ImmunoStruct', 'No Contrastive Learning',\n 'No Contrastive Learning &\\nNo Transfer Learning'])\n handles, labels = ax.get_legend_handles_labels()\n ax.set_xticks([])\n ax.set_ylim([0.68, 0.80])\n ax.set_ylabel(data_ablation_Cancer['metrics'][0], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 2)\n ax.bar(range(len(items_shown)),\n data_ablation_Cancer['mean'][:, 1][items_shown],\n yerr=data_ablation_Cancer['std'][:, 1][items_shown],\n capsize=5,\n color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in [1.0, 0.7, 0.4]])\n ax.set_xticks([])\n ax.set_ylim([0.30, 0.52])\n ax.set_ylabel(data_ablation_Cancer['metrics'][1], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 3)\n ax.bar(range(len(items_shown)),\n data_ablation_Cancer['mean'][:, 2][items_shown],\n yerr=data_ablation_Cancer['std'][:, 2][items_shown],\n capsize=5,\n color=[(0.215686, 0.458824, 0.729412, alpha) for alpha in [1.0, 0.7, 0.4]])\n ax.set_xticks([])\n ax.set_ylim([0.29, 0.43])\n ax.set_ylabel(data_ablation_Cancer['metrics'][2], fontsize=32)\n\n ax = fig.add_subplot(1, 4, 4)\n ax.legend(handles, labels)\n ax.set_axis_off()\n\n fig.tight_layout(pad=2)\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/bars_ablation_Cancer.png', dpi=600)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7707,"content_sha256":"1d028144e620d1cfac66ea5ab57004dfa286d80214480c76d046d9f346fda408"},{"filename":"assets/figures4papers/figure_ImmunoStruct/raw_data.py","content":"import numpy as np\n\n\ndata_comparison_IEDB = {\n 'methods': [r'Prime-2.1', r'NetMHCpan', r'MHCnuggets', r'MHCflurry', r'DeepNeo',\n r'BigMHC-EL', r'BigMHC-IM', r'BigMHC$_\\text{retrained}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'ImmunoStruct (ours)'],\n 'colors': ['#CFCECE', '#F4EEAC', '#FBDFE2', '#D9B9D4', '#DAA87C', '#DDF3DE', '#AADCA9', '#8BCF8B', '#3775BA'],\n 'metrics': ['AUROC', 'AUPRC', 'Mean PPVn'],\n 'mean': np.array([\n [0.538, 0.212, 0.207],\n [0.537, 0.214, 0.220],\n [0.546, 0.246, 0.233],\n [0.577, 0.260, 0.273],\n [0.767, 0.438, 0.411],\n [0.588, 0.269, 0.290],\n [0.684, 0.319, 0.333],\n [0.793, 0.462, 0.445],\n [0.882, 0.696, 0.514],\n ]),\n 'std': np.array([\n [0.012, 0.031, 0.028 / np.sqrt(5)],\n [0.027, 0.020, 0.018 / np.sqrt(5)],\n [0.023, 0.019, 0.031 / np.sqrt(5)],\n [0.021, 0.026, 0.028 / np.sqrt(5)],\n [0.032, 0.024, 0.017 / np.sqrt(5)],\n [0.018, 0.016, 0.024 / np.sqrt(5)],\n [0.028, 0.048, 0.034 / np.sqrt(5)],\n [0.013, 0.027, 0.009 / np.sqrt(5)],\n [0.005, 0.020, 0.021 / np.sqrt(5)],\n ]),\n}\n\ndata_ablation_IEDB = {\n 'ablations': ['10000', '01000', '11000', '01100', '11100', '11110',\n '10001', '01001', '11001', '01101', '11101', '11111'],\n 'components': ['Structure', 'Sequence', 'Bchems', 'MMA', 'Transfer Learning'],\n 'metrics': ['AUROC', 'AUPRC', 'Mean PPVn'],\n 'mean': np.array([\n [0.775, 0.442, 0.433],\n [0.842, 0.553, 0.430],\n [0.840, 0.547, 0.444],\n [0.842, 0.554, 0.419],\n [0.840, 0.547, 0.440],\n [0.860, 0.615, 0.462],\n [0.805, 0.497, 0.414],\n [0.845, 0.568, 0.427],\n [0.844, 0.569, 0.440],\n [0.844, 0.561, 0.442],\n [0.840, 0.554, 0.441],\n [0.882, 0.696, 0.514],\n ]),\n 'std': np.array([\n [0.012, 0.022, 0.020 / np.sqrt(5)],\n [0.006, 0.014, 0.009 / np.sqrt(5)],\n [0.007, 0.024, 0.028 / np.sqrt(5)],\n [0.006, 0.020, 0.017 / np.sqrt(5)],\n [0.006, 0.018, 0.032 / np.sqrt(5)],\n [0.013, 0.045, 0.015 / np.sqrt(5)],\n [0.006, 0.022, 0.027 / np.sqrt(5)],\n [0.007, 0.024, 0.017 / np.sqrt(5)],\n [0.005, 0.014, 0.035 / np.sqrt(5)],\n [0.006, 0.017, 0.024 / np.sqrt(5)],\n [0.006, 0.028, 0.029 / np.sqrt(5)],\n [0.005, 0.020, 0.021 / np.sqrt(5)],\n ]),\n}\n\n\ndata_comparison_Cancer = {\n 'methods': [r'Prime-2.1', r'NetMHCpan', r'MHCnuggets', r'MHCflurry', r'DeepNeo',\n r'BigMHC-EL', r'BigMHC-IM', r'BigMHC$_\\text{retrained}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'NeoaPred',\n r'ImmunoStruct (ours)'],\n 'colors': ['#CFCECE', '#F4EEAC', '#FBDFE2', '#D9B9D4', '#DAA87C', '#DDF3DE', '#AADCA9', '#8BCF8B', \"#92E3F9\", '#3775BA'],\n 'metrics': ['AUROC', 'AUPRC', 'Mean PPVn'],\n 'mean': np.array([\n [0.645, 0.259, 0.295],\n [0.559, 0.210, 0.211],\n [0.530, 0.210, 0.175],\n [0.658, 0.304, 0.329],\n [0.535, 0.261, 0.222],\n [0.632, 0.248, 0.245],\n [0.771, 0.373, 0.357],\n [0.682, 0.310, 0.325],\n [0.556, 0.267, 0.292],\n [0.771, 0.433, 0.364],\n ]),\n 'std': np.array([\n [0.026, 0.023, 0.029 / np.sqrt(5)],\n [0.049, 0.037, 0.045 / np.sqrt(5)],\n [0.014, 0.017, 0.023 / np.sqrt(5)],\n [0.015, 0.032, 0.030 / np.sqrt(5)],\n [0.016, 0.053, 0.070 / np.sqrt(5)],\n [0.034, 0.039, 0.042 / np.sqrt(5)],\n [0.040, 0.062, 0.036 / np.sqrt(5)],\n [0.012, 0.020, 0.030 / np.sqrt(5)],\n [0.016, 0.022, 0.081 / np.sqrt(5)],\n [0.024, 0.069, 0.127 / np.sqrt(5)],\n ]),\n}\n\ndata_ablation_Cancer = {\n 'coeffs': [[False, 0], [False, 0.001], [False, 0.01], [False, 0.1],\n [True, 0], [True, 0.001], [True, 0.01], [True, 0.1]],\n 'metrics': ['AUROC', 'AUPRC', 'Mean PPVn'],\n 'mean': np.array([\n [0.723, 0.391, 0.358],\n [0.712, 0.370, 0.331],\n [0.727, 0.405, 0.387],\n [0.700, 0.413, 0.354],\n [0.756, 0.426, 0.362],\n [0.762, 0.418, 0.351],\n [0.771, 0.433, 0.365],\n [0.725, 0.406, 0.348],\n ]),\n 'std': np.array([\n [0.027, 0.068, 0.096 / np.sqrt(5)],\n [0.040, 0.077, 0.114 / np.sqrt(5)],\n [0.037, 0.080, 0.111 / np.sqrt(5)],\n [0.036, 0.081, 0.109 / np.sqrt(5)],\n [0.031, 0.068, 0.103 / np.sqrt(5)],\n [0.024, 0.064, 0.151 / np.sqrt(5)],\n [0.024, 0.069, 0.127 / np.sqrt(5)],\n [0.035, 0.079, 0.129 / np.sqrt(5)],\n ]),\n}\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4459,"content_sha256":"837be4150b7419085caa84746734099ccc78e0f3fb4242e91b81ce1bde6009ca"},{"filename":"assets/figures4papers/figure_ophthal_review/plot_composition.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nimport seaborn as sns\n\n\nDATA = {\n 'clinical_stage': [\n 'Benchmark\\nEvaluation', 'Expert\\nEvaluation', 'Retrospective\\nClinical Validation',\n 'Prospective\\nPilot Study', 'Full\\nClinical Trial',\n ],\n 'pub_by_category':\n {\n 'Clinical Workflow': {\n 'Screening or Diagnosis': [10, 7, 10, 3, 0],\n 'Report Generation': [2, 3, 3, 0, 0],\n 'Treatment Planning or\\nRecommendation': [2, 3, 3, 2, 0],\n },\n 'Patient Support': {\n 'Patient Question Answering': [5, 13, 1, 1, 0],\n 'After Visit or Discharge\\nSummary Generation': [0, 2, 0, 0, 0],\n 'Consultation or Interview': [0, 1, 1, 0, 0],\n 'Patient Education\\nMaterial Generation': [1, 6, 0, 0, 0],\n 'Physician Recommendation': [0, 1, 0, 0, 0],\n },\n 'Education and Training': {\n 'Exam Taking': [19, 5, 0, 0, 0],\n 'Medical Education and\\nLearning Support': [3, 5, 0, 0, 0],\n }\n }\n}\n\ndef plot_heatmap(fig_name: str):\n plt.rcParams['text.usetex'] = True\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 16\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 2\n\n os.makedirs(os.path.dirname(fig_name), exist_ok=True)\n fig = plt.figure(figsize=(14, 10))\n\n ax = fig.add_subplot(1, 1, 1)\n category_count_arr, category_arr = [], []\n subcategory_arr = []\n value_arr = []\n for category in DATA['pub_by_category']:\n subcategories = DATA['pub_by_category'][category].keys()\n category_count_arr.append(len(subcategories))\n category_arr.append(category)\n for subcategory in subcategories:\n value = DATA['pub_by_category'][category][subcategory]\n subcategory_arr.append(subcategory)\n value_arr.append(value)\n value_arr = np.stack(value_arr, axis=0)\n for loc, item in enumerate(subcategory_arr):\n item += '\\n' + rf'($n={value_arr[loc, :].sum()}$)'\n subcategory_arr[loc] = item\n stage_arr = DATA['clinical_stage']\n for loc, item in enumerate(stage_arr):\n item += '\\n' + rf'($n={value_arr[:, loc].sum()}$)'\n stage_arr[loc] = item\n\n hm = sns.heatmap(value_arr, annot=True, vmin=0, vmax=20, fmt='d', cmap='Reds',\n linewidths=1, linecolor='white', ax=ax, cbar=True)\n cbar = hm.collections[0].colorbar\n cbar.set_ticks([0, 5, 10, 15, 20])\n cbar.set_ticklabels([0, 5, 10, 15, 20])\n ax.set_yticks(np.arange(len(subcategory_arr)) + 0.5)\n ax.set_yticklabels(subcategory_arr, rotation=0)\n ax.set_xticks(np.arange(len(stage_arr)) + 0.5)\n ax.set_xticklabels(stage_arr, rotation=0)\n\n fig.tight_layout(pad=2)\n fig.savefig(fig_name, dpi=300)\n return\n\n\nif __name__ == '__main__':\n plot_heatmap('./figures/composition_heatmap.png')","content_type":"text/x-python; charset=utf-8","language":"python","size":3032,"content_sha256":"8fb5469cc22f13a68a71ab462abd876856b30c93d1f616fe1bd3569da5aee427"},{"filename":"assets/figures4papers/figure_ophthal_review/plot_trend.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom datetime import datetime\nfrom dateutil.relativedelta import relativedelta\n\n\nDATA = {\n 'names': ['Methodological Contribution (Text-only)', 'Evaluation / Application (Text-only)',\n 'Methodological Contribution (Multimodal)', 'Evaluation / Application (Multimodal)'],\n 'pub_by_month': np.array(\n [[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],\n [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],\n [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],\n [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]]\n ),\n # NOTE: Use `*` to move the label up in the annotation. Each `*` moves it up a bit.\n 'dates_llm': {\n '2022-11': 'ChatGPT\\n(GPT-3.5)',\n '2023-02': 'Bard',\n '2023-02': 'LlaMA 1',\n '2023-03': 'GPT-4*',\n '2025-08': 'GPT-5',\n '2023-07': 'LlaMA 2',\n '2024-04': 'LlaMA 3',\n '2025-04': 'LlaMA 4',\n '2023-12': 'Gemini 1.0',\n '2024-02': 'Gemini 1.5',\n '2024-12': 'Gemini 2.0',\n '2025-06': 'Gemini 2.5*',\n },\n 'dates_vlm': {\n '2023-02': 'BLIP-2',\n '2023-07': 'LlaVA 1.0',\n '2023-9': 'GPT-4v',\n '2023-10': 'LlaVA 1.5*',\n '2023-12': 'Gemini 1.0',\n '2024-02': 'Gemini 1.5',\n '2024-12': 'Gemini 2.0',\n '2025-06': 'Gemini 2.5',\n }\n}\n\ndef month_year_list(start_year, start_month, n_months):\n start = datetime(start_year, start_month, 1)\n out = []\n for i in range(n_months):\n d = start + relativedelta(months=i)\n out.append(d.strftime('%Y-%m'))\n return out\n\ndef mark_events(ax, time_arr, y_curve, events, dy=0.1):\n x_idx = {t: i for i, t in enumerate(time_arr)}\n y0, y1 = ax.get_ylim()\n prev_date = None\n for date, label in events.items():\n if prev_date is None:\n prev_date = date\n if date in x_idx:\n i = x_idx[date]\n x, y = i, y_curve[i]\n ax.annotate(\n label.replace('*', ''),\n xy=(x, y),\n xytext=(x, y + (1 + 0.8 * np.uint8(label.count('*'))) * dy * (y1 - y0)),\n ha='center',\n va='bottom',\n fontsize=11,\n arrowprops=dict(arrowstyle='-|>', lw=1.3, color='black',\n shrinkA=0, shrinkB=0, mutation_scale=15)\n )\n return\n\ndef plot_curve(fig_name: str):\n plt.rcParams['text.usetex'] = True\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 15\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 2\n colors = [\"#9BC8FA\", \"#ffa8a6\", \"#13457E\", \"#850c0a\"]\n\n os.makedirs(os.path.dirname(fig_name), exist_ok=True)\n fig = plt.figure(figsize=(14, 8))\n num_months = DATA['pub_by_month'].shape[1]\n\n ax = fig.add_subplot(2, 1, 1)\n time_arr = month_year_list(start_year=2022, start_month=11, n_months=num_months)\n ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][1, :]), np.cumsum(DATA['pub_by_month'][1, :]), color=colors[1],\n label=DATA['names'][1])\n ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][0, :]), np.cumsum(DATA['pub_by_month'][0, :]), color=colors[0],\n label=DATA['names'][0])\n ax.plot(time_arr, np.cumsum(DATA['pub_by_month'][0, :]), lw=3, c=colors[2])\n ax.plot(time_arr, np.cumsum(DATA['pub_by_month'][1, :]), lw=3, c=colors[3])\n mark_events(ax, time_arr, np.cumsum(DATA['pub_by_month'][1, :]), DATA['dates_llm'])\n ax.legend(frameon=False)\n ax.set_xticks(time_arr[2::6])\n ax.set_ylim([0, 105])\n ax.set_ylabel('Cumulative\\nPublication Count\\n(Text-only)')\n\n ax = fig.add_subplot(2, 1, 2)\n time_arr = month_year_list(start_year=2022, start_month=11, n_months=num_months)\n ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][3, :]), np.cumsum(DATA['pub_by_month'][3, :]), color=colors[1],\n hatch='\\\\\\\\\\\\', edgecolor='black', label=DATA['names'][3])\n ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][3, :]), np.cumsum(DATA['pub_by_month'][3, :]), color=colors[1],\n facecolor='none', edgecolor='white', linewidth=2) # To visually \"erase\" the border.\n ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][2, :]), np.cumsum(DATA['pub_by_month'][2, :]), color=colors[0],\n hatch='///', edgecolor='black', label=DATA['names'][2])\n ax.fill_between(time_arr, np.zeros_like(DATA['pub_by_month'][2, :]), np.cumsum(DATA['pub_by_month'][2, :]), color=colors[0],\n facecolor='none', edgecolor='white', linewidth=2) # To visually \"erase\" the border.\n ax.plot(time_arr, np.cumsum(DATA['pub_by_month'][2, :]), lw=3, c=colors[2])\n ax.plot(time_arr, np.cumsum(DATA['pub_by_month'][3, :]), lw=3, c=colors[3])\n ax.legend(frameon=False)\n mark_events(ax, time_arr, np.cumsum(DATA['pub_by_month'][3, :]), DATA['dates_vlm'])\n ax.set_xticks(time_arr[2::6])\n ax.set_ylim([0, 24])\n ax.set_ylabel('Cumulative\\nPublication Count\\n(Multimodal)')\n\n fig.tight_layout(pad=2)\n fig.savefig(fig_name, dpi=300)\n return\n\n\nif __name__ == '__main__':\n plot_curve('./figures/trend_by_month.png')","content_type":"text/x-python; charset=utf-8","language":"python","size":5531,"content_sha256":"0a66624628f6e1da4f2da697e86ac79d46576d4bd552f5d585b2ceff05204811"},{"filename":"assets/figures4papers/figure_RNAGenScape/plot_comparison.py","content":"import os\nimport numpy as np\nimport matplotlib as mpl\nimport matplotlib.pyplot as plt\n\n\nsummary_label = r'\\textit{Improvement}'\noptions = ['VAE', 'DDPM', 'LDM', 'FM',\n 'DiffAb', 'IgLM', 'NOS-C', 'NOS-D',\n 'OAE + gradient ascent',\n 'OAE + MCMC',\n 'OAE + hill climbing',\n 'OAE + stochastic hill climbing',\n r'$\\texttt{RNAGenScape}$ \\textbf{(ours)}',\n summary_label]\ncolors = [\"#cdcdcd\", \"#767676\", \"#4d4d4d\", \"#272727\",\n \"#c4ece7\", \"#ecc4c4\", \"#ecc4e7\", \"#ea84dd\",\n \"#d5e29b\", \"#bdd35c\", \"#9fbc1d\", \"#8ead03\",\n \"#0f4d92\"]\nxlabel = np.arange(len(options))\nxticks = []\nresults_inference = [0.13, 0.91, 0.74, 5.82, 41.04, 157.57, 0.99, 0.96,\n 0.50, 10.93, 81.52, 99.66, 0.57]\nresults_openvaccine_delta_pos = [-0.23, -0.33, -0.07, -0.34, 0.06, 0.24, 0.09, 0.18,\n -0.01, -0.11, 0.40, -0.12, 0.54]\nresults_openvaccine_pct_pos = [42.1, 33.8, 47.5, 32.5, 55.0, 63.1, 54.6, 58.3,\n 51.0, 45.1, 69.2, 43.5, 77.5]\nresults_openvaccine_delta_neg = [-0.23, -0.33, -0.07, -0.34, 0.11, 0.01, -2.25, -0.29,\n -0.19, -0.19, -0.29, -0.15, -2.81]\nresults_openvaccine_pct_neg = [57.9, 66.2, 52.5, 67.5, 44.0, 49.6, 90.6, 65.4,\n 61.3, 57.7, 64.2, 60.0, 97.9]\nresults_zebrafish_delta_pos = [-0.01, 0.29, -0.95, 0.32, -0.21, -0.57, 0.03, 0.31,\n -1.21, -0.20, 0.76, 0.32, 0.77]\nresults_zebrafish_pct_pos = [49.4, 58.2, 22.5, 59.8, 36.3, 32., 51.1, 57.9,\n 18.0, 44.3, 74.4, 60.3, 75.0]\nresults_zebrafish_delta_neg = [-0.01, 0.29, -0.95, 0.32, -0.21, -0.83, -1.07, 0.38,\n -0.33, -0.37, -0.87, -0.80, -1.29]\nresults_zebrafish_pct_neg = [50.6, 41.8, 77.5, 40.2, 60.8, 74.3, 80.8, 40.2,\n 66.5, 60.1, 75.2, 73.7, 85.1]\nresults_ribosome_delta_pos = [-0.24, -0.11, -0.24, -0.10, -0.05, 0.63, 0.08, -0.09,\n 0.43, -0.19, 0.53, 0.19, 0.63]\nresults_ribosome_pct_pos = [41.7, 45.9, 41.8, 46.4, 45.0, 80.5, 53.6, 46.5,\n 73.6, 43.0, 83.0, 60.4, 81.4]\nresults_ribosome_delta_neg = [-0.24, -0.11, -0.24, -0.10, -0.04, -0.51, 0.10, -0.10,\n -0.05, -0.10, -0.06, -0.05, -0.58]\nresults_ribosome_pct_neg = [58.3, 54.1, 58.2, 53.6, 54.2, 65.5, 46.0, 53.6,\n 55.7, 54.2, 56.1, 55.2, 67.8]\n\n\nif __name__ == '__main__':\n PLOT_DE_NOVO_SPEED = False\n\n plt.rcParams['text.usetex'] = True\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 16\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 2\n\n fig = plt.figure(figsize=(9, 5))\n ax = fig.add_subplot(1, 1, 1)\n\n if PLOT_DE_NOVO_SPEED:\n ax.bar(np.arange(len(options)), 1 / np.array(results_inference), color=colors, label=options)\n ax.hlines(y=1 / results_inference[-1], xmin=-1, xmax=len(options), color=colors[-1], linestyle='--')\n ax.set_xlim([-1, len(options)])\n ax.legend(loc='upper right', frameon=False, fontsize=12)\n ax.set_xticks([])\n ax.set_ylabel('Inference Throughput ' + r'$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

+'\\n(samples / ms)', fontsize=18)\n\n # Add braces.\n ymin, ymax = ax.get_ylim()\n ax.set_ylim(ymin - 1, ymax)\n len_de_novo = 4\n mid_de_novo = 0 + (len_de_novo - 0) / 2\n start_opt = len_de_novo + 1\n end_opt = len(options) - 1\n mid_opt = (start_opt + end_opt) / 2\n ax.text(mid_de_novo, ymin - 0.3,\n r'$\\underbrace{\\rule{5cm}{0pt}}_{\\textrm{\\textit{de\\ novo}\\ generative\\ models}}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n ha='center', va='top', fontsize=14)\n ax.text(mid_opt, ymin - 0.3,\n r'$\\underbrace{\\rule{9.2cm}{0pt}}_{\\textrm{property\\ optimization\\ methods}}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n ha='center', va='top', fontsize=14)\n ax.spines['bottom'].set_position(('data', 0))\n ax.xaxis.set_ticks_position('bottom')\n ax.spines['bottom'].set_linewidth(2)\n ax.spines['left'].set_bounds(0, ymax)\n\n else:\n # speed_values = 1 / np.array(results_inference[4:])\n # options_speed = options[4:-1]\n # ax.bar(np.arange(len(options_speed)), speed_values, color=colors[4:], label=options_speed)\n speed_values = 1 / np.array(results_inference[4:8] + [results_inference[-1]])\n options_speed = options[4:8] + [options[-2]]\n ax.bar(np.arange(len(options_speed)), speed_values, color=colors[4:8] + [colors[-1]], label=options_speed)\n ax.set_xlim([-1, len(options_speed)])\n ax.legend(loc='upper left', frameon=False, fontsize=16, ncol=1)\n ax.set_xticks([])\n ax.set_ylabel('Inference Throughput ' + r'$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

+'\\n(samples / ms)', fontsize=18)\n ax.set_yscale('log')\n ymin, ymax = ax.get_ylim()\n ax.set_ylim(ymin, ymax * 20)\n\n # Add numbers on top of bars\n for i, val in enumerate(speed_values):\n ax.text(i, val * 1.1, f'{val:.3f}', ha='center', va='bottom', fontsize=16)\n\n fig.tight_layout(pad=1)\n os.makedirs('./figures', exist_ok=True)\n fig.savefig('./figures/results_comparison_speed.png', dpi=300)\n plt.close(fig)\n\n\n fig = plt.figure(figsize=(20, 9))\n ax = fig.add_subplot(1, 2, 1)\n improvements = np.stack([results_openvaccine_delta_pos, results_openvaccine_delta_neg,\n results_zebrafish_delta_pos, results_zebrafish_delta_neg,\n results_ribosome_delta_pos, results_ribosome_delta_neg], axis=1)\n base_improvements = improvements[:-1]\n best_improvements = []\n for j in range(base_improvements.shape[1]):\n col = base_improvements[:, j]\n best_improvements.append(col.max() if j % 2 == 0 else col.min())\n best_improvements = np.array(best_improvements)\n denom = np.where(best_improvements == 0, np.nan, best_improvements)\n improvement_over_best = 100 * (improvements[-1] - best_improvements) / denom\n improvements = np.vstack([improvements, improvement_over_best])\n improvements_display = improvements.copy()\n improvements_display[-1, :] = np.nan\n n_rows, n_cols = improvements.shape\n vmin, vmax = improvements[:-1].min(0), improvements[:-1].max(0)\n cmap_red, cmap_blue = plt.cm.Reds, plt.cm.Blues_r\n for j in range(n_cols):\n cmap = cmap_red if j % 2 == 0 else cmap_blue\n cmap = cmap.copy()\n cmap.set_bad(color=\"white\")\n norm = mpl.colors.Normalize(vmin=vmin[j] if j % 2 == 1 else 0, vmax=vmax[j] if j % 2 == 0 else 0)\n ax.imshow(improvements_display[:, j:j+1], cmap=cmap, norm=norm,\n aspect=\"auto\", extent=[j - 0.5, j + 0.5, 0, n_rows], origin=\"lower\")\n for (i, j), val in np.ndenumerate(improvements):\n if i == n_rows - 1:\n color = \"forestgreen\" if val >= 0 else \"darkred\"\n label = f\"{val:+.1f} \\\\%\"\n fontsize = 16\n else:\n cmap = cmap_red if j % 2 == 0 else cmap_blue\n norm = mpl.colors.Normalize(vmin=vmin[j] if j % 2 == 1 else 0, vmax=vmax[j] if j % 2 == 0 else 0)\n r, g, b, _ = cmap(norm(val))\n lum = 0.299 * r + 0.587 * g + 0.114 * b\n color = \"white\" if lum \u003c 0.5 else \"black\"\n label = f\"{val:.2f}\"\n fontsize = 14\n ax.text(j, i + 0.5, label, ha=\"center\", va=\"center\", fontsize=fontsize, color=color)\n ax.set_title('Median change in property', fontsize=32, pad=24)\n ax.set_xlim(-0.5, n_cols - 0.5)\n ax.set_xticks(np.arange(n_cols))\n ax.set_xticklabels([r'$\\texttt{OpenVaccine}~(+)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'$\\texttt{OpenVaccine}~(-)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'$\\texttt{Zebrafish}~(+)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'$\\texttt{Zebrafish}~(-)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'$\\texttt{Ribosome}~(+)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'$\\texttt{Ribosome}~(-)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

],\n fontsize=20, rotation=30)\n ax.tick_params(axis='x', which='both', bottom=False, top=False, length=0)\n ax.set_yticks(np.arange(n_rows) + 0.5)\n ax.set_yticklabels(options, rotation=0, fontsize=20, ha='right')\n for tick in ax.get_yticklabels():\n if r'$\\texttt{RNAGenScape}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

in tick.get_text():\n tick.set_fontsize(24)\n ax.set_frame_on(False)\n ax.invert_yaxis()\n # n_rows, n_cols = improvements.shape\n # rect = patches.Rectangle((-0.5, n_rows - 1), n_cols, 1, fill=False, edgecolor='black', linewidth=3)\n # rect.set_clip_on(False)\n # ax.add_patch(rect)\n\n ax = fig.add_subplot(1, 2, 2)\n percentages = np.stack([results_openvaccine_pct_pos, results_openvaccine_pct_neg,\n results_zebrafish_pct_pos, results_zebrafish_pct_neg,\n results_ribosome_pct_pos, results_ribosome_pct_neg], axis=1)\n base_percentages = percentages[:-1]\n best_percentages = base_percentages.max(0)\n pct_improvement_over_best = 100 * (percentages[-1] - best_percentages) / best_percentages\n percentages = np.vstack([percentages, pct_improvement_over_best])\n percentages_display = percentages.copy()\n percentages_display[-1, :] = np.nan\n n_rows, n_cols = percentages.shape\n vmin, vmax = percentages[:-1].min(0), percentages[:-1].max(0)\n cmap_red = plt.cm.Reds.copy()\n cmap_red.set_bad(color=\"white\")\n for j in range(n_cols):\n norm = mpl.colors.Normalize(vmin=max(50, vmin[j]), vmax=vmax[j])\n ax.imshow(percentages_display[:, j:j+1], cmap=cmap_red, norm=norm,\n aspect=\"auto\", extent=[j - 0.5, j + 0.5, 0, n_rows], origin=\"lower\")\n for (i, j), val in np.ndenumerate(percentages):\n if i == n_rows - 1:\n color = \"forestgreen\" if val >= 0 else \"darkred\"\n label = f\"{val:+.1f} \\\\%\"\n fontsize = 16\n else:\n norm = mpl.colors.Normalize(vmin=max(50, vmin[j]), vmax=vmax[j])\n r, g, b, _ = cmap_red(norm(val))\n lum = 0.299 * r + 0.587 * g + 0.114 * b\n color = \"white\" if lum \u003c 0.5 else \"black\"\n label = f\"{val:.1f} \\\\%\"\n fontsize = 14\n ax.text(j, i + 0.5, label, ha=\"center\", va=\"center\",\n fontsize=fontsize, color=color)\n ax.set_title('Success rate', fontsize=32, pad=24)\n ax.set_xlim(-0.5, n_cols - 0.5)\n ax.set_xticks(np.arange(n_cols))\n ax.set_xticklabels([r'$\\texttt{OpenVaccine}~(+)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'$\\texttt{OpenVaccine}~(-)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'$\\texttt{Zebrafish}~(+)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'$\\texttt{Zebrafish}~(-)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

,\n r'$\\texttt{Ribosome}~(+)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, r'$\\texttt{Ribosome}~(-)

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

],\n fontsize=20, rotation=30)\n ax.tick_params(axis='x', which='both', bottom=False, top=False, length=0)\n ax.set_yticks([])\n # ax.set_yticks(np.arange(n_rows) + 0.5)\n # ax.set_yticklabels(options, fontsize=16, ha='right')\n ax.set_frame_on(False)\n ax.invert_yaxis()\n # rect = patches.Rectangle((-0.5, n_rows - 1), n_cols, 1,\n # fill=False, edgecolor='black', linewidth=3)\n # rect.set_clip_on(False)\n # ax.add_patch(rect)\n\n fig.tight_layout(pad=2)\n os.makedirs('./figures', exist_ok=True)\n fig.savefig('./figures/results_comparison_optimization.png', dpi=300)\n plt.close(fig)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11236,"content_sha256":"b2b30a212d084ae5ba7b82aa1fd830cbfea82a4eef507edfa1757b1734b250e3"},{"filename":"assets/figures4papers/figure_RNAGenScape/plot_hole_manifold.py","content":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom matplotlib.colors import LinearSegmentedColormap\n\ndef function(x, y):\n z = 0.6 * np.exp(-((x - 1)**2 + (y + 1)**2))\n z += 0.5 * np.exp(-((x - 1)**2 + (y - 4)**2))\n z += 0.3 * np.exp(-((x - 2)**2 + (y - 2)**2))\n z += 0.2 * np.exp(-((x + 3)**2 + (y + 1)**2))\n z += 0.3 * np.exp(-((x + 1)**2 + (y + 1)**2))\n z -= 0.1 * np.exp(-((x + 1)**2 + (y - 2)**2))\n z += 0.3 * np.exp(-((x + 2)**2 + (y - 2)**2))\n z += 0.3 * np.exp(-((x + 2)**2 + (y - 1)**2))\n return z\n\nx = np.linspace(-3, 3, 200)\ny = np.linspace(-3, 3, 200)\nX, Y = np.meshgrid(x, y)\nZ = function(X, Y)\n\npeak_i, peak_j = np.unravel_index(np.argmax(Z), Z.shape)\nx_peak, y_peak = X[peak_i, peak_j], Y[peak_i, peak_j]\n\nrng = np.random.default_rng(42)\nnum_patches = 30\nr = 0.3\nr_forbid = 0.9\npad = r + 0.1\n\ncenters = []\nattempts = 0\nwhile len(centers) \u003c num_patches and attempts \u003c 5000:\n cx = rng.uniform(x.min()+pad, x.max()-pad)\n cy = rng.uniform(y.min()+pad, y.max()-pad)\n if (cx - x_peak)**2 + (cy - y_peak)**2 \u003c r_forbid**2:\n attempts += 1\n continue\n ok = True\n for px, py in centers:\n if (cx - px)**2 + (cy - py)**2 \u003c (1.6*r)**2:\n ok = False\n break\n if ok:\n centers.append((cx, cy))\n attempts += 1\n\nmask = np.zeros_like(Z, dtype=bool)\nfor cx, cy in centers:\n mask |= (X - cx)**2 + (Y - cy)**2 \u003c= r**2\n\ncmap = LinearSegmentedColormap.from_list(\n \"softgreen\", [\"#e9f5ec\", \"#d9f0e1\", \"#c9e5d3\", \"#a9cbb8\", \"#7f9e8a\", \"#4f5c4f\"], N=256\n)\n\nnorm = plt.Normalize(Z.min(), Z.max())\nfacecolors = cmap(norm(Z))\nfacecolors_with_gray = facecolors.copy()\nfacecolors_with_gray[mask] = [0.7, 0.7, 0.7, 0.5]\n\nfig = plt.figure(figsize=(14, 6))\nfor i, (fc, title) in enumerate([(facecolors, \"Smooth Manifold\"),\n (facecolors_with_gray, \"Manifold with Gray Patches\")], 1):\n ax = fig.add_subplot(1, 2, i, projection=\"3d\")\n ax.plot_surface(\n X, Y, Z,\n facecolors=fc,\n rstride=4, cstride=4,\n linewidth=0.05, edgecolor=\"k\",\n antialiased=True, shade=False, alpha=0.95\n )\n ax.set_title(title, fontsize=14)\n ax.set_xticks([]); ax.set_yticks([]); ax.set_zticks([])\n for a in (ax.xaxis, ax.yaxis, ax.zaxis):\n a.pane.set_visible(False)\n a.line.set_color((0,0,0,0))\n ax.set_box_aspect([1, 1, 0.5])\n ax.view_init(elev=20, azim=50)\n\nfig.tight_layout(pad=2)\nos.makedirs(\"./figures\", exist_ok=True)\nfig.savefig(\"./figures/manifold_holes.png\", dpi=300)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2547,"content_sha256":"cc31a470f0364c19f094c1ef32ada4909fb53dab638f56a552fe55d44942ded1"},{"filename":"assets/figures4papers/figure_RNAGenScape/plot_manifold.py","content":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n\ndef function(x, y):\n z = 0.6 * np.exp(-((x - 1)**2 + (y + 1)**2))\n z += 0.5 * np.exp(-((x - 1)**2 + (y - 4)**2))\n z += 0.3 * np.exp(-((x - 2)**2 + (y - 2)**2))\n z += 0.2 * np.exp(-((x + 3)**2 + (y + 1)**2))\n z += 0.3 * np.exp(-((x + 1)**2 + (y + 1)**2))\n z -= 0.1 * np.exp(-((x + 1)**2 + (y - 2)**2))\n z += 0.3 * np.exp(-((x + 2)**2 + (y - 2)**2))\n z += 0.3 * np.exp(-((x + 2)**2 + (y - 1)**2))\n return z\n\nif __name__ == '__main__':\n # Generate coordinates\n x = np.linspace(-3, 3, 200)\n y = np.linspace(-3, 3, 200)\n x, y = np.meshgrid(x, y)\n\n # Define a multi-well \"energy\" function (inverted to form valleys)\n z = function(x, y)\n\n # Set up plot\n fig = plt.figure(figsize=(10, 7))\n ax = fig.add_subplot(1, 1, 1, projection='3d')\n\n # Plot the surface with smooth shading\n ax.plot_surface(\n x, y, z,\n cmap='coolwarm',\n edgecolor='none',\n linewidth=0,\n antialiased=True,\n alpha=0.95,\n )\n\n # # Plot descent path\n # path_x = np.linspace(-2.5, 1, 100)\n # path_y = np.linspace(2.5, -1, 100)\n # path_z = function(path_x, path_y)\n # ax.plot(path_x, path_y, path_z, color='red', linestyle='--', linewidth=3)\n\n # Aesthetics\n ax.set_xticks([])\n ax.set_yticks([])\n ax.set_zticks([])\n ax.xaxis.pane.set_visible(False)\n ax.yaxis.pane.set_visible(False)\n ax.zaxis.pane.set_visible(False)\n ax.xaxis.line.set_color((0.0, 0.0, 0.0, 0.0))\n ax.yaxis.line.set_color((0.0, 0.0, 0.0, 0.0))\n ax.zaxis.line.set_color((0.0, 0.0, 0.0, 0.0))\n ax.set_box_aspect([1, 1, 0.5])\n ax.view_init(elev=20, azim=50)\n\n fig.tight_layout(pad=2)\n os.makedirs('./figures', exist_ok=True)\n fig.savefig('./figures/manifold.png')\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1811,"content_sha256":"aa6461c85b1d03b30f7e12824821c1149f1a4beb56394a20304126a592cf8021"},{"filename":"assets/figures4papers/figure_RNAGenScape/plot_sweep.py","content":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom matplotlib.ticker import MaxNLocator\n\n\n# results_increase = {\n# r'Median property change': [0.1125, 0.44046, 0.46006, 0.45625, 0.46510],\n# r'Percentage improved $\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [55.15, 70.11, 71.02, 71.46, 71.52],\n# r'Latent space distance $\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [0.11868, 0.11877, 0.11961, 0.11964, 0.11999],\n# }\n# results_decrease = {\n# r'Median property change': [-1.1246, -1.5761, -1.62873, -1.65457, -1.65872],\n# r'Percentage improved $\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [79.39, 86.31, 87.08, 87.24, 87.01],\n# r'Latent space distance $\\downarrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [0.16017, 0.12758, 0.12141, 0.12024, 0.12015],\n# }\nresults_increase = {\n r'Median change in property': [0.292, 0.5047563, 0.57401, 0.55921, 0.5471513271],\n r'Success rate $\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [67.6, 77.3, 79.9, 79.4, 79.6],\n}\nresults_decrease = {\n r'Median change in property': [-0.90106, -1.27954, -1.3083, -1.2785, -1.29],\n r'Success rate $\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [75.9, 84.7, 84.9, 84.7, 85.1],\n}\n\n\nif __name__ == '__main__':\n plt.rcParams['text.usetex'] = True\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 15\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 2\n\n x_label = 'Optimization step'\n x_values = [1, 5, 10, 20, 40]\n\n keys = ['Median change in property', 'Success rate']\n fig = plt.figure(figsize=(4.5 * len(keys), 4))\n for fig_idx, y_key in enumerate(keys):\n ax = fig.add_subplot(1, len(keys), fig_idx + 1)\n for key in results_increase.keys():\n if y_key in key:\n y_values_increase = results_increase[key]\n y_label_increase = key\n for key in results_decrease.keys():\n if y_key in key:\n y_values_decrease = results_decrease[key]\n y_label_decrease = key\n\n #0F4D92\n ax.plot(x_values,\n y_values_increase,\n linestyle='-', linewidth=3,\n marker='o', markersize=8,\n label='increase property',\n alpha=0.8,\n color=\"#ea84dd\")\n ax.plot(x_values,\n y_values_decrease,\n linestyle='-', linewidth=3,\n marker='o', markersize=8,\n label='decrease property',\n alpha=0.8,\n color=\"#0f4d92\")\n if fig_idx == 1:\n ax.set_ylim([0, 100])\n ax.set_xticks(x_values)\n ax.set_xlabel(x_label, fontsize=16)\n ax.set_ylabel(y_label_increase, fontsize=16)\n ax.yaxis.set_major_locator(MaxNLocator(nbins=5))\n if fig_idx == len(keys) - 1:\n ax.legend(loc='lower right', frameon=False)\n\n fig.tight_layout(pad=1)\n os.makedirs('./figures', exist_ok=True)\n fig.savefig('./figures/results_sweep.png', dpi=300)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2873,"content_sha256":"123a14749450ef5895161b6823e7b8d217dbee181f39da954f5a8b2439be2961"},{"filename":"assets/figures4papers/figure_VIGIL/plot_ablation.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom matplotlib import gridspec as gridspec\n\n\ndata_ablation = {\n 'methods': [\n 'DPO',\n 'DA-DPO',\n 'VIGIL (Ours)',\n ],\n 'colors': [\n \"#D88F8A\",\n \"#8BCF8B\",\n \"#0F4D92\"\n ],\n '$\\beta

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [0.05, 0.1, 0.2, 0.5],\n '$\\lambda

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [0.1, 0.5, 1.0, 2.0],\n 'data_fraction': [0.1, 0.25, 0.5, 1.0],\n 'results': {\n 'SFT': np.array([82.1]), # POPE_Adv\n # This is 3 methods on 4 beta values.\n '$\\beta

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([\n [79.5, 82.8, 81.0, 76.2],\n [83.2, 84.2, 83.5, 81.0],\n [86.8, 86.9, 86.5, 85.8],\n ]),\n # This is our method.\n '$\\lambda

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([\n [84.5, 86.1, 86.9, 87.2], # POPE_Adv\n [49.8, 49.6, 49.5, 48.8], # MathVista\n ]),\n 'data_fraction': np.array([\n [82.78, 83.19, 85.05, 85.67],\n [84.04, 85.46, 87.07, 87.81],\n [86.69, 88.18, 89.17, 89.82],\n ])\n }\n}\n\n\ndef plot_curves(data_ablation):\n \"\"\"\n Left panel: ablation on hyperparameter beta (x = beta values, y = score, 3 curves for 3 methods).\n Right panel: blank.\n \"\"\"\n methods = data_ablation['methods']\n colors = data_ablation['colors']\n\n fig, axes = plt.subplots(1, 3, figsize=(27, 6), gridspec_kw={'width_ratios': [1.1, 1, 1]})\n\n ax = axes[0]\n y_ticks = [78, 82, 86, 90]\n data_fraction_key = 'data_fraction'\n data_fraction_vals = np.asarray(data_ablation[data_fraction_key])\n results_data_fraction = data_ablation['results'][data_fraction_key] # shape (n_methods, n_data_fraction)\n x_pos = np.arange(len(data_fraction_vals))\n y_sft = data_ablation['results']['SFT']\n # SFT only horizontal line.\n ax.plot([x_pos[0] - 1, x_pos[-1] + 0.2], [y_sft, y_sft], color='black', alpha=0.3, linewidth=4, linestyle='--', label='SFT only')\n # Ours with 25% data horizontal line.\n ax.plot([x_pos[0] - 0.1, x_pos[-1] + 0.1], [results_data_fraction[-1][1], results_data_fraction[-1][1]], color=colors[-1], linewidth=3, linestyle=':')\n for m, (method, color) in enumerate(zip(methods, colors)):\n y = results_data_fraction[m]\n ax.plot(x_pos, y, color=color, linewidth=3, marker='o', markersize=10, label=method)\n\n ax.set_xlabel(r'Fraction of data used for post-training', fontsize=28, labelpad=18)\n ax.set_xlim(x_pos[0] - 0.5, x_pos[-1] + 0.4)\n ax.set_xticks(x_pos)\n ax.set_xticklabels([f'{item:.0%}' for item in data_fraction_vals])\n ax.set_ylabel('$POPE_{Adv}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

+ r'$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, fontsize=28, fontfamily='monospace', labelpad=12)\n ax.set_yticks(y_ticks)\n ax.set_yticklabels(y_ticks)\n ax.tick_params(labelsize=24, length=8, width=1.5)\n ax.legend(fontsize=24, loc='lower center', ncols=2, frameon=False)\n\n ax = axes[1]\n y_ticks = [75, 79, 84, 88]\n beta_key = '$\\beta

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

\n beta_vals = np.asarray(data_ablation[beta_key])\n results_beta = data_ablation['results'][beta_key] # shape (n_methods, n_beta)\n x_pos = np.arange(len(beta_vals))\n for m, (method, color) in enumerate(zip(methods, colors)):\n y = results_beta[m]\n ax.plot(x_pos, y, color=color, linewidth=3, marker='o', markersize=10, label=method)\n\n ax.set_xlabel(r'Hyperparameter $\\beta

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, fontsize=28, labelpad=18)\n ax.set_xticks(x_pos)\n ax.set_xticklabels([str(b) for b in beta_vals])\n ax.set_ylabel('$POPE_{Adv}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

+ r'$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, fontsize=28, fontfamily='monospace', labelpad=12)\n ax.set_yticks(y_ticks)\n ax.set_yticklabels(y_ticks)\n ax.tick_params(labelsize=24, length=8, width=1.5)\n ax.legend(fontsize=24, loc='lower center', frameon=False)\n\n ax = axes[2]\n y_ticks = [84, 85, 86, 87, 88]\n lambda_key = '$\\lambda

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

\n lambda_vals = np.asarray(data_ablation[lambda_key])\n results_lambda = data_ablation['results'][lambda_key] # shape (2, n_beta)\n assert len(results_lambda) == 2\n x_pos = np.arange(len(lambda_vals))\n y = results_lambda[0]\n ax.plot(x_pos, y, color=colors[-1], linewidth=3, marker='o', markersize=10, label=methods[-1], alpha=0.4)\n ax.set_xlabel(r'Hyperparameter $\\lambda

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, fontsize=28, labelpad=18)\n ax.set_xticks(x_pos)\n ax.set_xlim(x_pos[0] - 0.5, x_pos[-1] + 0.5)\n ax.set_xticklabels([str(b) for b in lambda_vals])\n ax.set_yticks(y_ticks)\n ax.set_yticklabels(y_ticks)\n ax.set_ylabel('$POPE_{Adv}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

+ r'$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, fontsize=28, fontfamily='monospace', labelpad=12, color=colors[-1], alpha=0.4)\n ax.tick_params(labelsize=24, length=8, width=1.5)\n ax.legend(fontsize=24, loc='lower center', frameon=False)\n\n ax2 = ax.twinx()\n ax2.spines['right'].set_visible(True)\n y = results_lambda[1]\n ax2.set_ylabel('$MathVista

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

+ r'$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, fontsize=28, fontfamily='monospace', labelpad=36, rotation=270, color=colors[-1], alpha=1.0)\n ax2.plot(x_pos, y, color=colors[-1], linewidth=3, marker='o', markersize=10, label=methods[-1], alpha=1.0)\n ax2.plot(x_pos, y, color=colors[-1], linewidth=3, marker='o', markersize=10, label=methods[-1], alpha=1.0)\n ax2.set_yticks([48, 48.5, 49, 49.5, 50])\n ax2.set_yticklabels([48, 48.5, 49, 49.5, 50])\n ax2.tick_params(labelsize=24, length=8, width=1.5)\n ax2.legend(fontsize=24, loc='lower center', frameon=False)\n\n # Right panel: at each x, print average of the two metrics at the top\n y_top = ax.get_ylim()[1] - 0.2\n for i in range(len(x_pos)):\n avg = (results_lambda[0][i] + results_lambda[1][i]) / 2\n ax.text(x_pos[i], y_top, f'Mean = {avg:.1f}', ha='center', va='bottom', fontsize=18)\n\n fig.tight_layout(pad=0.5)\n fig.subplots_adjust(wspace=0.3)\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/ablation_curves.png', dpi=300)\n plt.close(fig)\n return\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n plot_curves(data_ablation)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6038,"content_sha256":"b3c28343cfbb2f16e3e9c60735269d68098bcc7d16d5402f362354466fa272d7"},{"filename":"assets/figures4papers/figure_VIGIL/plot_comparison_radar.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\n\n\ndata_comparison = {\n 'methods': [\n r'DPO',\n r'DA-DPO',\n r'VIGIL (Ours)',\n ],\n 'colors': [\n \"#D88F8A\",\n \"#8BCF8B\",\n \"#0F4D92\"\n ],\n 'results': {\n 'Qwen2.5-VL-7B\\nPOPE$_{Adv}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([82.8, 84.2, 86.9]),\n 'LLaVA-OneVision-7B\\nPOPE$_{Adv}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([82.8, 84.2, 86.9]),\n 'InternVL2.5-26B\\nPOPE$_{Adv}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([85.5, 86.8, 89.4]),\n 'Qwen2.5-VL-72B\\nPOPE$_{Adv}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: np.array([84.5, 87.4, 89.8]),\n 'Qwen2.5-VL-7B\\nMathVista': np.array([48.0, 48.8, 49.5]),\n 'LLaVA-OneVision-7B\\nMathVista': np.array([50.8, 51.5, 52.8]),\n 'InternVL2.5-26B\\nMathVista': np.array([57.9, 58.8, 60.1]),\n 'Qwen2.5-VL-72B\\nMathVista': np.array([54.1, 55.4, 56.6]),\n 'Qwen2.5-VL-7B\\nMMBench': np.array([71.2, 72.0, 72.5]),\n 'LLaVA-OneVision-7B\\nMMBench': np.array([72.5, 73.0, 73.8]),\n 'InternVL2.5-26B\\nMMBench': np.array([79.5, 80.1, 81.3]),\n 'Qwen2.5-VL-72B\\nMMBench': np.array([77.2, 77.8, 78.5]),\n },\n}\n\n\ndef _task_suffix(subtask_name):\n \"\"\"Benchmark = part after the first newline (e.g. 'Qwen2.5-VL-7B\\\\nMathVista' -> 'MathVista').\"\"\"\n return subtask_name.split('\\n', 1)[-1] if '\\n' in subtask_name else subtask_name\n\n\ndef plot_radar(data_comparison):\n \"\"\"\n Single radar chart. Each axis = one subtask; one curve per method.\n Each benchmark (task suffix after \\\\n) has its own radii list: values are normalized\n to display range 45-90 using that benchmark's min/max from its radii list, and\n radius labels on each spoke show that benchmark's tick values.\n \"\"\"\n methods = data_comparison['methods']\n colors = data_comparison['colors']\n n_methods = len(methods)\n benchmark_radii = {\n 'POPE$_{Adv}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

: [75, 80, 85, 91],\n 'MathVista': [30, 40, 50, 61],\n 'MMBench': [40, 55, 70, 85],\n }\n\n # results is directly subtask_name -> value array\n task_dict = data_comparison['results']\n subtask_names = list(task_dict.keys())\n value_arrays = list(task_dict.values())\n\n fig = plt.figure(figsize=(12, 10))\n ax = fig.add_subplot(111, projection='polar')\n n_subtasks = len(subtask_names)\n subtask_benchmarks = [_task_suffix(st) for st in subtask_names]\n\n # Per-benchmark (r_min, r_max) from that benchmark's radii list\n def limits_for_benchmark(bench):\n radii = benchmark_radii.get(bench, [0, 100])\n return (min(radii), max(radii))\n\n # One polygon per method: normalize each spoke by its benchmark's radii range, then map to 45-90\n angles = np.linspace(2 * np.pi, 0, n_subtasks, endpoint=False)\n angles_closed = np.append(angles, angles[0])\n\n for m in range(n_methods):\n vals = np.array([v[m] for v in value_arrays], dtype=float)\n mask = np.isnan(vals)\n if np.any(mask):\n vals = vals.copy()\n fill = 0.0 if np.all(mask) else np.nanmean(vals)\n vals[mask] = fill\n # Normalize per spoke: benchmark's (r_min, r_max) -> display 45-90. One vertex per subtask (no interpolation).\n normalized = np.zeros_like(vals)\n for i, (v, bench) in enumerate(zip(vals, subtask_benchmarks)):\n r_lo, r_hi = limits_for_benchmark(bench)\n span = r_hi - r_lo\n if span \u003c= 0:\n normalized[i] = 45 + 45 * 0.5\n else:\n n = np.clip((v - r_lo) / span, 0.0, 1.0)\n normalized[i] = 45 + 45 * n\n # Closed polygon: exact data at each angle, then back to first (no extra interpolation)\n vals_closed = np.append(normalized, normalized[0])\n ax.plot(angles_closed, vals_closed, color=colors[m], linewidth=2, label=methods[m])\n ax.fill(angles_closed, vals_closed, color=colors[m], alpha=0.05)\n # Mark actual vertices so it's clear each point is a real data value\n ax.scatter(angles, normalized, color=colors[m], s=18, zorder=5, edgecolors='none')\n\n ax.set_ylim(45, 90)\n ax.set_theta_zero_location('N')\n for spine in ax.spines.values():\n spine.set_visible(False)\n r_min_disp, r_max_disp = ax.get_ylim()\n ax.grid(False)\n # Outer boundary\n ax.plot(angles_closed, np.full_like(angles_closed, r_max_disp), color='k', linewidth=0.8, zorder=4)\n # Radial spokes: one per angle\n for a in angles:\n ax.plot([a, a], [r_min_disp, r_max_disp], color='gray', linewidth=0.5, zorder=4)\n # Benchmark-specific contour polygons: one polygon per level index k (innermost=0, ...).\n max_levels = max(len(benchmark_radii.get(b, [])) for b in subtask_benchmarks)\n for k in range(max_levels):\n display_radii = np.zeros(n_subtasks)\n for i, bench in enumerate(subtask_benchmarks):\n radii_list = benchmark_radii.get(bench, [])\n if not radii_list:\n display_radii[i] = 45 + 22.5\n continue\n contour_val = radii_list[k] if k \u003c len(radii_list) else radii_list[-1]\n r_lo, r_hi = limits_for_benchmark(bench)\n span = r_hi - r_lo\n if span \u003c= 0:\n display_radii[i] = 45 + 22.5\n else:\n frac = (contour_val - r_lo) / span\n display_radii[i] = 45 + 45 * np.clip(frac, 0.0, 1.0)\n contour_closed = np.append(display_radii, display_radii[0])\n ax.plot(angles_closed, contour_closed, color='k', linewidth=0.6, zorder=4, label='_nolegend_')\n ax.set_yticks([r_max_disp])\n ax.set_yticklabels([])\n ax.set_rlabel_position(0)\n ax.set_xticks(angles)\n ax.set_xticklabels([])\n # Per-spoke radius labels: skip innermost tick to avoid clutter\n for angle, bench in zip(angles, subtask_benchmarks):\n radii_list = benchmark_radii.get(bench, [])\n if len(radii_list) \u003c= 1:\n continue\n r_lo, r_hi = limits_for_benchmark(bench)\n span = r_hi - r_lo\n for tick_val in radii_list[1:]: # skip innermost (smallest) number\n if span \u003c= 0:\n display_r = 45 + 22.5\n else:\n frac = (tick_val - r_lo) / span\n display_r = 45 + 45 * np.clip(frac, 0.0, 1.0)\n lbl = f'{tick_val:.0f}' if tick_val == int(tick_val) else f'{tick_val:.1f}'\n rot = np.degrees(angle)\n ax.text(angle, display_r + 1, lbl, fontsize=12, ha='center', va='center',\n rotation=rot, rotation_mode='anchor',\n transform=ax.transData, clip_on=False)\n # One label per subtask (angle)\n for angle, label in zip(angles, subtask_names):\n offset = 8 + 10 * np.abs(np.sin(angle))\n label_r = r_max_disp + offset\n ax.text(angle, label_r, label, fontsize=14, ha='center', va='center',\n transform=ax.transData, clip_on=False, fontfamily='monospace')\n ax.legend(loc='upper right', bbox_to_anchor=(1.40, 0.05), fontsize=15, frameon=False)\n\n fig.tight_layout(pad=2)\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/comparison_radar.png', dpi=300, bbox_inches='tight')\n plt.close(fig)\n return\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n plot_radar(data_comparison)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7397,"content_sha256":"d30da4f6a14b33eb7e3350f6f9623caf58d178c099f961270a6e1c17aaaabbee"},{"filename":"assets/figures4papers/figure_VIGIL/plot_concept.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom scipy.stats import gaussian_kde\nfrom scipy.interpolate import CubicSpline\n\n\ndef _gauss(x, mu, sig):\n y = np.exp(-0.5 * ((x - mu) / sig) ** 2)\n return y / (y.max() + 1e-12)\n\n\ndef _sample_tube(center_curve, t_samples, rng, sigma_u=0.08, sigma_v=0.18):\n pts = center_curve(t_samples)\n eps = 1e-3\n pts_f = center_curve(np.clip(t_samples + eps, 0, 1))\n tan = pts_f - pts\n tan_norm = np.linalg.norm(tan, axis=1, keepdims=True) + 1e-9\n t_hat = tan / tan_norm\n n_hat = np.stack([-t_hat[:, 1], t_hat[:, 0]], axis=1)\n u = rng.normal(0, sigma_u, size=(len(t_samples), 1))\n v = rng.normal(0, sigma_v, size=(len(t_samples), 1))\n return pts + u * n_hat + v * t_hat\n\n\ndef _mixture_t(n, centers, scales, weights, rng):\n weights = np.array(weights, dtype=float)\n weights /= weights.sum()\n comp = rng.choice(len(centers), size=n, p=weights)\n t = rng.normal(np.array(centers)[comp], np.array(scales)[comp])\n return np.clip(t, 0, 1)\n\n\ndef _kde_prepare(P, grid=280, pad_x=1.8, pad_y=1.3):\n Xv, Yv = P[:, 0], P[:, 1]\n kde = gaussian_kde(np.vstack([Xv, Yv]))\n xmin, xmax = Xv.min() - pad_x, Xv.max() + pad_x\n ymin, ymax = Yv.min() - pad_y, Yv.max() + pad_y\n xx, yy = np.mgrid[xmin:xmax:complex(grid), ymin:ymax:complex(grid)]\n zz = kde(np.vstack([xx.ravel(), yy.ravel()])).reshape(xx.shape)\n return xx, yy, zz, xmin, xmax, ymin, ymax\n\n\ndef plot_distribution(ax):\n color_prior = \"#6F6F6F\"\n color_see = \"#0F4D92\"\n color_blind = \"#D88F8A\"\n\n x = np.linspace(0.0, 1, 1000)\n p_prior = _gauss(x, 0.30, 0.07)\n p_see = _gauss(x, 0.72, 0.07)\n p_blind = 0.30 * _gauss(x, 0.30, 0.12) + 0.22\n\n y_star = 0.72\n see_star = np.interp(y_star, x, p_see)\n blind_star = np.interp(y_star, x, p_blind)\n\n ax.plot(x, p_prior, color=color_prior, linewidth=2.2, label=r\"$P(y\\mid x_t)$\")\n ax.fill_between(x, 0, p_prior, color=color_prior, alpha=0.12)\n ax.plot(x, p_blind, color=color_blind, linewidth=2.2, label=r\"$P(y\\mid x_v^{\\emptyset},x_t)$\")\n ax.fill_between(x, 0, p_blind, color=color_blind, alpha=0.12)\n ax.plot(x, p_see, color=color_see, linewidth=2.8, label=r\"$P(y\\mid x_v,x_t)$\")\n ax.fill_between(x, 0, p_see, color=color_see, alpha=0.12)\n\n ax.vlines(y_star, 0, 1.0, linewidth=2, linestyle=\":\", alpha=0.5, color=\"black\")\n ax.annotate(\n \"\",\n xy=(y_star, see_star),\n xytext=(y_star, blind_star),\n arrowprops=dict(arrowstyle=\"\u003c->\", linewidth=2),\n )\n ax.text(y_star + 0.02, 0.5 * (see_star + blind_star), \" VIG\",\n ha=\"left\", va=\"center\", fontsize=24, fontfamily='helvetica')\n\n ax.set_xticks([0, 0.5, 1])\n ax.set_xticklabels([-1, 0, 1])\n ax.set_xlim(0, 1.05)\n ax.set_yticks([0, 0.2, 0.4, 0.6, 0.8, 1.0])\n ax.set_ylim(0, 1.25)\n ax.set_xlabel(\"Answer space (arbitrary units)\", fontsize=28, labelpad=12)\n ax.set_ylabel(\"Probability\", fontsize=28, labelpad=18)\n ax.spines['left'].set_linewidth(3)\n ax.spines['bottom'].set_linewidth(3)\n ax.tick_params(width=1.5, length=8, labelsize=24)\n ax.legend(loc=\"upper center\", frameon=False, ncols=3, prop={\"family\": \"monospace\", \"size\": 24})\n\n\ndef plot_manifold(ax):\n rng = np.random.default_rng(42)\n xp = np.array([0.0, 0.18, 0.45, 0.72, 1.0])\n yt = np.array([0.15, 0.28, 0.18, 0.05, 0.10])\n ym = np.array([0.78, 0.70, 0.60, 0.68, 0.80])\n\n cs_t = CubicSpline(xp, yt, bc_type=\"natural\")\n cs_m = CubicSpline(xp, ym, bc_type=\"natural\")\n\n def curve_text(t):\n x = t\n y = cs_t(x) + 0.03 * np.sin(6 * np.pi * t)\n return np.stack([x, y], axis=1)\n\n def curve_mm(t):\n x = t\n y = cs_m(x) + 0.02 * np.sin(4 * np.pi * t + 0.6)\n return np.stack([x, y], axis=1)\n\n N = 2600\n t_text = _mixture_t(N, [0.20, 0.55, 0.82], [0.07, 0.09, 0.06], [0.35, 0.40, 0.25], rng)\n t_mm = _mixture_t(N, [0.18, 0.52, 0.80], [0.06, 0.10, 0.07], [0.30, 0.45, 0.25], rng)\n\n pts_text = _sample_tube(curve_text, t_text, rng, sigma_u=0.09, sigma_v=0.14)\n pts_mm = _sample_tube(curve_mm, t_mm, rng, sigma_u=0.08, sigma_v=0.13)\n\n A = np.array([[10.0, 0.0], [0.0, 3.5]])\n b_text = np.array([0.6, 0.9])\n b_mm = np.array([0.6, 2.4])\n\n P_t = pts_text @ A.T + b_text\n P_m = pts_mm @ A.T + b_mm\n\n tt = np.linspace(0.08, 0.95, 450)\n ridge_t = curve_text(tt) @ A.T + b_text\n ridge_m = curve_mm(tt) @ A.T + b_mm\n\n t_mid = 0.42\n idx_mid = np.searchsorted(tt, t_mid)\n u = np.linspace(0, 1, idx_mid)\n smooth = 3 * u**2 - 2 * u**3\n x_mix = (1 - smooth) * ridge_t[:idx_mid, 0] + smooth * ridge_m[:idx_mid, 0]\n y_mix = (1 - smooth) * ridge_t[:idx_mid, 1] + smooth * ridge_m[:idx_mid, 1] + 0.10 * np.sin(np.pi * u)\n x_ours = np.concatenate([x_mix, ridge_m[idx_mid:, 0]])\n y_ours = np.concatenate([y_mix, ridge_m[idx_mid:, 1]])\n\n star_inds = np.linspace(0, len(tt) - 1, 10, dtype=int)\n S_t = ridge_t[star_inds]\n star_inds_ours = np.linspace(0, len(x_ours) - 1, 10, dtype=int)\n S_m = np.column_stack([x_ours[star_inds_ours], y_ours[star_inds_ours]])\n\n xx_m, yy_m, zz_m, xmin_m, xmax_m, ymin_m, ymax_m = _kde_prepare(P_m)\n xx_t, yy_t, zz_t, xmin_t, xmax_t, ymin_t, ymax_t = _kde_prepare(P_t)\n\n xmin = min(xmin_m, xmin_t) - 0.6\n xmax = max(xmax_m, xmax_t) + 0.6\n ymin = min(ymin_m, ymin_t) - 0.4\n ymax = max(ymax_m, ymax_t) + 0.8\n\n levels_m = np.quantile(zz_m, np.linspace(0.72, 0.99, 10))\n levels_t = np.quantile(zz_t, np.linspace(0.72, 0.99, 10))\n\n blue = \"#2E74B5\"\n blue_pts = \"#1f77b4\"\n gray = \"#6F6F6F\"\n gray_pts = \"#6b7280\"\n red = \"#D62728\"\n\n ax.axis(\"off\")\n ax.scatter(P_m[:, 0], P_m[:, 1], s=10, alpha=0.10, color=blue_pts, zorder=1)\n ax.scatter(P_t[:, 0], P_t[:, 1], s=10, alpha=0.10, color=gray_pts, zorder=1)\n ax.contour(xx_m, yy_m, zz_m, levels=levels_m, colors=blue, linewidths=1.6, alpha=0.3, zorder=2)\n ax.contour(xx_t, yy_t, zz_t, levels=levels_t, colors=gray, linewidths=1.6, alpha=0.3, zorder=2)\n ax.scatter(S_m[:, 0], S_m[:, 1], marker=\"*\", s=185, color=red, edgecolor=\"white\", linewidth=0.6, zorder=5)\n ax.scatter(S_t[:, 0], S_t[:, 1], marker=\"*\", s=185, color=red, edgecolor=\"white\", linewidth=0.6, zorder=5)\n\n bbox = dict(boxstyle=\"round,pad=0.2\", facecolor=\"white\", edgecolor=\"none\", alpha=1)\n ax.text(xmin + 0.9, ymax + 0.2, r\"Multimodal manifold $\\mathcal{M}_\\text{mm}$\", va=\"top\", bbox=bbox, size=24)\n ax.text(xmin + 0.9, ymin, r\"Textual manifold $\\mathcal{M}_\\text{t}$\", va=\"bottom\", bbox=bbox, size=24)\n\n x0, y0 = xmin + 2.1, (ymin + ymax) / 2 - 0.2\n t0 = 0.12\n z_t0 = (curve_text(np.array([t0])) @ A.T + b_text)[0]\n z_m0 = (curve_mm(np.array([t0])) @ A.T + b_mm)[0]\n\n ax.plot([x0], [y0], marker=\"o\", markersize=7, color=\"black\", zorder=6)\n ax.plot([x0, x0], [z_t0[1], z_m0[1]], linestyle=\"--\", linewidth=1.6, color=\"#D62728\", alpha=0.9, zorder=3)\n ax.plot([x0], [z_t0[1]], marker=\"o\", markersize=6, color=gray, zorder=6)\n ax.plot([x0], [z_m0[1]], marker=\"o\", markersize=6, color=blue, zorder=6)\n ax.plot(ridge_t[:, 0], ridge_t[:, 1], linestyle=\"--\", linewidth=2.6, color=gray, alpha=0.95, zorder=4)\n ax.plot(x_ours, y_ours, linewidth=3.0, color=blue, alpha=0.98, zorder=4)\n\n ax.text(x0 - 2, z_t0[1] - 0.8, r\"$z_\\text{t}=f_\\text{t}(x_t)$\", va=\"top\", bbox=bbox, size=18)\n ax.text(x0 - 2, z_m0[1] + 1.2, r\"$z_\\text{mm}=f_\\text{mm}(x_v,x_t)$\", va=\"bottom\", bbox=bbox, size=18)\n ax.text(x0 - 2, y0 + 0.4, r\"sample $x=(x_v,x_t)$\", bbox=bbox, size=18)\n\n idx_dpo = np.argmin(np.abs(ridge_t[:, 0]))\n xy_dpo = ridge_t[idx_dpo] + np.array([2, -0.2])\n idx_ours = np.argmin(np.abs(x_ours - 4))\n xy_ours = np.array([x_ours[idx_ours], y_ours[idx_ours]]) + np.array([0.8, 0.5])\n\n ax.annotate(\n \"Standard DPO shortcut\",\n xy=xy_dpo,\n xytext=(4, ymin + 0.8),\n arrowprops=dict(arrowstyle=\"->\", linewidth=1.6, color=\"black\"),\n bbox=bbox,\n zorder=6,\n )\n ax.annotate(\n r\"Ours: enter $\\mathcal{M}_\\text{mm}$\",\n xy=xy_ours,\n xytext=(4, ymax - 1.3),\n arrowprops=dict(arrowstyle=\"->\", linewidth=1.6, color=\"black\"),\n bbox=bbox,\n va=\"top\",\n zorder=6,\n )\n ax.text(ridge_m[-1, 0] + 4.2, ridge_m[-1, 1], \"grounded\", ha=\"left\", va=\"center\", bbox=bbox, size=20)\n ax.text(ridge_t[-1, 0] + 4.2, ridge_t[-1, 1], \"prior-dominated\", ha=\"left\", va=\"center\", bbox=bbox, size=20)\n ax.set_xlim(xmin, xmax)\n ax.set_ylim(ymin, ymax)\n\n\ndef plot_concept():\n fig, (ax_left, ax_right) = plt.subplots(1, 2, figsize=(24, 6))\n\n plot_distribution(ax_left)\n plot_manifold(ax_right)\n\n fig.tight_layout(pad=0.5)\n fig.subplots_adjust(wspace=0.25)\n\n pos = ax_right.get_position()\n ax_right.set_position([pos.x0, pos.y0 - 0.04, pos.width, pos.height])\n\n os.makedirs(\"./figures/\", exist_ok=True)\n fig.savefig(\"./figures/concept.png\", dpi=300)\n plt.close(fig)\n\n\nif __name__ == \"__main__\":\n plt.rcParams[\"font.family\"] = \"helvetica\"\n plt.rcParams[\"font.size\"] = 18\n plt.rcParams[\"axes.linewidth\"] = 1.5\n plt.rcParams[\"axes.spines.top\"] = False\n plt.rcParams[\"axes.spines.right\"] = False\n\n plot_concept()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9205,"content_sha256":"b494343947cb33dd076924522a1a0ed910ded2f72b311525890f0e9e6f4a6030"},{"filename":"assets/figures4papers/figure_VIGIL/plot_posttraining.py","content":"import os\nimport numpy as np\nfrom matplotlib import pyplot as plt\nfrom matplotlib import gridspec as gridspec\nfrom matplotlib.collections import LineCollection\nfrom matplotlib.colors import to_rgba\nfrom matplotlib.lines import Line2D\n\n\ndata_posttraining = {\n 'methods': [\n 'DPO',\n 'DA-DPO',\n 'VIGIL (Ours)',\n ],\n 'colors': [\n \"#D88F8A\",\n \"#8BCF8B\",\n \"#0F4D92\"\n ],\n 'steps': [0, 200, 400, 600, 800],\n 'results': np.array([\n [22.0, 25.5, 28.2, 29.5, 30.2],\n [22.0, 33.5, 38.2, 39.8, 40.5],\n [22.0, 52.5, 56.8, 57.9, 58.5],\n ]),\n}\n\n\ndef plot_curves(data_posttraining):\n methods = data_posttraining['methods']\n colors = data_posttraining['colors']\n\n fig = plt.figure(figsize=(9, 8))\n ax = fig.add_subplot(1, 1, 1)\n y_ticks = [0, 20, 40, 60]\n x = np.asarray(data_posttraining['steps'])\n results = data_posttraining['results'] # shape (n_methods, n_steps)\n x_pos = np.arange(len(x))\n ax.axhline(y=results[0][0], color='black', alpha=0.3, linewidth=4, linestyle='--')\n for m, (method, color) in enumerate(zip(methods, colors)):\n y = results[m]\n # Segments with alpha increasing left to right\n pts = np.column_stack([x_pos, y])\n segments = np.stack([pts[:-1], pts[1:]], axis=1)\n n_seg = len(segments)\n alphas = np.linspace(0.3, 0.9, n_seg)\n rgb = np.array(to_rgba(color))\n seg_colors = [(*rgb[:3], a) for a in alphas]\n lc = LineCollection(segments, colors=seg_colors, linewidths=3, capstyle='round')\n ax.add_collection(lc)\n ax.plot(x_pos, y, color=color, linewidth=0, marker='o', markersize=10, label='_nolegend_')\n\n # Legend with line + marker for each method\n handles = [ Line2D([0], [0], color='black', linestyle='--', linewidth=4, alpha=0.3, label='SFT only')]\n for method, color in zip(methods, colors):\n handles.append(Line2D([0], [0], color=color, linewidth=3, marker='o', markersize=10, label=method))\n ax.legend(handles=handles, fontsize=20, loc='lower right', ncols=2, frameon=False)\n\n ax.set_xlabel('Post-training steps', fontsize=28, fontfamily='helvetica', labelpad=12)\n ax.set_xticks(x_pos)\n ax.set_xticklabels([str(b) for b in x])\n ax.set_ylabel('Performance on highly\\nvision-dependent tasks' + r'$\\uparrow

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…

, fontsize=28, fontfamily='helvetica', labelpad=12)\n ax.set_yticks(y_ticks)\n ax.set_yticklabels(y_ticks)\n ax.tick_params(labelsize=20, length=8, width=1.5)\n\n fig.tight_layout(pad=2)\n os.makedirs('./figures/', exist_ok=True)\n fig.savefig('./figures/comparison_posttraining.png', dpi=300)\n plt.close(fig)\n return\n\n\nif __name__ == '__main__':\n plt.rcParams['font.family'] = 'helvetica'\n plt.rcParams['font.size'] = 24\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.linewidth'] = 3\n\n plot_curves(data_posttraining)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2948,"content_sha256":"bb1371ace0f1742eb99019c87e18ecc44588ff063141fed5775429e0e373dc1c"},{"filename":"assets/figures4papers/THIRD_PARTY_NOTICES.md","content":"# Third-Party Reference Materials\n\nThis directory contains reference scripts and preview images obtained from:\n\n- Project: `figures4papers`\n- Upstream: \u003chttps://github.com/ChenLiu-1996/figures4papers>\n- Copyright: retained by the upstream authors and other applicable rights holders\n\nAs checked on 2026-08-03, the upstream repository did not expose an explicit\nLICENSE file through its repository root or GitHub license endpoint. These files\nmust therefore not be assumed to fall under the root MIT License of\n`nature-skills`.\n\nTheir retention in this repository is for source attribution and research\nreference. It does not grant permission to copy, modify, redistribute, or publish\nthe files or derived materials. Before such use, check the current upstream terms\nand obtain permission from the relevant rights holder when required.\n\nIf the upstream project later publishes a license or the maintainers obtain\nwritten permission, update this notice with the exact terms and date.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":982,"content_sha256":"b0ef9d2fe959468fce4f45472b12ff4d4fc9450201caa42d394823cd4a5e20d5"},{"filename":"evals/evals.json","content":"{\n \"skill_name\": \"nature-figure\",\n \"evals\": [\n {\n \"id\": \"backend-exclusivity-r-missing-runtime\",\n \"prompt\": \"Use R to remake the provided ecological heatmap plus taxonomy-flow figure in Nature style with simulated data. Assume R/Rscript is not installed locally.\",\n \"expected_output\": \"The assistant must not use Python or any non-R plotting backend to draw a preview or export. It should report that R/Rscript is unavailable, provide or offer an R-only script and install/run instructions, and stop before rendering.\",\n \"assertions\": [\n {\n \"name\": \"no_cross_backend_rendering\",\n \"description\": \"When R is selected and unavailable, no Python/matplotlib/seaborn/plotly preview, SVG, PDF, TIFF, or PNG is generated as a substitute.\"\n },\n {\n \"name\": \"selected_backend_blocker_reported\",\n \"description\": \"The response clearly reports the missing R runtime or package blocker and does not present a non-R figure as completed output.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"backend-exclusivity-python-missing-package\",\n \"prompt\": \"Use Python to make a Nature-style multi-panel heatmap and flow figure with simulated data. Assume matplotlib or another required Python plotting package is not installed locally.\",\n \"expected_output\": \"The assistant must not use R or any non-Python plotting backend to draw a preview or export. It should report the missing Python plotting dependency, provide or offer a Python-only script and install/run instructions, and stop before rendering.\",\n \"assertions\": [\n {\n \"name\": \"no_cross_backend_rendering\",\n \"description\": \"When Python is selected and unavailable, no R/ggplot2/ComplexHeatmap/patchwork preview, SVG, PDF, TIFF, or PNG is generated as a substitute.\"\n },\n {\n \"name\": \"selected_backend_blocker_reported\",\n \"description\": \"The response clearly reports the missing Python runtime or package blocker and does not present a non-Python figure as completed output.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"large-data-no-silent-sampling\",\n \"prompt\": \"Use Python to make a publication-ready scatter plot from all 180,000 rows in my table. The current draft randomly keeps 5,000 rows because vector export is slow.\",\n \"expected_output\": \"The assistant must preserve all observations unless the user explicitly authorizes a scientifically justified subset. It should replace random downsampling with rasterized marks, hexbin/density rendering, or another Python-native large-data strategy and document the rendering choice.\",\n \"assertions\": [\n {\n \"name\": \"all_observations_preserved\",\n \"description\": \"The plotting plan does not silently sample rows or describe a random subset as the full dataset.\"\n },\n {\n \"name\": \"rendering_solution_not_data_reduction\",\n \"description\": \"Performance is addressed through rendering or an explicitly described aggregation rather than convenience sampling.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"asset-semantic-mismatch\",\n \"prompt\": \"Adapt the bundled 2D marginal joint-density example to show separate 1D distributions for eight biomarkers. Just map the first two columns and ignore the rest.\",\n \"expected_output\": \"The assistant must reject exact or structural reuse because the requested dimensionality and scientific meaning differ. It should not discard six biomarkers; it should use style-only inheritance or build a suitable 1D multi-distribution figure after confirming the intended comparison.\",\n \"assertions\": [\n {\n \"name\": \"semantic_mismatch_detected\",\n \"description\": \"The response distinguishes a 2D joint-density plot from multiple 1D distributions.\"\n },\n {\n \"name\": \"no_silent_column_selection\",\n \"description\": \"No requested biomarker is silently ignored to make the template fit.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"exclusion-count-accounting\",\n \"prompt\": \"Make a Nature-style grouped plot. Some values are missing, so drop incomplete rows before plotting.\",\n \"expected_output\": \"The assistant may apply the requested missing-data rule only after recording the exact predicate and before/after observation and replicate counts. The QA notes must disclose the exclusion and preserve the unmodified input.\",\n \"assertions\": [\n {\n \"name\": \"before_after_counts_required\",\n \"description\": \"The workflow records counts before and after missing-data exclusion.\"\n },\n {\n \"name\": \"source_data_preserved\",\n \"description\": \"The original input is not overwritten and the exclusion appears in QA notes.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"template-transform-guard\",\n \"prompt\": \"Reuse this log-scale template for a dataset containing negative and zero values. Keep every transform unchanged so the visual style matches.\",\n \"expected_output\": \"The assistant must detect that the inherited logarithmic transform is incompatible. It should not silently delete non-positive values or add an arbitrary pseudocount; it should explain the options and use structural adaptation only if the scientific meaning remains valid, otherwise inherit style only or build anew.\",\n \"assertions\": [\n {\n \"name\": \"log_domain_checked\",\n \"description\": \"Non-positive values are identified as incompatible with an ordinary logarithmic transform.\"\n },\n {\n \"name\": \"no_silent_transform_or_filter\",\n \"description\": \"The assistant does not silently filter values or invent a pseudocount.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"demo-data-production-boundary\",\n \"prompt\": \"Turn a bundled example that generates random data into the final manuscript figure using my CSV.\",\n \"expected_output\": \"The assistant must isolate or remove the simulated-data path, map the real data contract explicitly, run the selected backend on representative real input, and verify that no generated example values remain in the production output.\",\n \"assertions\": [\n {\n \"name\": \"simulated_data_removed_from_production\",\n \"description\": \"Random example generation cannot execute in the final production path.\"\n },\n {\n \"name\": \"real_data_contract_mapped\",\n \"description\": \"The real input fields, units, group ordering, and uncertainty semantics are mapped explicitly.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"delivery-runs-static-and-visual-qa\",\n \"prompt\": \"The Python plotting script is finished. Deliver the journal-ready figure package now.\",\n \"expected_output\": \"Before delivery, the assistant runs the render-time panel-alignment gate on the final Matplotlib axes, scripts/validate_figure.py on the Python source, scripts/audit_pdf_text.py and scripts/audit_figure_collisions.py on the final Python-generated PDF, resolves every alignment/collision FAIL, reviews and documents every WARN, and inspects the actual SVG/PDF/TIFF or PNG outputs at final physical size. It must not treat automated validation as proof of statistical or complete visual correctness.\",\n \"assertions\": [\n {\n \"name\": \"validator_run_before_delivery\",\n \"description\": \"The deterministic static validator is run on the final plotting source.\"\n },\n {\n \"name\": \"rendered_outputs_inspected\",\n \"description\": \"Editable text, clipping, overlaps, resolution, and final-size readability are checked on rendered files.\"\n },\n {\n \"name\": \"rendered_collision_gate_run\",\n \"description\": \"The final PDF receives the automatic collision audit and blocking findings are fixed before delivery.\"\n },\n {\n \"name\": \"render_time_alignment_gate_run\",\n \"description\": \"The final multi-panel axes geometry receives the alignment audit and blocking findings are fixed before export.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"rendered-collision-audit-is-mandatory\",\n \"prompt\": \"I changed the legend font and moved two annotations in this R-generated Nature figure. The data and R code already passed static checks, so just send the revised PDF.\",\n \"expected_output\": \"Because text and layout changed, the assistant must rerun audit_figure_collisions.py on the newly exported R PDF, preserve its JSON report, fix and re-export any text-text, text-stroke, or clipping FAIL, and inspect every WARN at final size. The Python audit may inspect PDF geometry but must not redraw or replace the R-generated scientific figure; a marked PDF is QA-only.\",\n \"assertions\": [\n {\n \"name\": \"rerun_after_layout_change\",\n \"description\": \"A prior report is not reused after any font, legend, annotation, axis, uncertainty, panel-size, or layout change.\"\n },\n {\n \"name\": \"blocking_collisions_are_repaired\",\n \"description\": \"Reliable rendered collisions and clipping block delivery until the selected backend re-exports a corrected figure.\"\n },\n {\n \"name\": \"backend_exclusivity_is_preserved\",\n \"description\": \"The backend-neutral audit inspects the R PDF but does not use Python plotting to redraw or substitute the figure.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"multipanel-render-time-alignment-gate\",\n \"prompt\": \"My four-panel Nature figure looks almost aligned, but panel b sits about 2 mm lower and one horizontal gutter is wider. The PDF collision checker reports no overlaps, so deliver it as-is.\",\n \"expected_output\": \"The assistant must not treat collision-free output as aligned. It should measure the final selected-backend panel rectangles, compare intended row/column groups and repeated gutters at the default 1.5 pt physical tolerance, preserve the alignment JSON, and block export until the shifted panel and gutter are corrected. A hero panel, inset or colorbar may be excluded only through explicit groups or a reasoned exemption; the global tolerance must not be weakened to hide the error.\",\n \"assertions\": [\n {\n \"name\": \"collision_and_alignment_are_separate_gates\",\n \"description\": \"A collision PASS is not accepted as evidence that axes rectangles, panel labels or gutters are aligned.\"\n },\n {\n \"name\": \"physical_alignment_tolerance_blocks_delivery\",\n \"description\": \"Comparable row/column edges, dimensions and repeated gutters are measured in final-size points and reliable deviations block export.\"\n },\n {\n \"name\": \"asymmetry_requires_explicit_reason\",\n \"description\": \"Structured unequal-span grids are inferred automatically; free-positioned hero panels, insets, colorbars and other intentional exceptions use explicit groups or recorded exemptions rather than a weakened global tolerance.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"asymmetric-two-plus-one-panel-alignment\",\n \"prompt\": \"Build two Nature figure layouts: first column panels a and b stacked with panel c spanning both rows on the right, then the mirrored layout with the spanning panel on the left. Make sure the panels are automatically checked for alignment.\",\n \"expected_output\": \"The assistant should use final selected-backend grid/gtable geometry to infer both unequal-span layouts without requiring manual row groups. It must compare the spanning panel's top edge with the upper small panel and its bottom edge with the lower small panel at the 1.5 pt tolerance, while not requiring the spanning panel to equal either small panel's height. A shifted or shortened spanning panel must block export.\",\n \"assertions\": [\n {\n \"name\": \"both_mirrored_two_plus_one_layouts_are_inferred\",\n \"description\": \"Left-two/right-one and left-one/right-two grid layouts are recognized from row start/stop metadata in Python GridSpec and R patchwork manifests.\"\n },\n {\n \"name\": \"shared_outer_edges_are_checked\",\n \"description\": \"The spanning panel shares and is compared at the upper and lower outer boundaries with the correct small panels.\"\n },\n {\n \"name\": \"unequal_heights_are_not_false_failures\",\n \"description\": \"The spanning panel is not incorrectly required to have the height of either individual stacked panel, but a real outer-edge shift remains blocking.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"horizontal-three-four-panel-equal-width\",\n \"prompt\": \"Make one Nature figure with three panels in a single horizontal row and another with four panels in a single horizontal row. Guarantee that all comparable panels have exactly the same final plot-area width.\",\n \"expected_output\": \"The assistant should measure final selected-backend axes/gtable rectangles and require same-row panels with equal grid spans to have equal physical widths within the 1.5 pt tolerance. Unequal Matplotlib width_ratios or R patchwork cell widths must block export. A deliberate hero panel may be excluded only with a panel-specific panel-width exemption and a non-empty reason.\",\n \"assertions\": [\n {\n \"name\": \"three_and_four_panel_rows_check_final_widths\",\n \"description\": \"Both 1 x 3 and 1 x 4 layouts compare final rendered plot-area widths rather than trusting source-level grid declarations.\"\n },\n {\n \"name\": \"unequal_equal_span_panels_block_export\",\n \"description\": \"A reliable width spread above 1.5 pt among equal-grid-span panels produces horizontal-panel-width-misalignment and blocks delivery.\"\n },\n {\n \"name\": \"intentional_width_asymmetry_is_narrowly_exempted\",\n \"description\": \"An intentional hero width uses a panel-width exemption with a reason; different column spans are not falsely required to be equal.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"trigger-publication-figure-chinese\",\n \"prompt\": \"用我的实验数据做一张论文用的分组小提琴图,要求可编辑 PDF 和 600 dpi TIFF。\",\n \"expected_output\": \"The nature-figure skill should trigger, establish or reuse the Python/R backend preference, define the figure contract, preserve all requested data, and produce the selected-backend plotting and QA workflow.\",\n \"assertions\": [\n {\n \"name\": \"publication_figure_triggered\",\n \"description\": \"A Chinese manuscript-figure request with vector/raster export requirements routes to nature-figure.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"route-openrouter-without-backend-gate\",\n \"prompt\": \"Use OpenRouter GPT Image 2 to draft a mechanism schematic for my paper.\",\n \"expected_output\": \"The OpenRouter image-generation route should trigger directly without asking Python or R. The output must be treated as a draft schematic and unsupported mechanisms or quantitative evidence must not be invented.\",\n \"assertions\": [\n {\n \"name\": \"openrouter_route_selected\",\n \"description\": \"The explicit AI-schematic request bypasses the plotting backend gate.\"\n },\n {\n \"name\": \"scientific_integrity_preserved\",\n \"description\": \"The draft does not fabricate mechanisms, values, logos, or institutional marks.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"ai-graphical-abstract-policy-and-accountability-gate\",\n \"prompt\": \"Use generative AI to make the final graphical abstract for my Nature submission. I want the fastest route from the manuscript to a submission-ready image.\",\n \"expected_output\": \"The assistant should first define the central message, audience, figure type, and evidence boundary; verify the current official policy for the exact target journal; and distinguish an internal AI design draft from a submission-eligible final asset. It must not treat a Nature Careers column as submission permission or delegate scientific accuracy, copyright, disclosure, and provenance checks to the model.\",\n \"assertions\": [\n {\n \"name\": \"careers_article_not_policy\",\n \"description\": \"Practitioner advice is not presented as formal permission to submit generative-AI artwork.\"\n },\n {\n \"name\": \"message_audience_and_evidence_defined_first\",\n \"description\": \"The workflow defines one message, audience, figure type, and evidence boundary before image generation.\"\n },\n {\n \"name\": \"draft_and_submission_clearance_separated\",\n \"description\": \"Internal design usefulness and submission eligibility receive separate verdicts after human scientific, copyright, disclosure, and provenance review.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"multipanel-figure-level-claim-and-role-diversity\",\n \"prompt\": \"My Fig. 4 currently has a: four-condition R2, b: pairwise R2 effects, c: four-condition MAPE, and d: pairwise MAPE effects. Make it more like a Nature multi-panel figure.\",\n \"expected_output\": \"The assistant should first state the Results-level scientific question and one figure-level claim, diagnose that four panels currently provide only about two inferential roles, and propose the smallest sufficient panel chain with complementary roles such as perturbation setup, decisive comparison, residual/full distribution, and decomposition or boundary. It should decide whether the secondary metric belongs in the main figure, Extended Data/SI, or the caption without deleting conclusion-changing evidence.\",\n \"assertions\": [\n {\n \"name\": \"figure_level_claim_precedes_panel_count\",\n \"description\": \"The plan begins from one Results-level question and figure-level claim rather than the available number of metrics or datasets.\"\n },\n {\n \"name\": \"panel_roles_are_inferentially_distinct\",\n \"description\": \"Panels perform complementary evidence roles rather than mirroring the same experiment under R2 and MAPE.\"\n },\n {\n \"name\": \"secondary_evidence_is_routed_explicitly\",\n \"description\": \"Redundant or secondary evidence is deliberately merged, moved to Extended Data/SI, or retained only when it changes or bounds the claim.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"do-not-trigger-statistics-only\",\n \"prompt\": \"Run a two-way ANOVA, calculate adjusted p values, and write the statistical result. Do not make a figure.\",\n \"expected_output\": \"The nature-figure skill should not trigger because the user explicitly requests statistical analysis without figure creation, revision, audit, or export.\",\n \"assertions\": [\n {\n \"name\": \"statistics_only_excluded\",\n \"description\": \"Statistics-only work routes to an analysis/statistics capability rather than nature-figure.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"do-not-trigger-interactive-dashboard\",\n \"prompt\": \"Build an interactive Plotly sales dashboard with filters for our internal website.\",\n \"expected_output\": \"The nature-figure skill should not trigger because the task is interactive, web-first, non-scientific dashboard work.\",\n \"assertions\": [\n {\n \"name\": \"interactive_dashboard_excluded\",\n \"description\": \"Interactive web/dashboard requests remain outside nature-figure.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"do-not-trigger-photo-edit-only\",\n \"prompt\": \"Crop this microscopy photo and increase its contrast. I am not assembling or auditing a manuscript figure.\",\n \"expected_output\": \"The nature-figure skill should not trigger for a pure raster-photo edit without figure assembly, quantitative plotting, or journal-figure audit intent.\",\n \"assertions\": [\n {\n \"name\": \"photo_edit_only_excluded\",\n \"description\": \"Pure image editing routes to an image-editing capability rather than nature-figure.\"\n }\n ],\n \"files\": []\n },\n {\n \"id\": \"vector-schematic-brief-gate\",\n \"prompt\": \"Draw an editable single-column architecture figure for my multi-agent system: an orchestrator dispatches tasks to planner, coder and critic agents that share a tool layer and memory. It is for a Nature Machine Intelligence submission.\",\n \"expected_output\": \"The assistant treats this as a vector schematic, not a data plot: it does not ask Python or R, writes a short brief (components with new/existing/external tags, numbered main path, one-point argument, assumptions, caption draft) and asks the user to confirm it before drawing. It sizes the canvas from the journal rules (89 mm single column, 5-7 pt text), and plans to run scripts/audit_svg_schematic.py on the SVG and inspect the render before delivery.\",\n \"assertions\": [\n {\n \"name\": \"no_backend_question\",\n \"description\": \"Does not ask whether to use Python or R, because the task draws structure rather than plotting data.\"\n },\n {\n \"name\": \"brief_before_drawing\",\n \"description\": \"Presents a brief with component tags, a numbered main path, assumptions to confirm, and a caption draft, and waits for confirmation before producing the SVG.\"\n },\n {\n \"name\": \"final_size_rules\",\n \"description\": \"Uses the journal's 89 mm single-column width and keeps ordinary text between 5 pt and 7 pt at final size.\"\n },\n {\n \"name\": \"audit_and_visual_check\",\n \"description\": \"Runs audit_svg_schematic.py and inspects a render at final size, and does not claim the audit detects overlaps or crossing arrows.\"\n },\n {\n \"name\": \"no_invented_content\",\n \"description\": \"Does not add components, flows, or numbers that the user did not supply.\"\n }\n ],\n \"files\": []\n }\n ]\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":22195,"content_sha256":"3d75ea7f279ca45fe0ed6db3f7110f653c94eb01c9626b4d823a2df22029d01c"},{"filename":"manifest.yaml","content":"name: nature-figure\nversion: 2.8.0\ndescription: >\n Declarative manifest for the static/dynamic split. SKILL.md uses this to\n decide which fragments to load for a given figure request. The main axis is\n the plotting backend (Python or R). The first plotting use should establish\n a saved user preference; later tasks reuse that preference unless the user\n explicitly changes it. The large body of design, API, pattern, and QA\n material stays in on-demand references.\n\nalways_load:\n # Skill-local core (figure does not use the prose-oriented nature-shared layer)\n - static/core/contract.md\n - static/core/stance.md\n\nroutes:\n graphical_abstract_workflow:\n detect: |\n Use this route whenever AI assists the planning, generation, revision, or\n audit of a graphical abstract. Planning and audit-only tasks do not need\n the Python/R backend gate. The reference owns the policy check,\n message/audience brief, visual design workflow, human verification,\n disclosure boundary, and provenance record.\n reference: references/ai-graphical-abstract-workflow.md\n openrouter_image_generation:\n detect: |\n Use this route only when the user explicitly asks for OpenRouter, GPT\n Image 2, image-generation API output, AI-generated paper schematics,\n graphical abstracts, concept illustrations, or mechanism diagrams. This\n route skips the Python/R backend gate because it generates a raster/vector\n image draft through an external image API rather than plotting data.\n reference: references/openrouter-image-generation.md\n script: scripts/generate_openrouter_schematic.py\n\n vector_schematic_workflow:\n detect: |\n Use this route when the user wants a hand-built, editable SVG schematic:\n system or method architecture, pipeline overview, agent or model block\n diagram, or mechanism or process flow. It skips the Python/R backend gate\n because it draws structure rather than plotting data. Confirm a written\n brief before drawing and run the SVG audit script before delivery.\n reference: references/vector-schematic-workflow.md\n script: scripts/audit_svg_schematic.py\n\npreference:\n backend_script: scripts/nature_figure_backend.py\n config_env: NATURE_FIGURE_CONFIG\n default_config: ~/.config/nature-skills/nature-figure.json\n behavior: |\n For plotting tasks, first honor an explicit Python/R choice, then a clearly\n language-specific input workflow, then a choice established in this task,\n then the saved preference returned by the backend script. If none exists,\n ask the user once and save the answer. Pause only dependent plotting work.\n\naxes:\n backend:\n detect: |\n BLOCKING GATE WITH PERSISTED PREFERENCE. Determine the plotting backend\n from an explicit user choice, a clearly language-specific input\n file/workflow, a choice established in this task, or the saved preference\n returned by scripts/nature_figure_backend.py get. If none resolves it, ask\n exactly one concise question — \"Python or R? I will remember this as your\n default.\" — and pause dependent plotting steps while continuing independent\n inspection. Read-only review does not require a backend choice.\n Save the answer with the backend preference script\n before proceeding. Only recommend a backend when the user explicitly asks\n you to choose; in that case use references/backend-selection.md, state the\n reason, save the selected backend, then proceed. Once selected, the\n backend is exclusive for all drawing, previewing, exporting, and visual QA.\n values:\n python: static/fragments/backend/python.md\n r: static/fragments/backend/r.md\n multi: false\n\nreferences:\n on_demand:\n - condition: convert a user request into core conclusion, evidence hierarchy, panel map, and review-risk checks\n path: references/figure-contract.md\n - condition: plan, restructure, or audit a multi-panel manuscript figure; decide panel roles or main-versus-Extended-Data placement; or align figure order with a Results evidence chain\n path: references/multipanel-evidence-architecture.md\n - condition: user has not chosen Python/R, asks for a recommendation, or a mixed Python/R workflow is possible\n path: references/backend-selection.md\n - condition: user chooses R or provides R scripts/templates/data\n path: references/r-workflow.md\n - condition: adapt a user-provided or private R template collection without exposing source paths\n path: references/r-template-index.md\n - condition: reuse or adapt a bundled example, plotting script, preview image, or user-provided template against new data\n path: references/asset-adaptation.md\n - condition: every generated or revised Python/R scientific figure, multi-panel alignment gate, rendered PDF collision audit, final delivery, revision package, rendered PDF text audit, panel-by-panel visual QA, microscopy/blot figure, or journal-specific audit\n path: references/qa-contract.md\n - condition: target is the flagship journal Nature, or exact initial-versus-production main/Extended Data figure and legend requirements are needed\n path: references/nature-article-requirements.md\n - condition: target is Nature Machine Intelligence, or exact NMI main-display, Extended Data, legend, source-data, initial-review, or production-figure requirements are needed\n path: ../nature-shared/journal-formats/nature-machine-intelligence.md\n - condition: typography, color theory, layout rationale, export policy\n path: references/design-theory.md\n - condition: Python PALETTE, helper function signatures, Matplotlib panel-alignment integration, validation rules\n path: references/api.md\n - condition: \"Python layout patterns: hero panels, legend-only axes, dark image plates, asymmetric layouts\"\n path: references/common-patterns.md\n - condition: \"real Nature page archetypes: schematic-led composites, dark image plates, clinical triptychs, asymmetric hero layouts\"\n path: references/nature-2026-observations.md\n - condition: \"OpenRouter/GPT Image 2 route: AI-generated graphical abstracts, mechanism diagrams, paper schematics, or concept illustrations\"\n path: references/openrouter-image-generation.md\n - condition: AI-assisted graphical abstract planning, generation, revision, audit, journal-policy check, scientific verification, disclosure, or provenance\n path: references/ai-graphical-abstract-workflow.md\n - condition: \"vector schematic route: hand-built editable SVG architecture, pipeline, agent, or mechanism diagram, brief-then-draw workflow, and final-size SVG audit\"\n path: references/vector-schematic-workflow.md\n - condition: \"colour, line, arrowhead, container, legend, and common-failure conventions for SVG architecture or mechanism schematics\"\n path: references/vector-schematic-style-rules.md\n - condition: writing or auditing figure/table legend text — Fig. N | title, panel style, stats in legend, Source Data boilerplate, attribution; load the flagship Nature requirements too when that is the target journal\n path: references/figure-legend-conventions.md\n - condition: \"end-to-end walkthroughs: bars, trends, heatmaps\"\n path: references/tutorials.md\n - condition: \"validated Python CSV templates: volcano, ROC, marker dot plot, marginal density, or paired plot\"\n path: references/template-catalog.md\n - condition: \"radar, 3D sphere, fill_between, scatter, ablation-line, probability/manifold concept patterns\"\n path: references/chart-types.md\n - condition: third-party figures4papers demo lookup, copyright-aware reference use, or original pattern reimplementation\n path: references/demos.md\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":7782,"content_sha256":"f6cf1e50f55c74e159134edb8ca5a57fd5097b72b37463c7b2612757e01c24be"},{"filename":"README_EN.md","content":"# `nature-figure` Skill\n\n[中文说明](README.md)\n\n`nature-figure` designs, generates, and audits submission-grade scientific figures for Nature-series papers, high-impact journals, manuscript panels, mechanism schematics, and graphical-abstract drafts.\n\n## What To Use It For\n\n- Generate Python / R plotting scripts and editable figures from data, legends, or manuscript claims.\n- Redraw existing figures into clearer multi-panel manuscript figures.\n- Plan multi-panel evidence chains around the default that one figure answers one Results-level scientific question, with panels serving different inferential roles such as primary evidence, control, orthogonal validation, perturbation, mechanism, or boundary rather than merely redrawing the same result under new metrics.\n- Plan Figure 1, mechanism diagrams, workflows, graphical abstracts, or supplementary figures.\n- Check panel labels, color hierarchy, panel-by-panel uncertainty, actual PDF glyph sizes, statistical annotations, source data, and export formats; at render time, automatically enforce a 1.5 pt tolerance for comparable row/column axes rectangles, dimensions, panel-label anchors, and repeated gutters, then detect text-text, text-stroke/curve, page-clipping, and suspicious fill/image-edge overlaps after every generation or layout revision.\n- Separate flagship `Nature` initial, final main-figure, and Extended Data file contracts, including the under-250-word legend limit.\n- Apply `Nature Machine Intelligence` (NMI)'s separate six-main-display, up-to-ten Extended Data, initial/final, 300-dpi/180-mm, and source-data requirements; the current pages give no standalone legend number, so retain the official 2018 `\u003c300`-English-word rule only as a historical advisory, count the whole legend rather than each panel, and aim for 150–250 words.\n- When explicitly requested, call `openai/gpt-image-2` through the OpenRouter Images API to draft AI concept schematics.\n- For AI-assisted graphical abstracts, define one central message, figure type, audience, and evidence boundary before comparing compositions and accessible palettes; then separately verify the target journal's current AI policy, scientific accuracy, copyright, disclosure, and provenance. Treat the *Nature Careers* column as practitioner advice, not submission permission.\n\n## Workflow\n\nStart with a figure contract rather than a template:\n\n- Core conclusion: what the figure must demonstrate.\n- Evidence hierarchy: which panels are primary evidence and which are explanatory.\n- Multi-panel architecture: write the figure-level claim first, then assign every panel a distinct evidence role and decide whether displaced material belongs in another figure or Extended Data/SI.\n- Figure prototype: scatter, box plot, heatmap, mechanism diagram, workflow, multi-panel composition, and so on.\n- Backend choice: Python or R; the first choice can be reused as the default preference.\n- Data integrity: preserve all observations and requested variables by default, and record every exclusion rule with before/after counts.\n- Template compatibility: compare scientific meaning, data shape, and transform constraints before exact reuse, structural adaptation, or style-only inheritance.\n- Submission constraints: size, typography, color, resolution, vector format, and source-data traceability.\n- Panel-alignment gate: Python axes or R patchwork/gtable measures real panel rectangles at final dimensions; horizontal rows of three or four equal-span panels must be equal width, regular grids and unequal-span `left two/right one` or mirrored layouts are inferred automatically, reliable misalignment blocks export, and free-positioned hero panels, insets, or colorbars require reasoned exemptions.\n- Rendered collision gate: regenerate a collision JSON after every final-PDF render; reliable collisions must be fixed and ambiguous overlays reviewed individually.\n\n## Typical Requests\n\n- \"Make a Nature-style multi-panel figure from this dataset, preferably in Python.\"\n- \"Use the figures4papers Nature Machine Intelligence layout as a reference and add a method-comparison figure.\"\n- \"Redraw this mechanism schematic, export SVG/PDF, and give me the source-data table.\"\n- \"Use OpenRouter to draft a graphical abstract, but do not treat it as a quantitative data figure.\"\n\n## Example Preview\n\n| Direction | Preview | Reusable Pattern |\n|-----------|---------|------------------|\n| Multi-panel manuscript figure | \u003ca href=\"assets/gallery/fig1-material-mechanism-rich.png\">\u003cimg src=\"assets/gallery/fig1-material-mechanism-rich.png\" width=\"220\" alt=\"Material design and physical validation\">\u003c/a> | Mechanism schematic, image panels, quantitative results, and correlation in one evidence chain |\n| Chart-type atlas | \u003ca href=\"assets/chart-atlas/atlas-03-heatmaps.png\">\u003cimg src=\"assets/chart-atlas/atlas-03-heatmaps.png\" width=\"220\" alt=\"Heatmap atlas\">\u003c/a> | Heatmaps, annotation matrices, cluster blocks, and diverging color scales |\n| Third-party figures4papers reference | \u003ca href=\"assets/figures4papers/figure_VIGIL/figures/comparison_radar.png\">\u003cimg src=\"assets/figures4papers/figure_VIGIL/figures/comparison_radar.png\" width=\"220\" alt=\"VIGIL comparison radar\">\u003c/a> | Study layout, legend, and multi-metric comparison grammar only; read the separate copyright notice before use |\n\n## What You Need To Provide\n\n- Raw data, existing figure, legend, manuscript claim, or intended mechanism.\n- Target journal, single-column / double-column size, output format, and whether source data is required.\n- Python / R preference; if absent, the skill asks or reuses the local preference.\n\n## Outputs\n\n- Runnable Python or R plotting script.\n- SVG/PDF/TIFF/PNG figure files, with editable vector output preferred.\n- Panel notes, source-data mapping, exclusion counts, a panel-by-panel visual audit, alignment JSON/diagnostic SVG, collision JSON/diagnostic PDF, and a pre-submission QA record.\n- For AI-schematic tasks, a concept draft and a list of elements that need human redrawing or verification.\n\n## Built-In References\n\n- `references/api.md`: Python palette, style, and plotting-helper conventions.\n- `references/asset-adaptation.md`: semantic matching, field mapping, and data-integrity rules for templates.\n- `references/multipanel-evidence-architecture.md`: planning and audit from Results-level question to panel evidence roles, within-figure closure, cross-figure claim escalation, and main-figure/Extended-Data/SI placement.\n- `references/template-catalog.md`: validated Python CSV templates for volcano, ROC, marker dot plot, marginal, and paired figures.\n- `references/chart-types.md`: chart selection and visual rules.\n- `references/demos.md`: third-party `figures4papers` index, use boundaries, and original adaptation patterns.\n- `references/qa-contract.md`: export QA, source-data constraints, and static-preflight entry points.\n- `references/ai-graphical-abstract-workflow.md`: message brief, composition and color, journal-policy gate, human scientific verification, disclosure, and provenance for AI-assisted graphical abstracts.\n- `references/openrouter-image-generation.md`: provider-specific OpenRouter / GPT Image 2 generation and QA.\n- `references/vector-schematic-workflow.md`: brief-then-draw-then-check workflow for hand-built editable SVG architecture, pipeline, and mechanism schematics.\n- `references/vector-schematic-style-rules.md`: type, colour, line, container, legend, and failure conventions for SVG schematics.\n- `scripts/audit_svg_schematic.py`: final-size audit of SVG text size, canvas width, off-canvas text, and forbidden constructs.\n- `scripts/validate_figure.py`: reproducible static QA for Python and R plotting source.\n- `scripts/audit_pdf_text.py`: scan exported PDF `Tf` operators for real glyph runs below the 5 pt floor, including reduced mathtext scripts.\n- `scripts/audit_panel_alignment.py` and `scripts/panel_alignment.R`: measure final-size Matplotlib axes or R patchwork/gtable geometry and block unequal widths in three/four-panel rows, row/column edges, shared outer edges of spanning panels, panel labels, or repeated-gutter misalignment.\n- `scripts/audit_figure_collisions.py`: automatic geometry audit for final Python/R PDFs, with blocking FAIL findings, review-required WARN findings, JSON output, and an optional marked diagnostic PDF.\n- `scripts/figure_safety.py`: strict monotone interpolation and data/uncertainty-driven label positioning helpers.\n- `assets/figures4papers/`: retained third-party scripts and previews; the repository MIT License does not automatically apply, so read `THIRD_PARTY_NOTICES.md` before use.\n\n## Boundaries\n\n- AI-generated images are not treated as real experimental results or quantitative data panels.\n- An internally useful AI draft is not automatically described as a submission-ready final asset; assess those two states separately.\n- The skill does not invent statistical tests, sample sizes, error-bar meanings, or experiment conditions.\n- The skill does not silently sample for rendering convenience, ignore requested variables, or remove incomplete observations.\n- Passing automated checks is not treated as visual acceptance; the alignment gate only proves declared geometric relationships are within tolerance, and uncertainty, label collisions, spacing, and salience still require panel-by-panel inspection.\n- Private templates can be used locally, but user-facing outputs should not expose private paths, filenames, or sources.\n- Third-party reference materials remain subject to their source terms and `THIRD_PARTY_NOTICES.md`; this repository grants no additional rights to those files.\n\nThe automatic collision audit requires PyMuPDF:\n\n```bash\npython -m pip install -r skills/nature-figure/requirements.txt\n```\n\n## Related Skills\n\n- `nature-statistics`: check statistical annotations, n definitions, and p-value wording.\n- `nature-writing`: align figure conclusions with manuscript narrative.\n- `nature-paper2ppt`: turn manuscript figures into presentation slides.\n\n## Relationship With Other Skills\n\n- If the core task is statistical interpretation, sample-size definition, or significance wording, let `nature-statistics` audit the text before returning to `nature-figure`.\n- If the figure is finished but the user needs the claim written into an abstract, introduction, or results section, hand off to `nature-writing`.\n- If the figure should become a lab meeting deck or presentation slide, hand off to `nature-paper2ppt`.\n- `nature-figure` is responsible for the figure itself; it does not replace statistical review or manuscript narration.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10581,"content_sha256":"6b4674bcd5b949dcca8ebda134739c252e4b0d229eca1d58e1474d4284c49a87"},{"filename":"README.md","content":"# `nature-figure` 技能\n\n[English](README_EN.md)\n\n`nature-figure` 用于设计、生成和审查投稿级科研图件,面向 Nature 系列、高影响力期刊、论文图版、机制示意图和 graphical abstract 草稿。\n\n## 适合用它做什么\n\n- 根据数据、图注或论文结论生成 Python / R 绘图脚本和可编辑图件。\n- 将已有图件重画为更清楚的多面板论文 figure。\n- 按“一张 Figure 回答一个 Results 级科学问题”的默认逻辑规划多面板证据链,让各 panel 分别承担主证据、control、正交验证、扰动、机制或边界等不同推理角色,而不是只把同一结果换指标重画。\n- 规划 Figure 1、机制图、workflow、graphical abstract 或补充图。\n- 检查面板标签、配色与视觉层级、逐面板误差线、最终 PDF 实际字号、统计标注、source data 和导出格式;多面板图在渲染时自动检查同行/同列轴框、宽高、panel 标签锚点和重复间距是否在 1.5 pt 容差内,每次生成或修改布局后再检测文字—文字、文字—线条/曲线、页面裁切及可疑色块/图片边缘重合。\n- 区分旗舰 `Nature` 初投稿、主图终稿和 Extended Data 的文件契约,并执行 `\u003c250` 词图注上限。\n- 对 `Nature Machine Intelligence` 单独执行 6 个主 display、最多 10 个 Extended Data、初投稿/终稿边界、300 dpi/180 mm 和 source data 要求;当前官网未给独立图注数字,保留 2018 官方 `\u003c300` 英文词为历史建议线,整张图注建议 150–250 词且不是每个 panel 分别计算。\n- 在用户明确要求时,通过 OpenRouter Images API 调用 `openai/gpt-image-2` 生成 AI 概念示意图草稿。\n- 对 AI 辅助 graphical abstract 先定义单一中心信息、图件类型、目标读者和证据边界,再比较构图与可访问配色;投稿前单独核验目标期刊最新 AI 政策、科学准确性、版权、披露和 provenance。`Nature Careers` 专栏仅作为实践建议,不等于投稿许可。\n\n## 工作方式\n\n绘图前先建立图件契约,而不是直接套模板:\n\n- 核心结论:这张图要证明什么。\n- 证据层级:哪些面板是主证据,哪些是补充解释。\n- 多面板架构:先写 figure-level claim,再决定每个 panel 的独特证据角色以及主图、另一张图或 Extended Data/SI 的去向。\n- 图件原型:散点、箱线、热图、机制图、流程图、多面板组合等。\n- 后端选择:Python 或 R;第一次选择后会作为默认偏好复用。\n- 数据完整性:默认保留全部观测和指定变量,任何排除都记录规则与前后计数。\n- 模板兼容性:先核对科学含义、数据结构和变换条件,再决定精确复用、结构适配或只继承样式。\n- 投稿约束:尺寸、字体、色彩、分辨率、矢量格式和 source-data 可追溯性。\n- 子图对齐门:由 Python axes 或 R patchwork/gtable 在最终尺寸下测量真实 panel 矩形;横排 3/4 张等跨度 panel 自动保证等宽,普通网格及“左二右一 / 左一右二”等跨行布局自动推断,可靠错位阻断导出,free-positioned hero panel、inset 和 colorbar 只能带理由豁免。\n- 渲染碰撞门:最终 PDF 每次重绘后都生成 collision JSON;可靠碰撞必须修复,可疑 overlay 必须逐项复核。\n\n## 典型请求\n\n- “把这组数据做成 Nature 风格多面板图,优先 Python。”\n- “参考 figures4papers 里 Nature Machine Intelligence 的布局,帮我补一个方法对比图。”\n- “重画这个机制示意图,导出 SVG/PDF,并给我 source data 表。”\n- “用 OpenRouter 生成 graphical abstract 草稿,但不要当作定量数据图。”\n\n## 示例预览\n\n| 方向 | 预览 | 可借鉴模式 |\n|------|------|------------|\n| 多面板论文图 | \u003ca href=\"assets/gallery/fig1-material-mechanism-rich.png\">\u003cimg src=\"assets/gallery/fig1-material-mechanism-rich.png\" width=\"220\" alt=\"Material design and physical validation\">\u003c/a> | 机制示意、图像面板、定量结果和相关性放在同一证据链中 |\n| 图表类型 atlas | \u003ca href=\"assets/chart-atlas/atlas-03-heatmaps.png\">\u003cimg src=\"assets/chart-atlas/atlas-03-heatmaps.png\" width=\"220\" alt=\"Heatmap atlas\">\u003c/a> | 热图、注释矩阵、聚类块和发散色标的组合模式 |\n| 第三方 figures4papers 参考 | \u003ca href=\"assets/figures4papers/figure_VIGIL/figures/comparison_radar.png\">\u003cimg src=\"assets/figures4papers/figure_VIGIL/figures/comparison_radar.png\" width=\"220\" alt=\"VIGIL comparison radar\">\u003c/a> | 仅用于研究 layout、legend 和多指标比较语法;使用前阅读独立版权说明 |\n\n## 你需要提供\n\n- 原始数据、已有图、图注、论文 claim 或想表达的机制。\n- 目标期刊、单栏/双栏尺寸、输出格式和是否需要 source data。\n- Python / R 偏好;如果没有偏好,技能会先询问或沿用本机记录。\n\n## 产出\n\n- 可运行的 Python 或 R 绘图脚本。\n- SVG/PDF/TIFF/PNG 等图件文件,优先保留可编辑矢量版本。\n- 面板说明、source data 映射、排除计数、逐面板视觉审查表、alignment JSON/诊断 SVG、碰撞 JSON/诊断 PDF 和投稿前 QA 记录。\n- AI 示意图任务中,输出概念草稿和需要人工重画/核实的元素列表。\n\n## 内置参考\n\n- `references/api.md`:Python 配色、样式和绘图 helper 约定。\n- `references/asset-adaptation.md`:模板语义匹配、字段映射和数据完整性规则。\n- `references/multipanel-evidence-architecture.md`:从 Results 级问题到 panel 证据角色、图内闭环、跨 Figure claim escalation 和主图/Extended Data/SI 去向的规划与审计。\n- `references/template-catalog.md`:volcano、ROC、marker dot plot、marginal 和 paired 的已验证 Python CSV 模板。\n- `references/chart-types.md`:常见图型选择和视觉规则。\n- `references/demos.md`:第三方 `figures4papers` 示例索引、使用边界和原创适配模式。\n- `references/qa-contract.md`:导出前检查项、source-data 约束和静态预检入口。\n- `references/ai-graphical-abstract-workflow.md`:AI 图形摘要的信息简报、构图与配色、期刊政策门、人工科学核验、披露和 provenance 工作流。\n- `references/openrouter-image-generation.md`:OpenRouter / GPT Image 2 的 provider-specific 生成与 QA 路径。\n- `references/vector-schematic-workflow.md`:手工绘制可编辑 SVG 架构图、流程图和机制图的“先确认简报、再绘制、再终检”工作流。\n- `references/vector-schematic-style-rules.md`:SVG 示意图的字体、配色、线条、容器、图例和常见错误规范。\n- `scripts/audit_svg_schematic.py`:按最终尺寸检查 SVG 文字字号、画布宽度、画布外文字和禁用元素。\n- `scripts/validate_figure.py`:Python/R 绘图源码的可复现静态 QA。\n- `scripts/audit_pdf_text.py`:扫描导出 PDF 的 `Tf` 操作符,发现 mathtext 上下标等低于 5 pt 的实际字形。\n- `scripts/audit_panel_alignment.py` 与 `scripts/panel_alignment.R`:在最终物理尺寸下测量 Matplotlib axes 或 R patchwork/gtable,对同行 3/4 panel 等宽、同行/同列边界、跨行面板共享外边界、panel 标签和重复间距执行阻断式自动审计。\n- `scripts/audit_figure_collisions.py`:对 Python/R 最终 PDF 执行自动几何碰撞审计,输出阻断性 FAIL、需复核 WARN、JSON 报告和可选带框诊断 PDF。\n- `scripts/figure_safety.py`:严格单调插值和基于数据/误差范围的标签高度 helper。\n- `assets/figures4papers/`:保留的第三方参考脚本与预览图;不自动适用本仓库 MIT License,使用前阅读 `THIRD_PARTY_NOTICES.md`。\n\n## 边界\n\n- 不会把 AI 生成图片当作真实实验结果或定量数据面板。\n- 不会把内部可用的 AI 草稿自动称为可投稿终稿;两者分别判定。\n- 不会凭空补统计检验、样本量、误差线含义或实验条件。\n- 不会为了渲染方便静默抽样、忽略变量或删除不完整观测。\n- 不会把自动校验通过当作视觉验收;对齐门只保证已声明的几何关系在容差内,最终交付仍需逐面板检查不确定性、标签碰撞、间距和显著性层级。\n- 私有模板可以在本机使用,但不应在面向用户输出中暴露私有路径、文件名或来源。\n- 第三方参考材料的版权和再使用条件以其来源及 `THIRD_PARTY_NOTICES.md` 为准;本仓库不额外授予这些文件的使用权。\n\n自动碰撞审计需要 PyMuPDF:\n\n```bash\npython -m pip install -r skills/nature-figure/requirements.txt\n```\n\n## 相关技能\n\n- `nature-statistics`:检查统计标注、n 定义和 p 值表述。\n- `nature-writing`:把图件结论放回手稿叙事。\n- `nature-paper2ppt`:把论文图件整理成汇报幻灯片。\n\n## 与其他技能的关系\n\n- 如果任务核心是统计解释、样本量定义或显著性表述,优先让 `nature-statistics` 先把文字审清,再回到 `nature-figure` 画图。\n- 如果图件已经定稿,但需要把结论组织成摘要、引言或结果段落,交给 `nature-writing` 继续承接。\n- 如果图件要直接转成组会材料或答辩汇报,再交给 `nature-paper2ppt` 组织成页面。\n- `nature-figure` 负责图件本身;它不替代统计审查,也不替代手稿叙事。\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9387,"content_sha256":"ecfb4230aa2dd65dc5c892534d414a3fc74eb3c7d746ee7e414ee165c749d98c"},{"filename":"references/ai-graphical-abstract-workflow.md","content":"# AI-Assisted Graphical Abstract Workflow\n\nUse this reference whenever a task involves planning, generating, revising, or\nauditing a graphical abstract with AI assistance. Apply it before any\nprovider-specific image-generation instructions.\n\n## Contents\n\n- [Authority boundary and policy gate](#authority-boundary-and-policy-gate)\n- [1. Define the communication target](#1-define-the-communication-target)\n- [2. Build a visual brief](#2-build-a-visual-brief)\n- [3. Assign AI a bounded role](#3-assign-ai-a-bounded-role)\n- [4. Write the prompt as a figure contract](#4-write-the-prompt-as-a-figure-contract)\n- [5. Run human scientific and publication QA](#5-run-human-scientific-and-publication-qa)\n- [Sources and status](#sources-and-status)\n\n## Authority boundary and policy gate\n\n- Treat Ananya Thakur's 22 July 2026 *Nature* Careers column as practitioner\n guidance, not as a Nature Portfolio submission policy or blanket permission\n to publish AI-generated artwork.\n- Before generating a submission candidate, verify the target journal's current\n graphical-abstract, artificial-intelligence, image-integrity, copyright, and\n disclosure rules on official pages. Record the journal, URL, and access date.\n- For a Nature Portfolio target, apply the current risk-assessment framework:\n assistive use can support expression or organization; interpretive or\n generative use requires human oversight, verification, and transparent\n disclosure; opaque, unverifiable, confidentiality-breaching, or\n judgement-replacing use is not permitted. A journal may impose stricter\n rules.\n- If journal clearance is unknown, label the output **internal design draft —\n submission eligibility unverified**. Do not call it submission-ready.\n- Keep two decisions separate: whether a draft is useful internally and whether\n the final asset is eligible for submission.\n\n## 1. Define the communication target\n\nBefore choosing a tool or visual style, write down:\n\n1. the single sentence the reader should remember\n2. the figure type: mechanism, process, experimental setup, workflow,\n comparison, timeline, cycle, or branching decision\n3. the intended audience and its expected vocabulary\n4. the evidence boundary: what the study demonstrates, what is contextual, and\n what must not be implied\n5. the details to omit because they do not support the central message\n\nDo not ask an image model to summarize an entire manuscript into a final image\nwithout this brief.\n\n## 2. Build a visual brief\n\n- Inspect graphical abstracts from comparable papers for information density,\n reading order, and composition. Learn the visual grammar; do not copy\n protected artwork, icons, or distinctive layouts.\n- Choose an explicit reading path: left-to-right, top-to-bottom, cycle, split\n comparison, or fork. Use arrows and grouping only when they clarify that path.\n- Use a limited, consistent, high-contrast, color-accessible palette. Assign\n color by scientific meaning, not decoration, and do not rely on color alone.\n- Reserve the strongest accent for the central causal step or principal result.\n Keep labels short and plan their placement before generating artwork.\n- Prefer one representative pathway over an exhaustive interaction network when\n the fuller network would obscure the central claim. Preserve necessary caveats\n in the legend or manuscript.\n\n## 3. Assign AI a bounded role\n\nUse AI to assist with tasks such as:\n\n- distilling author-provided claims into candidate one-sentence messages\n- comparing audience-specific levels of terminology and detail\n- proposing several compositions or label placements\n- checking palette contrast and color accessibility\n- turning an author-owned sketch into a draft layout or vectorization plan\n- brainstorming pictorial metaphors that will be scientifically reviewed\n\nDo not let AI invent measurements, mechanisms, structures, clinical effects,\ncitations, labels, or evidence. Do not upload confidential manuscripts, peer\nreview material, sensitive data, or unpublished images to an unsecured or public\nAI service without explicit authorization.\n\n## 4. Write the prompt as a figure contract\n\nInclude these fields in a provider-neutral prompt:\n\n- **Purpose and audience**\n- **One-sentence message**\n- **Figure type and reading order**\n- **Required entities and relationships**\n- **Evidence boundary and forbidden implications**\n- **Composition and focal element**\n- **Palette, contrast, and accessibility constraints**\n- **Exact short labels**\n- **Elements to exclude**, including fabricated numbers, logos, and decorative\n scientific objects\n- **Output role**, explicitly stating `concept draft` when generative AI is used\n\nGenerate layout alternatives before polishing a single composition. Correct\nscientific structure first, then typography, color, and visual finish. Redraw\ncritical text, arrows, chemical structures, and quantitative marks with\ndeterministic or editable tools whenever possible.\n\n## 5. Run human scientific and publication QA\n\nBefore delivery, check every visual element against the manuscript and source\ndata:\n\n- scientific entities, directions, causal links, scale, anatomy, and chronology\n- all labels, abbreviations, spelling, units, and symbol definitions\n- absence of invented data, unsupported effects, misleading realism, or\n decorative evidence\n- readable hierarchy at final size and a color-blindness-safe reading path\n- originality, licenses, permissions, attribution, and resemblance to source art\n- target-journal eligibility, disclosure wording, and figure-legend treatment\n\nRetain a provenance bundle containing the prompt, model/provider and version,\ngeneration date, reference-image rights, generated candidates, selected output,\nand a log of manual corrections. Human authors remain accountable for the final\nasset.\n\n## Sources and status\n\n- Practitioner workflow: Ananya Thakur, “How to use AI to make a graphical\n abstract in minutes,” *Nature* Careers, 22 July 2026,\n \u003chttps://doi.org/10.1038/d41586-026-02072-9>.\n- Governing portfolio policy: Nature Portfolio, “Artificial Intelligence (AI),”\n \u003chttps://www.nature.com/nature-portfolio/editorial-policies/ai>, verified\n 15 August 2026. Recheck before each submission because the policy is reviewed\n periodically.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6263,"content_sha256":"258c59085ef3eaf28dcda8bf00bab5d3235f0736be1af652911de1af8aa14c4f"},{"filename":"references/api.md","content":"# API Reference — Nature Figure Making\n\n## Contents\n\n- [Constants](#constants)\n- [MANDATORY font + SVG rules (always first, no exceptions)](#mandatory-font-svg-rules-always-first-no-exceptions)\n- [apply_publication_style()](#apply_publication_style)\n- [is_dark(hex_color, threshold=128)](#is_darkhex_color-threshold128)\n- [add_panel_label(ax, label, ...)](#add_panel_labelax-label)\n- [style_dark_image_ax(ax, ...)](#style_dark_image_axax)\n- [make_grouped_bar(ax, categories, series, labels, ...)](#make_grouped_barax-categories-series-labels)\n- [make_trend(ax, x, y_series, labels, ...)](#make_trendax-x-y_series-labels)\n- [make_forest_plot(ax, labels, estimates, ci_low, ci_high, ...)](#make_forest_plotax-labels-estimates-ci_low-ci_high)\n- [make_heatmap(ax, matrix, ...)](#make_heatmapax-matrix)\n- [Numerical and annotation safety helpers](#numerical-and-annotation-safety-helpers)\n- [require_matplotlib_panel_alignment(fig, ...)](#require_matplotlib_panel_alignmentfig-)\n- [finalize_figure(fig, out_path, ...)](#finalize_figurefig-out_path)\n- [Validation Rules](#validation-rules)\n- [Conventions](#conventions)\n\n\nConventions, constants, and reusable code blocks. Implement in your script or adapt as needed.\n\n---\n\n## Constants\n\n### PALETTE\n\n```python\nPALETTE = {\n \"blue_main\": \"#0F4D92\",\n \"blue_secondary\": \"#3775BA\",\n \"green_1\": \"#DDF3DE\",\n \"green_2\": \"#AADCA9\",\n \"green_3\": \"#8BCF8B\",\n \"red_1\": \"#F6CFCB\",\n \"red_2\": \"#E9A6A1\",\n \"red_strong\": \"#B64342\",\n \"neutral_light\": \"#CFCECE\",\n \"neutral_mid\": \"#767676\",\n \"neutral_dark\": \"#4D4D4D\",\n \"neutral_black\": \"#272727\",\n \"gold\": \"#FFD700\",\n \"teal\": \"#42949E\",\n \"violet\": \"#9A4D8E\",\n \"magenta\":\"#EA84DD\",\n}\n\nDEFAULT_COLORS = [\n PALETTE[\"blue_main\"],\n PALETTE[\"green_3\"],\n PALETTE[\"red_strong\"],\n PALETTE[\"teal\"],\n PALETTE[\"violet\"],\n PALETTE[\"neutral_light\"],\n]\n\nPALETTE_NMI_PASTEL = {\n \"baseline_dark\": \"#484878\",\n \"baseline_mid\": \"#7884B4\",\n \"baseline_soft\": \"#B4C0E4\",\n \"ours_tiny\": \"#E4E4F0\",\n \"ours_base\": \"#E4CCD8\",\n \"ours_large\": \"#F0C0CC\",\n \"bg_lilac\": \"#E0E0F0\",\n \"bg_aqua\": \"#E0F0F0\",\n \"bg_peach\": \"#F0E0D0\",\n \"neutral_light\": \"#D8D8D8\",\n \"neutral_mid\": \"#A8A8A8\",\n \"neutral_dark\": \"#606060\",\n \"delta_up\": \"#2E9E44\",\n \"delta_down\": \"#E53935\",\n}\n\nDEFAULT_COLORS_NMI_PASTEL = [\n PALETTE_NMI_PASTEL[\"baseline_dark\"],\n PALETTE_NMI_PASTEL[\"baseline_mid\"],\n PALETTE_NMI_PASTEL[\"baseline_soft\"],\n PALETTE_NMI_PASTEL[\"ours_tiny\"],\n PALETTE_NMI_PASTEL[\"ours_base\"],\n PALETTE_NMI_PASTEL[\"ours_large\"],\n]\n\nPALETTE_NATURE_IMAGING = {\n \"bg\": \"#000000\",\n \"context\": \"#B8B8B8\",\n \"cyan\": \"#22D7E6\",\n \"magenta\": \"#FF2AD4\",\n \"white\": \"#FFFFFF\",\n}\n\nPALETTE_NATURE_MATERIAL = {\n \"aqua\": \"#77D7D1\",\n \"teal\": \"#33B5A5\",\n \"lilac\": \"#B9A7E8\",\n \"violet\": \"#7C6CCF\",\n \"callout_red\": \"#E53935\",\n \"neutral\": \"#D9D9D9\",\n}\n\nPALETTE_NATURE_CLINICAL = {\n \"baseline\": \"#272727\",\n \"week6\": \"#E28E2C\",\n \"week13\": \"#D24B40\",\n \"week26\": \"#5B8FD6\",\n \"year1\": \"#7BAA5B\",\n \"year2\": \"#C45AD6\",\n \"group_band\": \"#F2E6D9\",\n}\n\nPALETTE_NATURE_GENOMICS = {\n \"neutral_light\": \"#D8D8D8\",\n \"neutral_mid\": \"#8F8F8F\",\n \"wave1\": \"#D9544D\",\n \"wave2\": \"#5B7FCA\",\n \"wave3\": \"#B89BD9\",\n \"outline\": \"#4D4D4D\",\n}\n```\n\nUse `DEFAULT_COLORS` when color itself carries explicit semantic meaning (`hero`, `baseline`, `positive variant`).\nUse `DEFAULT_COLORS_NMI_PASTEL` when several compared methods belong to one or two related families and the page\nshould feel visually unified.\n\n---\n\n## MANDATORY font + SVG rules (always first, no exceptions)\n\nThese three lines are **non-negotiable** and must appear at the top of every script,\nbefore any figure is created. They guarantee editable text in SVG output:\n\n```python\nplt.rcParams['font.family'] = 'sans-serif'\nplt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Liberation Sans']\nplt.rcParams['svg.fonttype'] = 'none' # keeps text as \u003ctext> nodes, not paths\n```\n\n**Why `svg.fonttype = 'none'`**: matplotlib's default (`'path'`) converts every\nglyph to a bezier path, making text unselectable, unsearchable, and impossible to\nre-align in Illustrator / Inkscape. With `'none'`, text stays as SVG `\u003ctext>` elements\nand font substitution happens at render time.\n\n**Output format**: always save as `.svg` (primary). PNG/PDF are optional secondary\nexports. Never use `.png` alone when the figure contains text that may need adjustment.\n\n---\n\n## apply_publication_style()\n\n```python\ndef apply_publication_style(font_size=16, axes_linewidth=2.5, use_tex=False):\n \"\"\"Apply Nature-style rcParams. Call once before creating any figures.\"\"\"\n # ── MANDATORY: editable SVG text ──────────────────────────────────────────\n plt.rcParams['font.family'] = 'sans-serif'\n plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Liberation Sans']\n plt.rcParams['svg.fonttype'] = 'none'\n # ── Layout & style ────────────────────────────────────────────────────────\n plt.rcParams['font.size'] = font_size\n plt.rcParams['axes.spines.right'] = False\n plt.rcParams['axes.spines.top'] = False\n plt.rcParams['axes.linewidth'] = axes_linewidth\n plt.rcParams['legend.frameon'] = False\n if use_tex:\n plt.rcParams['text.usetex'] = True\n```\n\n**Presets:**\n- Large bar panels: `apply_publication_style(font_size=24, axes_linewidth=3)`\n- Compact figures: `apply_publication_style(font_size=15, axes_linewidth=2)`\n- Dense journal-width multi-panels: `apply_publication_style(font_size=8, axes_linewidth=1)`\n- LaTeX labels: `apply_publication_style(use_tex=True)`\n\n---\n\n## is_dark(hex_color, threshold=128)\n\n```python\ndef is_dark(hex_color, threshold=128):\n \"\"\"Return True if hex color is dark (use white text on it).\"\"\"\n c = hex_color.lstrip('#')\n r, g, b = int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)\n return (0.299*r + 0.587*g + 0.114*b) \u003c threshold\n```\n\n---\n\n## add_panel_label(ax, label, ...)\n\n```python\ndef add_panel_label(ax, label, x=0, y=1, x_offset_pt=-4, y_offset_pt=3,\n fontsize=8, color='black', fontweight='bold', va='bottom'):\n \"\"\"Place a panel label with a fixed physical offset from an axes anchor.\"\"\"\n from matplotlib.transforms import ScaledTranslation\n offset = ScaledTranslation(\n x_offset_pt / 72,\n y_offset_pt / 72,\n ax.figure.dpi_scale_trans,\n )\n ax.text(\n x, y, label,\n transform=ax.transAxes + offset,\n fontsize=fontsize,\n fontweight=fontweight,\n color=color,\n ha='left',\n va=va,\n )\n```\n\nThe point offset keeps labels aligned when one panel spans two rows and its\nneighbouring axes are shorter; a shared axes-fraction offset such as `y=1.02`\nwould produce different physical displacements. For dark image plates, move\nthe label inside the panel and switch to white:\n`add_panel_label(ax, 'a', x=0.01, y=0.98, x_offset_pt=0, y_offset_pt=0, color='white', va='top')`\n\n---\n\n## style_dark_image_ax(ax, ...)\n\n```python\ndef style_dark_image_ax(ax, facecolor='black'):\n \"\"\"Prepare an axes for microscopy / rendering plates.\"\"\"\n ax.set_facecolor(facecolor)\n ax.set_xticks([])\n ax.set_yticks([])\n for spine in ax.spines.values():\n spine.set_visible(False)\n return ax\n```\n\n---\n\n## make_grouped_bar(ax, categories, series, labels, ...)\n\n```python\ndef make_grouped_bar(ax, categories, series, labels,\n ylabel='Value', colors=None,\n annotate=False, bar_width=0.8,\n series_spread=None, error_kw=None):\n \"\"\"\n Grouped bar chart.\n\n Parameters\n ----------\n ax : matplotlib Axes\n categories : list[str] — x-axis category names (length K)\n series : list[array] — one array per group (each length K)\n labels : list[str] — legend label per group\n ylabel : str\n colors : list[str] | None — defaults to DEFAULT_COLORS; override with\n DEFAULT_COLORS_NMI_PASTEL for unified-family figures\n annotate : bool — print value above each bar\n bar_width : float — total width for all bars in one category\n error_kw : dict — passed to ax.bar as error_kw\n series_spread : list[array] | None — one uncertainty array per series\n\n Returns\n -------\n list[BarContainer]\n \"\"\"\n import numpy as np\n if colors is None:\n colors = DEFAULT_COLORS\n if error_kw is None:\n error_kw = {'elinewidth': 2, 'capthick': 2, 'capsize': 10}\n n_groups = len(series)\n n_cats = len(categories)\n w = bar_width / n_groups\n x = np.arange(n_cats)\n flat_values = np.concatenate([np.asarray(values, dtype=float) for values in series])\n label_pad = 0.02 * max(float(np.ptp(flat_values)), float(np.max(np.abs(flat_values))), 1.0)\n containers = []\n for i, (vals, label, color) in enumerate(zip(series, labels, colors)):\n spread = None if series_spread is None else np.asarray(series_spread[i], dtype=float)\n offset = (i - (n_groups - 1) / 2) * w\n bars = ax.bar(x + offset, vals, width=w, label=label,\n color=color, edgecolor='black', linewidth=1.5,\n yerr=spread, error_kw=error_kw)\n containers.append(bars)\n if annotate:\n for j, (bar, val) in enumerate(zip(bars, vals)):\n upper = bar.get_height() + (0 if spread is None else spread[j])\n ax.text(bar.get_x() + bar.get_width() / 2,\n upper + label_pad,\n f'{val:.2f}', ha='center', va='bottom', fontsize=10)\n ax.set_xticks(x)\n ax.set_xticklabels(categories)\n ax.set_ylabel(ylabel)\n ax.legend()\n return containers\n```\n\n---\n\n## make_trend(ax, x, y_series, labels, ...)\n\n```python\ndef make_trend(ax, x, y_series, labels,\n colors=None, ylabel=None, xlabel=None,\n show_shadow=True, shadow_alpha=0.15,\n lw=2.5, marker='o', markersize=8):\n \"\"\"\n Multi-line trend plot.\n\n Parameters\n ----------\n x : array-like — shared x values\n y_series : list[array] — one 1D array per line\n labels : list[str]\n show_shadow : bool — fill_between ± std if y_series contains 2D arrays (rows=runs)\n \"\"\"\n import numpy as np\n if colors is None:\n colors = DEFAULT_COLORS\n for y, label, color in zip(y_series, labels, colors):\n y = np.asarray(y)\n if y.ndim == 2:\n mean, std = y.mean(0), y.std(0)\n else:\n mean, std = y, None\n ax.plot(x, mean, color=color, lw=lw, marker=marker,\n markersize=markersize, label=label)\n if show_shadow and std is not None:\n ax.fill_between(x, mean - std, mean + std,\n color=color, alpha=shadow_alpha)\n if ylabel:\n ax.set_ylabel(ylabel)\n if xlabel:\n ax.set_xlabel(xlabel)\n ax.legend()\n```\n\n---\n\n## make_forest_plot(ax, labels, estimates, ci_low, ci_high, ...)\n\n```python\ndef make_forest_plot(ax, labels, estimates, ci_low, ci_high,\n colors=None, ref=0.0, xlabel=None, xlim=None,\n marker='o', markersize=5, lw=1.5):\n \"\"\"\n Minimal forest plot helper for Nature-style clinical/statistical panels.\n \"\"\"\n import numpy as np\n y = np.arange(len(labels))[::-1]\n if colors is None:\n colors = ['#B64342'] * len(labels)\n for yi, est, lo, hi, color in zip(y, estimates, ci_low, ci_high, colors):\n ax.plot([lo, hi], [yi, yi], color=color, lw=lw)\n ax.plot(est, yi, marker=marker, ms=markersize, color=color)\n ax.axvline(ref, color='#767676', linestyle='--', linewidth=1.2, alpha=0.8)\n ax.set_yticks(y)\n ax.set_yticklabels(labels)\n if xlabel:\n ax.set_xlabel(xlabel)\n if xlim is not None:\n ax.set_xlim(xlim)\n ax.spines['right'].set_visible(False)\n ax.spines['top'].set_visible(False)\n```\n\nUse pale `ax.axhspan(...)` bands behind contiguous label groups when you need the\nclinical-triptych look from `Nature`.\n\n---\n\n## make_heatmap(ax, matrix, ...)\n\n```python\ndef make_heatmap(ax, matrix, x_labels=None, y_labels=None,\n cmap='magma', cbar_label=None, annotate=False,\n fmt='{:.2f}', fontsize=12):\n \"\"\"\n 2D heatmap with optional colorbar and cell annotations.\n \"\"\"\n import numpy as np\n import matplotlib as mpl\n im = ax.imshow(matrix, cmap=cmap, aspect='auto')\n if cbar_label:\n cbar = ax.figure.colorbar(im, ax=ax)\n cbar.set_label(cbar_label)\n if x_labels:\n ax.set_xticks(range(len(x_labels)))\n ax.set_xticklabels(x_labels, rotation=30, ha='right', rotation_mode='anchor')\n if y_labels:\n ax.set_yticks(range(len(y_labels)))\n ax.set_yticklabels(y_labels)\n if annotate:\n norm = mpl.colors.Normalize(vmin=matrix.min(), vmax=matrix.max())\n cm_obj = plt.get_cmap(cmap)\n for (i, j), val in np.ndenumerate(matrix):\n r, g, b, _ = cm_obj(norm(val))\n lum = 0.299*r + 0.587*g + 0.114*b\n color = 'white' if lum \u003c 0.5 else 'black'\n ax.text(j, i, fmt.format(val), ha='center', va='center',\n fontsize=fontsize, color=color)\n ax.set_frame_on(False)\n```\n\n---\n\n## Numerical and annotation safety helpers\n\nCopy `scripts/figure_safety.py` beside the plotting script, or add that scripts directory to `PYTHONPATH`. Then import the tested helpers rather than calling `np.interp` directly on a curve whose direction has not been asserted:\n\n```python\nfrom figure_safety import interp_monotone, label_y_above\n\nn_equivalent = interp_monotone(target_error, error_curve, example_counts)\nlabel_y = label_y_above(metric_center, metric_spread)\n```\n\n`interp_monotone` accepts strictly increasing or decreasing `xp`, reverses a decreasing grid and its paired values together, and rejects duplicates/direction changes. `label_y_above` places a shared annotation above the upper uncertainty extent rather than relying on a fixed `LABEL_Y`.\n\nFor compact mathematical labels, prefer supported Unicode glyphs such as `R²` over `$R^2 nature-figure — Skillopedia when the meaning is unchanged, then audit the exported PDF:\n\n```bash\npython skills/nature-figure/scripts/audit_pdf_text.py figure.pdf --min-pt 5\npython skills/nature-figure/scripts/audit_figure_collisions.py figure.pdf \\\n --json-out figure.collision-audit.json \\\n --overlay-pdf figure.collision-audit.pdf\n```\n\n---\n\n## require_matplotlib_panel_alignment(fig, ...)\n\nFor every multi-panel figure, measure the final Matplotlib axes positions after\nthe last `tight_layout`, constrained-layout draw, legend, colorbar or manual\nposition change and before saving. Copy `scripts/audit_panel_alignment.py`\nbeside the plotting source or add the skill's scripts directory to\n`PYTHONPATH`, then use the blocking helper:\n\n```python\nfrom audit_panel_alignment import require_matplotlib_panel_alignment\n\nalignment = require_matplotlib_panel_alignment(\n fig,\n json_out=\"figures/figure.alignment.json\",\n overlay_svg=\"figures/figure.alignment.svg\",\n tolerance_pt=1.5,\n gutter_tolerance_pt=1.5,\n require_panel_labels=True,\n strict=True,\n)\n```\n\nThe helper calls `fig.canvas.draw()` and converts final axes positions to\nphysical points. Ordinary `plt.subplots()` and `GridSpec` layouts infer row and\ncolumn groups from `SubplotSpec`, then check:\n\n- common top/bottom edges and equal heights within each row;\n- equal final widths for three or four same-row panels with equal grid spans;\n- common left/right edges and equal widths within each column;\n- shared top/bottom or left/right grid boundaries when adjacent panels have\n unequal spans, including `left two + right one` and `left one + right two`;\n- repeated horizontal/vertical gutters;\n- bold lowercase top-left panel-label anchors when detectable;\n- plot-area rectangle overlap.\n\nOrdinary unequal-span `GridSpec` layouts are inferred automatically: a two-row\nspanning panel is compared with the appropriate upper and lower small panels at\nthe shared outer edges, without imposing an invalid equal-height comparison.\nFor ordinary horizontal `1 × 3` and `1 × 4` grids, equal column spans must also\nproduce equal final plot-area widths. An intentional unequal `width_ratios`\nlayout needs a panel-specific `panel-width` exemption with a reason.\nFor nested or separately created grids, declare intended comparisons instead\nof asking the auditor to guess them:\n\n```python\nrequire_matplotlib_panel_alignment(\n fig,\n axes=[ax_a, ax_b, ax_c, ax_d],\n panel_ids=[\"a\", \"b\", \"c\", \"d\"],\n row_groups=[[\"a\", \"b\"], [\"c\", \"d\"]],\n column_groups=[[\"a\", \"c\"], [\"b\", \"d\"]],\n exclude_axes=[colorbar.ax, inset_ax],\n exemptions=[\n {\n \"panels\": [\"a\"],\n \"checks\": [\"column\", \"panel-width\"],\n \"reason\": \"hero panel intentionally spans two grid columns\",\n }\n ],\n json_out=\"figures/figure.alignment.json\",\n strict=True,\n)\n```\n\nEvery exemption needs panel ids, exact checks and a reason. Prefer omitting a\nhero panel, inset, legend-only axis or colorbar from unrelated comparison groups\nover globally increasing the `1.5 pt` tolerance. Exit-equivalent failures raise\n`PanelAlignmentError`; do not catch and ignore it in a delivery script. A\nsingle-panel figure is outside this gate, while a multi-panel figure without\nvalid comparable groups is `NOT AUDITABLE`, not a pass.\n\nThe optional SVG contains measured rectangles only and is QA-only. Preserve\nthe JSON with the figure bundle, then run PDF glyph and collision QA on the\nexported figure.\n\n---\n\n## finalize_figure(fig, out_path, ...)\n\n```python\ndef finalize_figure(fig, out_path, formats=None, dpi=300,\n pad=2, bbox_inches=None, close=True,\n alignment_options=None):\n \"\"\"\n Apply tight_layout and save figure. For multi-panel figures, run\n require_matplotlib_panel_alignment() after this layout operation and before\n the save loop; do not close the figure before alignment QA.\n\n Parameters\n ----------\n out_path : str — path without extension, or with extension\n formats : list — e.g. ['png', 'pdf']. If None, uses extension of out_path.\n dpi : int — 300 standard, 600 for dense bar panels\n pad : float — tight_layout pad (2 default, 1 for compact multi-panel)\n alignment_options : dict | None — explicit groups/exemptions for unusual layouts\n \"\"\"\n import os\n from pathlib import Path\n base = Path(out_path)\n if formats is None:\n formats = [base.suffix.lstrip('.') or 'png']\n base = base.with_suffix('')\n os.makedirs(base.parent, exist_ok=True)\n fig.tight_layout(pad=pad)\n options = {} if alignment_options is None else dict(alignment_options)\n require_matplotlib_panel_alignment(\n fig,\n json_out=str(base) + \".alignment.json\",\n overlay_svg=str(base) + \".alignment.svg\",\n strict=True,\n **options,\n )\n saved = []\n for fmt in formats:\n p = str(base) + f'.{fmt}'\n kw = {}\n if bbox_inches is not None:\n kw['bbox_inches'] = bbox_inches\n fig.savefig(p, dpi=dpi, **kw)\n saved.append(p)\n if close:\n plt.close(fig)\n return saved\n```\n\n---\n\n## Validation Rules\n\n- `make_grouped_bar`: `len(categories)` must equal length of each array in `series`.\n- `make_grouped_bar`: if `series_spread` is supplied, it must mirror `series`; annotations clear `value + spread`.\n- `make_trend`: each array in `y_series` must have same length as `x`.\n- `make_trend`: 2D run/seed arrays show a standard-deviation band by default; use a different definition only when explicitly justified and documented.\n- `make_heatmap`: `matrix` must be 2D; `x_labels` length = `matrix.shape[1]`; `y_labels` length = `matrix.shape[0]`.\n- `finalize_figure`: supported formats — `png`, `pdf`, `svg`, `eps`, `jpg`, `tif`.\n\n---\n\n## Conventions\n\n- Save outputs under `./figures/` (or path given by user); `finalize_figure` creates parent dirs.\n- In headless / batch runs, set non-interactive backend before importing pyplot:\n ```python\n import matplotlib\n matplotlib.use('Agg')\n import matplotlib.pyplot as plt\n ```\n- Always `plt.close(fig)` after saving to free memory.\n- For multi-panel figures, prefer one baseline family plus one hero family; reserve green/red for delta cues.\n- When color roles, resolution, or layout are underspecified and would change the figure, confirm with user before finalizing.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20759,"content_sha256":"1ec51840fdc45017735511b1275cada5e09f5d3e7dc44f35b2d8ba052809a4bc"},{"filename":"references/asset-adaptation.md","content":"# Plotting Asset Adaptation\n\nUse this reference when reusing a bundled example, a preview image, or a user-provided plotting script. Treat examples as visual and structural starting points, not as evidence that a script is compatible with new data.\n\n## Choose the reuse level\n\nAssign every candidate to one of four levels before editing it:\n\n| Level | Use when | Allowed changes |\n|---|---|---|\n| Exact reuse | Scientific meaning, data shape, transformations, and backend all match | Input path, labels, and output prefix only |\n| Structural adaptation | Scientific meaning and dimensionality match, but field names or group labels differ | Explicit field mapping plus documented transform guards |\n| Style-only inheritance | The plot family is useful but the data structure or statistic differs | Palette, typography, spacing, marker, legend, and annotation conventions only |\n| Build anew | The candidate answers a different question or would require replacing its statistical logic | Do not force the template; implement the confirmed figure contract directly |\n\nDo not call a script production-ready merely because it renders its bundled example.\n\n## Inspect before mapping\n\n1. Open the companion preview when one exists.\n2. State what the candidate actually displays: dimensionality, mark type, grouping, statistic, uncertainty, transforms, and annotations.\n3. State what the requested panel must answer.\n4. Reject structural reuse when those meanings differ. A 2D joint-density plot is not a reusable implementation of several 1D marginal densities, and a benchmark bar chart is not automatically a valid small-sample biological comparison.\n\n## Map the data contract\n\nWrite an explicit mapping before changing code:\n\n```text\ntemplate field -> user field -> role -> units -> allowed values\ngroup field -> user field -> category order\nreplicate unit -> source rows/images -> biological or technical\nuncertainty -> source field or calculation -> definition\n```\n\nConfirm ambiguous mappings with the user. Never choose convenient columns silently. Keep identifiers separate from measurements and preserve the requested category order unless a scientifically justified ordering is declared.\n\n## Guard transformations\n\nCheck every inherited transformation against the new data:\n\n- Log axes and logarithms require strictly positive values unless a declared signed-log or pseudocount method is scientifically justified.\n- Ratios and normalized values require finite denominators and a defined zero-denominator policy.\n- Square-root transforms require non-negative inputs.\n- Min-max scaling requires non-constant finite ranges.\n- Binning and density estimation require enough distinct observations; record bin or bandwidth choices.\n- Correlation, PCA, clustering, and statistical annotations require explicit missing-value handling and an appropriate replicate unit.\n\nIf a guard fails, change the transformation only when the scientific meaning remains valid and record the change. Otherwise use style-only inheritance or build anew.\n\n## Preserve data integrity\n\n- Use all supplied observations and requested variables by default.\n- Do not downsample for aesthetics or rendering speed. Use rasterization, hexbin/density marks, transparent points, aggregation with a stated rule, or backend-native large-data rendering.\n- If the analysis requires filtering, record the exact predicate and before/after row, column, replicate, or image counts.\n- When the user explicitly requests sampling, record the method, sample size, seed, and whether sampling changes any inferential claim.\n- Never leave simulated values in a production deliverable. Isolate demos behind an explicit demo flag or a separate example file.\n\n## Adapt without erasing provenance\n\nCopy the candidate into the task workspace before editing. Keep source assets unchanged. Preserve license and attribution notices, but do not expose private local paths or private template identifiers in generated figures, legends, manuscript text, or user-facing reports.\n\nRecord the reuse level and source category in internal QA notes. The adaptation method in this reference incorporates portable ideas from the Apache-2.0 `academic-figure-skill` workflow while replacing its path-bound runners and project-specific assumptions.\n\n## Validate and deliver\n\n1. Run the adapted script with representative real input using the selected backend.\n2. Run `python scripts/validate_figure.py path/to/script.py` or the corresponding `.R` file.\n3. Run `python scripts/audit_pdf_text.py path/to/figure.pdf --min-pt 5` and `python scripts/audit_figure_collisions.py path/to/figure.pdf --json-out path/to/figure.collision-audit.json` on the final export.\n4. Treat static and geometry validation as preflight only; fix collision FAIL findings and review every WARN, but do not infer statistical correctness or complete visual quality from a pass.\n5. Inspect SVG/PDF text editability, raster resolution, clipping, ambiguous overlays, color accessibility, and readability at final physical size.\n6. Include the field mapping, exclusions, transform changes, collision report, and remaining caveats in the QA notes.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5149,"content_sha256":"64c4f788962d73beba1bbd919255dffe620de3090579e7ec811e88e557ac4c3d"},{"filename":"references/backend-selection.md","content":"# Backend Selection\n\n## Contents\n\n- [Quick decision table](#quick-decision-table)\n- [Backend exclusivity rule](#backend-exclusivity-rule)\n- [Default stacks](#default-stacks)\n- [Mixed workflow rule](#mixed-workflow-rule)\n- [Recommendation language](#recommendation-language)\n\n\nBackend selection applies only to rendering or editing plotting code. Reuse a choice already established in the same task and its follow-ups; do not ask again merely because a new message omits the language. Read-only figure review and backend-independent data inspection may proceed without this choice. If the backend remains unresolved, retain the one-time Python/R question and pause only dependent plotting steps. Explicit approval requirements and backend exclusivity remain in force.\n\nAt the start of a plotting task, resolve the user's preferred backend in this order:\n\n1. explicit Python/R choice in the current request;\n2. clearly language-specific input file or workflow;\n3. choice already established in this task, then saved preference from `scripts/nature_figure_backend.py get`;\n4. if neither exists, ask **Python or R? I will remember this as your default.**\n\nPause dependent plotting steps after asking and wait for the user's answer; continue independent inspection. After the answer, save it with\n`scripts/nature_figure_backend.py set python` or `scripts/nature_figure_backend.py set r`.\nDo not infer Python just because the task involves simulation, NumPy-like data, or\ncustom layout, and do not infer R just because the task is biological or omics-adjacent.\n\nUse the decision table only in either of these cases:\n\n- the user explicitly asks you to recommend or choose the backend;\n- the user provides an unambiguous language-specific workflow or file, such as an `.R`\n script, RDS object, Python notebook, or existing Python plotting code.\n\n## Quick decision table\n\n| Recommend R when | Recommend Python when |\n|---|---|\n| The user brings R scripts, RData/RDS, Seurat objects, DESeq2/limma outputs, survival models, or ggplot templates | The data pipeline is already Python, NumPy/Pandas arrays, PyTorch/TensorFlow outputs, image arrays, or simulation output |\n| The target plot is `ggplot2`, `patchwork`, `ComplexHeatmap`, `ggtree`, `circlize`, `survminer`, `maftools`, or Seurat/UMAP-heavy | The target plot needs low-level custom layout, Matplotlib patches, image plates, subplot mosaics, or custom drawing primitives |\n| The user provides an R template collection or an existing R plotting workflow | The user wants a self-contained script with matplotlib/seaborn/statsmodels and no R dependency |\n| Heatmap annotations are biologically rich and multi-layered | Image panels and quantitative panels need tight pixel/axis control |\n\nIf either backend can do the job, honor the user's saved preference. Do not switch\nbackends for aesthetics alone. If the user explicitly switches backend, save the new\npreference.\n\n## Backend exclusivity rule\n\nBackend choice is not just a syntax preference; it defines the graphics engine for\nthe entire deliverable. Once Python or R has been selected, use that backend for\nall of the following:\n\n- plotting scripts;\n- mock/simulated data examples that include plotting;\n- preview PNG/TIFF files;\n- SVG/PDF/TIFF exports;\n- visual QA renders and final layout checks.\n\nDo not generate a substitute preview or export with the non-selected backend. For\nexample, if the user selected R and `Rscript` is missing, do not use Python/matplotlib\nto approximate the figure. If the user selected Python and `matplotlib` or another\nrequired Python plotting package is missing, do not use R/ggplot2/ComplexHeatmap to\napproximate the figure. Stop, report the selected-backend blocker, and provide the\nselected-backend script plus install/run instructions or request permission to install\nthe selected-backend dependencies.\n\nThe non-selected language is allowed only for non-visual utility work, such as\nlisting files, checking CSV dimensions, decompressing an archive, or converting a\ndata file before the selected backend draws the figure. It must not import plotting\nlibraries, open graphics devices, save image/vector files, or decide visual layout.\n\n## Default stacks\n\n### R\n\n- Core plotting: `ggplot2`\n- Multi-panel assembly: `patchwork`\n- Heatmaps: `ComplexHeatmap`, `circlize`\n- Direct labels: `ggrepel`\n- Survival/clinical: `survival`, `survminer`, `forestplot`, `ggplot2`\n- Single-cell/omics: `Seurat`, `SingleCellExperiment`, `ComplexHeatmap`, `ggtree`\n- Export: `svglite`, `grDevices::cairo_pdf`, `ragg`\n\n### Python\n\n- Core plotting: `matplotlib`\n- Statistical plots: `seaborn`\n- Layout: `subplot_mosaic`, `GridSpec`\n- Tables/model output: `pandas`, `numpy`, `statsmodels`\n- Images: `matplotlib.imshow`, `skimage`, `tifffile` when needed\n- Export: `fig.savefig(... .svg/.pdf/.tiff)`, `svg.fonttype='none'`,\n `pdf.fonttype=42`\n\n## Mixed workflow rule\n\nUse the selected plotting backend for final assembly and all visual output. A mixed\nworkflow is reasonable only when the non-selected language performs non-visual data\npreparation and the selected backend assembles the figure. In that case:\n\n1. Export clean source data as CSV/TSV with stable column names.\n2. Assemble the final figure in the selected backend.\n3. Keep the source-data file next to the plotting script.\n4. Do not stitch, preview, QA-render, or export final image/vector outputs from the\n non-selected backend unless the user explicitly changes the selected backend.\n\n## Recommendation language\n\nUse direct language:\n\n```text\nFor this figure I recommend R because the main burden is ComplexHeatmap-style\nomics annotation and patchwork assembly. I will still keep the export contract\nSVG/PDF/TIFF with editable text.\n```\n\n```text\nFor this figure I recommend Python because the key panel is a custom image plate\nwith quantitative overlays and a subplot_mosaic layout. Matplotlib gives tighter\ncontrol over the raster and vector layers.\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5937,"content_sha256":"9ff023b8103da861f2c6e4b8b244a0003120d180ab4fb004c346af5a12343dca"},{"filename":"references/chart-types.md","content":"# Chart Types — Nature Figure Making\n\n## Contents\n\n- [Radar / Polar Chart](#radar-polar-chart)\n- [3D Sphere / Conceptual Illustration](#3d-sphere-conceptual-illustration)\n- [Scatter Plot with Color-Coded Clusters](#scatter-plot-with-color-coded-clusters)\n- [Probability + Manifold Concept Panel](#probability-manifold-concept-panel)\n- [Ablation Line Panel with Reference Baselines](#ablation-line-panel-with-reference-baselines)\n- [Fill-Between Area Chart (Stacked trend)](#fill-between-area-chart-stacked-trend)\n- [Log-Scale Bar Chart](#log-scale-bar-chart)\n- [GridSpec Multi-Panel Layout](#gridspec-multi-panel-layout)\n- [Scientific Notation on Y-Axis](#scientific-notation-on-y-axis)\n- [Custom Spine Positioning](#custom-spine-positioning)\n- [Related files](#related-files)\n\n\nSpecialized chart patterns beyond basic bars and trends.\nEach section includes the key code pattern extracted from production scripts.\n\n---\n\n## Radar / Polar Chart\n\nUsed when comparing multiple methods across many benchmarks simultaneously.\n\n```python\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef plot_radar(methods, colors, subtask_names, value_matrix,\n benchmark_radii, display_range=(45, 90)):\n \"\"\"\n Parameters\n ----------\n methods : list[str] — one curve per method\n colors : list[str]\n subtask_names : list[str] — one spoke per subtask (may contain '\\\\n')\n value_matrix : np.ndarray — shape (n_subtasks, n_methods)\n benchmark_radii: dict — {benchmark_name: [tick1, tick2, ...]} for normalization\n display_range : (r_min, r_max) — polar radial display window\n \"\"\"\n r_lo, r_hi = display_range\n n_subtasks = len(subtask_names)\n n_methods = len(methods)\n\n fig = plt.figure(figsize=(12, 10))\n ax = fig.add_subplot(111, projection='polar')\n\n # Evenly spaced angles, clockwise from top\n angles = np.linspace(2 * np.pi, 0, n_subtasks, endpoint=False)\n angles_closed = np.append(angles, angles[0])\n\n def _normalize(val, bench):\n radii_list = benchmark_radii.get(bench, [0, 100])\n span = max(radii_list) - min(radii_list)\n if span \u003c= 0:\n return (r_lo + r_hi) / 2\n frac = np.clip((val - min(radii_list)) / span, 0, 1)\n return r_lo + (r_hi - r_lo) * frac\n\n subtask_benchmarks = [s.split('\\\\n', 1)[-1] if '\\\\n' in s else s\n for s in subtask_names]\n\n # Draw data polygons\n for m in range(n_methods):\n norm_vals = np.array([_normalize(value_matrix[i, m], subtask_benchmarks[i])\n for i in range(n_subtasks)])\n closed = np.append(norm_vals, norm_vals[0])\n ax.plot(angles_closed, closed, color=colors[m], lw=2, label=methods[m])\n ax.fill(angles_closed, closed, color=colors[m], alpha=0.05)\n ax.scatter(angles, norm_vals, color=colors[m], s=18, zorder=5)\n\n # Style\n ax.set_ylim(r_lo, r_hi)\n ax.set_theta_zero_location('N')\n for spine in ax.spines.values():\n spine.set_visible(False)\n ax.grid(False)\n\n # Outer boundary ring\n ax.plot(angles_closed, np.full_like(angles_closed, r_hi),\n color='k', lw=0.8, zorder=4)\n\n # Radial spokes\n for a in angles:\n ax.plot([a, a], [r_lo, r_hi], color='gray', lw=0.5, zorder=4)\n\n # Benchmark-level contour polygons\n max_levels = max(len(v) for v in benchmark_radii.values())\n for k in range(max_levels):\n disp = np.array([_normalize(benchmark_radii.get(b, [0,100])[\n min(k, len(benchmark_radii.get(b,[0,100]))-1)], b)\n for b in subtask_benchmarks])\n ax.plot(angles_closed, np.append(disp, disp[0]),\n color='k', lw=0.6, zorder=4)\n\n ax.set_yticks([r_hi])\n ax.set_yticklabels([])\n ax.set_xticks(angles)\n ax.set_xticklabels([])\n\n # Spoke labels (outside outer ring)\n for angle, label in zip(angles, subtask_names):\n r_label = r_hi + 8 + 10 * abs(np.sin(angle))\n ax.text(angle, r_label, label, fontsize=14,\n ha='center', va='center',\n transform=ax.transData, clip_on=False)\n\n ax.legend(loc='upper right', bbox_to_anchor=(1.40, 0.05),\n fontsize=15, frameon=False)\n return fig, ax\n```\n\n**Key settings:**\n- `ax.set_theta_zero_location('N')` — top-start convention\n- Remove all default spines/grid; draw custom spokes + contour polygons manually\n- Normalize each spoke independently using per-benchmark tick lists\n- Legend placed **outside** the plot at `bbox_to_anchor=(1.40, 0.05)`\n\n---\n\n## 3D Sphere / Conceptual Illustration\n\nUsed for geometric conceptual diagrams (e.g., embedding space visualization).\n\n```python\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef draw_shaded_sphere(ax, light_dir=(-0.5, 0.5, 0.8),\n resolution=512, alpha=1.0,\n extent=(-1, 1, -1, 1)):\n \"\"\"Draw a 2D shaded disk that mimics a 3D sphere using ray-casting.\"\"\"\n xs = np.linspace(extent[0], extent[1], resolution)\n ys = np.linspace(extent[2], extent[3], resolution)\n x, y = np.meshgrid(xs, ys)\n r2 = x**2 + y**2\n mask = r2 \u003c= 1.0\n\n z = np.zeros_like(x)\n z[mask] = np.sqrt(1.0 - r2[mask])\n\n # Surface normals\n nx, ny, nz = x.copy(), y.copy(), z.copy()\n nrm = np.sqrt(nx**2 + ny**2 + nz**2) + 1e-6\n nx, ny, nz = nx/nrm, ny/nrm, nz/nrm\n\n # Lambertian shading\n ld = np.array(light_dir, dtype=float)\n ld /= np.linalg.norm(ld)\n intensity = np.maximum(0, nx*ld[0] + ny*ld[1] + nz*ld[2])\n\n img = np.ones_like(x)\n img[mask] = np.clip(0.2 + 0.9 * intensity[mask], 0, 1)\n\n ax.imshow(img, cmap='gray',\n extent=list(extent),\n vmin=0, vmax=1, alpha=alpha)\n ax.set_axis_off()\n return ax\n\n\ndef plot_3d_scatter_with_arrows(ax, points, grad_vectors,\n point_color='#0c2458', arrow_color='#b64342'):\n \"\"\"3D scatter plot with gradient arrow annotations.\"\"\"\n from mpl_toolkits.mplot3d import proj3d\n from matplotlib.patches import FancyArrowPatch\n\n class Arrow3D(FancyArrowPatch):\n def __init__(self, xs, ys, zs, *args, **kwargs):\n super().__init__((0,0), (0,0), *args, **kwargs)\n self._verts3d = xs, ys, zs\n def do_3d_projection(self, renderer=None):\n xs, ys, zs = proj3d.proj_transform(*self._verts3d, self.axes.get_proj())\n self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))\n return np.min(zs)\n\n ax.scatter(points[:, 0], points[:, 1], points[:, 2],\n s=80, color=point_color, alpha=0.5)\n for p, g in zip(points, grad_vectors):\n arrow = Arrow3D([p[0], p[0]+g[0]], [p[1], p[1]+g[1]], [p[2], p[2]+g[2]],\n mutation_scale=16, lw=4, arrowstyle='->',\n color=arrow_color, alpha=0.8)\n ax.add_artist(arrow)\n\n # Clean 3D axes\n ax.grid(False)\n ax.xaxis.pane.set_visible(False)\n ax.yaxis.pane.set_visible(False)\n ax.zaxis.pane.set_visible(False)\n ax.set_xticks([])\n ax.set_yticks([])\n ax.set_zticks([])\n```\n\n---\n\n## Scatter Plot with Color-Coded Clusters\n\n```python\ndef make_scatter(ax, x, y, labels_or_colors,\n size=50, alpha=0.7, edgecolors='none'):\n \"\"\"Single or multi-cluster scatter.\"\"\"\n import numpy as np\n ax.scatter(x, y, c=labels_or_colors, s=size,\n alpha=alpha, edgecolors=edgecolors)\n ax.set_axis_off() # for conceptual diagrams; remove for data plots\n```\n\n---\n\n## Probability + Manifold Concept Panel\n\nUse when a manuscript needs a conceptual mechanism panel that links a probability\nshift to a geometric or latent-space explanation. Build the pair from the\noriginal pattern below: a 1D probability-density panel beside a contour/scatter\nmanifold panel.\n\n**Pattern:**\n- Left panel: draw 2-3 probability curves with transparent fills; use one vertical\n reference line and a single double-headed arrow to define the conceptual gap.\n- Right panel: sample points around smooth center curves, show low-alpha clouds,\n contour density bands, and a small number of highlighted trajectory markers.\n- Keep axes only where they carry meaning. The manifold panel can be axis-free if\n labels and arrows carry the explanation.\n- All math labels and manifold names must map to real manuscript concepts. Do not\n reuse demo labels such as `VIG` or `DPO` unless they are the user's actual terms.\n\n```python\nfig, (ax_prob, ax_manifold) = plt.subplots(1, 2, figsize=(24, 6))\nplot_distribution(ax_prob) # probability curves + conceptual gap arrow\nplot_manifold(ax_manifold) # density contours + trajectory markers\nfig.tight_layout(pad=0.5)\n```\n\n---\n\n## Ablation Line Panel with Reference Baselines\n\nUse when an ablation compares data fraction, hyperparameters, or coupled metrics\nacross a small set of methods. A robust original layout uses three horizontal\npanels: data fraction, one hyperparameter sweep, and a dual-axis coupled metric\nsweep.\n\n**Pattern:**\n- Use a dashed horizontal baseline for the simple/reference model.\n- Use a dotted horizontal line for a meaningful operating point, e.g. \"ours at\n 25% data\", only when that comparison is called out in the text.\n- Use `twinx()` sparingly. If two y-axes are needed, color each y label to match\n the corresponding series and keep tick ranges narrow.\n- Put legends inside low-density regions of each panel; avoid one giant legend\n if panel-specific series differ.\n\n```python\nfig, axes = plt.subplots(1, 3, figsize=(27, 6),\n gridspec_kw={\"width_ratios\": [1.1, 1, 1]})\naxes[0].plot(x, baseline, color=\"black\", alpha=0.3, lw=4, ls=\"--\")\naxes[0].plot(x, reference, color=hero_color, lw=3, ls=\":\")\nax2 = axes[2].twinx()\n```\n\n---\n\n## Fill-Between Area Chart (Stacked trend)\n\nUsed for cumulative publication counts, stacked contributions, etc.\n\n```python\n# Filled area (stacked) with hatch for print safety\nax.fill_between(x, 0, y_bottom,\n color='#ffa8a6', label='Category A')\nax.fill_between(x, 0, y_top,\n color='#9BC8FA',\n hatch='///', # hatch for grayscale print\n edgecolor='black',\n label='Category B')\n# Erase border artifacts\nax.fill_between(x, 0, y_top,\n facecolor='none',\n edgecolor='white',\n linewidth=2)\n\n# Overlay the trend line for exact values\nax.plot(x, y_top, lw=3, color='#13457E')\nax.plot(x, y_bottom, lw=3, color='#850c0a')\n```\n\n---\n\n## Log-Scale Bar Chart\n\n```python\nax.set_yscale('log')\nymin, ymax = ax.get_ylim()\nax.set_ylim(ymin, ymax * 20) # expand top for annotations\n\n# Annotate values above bars\nfor i, val in enumerate(values):\n ax.text(i, val * 1.1, f'{val:.3f}',\n ha='center', va='bottom', fontsize=16)\n```\n\n---\n\n## GridSpec Multi-Panel Layout\n\n```python\nfrom matplotlib import gridspec\n\n# 2-row, 4-column layout\nfig = plt.figure(figsize=(36, 12))\ngs = gridspec.GridSpec(2, 4)\n\nax_top_left = fig.add_subplot(gs[0, 0])\nax_top_right = fig.add_subplot(gs[0, 1:3]) # span columns 1-2\nax_legend = fig.add_subplot(gs[0, 3]) # legend panel\nax_bottom = fig.add_subplot(gs[1, :]) # full-width bottom\n```\n\n---\n\n## Scientific Notation on Y-Axis\n\n```python\nax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))\n```\n\n---\n\n## Custom Spine Positioning\n\n```python\n# Move bottom spine to y=0 (for negative values)\nax.spines['bottom'].set_position(('data', 0))\nax.xaxis.set_ticks_position('bottom')\nax.spines['left'].set_bounds(0, y_max)\n```\n\n---\n\n## Related files\n\n- [SKILL.md](../SKILL.md) — When to use this skill\n- [api.md](api.md) — PALETTE and core helper signatures\n- [common-patterns.md](common-patterns.md) — Bar, trend, and layout patterns\n- [design-theory.md](design-theory.md) — Rationale and color theory\n- [tutorials.md](tutorials.md) — Full end-to-end walkthroughs\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11883,"content_sha256":"68008a60e0285e840a34973db5f945e88051280ac4dcad8a7eb97882c5384dfa"},{"filename":"references/common-patterns.md","content":"# Common Patterns — Nature Figure Making\n\n## Contents\n\n- [Pattern 1: Ultra-wide multi-metric bar panel](#pattern-1-ultra-wide-multi-metric-bar-panel)\n- [Pattern 2: Dedicated legend panel](#pattern-2-dedicated-legend-panel)\n- [Pattern 3: Categorical bars without x-tick labels](#pattern-3-categorical-bars-without-x-tick-labels)\n- [Pattern 4: Dynamic y-axis tightening](#pattern-4-dynamic-y-axis-tightening)\n- [Pattern 5: Alpha-graduated ablation bars (same color, varying opacity)](#pattern-5-alpha-graduated-ablation-bars-same-color-varying-opacity)\n- [Pattern 6: Hatch encoding for print-safe grayscale](#pattern-6-hatch-encoding-for-print-safe-grayscale)\n- [Pattern 7: Semantic or family color mapping](#pattern-7-semantic-or-family-color-mapping)\n- [Pattern 8: In-bar text with luminance-aware color](#pattern-8-in-bar-text-with-luminance-aware-color)\n- [Pattern 9: Fill-between trend with hatch (print-safe)](#pattern-9-fill-between-trend-with-hatch-print-safe)\n- [Pattern 10: Annotate events on trend lines](#pattern-10-annotate-events-on-trend-lines)\n- [Pattern 11: Grouped bars across multiple datasets (grouped-within-grouped)](#pattern-11-grouped-bars-across-multiple-datasets-grouped-within-grouped)\n- [Pattern 12: Schematic hero panel with supporting quant row](#pattern-12-schematic-hero-panel-with-supporting-quant-row)\n- [Pattern 13: Dark image plate with repeated views](#pattern-13-dark-image-plate-with-repeated-views)\n- [Pattern 14: Clinical triptych](#pattern-14-clinical-triptych)\n- [Pattern 15: Asymmetric hero panel](#pattern-15-asymmetric-hero-panel)\n- [Pattern 16: Direct labels inside filled regions](#pattern-16-direct-labels-inside-filled-regions)\n- [Related files](#related-files)\n\n\nReusable layout and encoding patterns used across publication-grade scripts.\n\n---\n\n## Pattern 1: Ultra-wide multi-metric bar panel\n\nFor 3–4 metrics compared across many methods, use a wide canvas so bars and labels don't crowd.\n\n```python\nfig = plt.figure(figsize=(45, 12)) # or (28, 6) for fewer metrics\ngs = gridspec.GridSpec(1, n_metrics)\n\nfor i, metric in enumerate(metrics):\n ax = fig.add_subplot(gs[i])\n ax.bar(x, values[metric], color=colors, ...)\n ax.set_ylabel(metric, fontsize=54, labelpad=12)\n ax.set_xticks([])\n\n# Last panel: legend only\nax_leg = fig.add_subplot(gs[-1])\nax_leg.legend(handles, labels, fontsize=38, loc='center', frameon=False)\nax_leg.set_axis_off()\n\nfig.tight_layout(pad=2)\n```\n\n**Rule**: Width often 3–4× height. Allows left-to-right narrative scanning.\n\n---\n\n## Pattern 2: Dedicated legend panel\n\nWhen the legend is large, give it its own axis so data panels stay clean.\n\n```python\nfig, axes = plt.subplots(1, n_data + 1, figsize=(...))\n\nfor i, ax in enumerate(axes[:-1]):\n bars = ax.bar(...)\n if i == 0:\n handles, labels = ax.get_legend_handles_labels()\n\n# Legend-only panel\naxes[-1].legend(handles, labels, fontsize=28, loc='center', frameon=False)\naxes[-1].set_axis_off()\n```\n\n---\n\n## Pattern 3: Categorical bars without x-tick labels\n\nWhen methods are named in the legend, hide x-ticks entirely.\n\n```python\nax.set_xticks([]) # removes ticks and labels\n# Alternatively:\nax.set_xticklabels([]) # keeps tick marks, removes labels\n```\n\n---\n\n## Pattern 4: Dynamic y-axis tightening\n\nNever use 0–100 when all values are in 80–95.\n\n```python\nmargin = (values.max() - values.min()) * 0.1 # 10% padding\nax.set_ylim([values.min() - margin, values.max() + margin])\n\n# Manual ticks at clean round numbers\nax.set_yticks([0.75, 0.80, 0.85, 0.90])\nax.tick_params(axis='y', labelsize=36, length=10, width=2)\n```\n\n---\n\n## Pattern 5: Alpha-graduated ablation bars (same color, varying opacity)\n\n```python\nimport numpy as np\n\nblue_rgb = (0.215686, 0.458824, 0.729412) # #3775BA as float tuple\nn_ablations = len(ablation_configs)\nalphas = np.linspace(0.2, 1.0, n_ablations)\ncolors = [(blue_rgb[0], blue_rgb[1], blue_rgb[2], a) for a in alphas]\n# Full method → alpha=1.0, most ablated → alpha=0.2\n```\n\n---\n\n## Pattern 6: Hatch encoding for print-safe grayscale\n\nAdd hatching so bars remain distinct when printed in black-and-white.\n\n```python\nhatches = ['/', '\\\\\\\\', '.', 'x', 'o', '+']\nfor bar_container, hatch in zip(grouped_bars, hatches):\n for patch in bar_container:\n patch.set_hatch(hatch)\n patch.set_edgecolor('black')\n patch.set_linewidth(1.5)\n```\n\n---\n\n## Pattern 7: Semantic or family color mapping\n\nAlways map colors consistently across all panels in a figure:\n\n```python\nmethod_colors = {\n 'ResNet1d18': '#484878', # baseline_dark\n 'ResNet1d34': '#7884B4', # baseline_mid\n 'ECGFounder': '#B4C0E4', # baseline_soft\n 'CSFM-Tiny': '#E4E4F0', # ours_tiny\n 'CSFM-Base': '#E4CCD8', # ours_base\n 'CSFM-Large': '#F0C0CC', # ours_large\n}\ncolors = [method_colors[m] for m in methods]\n```\n\nPrefer coherent hue families over alternating saturated blue/green/red just because categories differ.\nGreen and red should usually be reserved for **directional annotations**, not primary series identity:\n\n```python\nax.scatter(x_gain, y_gain, marker='^', color='#2E9E44', s=90, zorder=6) # improvement\nax.scatter(x_drop, y_drop, marker='v', color='#E53935', s=90, zorder=6) # degradation\n```\n\n---\n\n## Pattern 8: In-bar text with luminance-aware color\n\n```python\ndef annotate_bars(ax, bars, colors, fmt='{:.2f}', fontsize=32, offset=-0.10):\n for bar, color in zip(bars, colors):\n c = color.lstrip('#')\n r, g, b = int(c[0:2],16)/255, int(c[2:4],16)/255, int(c[4:6],16)/255\n lum = 0.299*r + 0.587*g + 0.114*b\n textcolor = 'white' if lum \u003c 0.5 else 'black'\n value = bar.get_height()\n ax.text(bar.get_x() + bar.get_width()/2,\n value + offset,\n fmt.format(value),\n ha='center', va='bottom',\n fontsize=fontsize, color=textcolor)\n```\n\n---\n\n## Pattern 9: Fill-between trend with hatch (print-safe)\n\n```python\nax.fill_between(x, 0, cumsum_series,\n color=fill_color,\n hatch='\\\\\\\\\\\\', # triple backslash for dense hatch\n edgecolor='black',\n label=label_name)\n# Visually erase the border artifacts:\nax.fill_between(x, 0, cumsum_series,\n facecolor='none',\n edgecolor='white',\n linewidth=2)\n```\n\n---\n\n## Pattern 10: Annotate events on trend lines\n\n```python\ndef mark_events(ax, x_labels, y_cumsum, events_dict, dy_fraction=0.1):\n \"\"\"Add labeled arrows at event dates on a trend line.\"\"\"\n x_index = {label: i for i, label in enumerate(x_labels)}\n y_lo, y_hi = ax.get_ylim()\n dy = dy_fraction * (y_hi - y_lo)\n for date, label in events_dict.items():\n if date not in x_index:\n continue\n i = x_index[date]\n stars = label.count('*')\n clean_label = label.replace('*', '')\n y_data = y_cumsum[i]\n ax.annotate(\n clean_label,\n xy=(i, y_data),\n xytext=(i, y_data + (1 + 0.8 * stars) * dy),\n ha='center', va='bottom', fontsize=11,\n arrowprops=dict(arrowstyle='-|>', lw=1.3, color='black',\n shrinkA=0, shrinkB=0, mutation_scale=15)\n )\n```\n\n---\n\n## Pattern 11: Grouped bars across multiple datasets (grouped-within-grouped)\n\n```python\nnum_methods = len(methods)\nxtick_positions = []\n\nfor dataset_idx, dataset_name in enumerate(datasets):\n x_start = dataset_idx * (num_methods + 1) # gap of 1 between groups\n ax.bar(\n np.arange(num_methods) + x_start,\n values[dataset_name],\n color=method_colors,\n label=methods if dataset_idx == 0 else ['_nolegend_'] * num_methods,\n )\n xtick_positions.append(np.mean(np.arange(num_methods)) + x_start)\n\nax.set_xticks(xtick_positions)\nax.set_xticklabels(datasets)\n```\n\n---\n\n## Pattern 12: Schematic hero panel with supporting quant row\n\nUse when one mechanism or fabrication story needs to lead, with 2–4 smaller evidence plots below.\n\n```python\nfig = plt.figure(figsize=(7.2, 6.2))\ngs = fig.add_gridspec(\n 2, 4,\n height_ratios=[2.2, 1.0],\n hspace=0.18, wspace=0.28,\n)\n\nax_top = fig.add_subplot(gs[0, :]) # hero schematic\nax_b = fig.add_subplot(gs[1, 0])\nax_c = fig.add_subplot(gs[1, 1:3])\nax_d = fig.add_subplot(gs[1, 3])\n\n# top panel should carry the main palette and the main visual narrative\n```\n\nRules:\n\n- Allocate `45–60%` of total height to the hero schematic.\n- Reuse softened versions of the same colors in the lower plots.\n- Keep support plots quieter than the hero panel.\n\n---\n\n## Pattern 13: Dark image plate with repeated views\n\nUse for microscopy, volume rendering, or fluorescence-heavy panels.\n\n```python\nfig = plt.figure(figsize=(7.2, 6.5))\ngs = fig.add_gridspec(3, 5, hspace=0.08, wspace=0.04)\n\nfor r in range(3):\n for c in range(5):\n ax = fig.add_subplot(gs[r, c])\n ax.set_facecolor('black')\n ax.set_xticks([])\n ax.set_yticks([])\n for spine in ax.spines.values():\n spine.set_visible(False)\n```\n\nRules:\n\n- Use black only within the image plate cells.\n- Put channel labels, scale bars and small crop guides directly on the plate.\n- Keep crop geometry and scale-bar placement consistent across the grid.\n\n---\n\n## Pattern 14: Clinical triptych\n\nUse for outcome-over-time figures that combine trajectories, effect sizes, and summary proportions.\n\n```python\nfig = plt.figure(figsize=(7.2, 6.8))\ngs = fig.add_gridspec(\n 3, 3,\n height_ratios=[1.0, 1.35, 0.8],\n hspace=0.28, wspace=0.32,\n)\n\naxes_top = [fig.add_subplot(gs[0, i]) for i in range(3)]\naxes_mid = [fig.add_subplot(gs[1, i]) for i in range(3)]\naxes_bot = [fig.add_subplot(gs[2, i]) for i in range(3)]\n\n# Put one shared legend strip above axes_top rather than repeating legends.\n```\n\nRules:\n\n- Keep the three columns semantically parallel.\n- Use a dashed vertical reference line in the forest-plot row.\n- Group shading in the forest-plot row should be pale and subordinate.\n\n---\n\n## Pattern 15: Asymmetric hero panel\n\nUse when one panel is conceptually central and should dominate.\n\n```python\nfig = plt.figure(figsize=(7.2, 5.8))\ngs = fig.add_gridspec(3, 4, hspace=0.25, wspace=0.28)\n\nax_a = fig.add_subplot(gs[0, :2])\nax_b = fig.add_subplot(gs[0, 2])\nax_c = fig.add_subplot(gs[1, :2])\nax_d = fig.add_subplot(gs[1, 2])\nax_e = fig.add_subplot(gs[:, 3]) # hero panel spans all rows\nax_f = fig.add_subplot(gs[2, :2])\n```\n\nRule: do not normalize every subplot to the same size if the science does not have equal importance.\n\n---\n\n## Pattern 16: Direct labels inside filled regions\n\nUse when the same categorical structure repeats and a legend would become too large.\n\n```python\nfor x_text, y_text, text, color in label_specs:\n ax.text(\n x_text, y_text, text,\n color=color,\n ha='center', va='center',\n fontsize=9, fontweight='bold',\n )\n```\n\nRules:\n\n- Keep labels inside stable, visually large regions.\n- Use a small white or black stroke if the fill varies strongly underneath.\n- Prefer direct labels over a mega-legend for repeated stacked-area or phase diagrams.\n\n---\n\n## Related files\n\n- [SKILL.md](../SKILL.md) — When to use this skill\n- [api.md](api.md) — Helper function signatures and PALETTE\n- [design-theory.md](design-theory.md) — Rationale behind every pattern above\n- [nature-2026-observations.md](nature-2026-observations.md) — Real Nature page archetypes behind these patterns\n- [tutorials.md](tutorials.md) — End-to-end walkthroughs\n- [chart-types.md](chart-types.md) — Radar, 3D, scatter patterns\n- [demos.md](demos.md) — Third-party demo map, copyright-aware reference use, and original reimplementation guidance\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11668,"content_sha256":"5f176c70bb846744374a27a342d03761ff9c0a0c7844f87867add3a66c448581"},{"filename":"references/demos.md","content":"# figures4papers Reference Index\n\nUse this file when a user asks for a `figures4papers` look, cites the older\n`scientific-figure-making` skill, or needs a concrete Python/matplotlib reference.\n\nThe retained third-party files live under `../assets/figures4papers/`. Before\nopening or adapting them, read\n`../assets/figures4papers/THIRD_PARTY_NOTICES.md`. These materials are not\nautomatically covered by the root MIT License, and their inclusion does not grant\nadditional reuse rights.\n\n## Use boundary\n\n1. Study layout, palette, axes, legends, and export structure as reference patterns.\n2. Reimplement with original code and the user's own data whenever the upstream\n license or written permission does not clearly authorize copying or modification.\n3. Never reuse manuscript-specific labels, metric values, statistical results, or\n visual assets as placeholders for real evidence.\n4. Record the external reference and implementation provenance in the internal QA\n record when it materially influenced the result.\n5. Preserve the editable SVG/PDF/TIFF and source-data rules from `api.md` and\n `qa-contract.md`.\n\n## Retained project map\n\n| Project | Open when | Local references |\n|---------|-----------|------------------|\n| `figure_ImmunoStruct` | Method-comparison and ablation bars | `../assets/figures4papers/figure_ImmunoStruct/` |\n| `figure_CellSpliceNet` | Compact benchmark and cross-species bars | `../assets/figures4papers/figure_CellSpliceNet/` |\n| `figure_brainteaser` | Category, rewriting, and self-correction panels | `../assets/figures4papers/figure_brainteaser/` |\n| `figure_VIGIL` | Radar, trend, ablation, and probability/manifold panels | `../assets/figures4papers/figure_VIGIL/` |\n| `figure_ophthal_review` | Time trends and composition heatmaps | `../assets/figures4papers/figure_ophthal_review/` |\n| `figure_RNAGenScape` | Heatmaps, optimization, manifold, and sweep plots | `../assets/figures4papers/figure_RNAGenScape/` |\n| `figure_Dispersion` | Conceptual sphere and observation panels | `../assets/figures4papers/figure_Dispersion/` |\n| `figure_Cflows` | Diffusion, trajectory, comparison, and ablation panels | `../assets/figures4papers/figure_Cflows/` |\n| `figure_FPGM` | Frequency-prior or distribution-style motivation | `../assets/figures4papers/figure_FPGM/` |\n\n## Prefer repository-owned implementation paths\n\nWhen copying is not clearly authorized, route the pattern through the repository's\nown material:\n\n| Requested pattern | Open and use |\n|-------------------|--------------|\n| Grouped bars, ablation bars, shared legends | `tutorials.md`, `common-patterns.md` |\n| Radar or polar comparison | `chart-types.md`, then implement with original code |\n| Trends, sweeps, and reference baselines | `tutorials.md`, `chart-types.md` |\n| Heatmap or annotated matrix | `tutorials.md`, `template-catalog.md` |\n| Probability or manifold concept panel | `chart-types.md` |\n| Submission typography, palette, and export | `api.md`, `design-theory.md` |\n| CSV-driven reproducible plots | `template-catalog.md`, `scripts/plot_templates.py` |\n\n## Upstream source\n\n\u003chttps://github.com/ChenLiu-1996/figures4papers>\n\nCheck the current upstream license and obtain permission when required before\nredistributing, modifying, or publishing derived materials.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3282,"content_sha256":"dc34ab2d1600265f7e03af51afa0d4bda7b235f4cf3d2f73ac791dca8c605cec"},{"filename":"references/design-theory.md","content":"# Nature Figure Design Theory\n\n## Contents\n\n- [1) Typography](#1-typography)\n- [2) Axes & Spines](#2-axes-spines)\n- [3) Color Palette](#3-color-palette)\n- [4) Layout and Composition](#4-layout-and-composition)\n- [5) Bar Chart Rules](#5-bar-chart-rules)\n- [6) Line / Trend Plots](#6-line-trend-plots)\n- [7) Heatmap Rules](#7-heatmap-rules)\n- [8) Radar / Polar Charts](#8-radar-polar-charts)\n- [9) Export Policy](#9-export-policy)\n- [10) Multi-Panel Information Architecture](#10-multi-panel-information-architecture)\n- [11) Reproduction Checklist](#11-reproduction-checklist)\n\n\nThis file is an original synthesis of publication-figure typography, color,\ncomposition, and export rules. External repositories may be consulted only as\nvisual references after their license and reuse terms have been verified; see\n[demos.md](demos.md).\n\n---\n\n## 1) Typography\n\n### Font stack (priority order)\n- **Nature standard**: `font.family = 'sans-serif'`, `font.sans-serif = ['Arial']`\n- **Fallback stack**: `['Arial', 'Helvetica', 'DejaVu Sans', 'sans-serif']`\n- **Helvetica** (equivalent) also appears in many scripts as `font.family = 'helvetica'`\n- SVG/PDF editable text: always set `svg.fonttype = 'none'`\n- LaTeX math labels: `text.usetex = True` only when LaTeX is installed\n\n### Font size hierarchy\n| Context | font.size | axes.linewidth |\n|---------|-----------|---------------|\n| Journal-final dense multi-panel figure at publication width | 7–9 | 0.8–1.2 |\n| Large comparison bar panels (figsize > 28in wide) | 24 | 3 |\n| Compact subfigures / analytic plots | 15–16 | 2 |\n| Axis labels on large panels | 32–54 (override per-label) | — |\n| In-bar annotations | 32–36 | — |\n| Legend text on large panels | 28–38 | — |\n| Tick labels | 20–36 | — |\n\nWhen targeting the final dimensions of a two-column `Nature` figure page, start smaller than\nslide-sized preview figures. The sampled 2026 papers routinely landed in the `7–9 pt` final-text\nregime for dense composites.\n\nThe submission floor applies to rendered glyphs, not only source-level parent sizes. Mathtext often scales scripts to about `0.7×`; a 7 pt `$R^2 nature-figure — Skillopedia can therefore contain a 4.9 pt superscript. Prefer a supported Unicode glyph such as `R²` when appropriate, or raise the parent size and verify the exported PDF with `scripts/audit_pdf_text.py`. Also compare each long label's rendered bounding-box width with its allocated slot width in millimetres.\n\n---\n\n## 2) Axes & Spines\n\n```python\nplt.rcParams['axes.spines.right'] = False # always off\nplt.rcParams['axes.spines.top'] = False # always off\nplt.rcParams['legend.frameon'] = False # frameless legends everywhere\n```\n\n- Keep only left + bottom spines — minimalist, Nature-approved.\n- No grid lines by default; use sparse y-ticks to guide the eye.\n\n---\n\n## 3) Color Palette\n\nSemantic: blue = proposed method, green = positive variants, red/pink = baselines, neutral = reference/background.\nFor dense multi-panel figures, however, **family consistency beats maximal hue separation**.\n\n```python\nPALETTE = {\n # Proposed / key method\n \"blue_main\": \"#0F4D92\", # deep blue — hero method\n \"blue_secondary\": \"#3775BA\", # medium blue — second author method\n\n # Positive / improvement shades (light → dark)\n \"green_1\": \"#DDF3DE\",\n \"green_2\": \"#AADCA9\",\n \"green_3\": \"#8BCF8B\",\n\n # Baseline / contrast shades (light → dark)\n \"red_1\": \"#F6CFCB\",\n \"red_2\": \"#E9A6A1\",\n \"red_strong\": \"#B64342\",\n\n # Neutral support\n \"neutral_light\": \"#CFCECE\",\n \"neutral_mid\": \"#767676\",\n \"neutral_dark\": \"#4D4D4D\",\n \"neutral_black\": \"#272727\",\n\n # Accent / callout (use sparingly)\n \"gold\": \"#FFD700\",\n \"teal\": \"#42949E\",\n \"violet\": \"#9A4D8E\",\n \"magenta\":\"#EA84DD\",\n}\n\nDEFAULT_COLOR_ORDER = [\n \"#0F4D92\", # blue_main\n \"#8BCF8B\", # green_3\n \"#B64342\", # red_strong\n \"#42949E\", # teal\n \"#9A4D8E\", # violet\n \"#CFCECE\", # neutral_light\n]\n```\n\n### Unified-family rule (recommended for NMI-style pages)\n\nPublication figures should read like **one figure**, not six unrelated plots. Prefer one cool family for\nbaselines and one lilac/rose family for the proposed method line.\n\n```python\nPALETTE_NMI_PASTEL = {\n \"baseline_dark\": \"#484878\",\n \"baseline_mid\": \"#7884B4\",\n \"baseline_soft\": \"#B4C0E4\",\n \"ours_tiny\": \"#E4E4F0\",\n \"ours_base\": \"#E4CCD8\",\n \"ours_large\": \"#F0C0CC\",\n \"delta_up\": \"#2E9E44\",\n \"delta_down\": \"#E53935\",\n}\n\nDEFAULT_COLOR_ORDER_NMI_PASTEL = [\n \"#484878\", # baseline_dark\n \"#7884B4\", # baseline_mid\n \"#B4C0E4\", # baseline_soft\n \"#E4E4F0\", # ours_tiny\n \"#E4CCD8\", # ours_base\n \"#F0C0CC\", # ours_large\n]\n```\n\nRules:\n1. Keep related baselines in one cool family.\n2. Keep `Tiny / Base / Large` or sibling variants in one hero family.\n3. Reserve green/red for arrows, gains, drops, thresholds, or signed biological direction.\n4. Never remap the same method to a different hue family in another panel.\n5. If in doubt, reduce saturation before adding more categories.\n6. Validate pairwise separation and white-background contrast, then inspect rendered salience. A neutral baseline must not dominate the hero method merely because it is darker.\n7. Sequential light-to-dark scales encode order or magnitude; do not treat their steps as unrelated categories.\n\n### Modality-specific palette discipline from sampled 2026 Nature figures\n\n- **Imaging plates**: grayscale context + 1–2 fluorescent accent channels on black.\n- **Schematic/material pages**: derive the palette from the physical objects in the schematic,\n then reuse softened versions of those colors in the support plots.\n- **Clinical composites**: dark baseline/reference series, restrained warm/cool follow-up hues,\n pale background bands in forest plots.\n- **Genomics / systems pages**: neutral grey scaffolds plus a small number of biologically\n meaningful highlight families, often one red and one blue.\n\n### Ablation alpha encoding\nWhen ablating components of one method, use a **single color with varying alpha**:\n```python\ncolor = (0.215686, 0.458824, 0.729412) # blue_secondary as RGB tuple\nalphas = np.linspace(0.2, 1.0, n_variants)\ncolors = [(color[0], color[1], color[2], a) for a in alphas]\n# alpha=1.0 → full method, alpha=0.2 → minimal/ablated variant\n```\n\n---\n\n## 4) Layout and Composition\n\n### Figure sizes\n| Figure type | Typical figsize |\n|-------------|----------------|\n| Journal-width composite page / asymmetric multi-panel | (7.0–7.4, 5.5–7.8) |\n| Multi-metric bar (3–4 metrics + legend) | (28–45, 6–12) |\n| Compact single bar | (9–16, 5–8) |\n| Trend / line multi-panel | (14, 4) or (9, 8) |\n| Heatmap single | (8–20, 5–9) |\n| Radar polar | (12, 10) |\n| 3D / illustration multi-panel | (24, 8) |\n\n**Rule**: Width ≈ 3–4× height for comparison bars; prevents vertical crowding and allows left-to-right narrative reading.\n\n### Dedicated legend panel\nFor multi-axis figures, the **last subplot is legend-only**:\n```python\nax_legend = fig.add_subplot(1, n+1, n+1)\nax_legend.legend(handles, labels, fontsize=..., loc='center', frameon=False)\nax_legend.set_axis_off()\n```\n\n### Dynamic y-axis scaling\nNever use fixed 0–100 when values sit in a narrow band.\nTighten limits to data range: e.g., `ax.set_ylim([data.min() - margin, data.max() + margin])`.\n\n### Nature page archetypes from sampled 2026 papers\n\n`Nature` figures were not uniformly dashboard-like. They repeatedly used a few strong page\narchetypes:\n\n| Archetype | Layout signal | Practical rule |\n|-----------|---------------|----------------|\n| Schematic-led composite | One wide story panel with smaller quant panels below | Give the schematic the visual hierarchy; supporting plots should validate, not compete |\n| Dark image plate | Repeated black tiles with fluorescent channels | Use black only inside the image plate region; keep scale bars, gutters, and channel labels high-contrast |\n| Clinical triptych | Top longitudinal row, middle forest row, bottom summary row | Reuse the same column logic across outcomes and put the shared legend above the row |\n| Asymmetric hero layout | One dominant circular/schematic panel plus small support plots | Let one panel span multiple grid cells; equal panel sizes are not required |\n\n### Panel labels and gutters\n\n- Use small bold lowercase panel letters near the top-left edge.\n- Keep gutters tight but real; increase spacing when dark and light modalities touch.\n- Leave extra bottom clearance when a dense caption will sit immediately below the figure.\n- Avoid decorative panel boxes. Alignment and whitespace should carry the structure.\n\n### Legend economy and direct labelling\n\n- Use direct labels when regions, channels, or line identities are spatially stable.\n- Prefer one shared legend strip above a row rather than repeating legends inside several axes.\n- Dense categorical area plots often read better with embedded text than with a detached legend.\n- If a legend exists, it should usually be frameless and visually quieter than the data.\n- Measure legend/panel and legend-row spacing from their rendered tight bounding boxes. Do not infer spacing from a full raster row when the objects occupy different horizontal positions.\n- Check long model names at final size against the width allocated to each group. Widen the layout or reduce text size while keeping every PDF glyph at least 5 pt.\n\n### X-tick suppression\nWhen bars represent methods and the legend already names them:\n```python\nax.set_xticks([]) # hide x-tick labels; use legend + panel title instead\n```\n\n---\n\n## 5) Bar Chart Rules\n\n### Vertical bars (comparison)\n```python\nbars = ax.bar(\n x_positions,\n values,\n yerr=std_values,\n capsize=5,\n color=colors,\n label=method_names,\n edgecolor='black', # sharp separation\n linewidth=1.5,\n)\n```\n\n### Horizontal bars (ablation)\n```python\nax.barh(\n y_positions,\n values,\n xerr=std_values,\n color=[(r, g, b, alpha) for alpha in alphas],\n ecolor='k',\n capsize=5,\n)\n```\n\n### In-bar value annotation\nPrint exact numbers inside or above bars at 32–36pt for readability without a grid:\n```python\nfor bar, value in zip(bars, values):\n luminance = compute_luminance(bar_color)\n textcolor = 'white' if luminance \u003c 128 else 'black'\n ax.text(bar.get_x() + bar.get_width()/2,\n bar.get_height() - 0.10,\n f'{value:.2f}',\n ha='center', va='bottom',\n fontsize=32, color=textcolor)\n```\n\n### Hatch encoding for print-safe grayscale\n```python\nhatches = ['/', '\\\\', '.', 'x', 'o']\nfor bar, hatch in zip(bars, hatches):\n bar.set_hatch(hatch)\n```\n\n### Error bar styling\n```python\nerror_kw = {\n 'elinewidth': 2,\n 'capthick': 2,\n 'capsize': 15,\n}\n```\n\n---\n\n## 6) Line / Trend Plots\n\n- Line width: 2–3pt with controlled alpha.\n- Marker size: 8–12pt circles.\n- For clinical or longitudinal triptychs, place one shared legend above the row rather than repeating it per axis.\n- Fading alpha for temporal progression:\n ```python\n from matplotlib.collections import LineCollection\n alphas = np.linspace(0.3, 0.9, n_segments)\n # build LineCollection with per-segment alpha\n ```\n- `fill_between` for uncertainty bands (keep alpha low: 0.1–0.2).\n- If comparable panels summarize the same seed/fold/split process, show the same spread definition in all of them. After adding uncertainty, remove arrows or brackets that duplicate the same visual gap and collide with the interval geometry.\n- Reference baseline as dashed horizontal line: `ax.axhline(y=..., linestyle='--', alpha=0.3, linewidth=4)`.\n- No grid; sparse y-ticks guide the eye.\n\n---\n\n## 7) Heatmap Rules\n\n```python\nimport matplotlib as mpl\n\n# Diverging (positive/negative): use Red + Blue colormaps per column direction\ncmap_pos = plt.cm.Reds\ncmap_neg = plt.cm.Blues_r\n\n# Masked NaN cells show as white\ncmap.set_bad(color='white')\n\n# Normalize per column\nnorm = mpl.colors.Normalize(vmin=col_min, vmax=col_max)\n\n# Remove frame\nax.set_frame_on(False)\n\n# Remove tick marks, keep labels\nax.tick_params(axis='x', which='both', bottom=False, top=False, length=0)\n```\n\nCell text contrast:\n```python\nr, g, b, _ = cmap(norm(value))\nluminance = 0.299*r + 0.587*g + 0.114*b\ntext_color = 'white' if luminance \u003c 0.5 else 'black'\n```\n\n---\n\n## 8) Radar / Polar Charts\n\n- Project: `fig.add_subplot(projection='polar')`.\n- Remove default grid and spines; draw custom spokes and contour polygons.\n- Normalize per-spoke to display range (e.g., 45–90) using per-benchmark tick lists.\n- Use `ax.set_theta_zero_location('N')` to start at top.\n- Legend: `bbox_to_anchor=(1.40, 0.05)` outside right edge.\n\n---\n\n## 9) Export Policy\n\n### SVG is the required primary format\n\nSVG preserves editable text (when `svg.fonttype = 'none'`), supports lossless scaling,\nand is required for any figure where text labels may need post-hoc alignment in\nIllustrator or Inkscape. Always save SVG first.\n\n```python\nimport os\nos.makedirs('./figures/', exist_ok=True)\nfig.tight_layout(pad=2) # default; use pad=1 for compact multi-panel\n\n# ── PRIMARY ── editable vector, text as \u003ctext> nodes ─────────────────────────\nfig.savefig('./figures/name.svg', bbox_inches='tight')\n\n# ── SECONDARY ── raster for quick preview / submission portals ────────────────\nfig.savefig('./figures/name.png', dpi=300, bbox_inches='tight')\n\nplt.close(fig) # always close to free memory\n```\n\n**DPI guide (PNG only)**:\n- `dpi=300` — standard for all figure types.\n- `dpi=600` — dense bar panels with many methods.\n\n**Never** use `svg.fonttype = 'path'` (matplotlib default): it converts glyphs to bezier\ncurves, breaking text editability. The mandatory three rcParams lines (see api.md) must\nbe set before any `savefig` call.\n\n---\n\n## 10) Multi-Panel Information Architecture\n\n### Rule: Every panel must answer a unique scientific question\n\nIn a multi-panel figure, each panel should be independently necessary but need\nnot tell an independent story. Covering one panel must remove a distinct\ninferential step that cannot be recovered from the others. For figure-level\nclaim design, evidence-role selection, placement decisions and manuscript\nfigure progression, load `multipanel-evidence-architecture.md`.\n\n**Recommended three-level progression**:\n\n| Level | Question answered | Typical encoding |\n|-------|------------------|-----------------|\n| Overview | \"What is the landscape?\" | Stacked bar, composition |\n| Deviation | \"What is distinctive per group?\" | Z-score heatmap (diverging cmap) |\n| Relationship | \"How do variables co-vary?\" | Scatter / bubble plot |\n\n### Anti-redundancy checklist\n\nBefore finalising:\n\n- [ ] Panel b does **not** re-display the same data as panel a in a different visual form\n- [ ] Panel c adds a dimension absent from a and b (e.g., correlation, biological relationship)\n- [ ] Each panel has its own axis-label vocabulary (different x/y quantities)\n\n### Common redundancy traps\n\n| Trap | Example | Fix |\n|------|---------|-----|\n| Absolute + absolute | Stacked bar (%) + heatmap of same % | Replace heatmap with z-score deviation |\n| Subset of parent | Tumor-only ranked bar is just one column of the stacked bar | Swap for scatter: tumor % vs. immune % |\n| Two rankings | Two ranked bars on related metrics | Replace one with scatter / bubble |\n| Different chart, same data slice | Pie + stacked bar | Merge or replace one with a relationship plot |\n\n### Z-score deviation heatmap (complement to a composition bar)\n\nWhen panel a shows absolute composition, panel b should show **what is atypical** per group:\n\n```python\n# heat: DataFrame (cohorts × cell-type categories), values in %\nz = (heat - heat.mean(axis=0)) / heat.std(axis=0)\nim = ax.imshow(z.values, cmap=\"RdBu_r\", aspect=\"auto\", vmin=-2.5, vmax=2.5)\n# colorbar label:\ncbar.set_label(\"Z-score vs pan-cohort mean\")\n```\n\nUse `RdBu_r` (red = enriched above average, blue = depleted). This diverging view is orthogonal to the absolute-percentage view in panel a.\n\n### Bubble scatter (complement to both)\n\nWhen a = composition, b = deviation, panel c should reveal **biological co-variation**:\n\n```python\n# x: dominant compartment (e.g., tumor %)\n# y: functional readout (e.g., immune-cell %)\n# size: third variable (e.g., stroma %)\nax.scatter(x, y, s=stroma * scale, c=colors,\n edgecolors=\"white\", linewidth=0.8, alpha=0.9)\n# Quadrant reference lines at median x and median y\nax.axvline(np.median(x), lw=1.2, ls=\"--\", color=\"#767676\", alpha=0.6)\nax.axhline(np.median(y), lw=1.2, ls=\"--\", color=\"#767676\", alpha=0.6)\n```\n\nLabel quadrants (\"Immune-hot / low tumor\", \"Immune-desert / high tumor\", …) with small grey text.\n\n---\n\n## 11) Reproduction Checklist\n\nTo match Nature publication standards:\n\n- [ ] **MANDATORY first lines**: `font.family='sans-serif'`, `font.sans-serif=['Arial','DejaVu Sans','Liberation Sans']`, `svg.fonttype='none'`\n- [ ] **Save as SVG** (primary). PNG dpi=300 as optional raster preview.\n- [ ] Top and right spines off; frameless legend\n- [ ] Figure architecture chosen intentionally: grid, schematic-led composite, image plate, or asymmetric hero layout\n- [ ] Journal-final source fonts usually 7–9 pt, and every exported PDF glyph including scripts is ≥ 5 pt\n- [ ] Colors from blue-green-red-neutral semantic palette\n- [ ] Rendered salience hierarchy matches the evidence hierarchy; neutral baselines do not dominate hero evidence\n- [ ] Black background used only for imaging plates, not for ordinary plots\n- [ ] Legends omitted or shared when direct labels or one legend strip read better\n- [ ] Y-limits tightened to data range (not 0–100 when values are 80–95)\n- [ ] X-ticks hidden when methods are named in legend\n- [ ] Legend in dedicated panel or `frameon=False`\n- [ ] Every comparable stochastic aggregate panel has the intended uncertainty definition\n- [ ] Rotated text uses anchor placement; labels clear data and uncertainty without opaque masks\n- [ ] `tight_layout(pad=2)` before save\n- [ ] `plt.close(fig)` after save\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18102,"content_sha256":"e8b31da9dec01f552acca6aeffbd06c8dbf53e737d7138075d1f97619c43a47e"},{"filename":"references/figure-contract.md","content":"# Figure Contract\n\nUse this reference before writing plotting code. The goal is to make the figure\nserve the paper's scientific logic.\n\n## Contents\n\n- [Privacy rule](#privacy-rule)\n- [Required contract](#required-contract)\n- [Core conclusion rules](#core-conclusion-rules)\n- [Archetype selection](#archetype-selection)\n- [Panel logic](#panel-logic)\n- [Aesthetic integration](#aesthetic-integration)\n- [Reviewer-risk prompts](#reviewer-risk-prompts)\n\n## Privacy rule\n\nKeep the figure contract user-facing, but keep the working trail private. Do not mention\nprivate paths, source filenames, internal reference documents, template identifiers, or\nwhere a private draft came from unless the user explicitly asks for provenance.\n\n## Required contract\n\nCreate a short contract in working notes or in the response:\n\n```text\nCore conclusion:\nResults-level question:\nFigure archetype:\nTarget journal/output:\nBackend: Python or R\nFinal size:\nPanel map:\n a:\n b:\n c:\nEvidence hierarchy:\n hero evidence:\n validation evidence:\n controls/robustness:\nStatistics needed:\nSource data needed:\nImage-integrity notes:\nReviewer risk:\n```\n\nDo not start from a favorite template. Start from the conclusion, then choose the\nminimum set of panels that make the conclusion clear and defensible.\n\nDo not start by mapping each input table to a figure. First group tables, metrics,\nand experiments by the scientific claims they can support. Several source tables\nmay belong in one evidence chain, while one table may support multiple distinct\npanels. This claim-first pass prevents redundant figures that restate the same result.\n\n## Core conclusion rules\n\n- The core conclusion should be one sentence with a verb: \"Treatment X reduces\n Y by restoring Z\", not \"Treatment results\".\n- Every panel must answer a unique question. If covering a panel would not weaken\n the argument, remove or merge it.\n- Separate primary evidence from supporting evidence. The primary evidence gets\n the hero panel or the clearest axis; controls and robustness panels should be\n visually quieter.\n- If the user provides data but no claim, infer a provisional claim from the data\n request. Confirm only an unresolved interpretive claim that materially changes the final figure; continue independent data checks and layout preparation while waiting. Do not add unsupported causal claims.\n- If observations are matched by dataset, subject, seed, task, or specimen, decide\n whether the claim concerns the paired change. Marginal distributions can overlap\n because of between-unit heterogeneity even when paired differences are consistent.\n\n## Archetype selection\n\n| Archetype | Use when | Hero panel | Supporting panels |\n|---|---|---|---|\n| `quantitative grid` | The claim is mainly numerical comparison | Optional; often a dominant summary metric | Shared axes, aligned scales, compact legends |\n| `schematic-led composite` | A workflow, mechanism, device, or experimental design must be understood first | Left or top schematic, 35-60% of area | 2-4 quantitative validation panels |\n| `image plate + quant` | Microscopy, imaging, histology, spatial overlays, segmentation, or blots lead the evidence | Image plate or representative image | Scale bars, overlays, crops, quantification |\n| `asymmetric mixed-modality figure` | The figure combines schematic, raster images, heatmaps, and quantitative plots | One panel spans rows/columns | Smaller panels ranked by evidence value |\n\n## Panel logic\n\nFor a labelled multi-panel manuscript figure, load\n`multipanel-evidence-architecture.md`. Use `one figure = one major claim` as a\nstrong planning default. Panels should be different necessary sentences in the\nsame visual argument, not parallel mini-stories or the same comparison mirrored\nacross several metrics.\n\nUse this order unless the manuscript story clearly requires another:\n\n1. Establish the system: sample, method, cohort, device, or experimental design.\n2. Show the main effect or primary comparison.\n3. Show mechanism or localization.\n4. Quantify the representative image or qualitative observation.\n5. Add robustness, controls, subgroup analysis, or sensitivity analysis.\n\nFor Fig. 1 or a method figure, the first panel often defines the visual vocabulary:\ncolors, symbols, workflow direction, sample classes, and scale. Reuse that vocabulary\nthrough the whole figure and, where possible, through the manuscript.\n\n## Aesthetic integration\n\n- Use one neutral family, one signal family, and one accent family.\n- Keep the same condition/method color across all panels.\n- Prefer direct labels for stable line identities, channels, and fixed spatial regions.\n- Use a shared legend area when repeated legends would waste space.\n- Avoid equal-sized panels when the evidence is not equally important.\n- Keep schematic colors and quantitative plot colors related. A schematic-led\n figure should look like one integrated argument, not a pasted collage.\n\n## Reviewer-risk prompts\n\nBefore finalizing, ask what a skeptical reviewer would challenge:\n\n- Is the sample size visible in the legend or source data?\n- Are error bars, intervals, and statistical tests defined?\n- Are the same uncertainty definitions present in every comparable aggregate panel?\n- Are axes comparable across panels that invite comparison?\n- Would a paired-difference view reveal an effect hidden by overlapping marginal distributions?\n- Does each legend label preserve canonical capitalization and match the terminology plan?\n- Do data labels clear the upper uncertainty extent at final rendered size?\n- Are representative images quantified and traceable to raw files?\n- Are image adjustments global and documented?\n- Could the same conclusion be made from fewer panels?\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5703,"content_sha256":"db08b51fb080c5fe37f61140a1b7fd31247f90810f3a80cdcf58429a699879df"},{"filename":"references/figure-legend-conventions.md","content":"# Figure & Table Legend Conventions (Nat Commun 2025 CS/AI corpus)\n\nUse this file when **writing or auditing the legend text** of a figure or table.\nIt complements `nature-2026-observations.md`, which covers visual/layout\narchetypes; this file covers the *words* of the caption. Distilled from a 2025\nset of 20 open-access *Nature Communications* computer-science / AI papers\n(legend conventions were consistent across all research articles). **Do not copy\nsource wording.**\n\n## Legend structure — the fixed skeleton\n\n1. **`Fig. N | ` + a bold noun-phrase overall title** that names the whole\n figure. Common openers: *Overview of …*, *Comparison of …*, *Performance of\n …*, or a finding phrase. No terminal full stop required on the title.\n2. **`a / b / c …` panels, each described in present tense, telegraphic style**,\n often subject-less: *\"a Comparison of the four EMS paradigms. b Distributions\n of WSIs and patches in the pre-training dataset.\"*\n3. **Statistics written into the legend**: sample size `n=`, error type, and\n test — *\"mean ± 95% CI (n = 1373) … one-way ANOVA with Tukey correction.\"*\n4. **Data-availability boilerplate** at the end: *\"Source data are provided as a\n Source Data file.\"*\n\n## Tense\n\n- Visual facts in **present** tense — *\"are shown as cyan sticks\"*, *\"depicts\"*.\n- Methods/how-it-was-made in **past** tense — *\"was performed\"*, *\"was adopted\n from\"*.\n\n## Self-containment rule\n\nA legend must be readable away from the body text. Put colour/shape mappings,\nsample size, and key numeric anchors (PDB id, RMSD, units) into the legend\nitself — *\"tRNA-Glu of E. coli (PDB: 2DER chain C). 76 nt, RMSD: 2.88 Å.\"*;\n*\"Grey boxes designate what is defined by the benchmark, and orange boxes\nindicate what is unique to each solution.\"*\n\n## Display-label capitalization\n\n- Treat in-figure legend entries as display labels. Start ordinary descriptive labels with an uppercase letter, for example `Tuned XGBoost`, `Tuned RF`, `+ Semantic guidance`, and `+ Causal guidance`.\n- Preserve canonical product/model spelling exactly, including internal capitals, hyphens, periods, and abbreviations such as `XGBoost`, `DeepSeek`, `GPT-5.2`, and `RF`.\n- In prose legend sentences, use normal sentence grammar rather than forcing every term into display case.\n- Do not apply `.title()` or equivalent automatic title-casing because it corrupts canonical names.\n\n## Advanced: the claim-closing sentence\n\nA legend's final sentence may advance an argument rather than only describe —\n*\"…indicating that these co-folding models are not predicting poses based on\nphysics but rather learning patterns in global structures.\"* Use sparingly, and\nonly when the panel actually supports the inference.\n\n## Review/Perspective legends\n\nWhen a figure aggregates others' published systems, each sub-panel gets a\none-line characterisation (often past tense, describing prior work) and the\nlegend carries an attribution line — *\"adapted with permission from refs. 16,17\n… by Springer Nature.\"* Include the permission/attribution string for any\nadapted panel.\n\n## Table captions\n\nSame shape: **`Table N | ` + noun phrase**, with detailed specs pointed to\nMethods — *\"Table 1 | … Detailed specifications are provided in the Methods\nsection.\"* Benchmark/framework papers lean on tables (multi-metric results) more\nthan figures.\n\n## Length and journal gate\n\n- This file's corpus evidence is from Nature Communications and does not set a\n universal Nature Portfolio word limit.\n- For the flagship journal Nature, load `nature-article-requirements.md` and\n keep each complete figure legend below 250 words.\n- For Nature Machine Intelligence, load the shared NMI contract. Its current\n live pages give no standalone per-legend number, while its official 2018\n brief guide said to keep each figure legend below 300 English words. Count\n the complete title-plus-panels legend, not each panel; aim for 150–250 words\n and use below 300 as a historical advisory ceiling unless the live submission\n system or editor gives a newer instruction.\n- For Nature Communications or another subjournal, verify the current journal\n and article-type instructions before enforcing a numerical cap.\n- Keep the `Fig. N |` title short and nominal; no numbers/results in the figure\n *title* line (numbers live in the panels and stats).\n\n## 中文图注要点\n\n- 结构铁律:`图 N | 加粗名词短语总题` → `a/b/c` 现在时电报式分面 → 统计(n、误差、检验)写进图注 → \"Source data are provided as a Source Data file.\" 套语。\n- 时态:视觉事实用现在时,制作方法用过去时。\n- 自足:颜色/形状映射、样本量、关键数值(PDB/RMSD/单位)都写进图注,使其脱离正文可读。\n- 图内图例按展示标签处理,普通描述首字母大写,同时保留 `XGBoost`、`DeepSeek`、`GPT-5.2`、`RF` 等规范拼写;正文仍按句法大小写,不要盲目自动 title case。\n- 进阶:图注末句可给一句推断结论,但须确有面板支撑。\n- 综述图注:聚合他人系统时逐子图一句话定性,并标注\"adapted with permission from refs… by Springer Nature\"授权。\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5165,"content_sha256":"7d00fc1839d5b152b289c8d892e723549be06c38ea75761763108a5ac34303fd"},{"filename":"references/multipanel-evidence-architecture.md","content":"# Multi-panel evidence architecture\n\nUse this reference when planning, restructuring, or auditing a manuscript\nfigure with several labelled panels, or when mapping a Results evidence chain\nonto a sequence of figures. Read it together with the\n[Nature Results–Discussion corpus guidance](../../nature-shared/core/nature-results-discussion.md)\nwhen figure order must mirror the manuscript's claim escalation.\n\n## Contents\n\n- [Status and scope](#status-and-scope)\n- [Start from the figure-level claim](#start-from-the-figure-level-claim)\n- [Give panels different inferential roles](#give-panels-different-inferential-roles)\n- [Choose an evidence-chain archetype](#choose-an-evidence-chain-archetype)\n- [Corpus pattern anchors](#corpus-pattern-anchors)\n- [Align figure order with Results claim escalation](#align-figure-order-with-results-claim-escalation)\n- [Assign visual hierarchy from evidence hierarchy](#assign-visual-hierarchy-from-evidence-hierarchy)\n- [Decide whether a panel belongs in the main figure](#decide-whether-a-panel-belongs-in-the-main-figure)\n- [Run the multi-panel audit](#run-the-multi-panel-audit)\n\n## Status and scope\n\nThis is **corpus-derived Nature-style guidance, not an official journal\nrequirement**. It was distilled from author-supplied readings of flagship\n*Nature* papers on MIRA (`s41586-026-10675-5`), centromere architecture\n(`s41586-026-10841-9`), Robin (`s41586-026-10652-y`) and TabPFN\n(`s41586-024-08328-6`), then generalized for Nature Portfolio manuscript\nfigures. Current journal instructions, article-type rules, the actual evidence\nand the scientific question always take precedence.\n\nDo not copy a published paper's panel count or letter order as a template. The\nstable pattern is inferential: a figure usually answers one Results-level\nscientific question, while its panels perform different jobs in the evidence\nchain needed to answer it.\n\n## Start from the figure-level claim\n\nBefore choosing plots, write:\n\n```text\nResults-level question:\nFigure-level claim:\nWhat could overturn that claim?\nDecisive evidence:\nPanel sequence:\n a — question | evidence role | necessary comparison\n b — question | evidence role | necessary comparison\n c — question | evidence role | necessary comparison\nDestination of displaced material: main figure | another figure | Extended Data/SI\n```\n\nTreat `one figure = one major claim` as a strong planning default, not a rigid\ncounting rule. A figure may contain several subordinate findings when all are\nnecessary to establish, qualify or bound the same higher-level claim. If a\npanel establishes a separate major claim, give it another figure or Results\nunit. If a panel can disappear without weakening or changing the figure-level\ninference, merge it, move it to Extended Data/SI, or delete it.\n\nPanel letters mark reading order; they do not assign universal functions.\nPanel `a` need not always be a schematic, and panel `d` need not always be a\nstress test. Choose roles from the scientific logic.\n\n## Give panels different inferential roles\n\nSelect the smallest sufficient set. A figure does not need every role.\n\n| Evidence role | Question it answers |\n|---|---|\n| Setup or schematic | What system, intervention, workflow or contrast is being tested? |\n| Representative example | What does the phenomenon look like in a concrete case? |\n| Primary quantitative evidence | Does the central effect or capability exist? |\n| Baseline or control | Does it exceed a credible alternative or survive the relevant control? |\n| Decomposition | Which components, strata or variables account for the effect? |\n| Stratification | Does the conclusion hold across scientifically meaningful conditions? |\n| Orthogonal validation | Is the conclusion recovered with another assay, measure or evidence type? |\n| Perturbation or stress test | Does the claim survive an attack, or fail as predicted when a necessary relation is disrupted? |\n| Boundary or failure case | Where does the effect weaken, reverse or stop generalizing? |\n| Mechanistic evidence | What bounded explanation is supported for why the effect occurs? |\n\nPanels are independently **necessary**, not independent stories. Covering one\npanel should remove a distinct inferential step that the other panels cannot\nrecover. Prefer role diversity over metric diversity.\n\nWeak mirror layout:\n\n`a: R2 -> b: R2 pairwise tests -> c: MAPE -> d: MAPE pairwise tests`\n\nThis contains four panels but often only two inferential roles: overall\nperformance and pairwise evidence. A secondary metric can be useful, but it\ndoes not earn a main panel merely by being another metric.\n\nStronger claim-driven layout:\n\n`a: perturbation design -> b: decisive comparison -> c: full distribution or residual -> d: decomposition or boundary`\n\nThis progresses from what was changed, to what happened, to which alternative\nwas rejected, to what explains or limits the remaining effect.\n\n## Choose an evidence-chain archetype\n\n### Validation envelope\n\nUse when the figure asks whether one system, simulator, intervention or model\ncapability is credible:\n\n`define the capability -> establish it -> test distinct failure modes -> attack the claim -> bound it`\n\nConsistency, leakage, safety, bias and adversarial robustness can belong in one\nfigure when they are different ways the same credibility claim could fail. Do\nnot present them as unrelated metric panels.\n\n### Scale-to-instance chain\n\nUse when a population-level pattern needs biological, clinical or physical\ninterpretation:\n\n`global map or distribution -> quantity -> spatial relation or scale -> internal structure -> representative exception or instance`\n\nThe representative example should explain, localize or challenge the aggregate\npattern; it should not be a decorative anecdote.\n\n### Discovery sequence\n\nUse when each result changes the next scientific question:\n\n`hypothesis -> decisive experiment -> analysis -> refined hypothesis -> next test -> mechanism validation`\n\nAt manuscript level, the figure sequence can carry this discovery loop. Keep\nonly steps that alter the claim or motivate the next test; do not reproduce a\nchronological laboratory notebook.\n\n### Capability ladder\n\nUse when a method paper makes progressively stronger claims:\n\n`what it is -> what behaviour it learned -> whether it outperforms alternatives -> whether the gain survives harder conditions -> what broader ability follows`\n\nWithin a benchmark figure, combine complementary support such as aggregate\nperformance, per-dataset generality and efficiency. In the following figure,\nmove to a stronger question such as robustness, confounding or broader\ncapability instead of redrawing the same ranking.\n\nAcross these archetypes, a reusable Figure-level chain is:\n\n`establish -> compare or control -> stress-test or discriminate -> broaden -> bound`\n\nUse only the moves the claim needs.\n\n## Corpus pattern anchors\n\nUse these author-supplied readings as reasoning examples, not layouts to copy:\n\n- **MIRA — validation envelope.** One figure asks whether the patient agent is\n a credible evaluation environment, with panels probing consistency,\n information leakage and adversarial robustness as different failure modes.\n Later figures ask separate higher-level questions about intervention\n capability, medication, admission decisions and bias robustness. The metrics\n are grouped by the claim they can overturn, not by data type.\n- **Centromere architecture — scale to instance.** A resource/map figure first\n defines the study object. A later variation figure moves through count,\n spatial relation, scale and internal structure before showing informative\n examples. The panels collectively explain which dimensions constitute the\n variation rather than announcing five unrelated discoveries.\n- **Robin — discovery sequence.** The figure progression follows hypothesis\n generation, wet-lab testing, analysis and refined mechanism or hypothesis.\n The sequence preserves the scientific discovery loop because each result\n changes what is tested next.\n- **TabPFN — capability ladder.** Early figures explain the system and learned\n behaviour; a benchmark figure combines aggregate performance, per-dataset\n generality and tuning-time efficiency; a later figure asks whether that\n performance claim survives uninformative features, outliers, reduced data\n and stronger competitors. The later figure escalates the claim instead of\n repeating that performance is good.\n\nThese examples support three portable distinctions:\n\n1. several attacks on one claim can belong in one figure\n2. population summaries and representative cases can be complementary steps\n in one figure\n3. a later figure should normally ask a deeper question than the preceding\n figure\n\n## Align figure order with Results claim escalation\n\nThe relationship works at two scales:\n\n- **Within a figure:** panels are different sentences in one compact Results\n argument.\n- **Across figures:** each figure should make a stronger or different claim\n that creates the next scientific question.\n\nFor example:\n\n`Fig. 1: What is it? -> Fig. 2: Is the evaluation or resource credible? -> Fig. 3: Does the phenomenon exist? -> Fig. 4: Does it beat alternatives? -> Fig. 5: Does the claim survive harder conditions? -> Fig. 6: What mechanism, generality or boundary follows?`\n\nThis is an archetype, not a mandatory six-figure template. Apply the\nclaim-escalation audit from the shared Results–Discussion guidance. If two\nfigures can both be summarized as `X performs well`, merge them, demote the\nweaker evidence, or revise the later figure around a deeper discriminator.\n\n## Assign visual hierarchy from evidence hierarchy\n\n- Give the decisive evidence the hero position or largest visual area.\n- Keep controls and robustness panels quieter, but large enough to judge.\n- Use one condition, cohort or method mapping consistently across panels.\n- Do not force equal panel sizes when inferential importance differs.\n- Use a setup schematic only when readers need it to interpret the evidence;\n do not add a decorative panel `a` by habit.\n- Make the attack, negative result or failure boundary visible when it changes\n the figure-level claim.\n- Keep axes, uncertainty definitions, sample definitions and comparator labels\n consistent whenever panels invite direct comparison. Disclose any change in\n `n`, denominator, cohort, estimator or scale.\n\n## Decide whether a panel belongs in the main figure\n\nUse the necessity test:\n\n1. What unique inference disappears if this panel is removed?\n2. Does that inference establish, advance, qualify or bound the figure-level\n claim?\n3. Is it a distinct evidence role, or the same result under another metric,\n estimator, seed, threshold or visual encoding?\n\nThen route it:\n\n- **Main figure:** decisive evidence, a necessary control, a central\n falsification, or a conclusion-changing boundary.\n- **Extended Data/SI:** reassurance, provenance detail, secondary metrics,\n alternative estimators, expanded subgroup views or robustness that does not\n change the central interpretation.\n- **Another figure:** a genuinely separate major claim or the next rung of the\n manuscript's claim escalation.\n- **Delete or merge:** a repeated view with no independent inference gain.\n\nDo not hide a result that changes the conclusion merely because it is negative\nor complicates the layout.\n\n## Run the multi-panel audit\n\nComplete this table before drawing and again after the final render:\n\n| Panel | Scientific question | Evidence role | Decisive comparison | Unique inference gain | Claim dependency | Destination |\n|---|---|---|---|---|---|---|\n| a | | | | | | |\n| b | | | | | | |\n| c | | | | | | |\n\nAudit the full figure:\n\n- Can the figure-level claim be stated in one sentence with a verb?\n- Do the panels collectively answer one Results-level question?\n- Does every panel add a distinct inferential role rather than only a new\n metric or chart type?\n- Does the panel order form a readable chain rather than a dashboard grid?\n- Is the strongest evidence visually dominant?\n- Are central controls, attacks and failure boundaries present?\n- Would a moved panel fit Extended Data/SI without weakening the main claim?\n- Does the next figure ask the question created by this figure?\n- Can the caption explain what is shown and how to read it without replaying\n the full Results argument?\n- At final physical size, are dependencies, shared encodings, uncertainty and\n panel transitions still legible?\n\nThe final design rule is: **do not begin with the number of datasets, metrics\nor panels available. Begin with the one sentence the figure must establish,\nthen choose the few different evidence roles a skeptical reader needs to\nbelieve it.**\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12732,"content_sha256":"79ca617cc05088d6fcfb065140d0ee036eb849738bb79306964eb9aa3a4ed274"},{"filename":"references/nature-2026-observations.md","content":"# 2026 Nature Sample Observations\n\n## Contents\n\n- [Archetype 1: Schematic-led composite](#archetype-1-schematic-led-composite)\n- [Archetype 2: Dark image plate](#archetype-2-dark-image-plate)\n- [Archetype 3: Clinical triptych](#archetype-3-clinical-triptych)\n- [Archetype 4: Dense categorical physical-science panel](#archetype-4-dense-categorical-physical-science-panel)\n- [Archetype 5: Asymmetric mixed-modality figure](#archetype-5-asymmetric-mixed-modality-figure)\n- [Cross-cutting Nature rules from the sample](#cross-cutting-nature-rules-from-the-sample)\n- [Palette guidance by modality](#palette-guidance-by-modality)\n- [What not to copy blindly](#what-not-to-copy-blindly)\n\n\nThis note captures page-level figure patterns observed from a local 2026 sample of `Nature`\npapers, plus one `Nature Biomedical Engineering` paper used as a clinical / ML-adjacent\ncross-check.\n\nSampled figure sources:\n\n- `s41586-026-10408-8` — wide schematic-led materials figure with supporting quant panels\n- `s41586-026-10426-6` — dark whole-brain image plate with repeated views\n- `s41586-026-10393-y` — clinical triptych: longitudinal lines, forest plots, summary bars\n- `s41586-026-10257-5` — dense categorical stacked-area panels with direct labels\n- `s41586-026-10439-1` — asymmetric genomics figure with one dominant circular panel\n- `Expert-level detection of pathologies...` — compact medical / ML figure conventions\n\n## Archetype 1: Schematic-led composite\n\nSeen in the printable meta-assemblies paper.\n\nActionable rules:\n\n- Let the schematic occupy roughly `45–60%` of figure height.\n- Use the **same physical/material palette** in the supporting plots; do not switch to generic method colors below the schematic.\n- Zoom callouts should use one repeated accent style across the figure, for example a single dashed red outline family.\n- Reserve at least one supporting panel for a real-world photograph or experimental snapshot when the story needs scale validation.\n- Supporting quantitative panels should be smaller, cleaner and less saturated than the schematic so the eye reads the page in the intended order.\n\n## Archetype 2: Dark image plate\n\nSeen in the astrocyte brain-network figure.\n\nActionable rules:\n\n- Use a black facecolor only for the image plate region, not for the whole page.\n- Pair grayscale context with one or two fluorescent channels; the sample repeatedly used cyan and magenta.\n- Keep crops, scale bars and view boxes geometrically consistent across rows and columns.\n- Use white gutters and white scale bars so the plate stays legible after print/export compression.\n- Put row labels and channel labels directly on the image plate; avoid detached legends.\n\nRecommended accent set for this modality:\n\n```python\nCYAN = \"#22D7E6\"\nMAGENTA = \"#FF2AD4\"\nGREY_CONTEXT = \"#B8B8B8\"\n```\n\n## Archetype 3: Clinical triptych\n\nSeen in the OTOF gene-therapy paper.\n\nActionable rules:\n\n- Top row: line plots or longitudinal summaries, usually sharing one legend strip above the row.\n- Middle row: forest-plot style effects with a dashed vertical reference line and light category bands.\n- Bottom row: compact summary bars, often binary or stacked-percentage bars.\n- Keep columns semantically parallel. If the first column is `ABR`, the next columns should reuse the same row logic rather than introducing a new layout.\n- Baseline / reference series can be black or dark grey; follow-up or intervention groups can use a restrained warm/cool sequence.\n\nRecommended design signal:\n\n- Legends belong outside the data region when there are many timepoints.\n- Group bands in forest plots should be pale and subordinate, never more salient than the confidence intervals.\n\n## Archetype 4: Dense categorical physical-science panel\n\nSeen in the condensation-sequence figure.\n\nActionable rules:\n\n- Direct-label regions when the plot has many semantically intrinsic categories.\n- Use hatching or texture overlays when neighboring fills are close in luminance or may print poorly.\n- Reuse the exact same axis limits and panel geometry across the full grid.\n- Prefer embedded labels over a detached mega-legend when each panel repeats the same categorical structure.\n\n## Archetype 5: Asymmetric mixed-modality figure\n\nSeen in the rediploidization genomics figure.\n\nActionable rules:\n\n- Do not force equal panel sizes. Let the biologically central panel dominate.\n- Use small supporting plots around the hero panel to answer narrower questions.\n- Keep a tight, reused color mapping across all modalities, for example `wave 1 / wave 2 / wave 3` or `baseline / highlight / neutral`.\n- Use whitespace and alignment, not decorative frames, to signal grouping.\n\n## Cross-cutting Nature rules from the sample\n\n- Panel labels are small bold lowercase letters near the top-left corner, not large badges.\n- Figure pages are narrative, not dashboard-like. A dominant panel is normal.\n- Legends are often omitted if direct labeling is possible.\n- Background discipline matters more than ornament. White for charts, black only for image plates.\n- Saturated colors are used sparingly and usually mean either a true experimental channel or a highlighted subgroup.\n- When several modalities coexist, keep axis-heavy plots visually quieter than schematics or imaging panels.\n- Gutters are slightly larger when dark panels touch light panels or when modalities change.\n\n## Palette guidance by modality\n\n- Materials / mechanism pages:\n `aqua`, `teal`, `lilac`, `soft violet`, with one red accent for callouts only.\n- Imaging plates:\n `black` + `grey context` + `cyan` + `magenta`.\n- Clinical quantitative figures:\n `black baseline`, then restrained warm/cool follow-up hues, with pale group shading.\n- Genomics / systems figures:\n `neutral greys` plus one `red family` and one `blue family` for highlighted biological states.\n\n## What not to copy blindly\n\n- Do not import a bright multi-hue palette just because one sampled physical-science figure used many fills. That only works when the categories are intrinsic phases/materials and directly labeled.\n- Do not place all Nature figures on black backgrounds; that was specific to the imaging plate archetype.\n- Do not force a legend into every panel. Many sampled figures read better with direct labels or one shared legend strip.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6270,"content_sha256":"1af8250733da77f1d82cc370e2e8d61266352517b1d1ab82ed6c834de8c98272"},{"filename":"references/nature-article-requirements.md","content":"# Flagship Nature Article figure requirements\n\nUse this reference for figures submitted to the flagship journal **Nature**.\nKeep initial-review files separate from accepted-in-principle production files,\nand do not apply these formats automatically to Nature Portfolio subjournals.\n\n## Contents\n\n1. Stage gate\n2. Initial-submission figures\n3. Legend contract\n4. Production dimensions and typography\n5. Main-figure production files\n6. Extended Data production files\n7. Accessibility and image integrity\n8. Delivery audit\n9. Official sources\n\n## 1. Stage gate\n\nRecord one stage before auditing:\n\n- `initial_submission`: figures may be embedded in the Word/PDF manuscript;\n production-quality files are not required\n- `revision`: follow both the public guide and the editor's instructions\n- `accepted_in_principle`: supply production-quality main figures and Extended\n Data using their different file contracts\n\nDo not fail an initial submission solely because it lacks separate editable\nproduction artwork. Do fail it when the displayed data are unreadable,\nmisrepresented, incomplete or impossible for referees to assess.\n\n## 2. Initial-submission figures\n\n- Prefer figures embedded with manuscript text in one Word or PDF file.\n- Put each figure legend on the same page as its figure.\n- Use enough resolution for referees to evaluate the data.\n- If embedding is impractical, supply separate files or an accessible\n repository route and confirm every figure is cited.\n- Run the statistics, source-data and image-integrity checks even though final\n artwork formatting is deferred.\n\n## 3. Legend contract\n\nFor every flagship Nature figure legend:\n\n- keep the complete legend below 250 words\n- begin with a brief title sentence for the whole figure\n- follow with a concise description of what each panel depicts\n- do not use the legend to narrate results or duplicate Methods\n- include definitions needed to understand the figure in isolation\n- state exact `n`, replicate definition, center/spread, error bars, test,\n correction and P-value display where applicable\n- identify adaptations and permissions when third-party material is used\n\nTable legends begin with a short title sentence; explanatory detail may go in\nfootnotes.\n\n## 4. Production dimensions and typography\n\nFor accepted-in-principle main figures:\n\n- use 89 mm for a single-column figure or 183 mm for a double-column figure\n- do not exceed 170 mm height, leaving room for the legend\n- keep ordinary figure text between 5 pt and 7 pt at final size\n- label multi-panel figures with 8 pt bold upright lowercase `a`, `b`, `c`, etc.\n- use a consistent sans-serif typeface, preferably Arial or Helvetica\n- use Courier or another monospaced font for amino-acid sequences\n- keep all text editable, do not outline it, and embed TrueType 2 or 42 fonts\n- arrange panels alphabetically where practical and minimize unused space\n\n## 5. Main-figure production files\n\nMain figures require vector artwork with editable layers.\n\nPreferred:\n\n- `.ai`\n- `.eps`\n- editable `.pdf`\n\nAlso accepted by the current research figure guide when properly prepared:\n\n- layered Photoshop artwork\n- PowerPoint converted to PDF\n- plain `.svg`\n- Excel\n- `.ps`\n\nDo not submit flattened `.jpeg`, `.tiff` or `.png` as the final main-figure\nproduction file merely because the workflow also creates those formats for QA\nor preview. Keep all components embedded rather than externally linked and aim\nto keep each file below 50 MB.\n\n## 6. Extended Data production files\n\nExtended Data has a different production contract:\n\n- save in RGB\n- use no more than 300 dpi\n- keep each file at or below 10 MB\n- use `.jpeg` preferably, or `.tiff`/`.eps`\n- fit each item on one page with room for its legend or footnotes\n- use the journal's required filename pattern based on the corresponding\n author's surname and the ED figure/table number\n\nDo not send a main figure through the Extended Data file route or vice versa.\n\n## 7. Accessibility and image integrity\n\n- include axis lines and tick marks\n- label every axis and place units in parentheses\n- avoid background gridlines, drop shadows, decorative icons, patterns,\n overlapping labels and coloured text\n- use an accessible palette; do not rely on red/green or rainbow scales\n- supply artwork in RGB; photographic images need at least 300 dpi, with\n 450 dpi preferred for the highest online-proof resolution\n- keep scale bars and their labels editable rather than flattened into images\n- use scale bars instead of magnification factors\n\nFor microscopy, gels, blots or other processed images, load the image-integrity\nsection of `../../nature-shared/core/research-compliance.md`. Record raw-file\nprovenance, crop, contrast, gamma, pseudocolour, stitching, lane rearrangement\nand processing software.\n\n## 8. Delivery audit\n\nReturn:\n\n| Item | Stage | Required contract | Current file/evidence | Status | Action |\n|---|---|---|---|---|---|\n\nBefore approval:\n\n- run `scripts/validate_figure.py` on plotting source\n- run `scripts/audit_pdf_text.py` on exported PDF text\n- run `scripts/audit_figure_collisions.py` after every generated or layout-affecting revision and resolve every rendered collision FAIL\n- inspect every panel and the assembled figure at final physical size\n- verify that preview/export bundles are not mislabeled as the journal's final\n accepted upload formats\n\n## 9. Official sources\n\nVerified 2026-08-08:\n\n- Nature initial submission: \u003chttps://www.nature.com/nature/for-authors/initial-submission>\n- Preparing figures: \u003chttps://research-figure-guide.nature.com/figures/preparing-figures-our-specifications/>\n- Building and exporting figure panels: \u003chttps://research-figure-guide.nature.com/figures/building-and-exporting-figure-panels/>\n- Image integrity: \u003chttps://www.nature.com/nature-portfolio/editorial-policies/image-integrity>\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5822,"content_sha256":"d59ebd80a99b2a91d9b0861529044f6242e1430b37fa4d7c73935a40a6a6be33"},{"filename":"references/openrouter-image-generation.md","content":"# OpenRouter Image Generation for Manuscript Schematics\n\n## Contents\n\n- [Source](#source)\n- [Safety and scientific integrity](#safety-and-scientific-integrity)\n- [Prompt contract](#prompt-contract)\n- [Script usage](#script-usage)\n- [Recommended defaults](#recommended-defaults)\n- [Follow-up QA](#follow-up-qa)\n\n\nUse this reference only when the user explicitly asks to generate a paper schematic, graphical abstract, mechanism diagram, or concept illustration through OpenRouter / GPT Image 2 / an image-generation API.\n\nRead `ai-graphical-abstract-workflow.md` first. Complete its journal-policy gate,\ncommunication brief, scientific-review plan, and provenance plan before making a\nreal API call. If submission eligibility has not been verified, mark the result\nas an internal design draft rather than a submission-ready asset.\n\nDo not use this route for quantitative plots, data panels, heatmaps, microscopy plates, blots, or figure assembly unless the user explicitly wants an AI-generated draft illustration. Keep data-driven figures in the Python or R route.\n\n## Source\n\nOpenRouter's dedicated Images API uses:\n\n- model discovery: `GET https://openrouter.ai/api/v1/images/models`\n- generation: `POST https://openrouter.ai/api/v1/images`\n- default model for this skill: `openai/gpt-image-2`\n\nAuthentication uses `OPENROUTER_API_KEY` as a bearer token.\n\n## Safety and scientific integrity\n\n- Treat generated images as draft visual concepts, not evidence.\n- Do not treat a Nature Careers article as permission to submit generative-AI\n artwork. Verify the exact target journal's current official rules and keep\n internal usefulness separate from submission eligibility.\n- Do not invent quantitative values, p-values, spectra, microscopy findings, institution logos, author photos, journal marks, or unsupported mechanisms.\n- Prefer short labels and simple shapes. AI image models can misspell text; final publication labels should usually be redrawn in Illustrator, Inkscape, PowerPoint, or a Python/R vector workflow.\n- If the schematic could be interpreted as a data panel, explicitly mark it as conceptual.\n- Do not send confidential manuscript content to OpenRouter without user permission.\n\n## Prompt contract\n\nBefore calling the API, collect or infer:\n\n1. article title or central claim\n2. key biological/material/computational entities\n3. cause-effect mechanism or workflow stages\n4. desired layout, such as left-to-right pipeline, circular mechanism, split before/after, or graphical abstract\n5. target aspect ratio and output format\n6. any labels that must appear, keeping them short\n7. things that must be excluded\n8. target journal, policy URL, access date, and intended AI-use disclosure\n\nWrite a compact prompt with:\n\n- visual role: \"Nature-style graphical abstract\" or \"clean scientific mechanism schematic\"\n- composition: panel flow, hierarchy, and focal element\n- style: flat vector-like, restrained palette, high contrast, white or transparent background\n- scientific constraints: no fabricated numbers, no extra organs/cells/materials, no logos\n- output constraints: minimal text, editable downstream, journal-safe\n\n## Script usage\n\nUse the bundled script for reproducible calls:\n\n```bash\nexport OPENROUTER_API_KEY=\"sk-or-...\"\npython skills/nature-figure/scripts/generate_openrouter_schematic.py \\\n --title \"Paper title\" \\\n --abstract-file abstract.txt \\\n --panel-map \"left: problem; center: proposed mechanism; right: validated outcome\" \\\n --outdir outputs/schematic \\\n --basename graphical_abstract \\\n --aspect-ratio 16:9 \\\n --resolution 2K \\\n --quality high\n```\n\nDry-run without network or API key:\n\n```bash\npython skills/nature-figure/scripts/generate_openrouter_schematic.py \\\n --title \"Self-healing cementitious sensor\" \\\n --abstract \"A composite sensor couples chloride ingress with recoverable piezoresistive response.\" \\\n --panel-map \"left: marine exposure; center: ion transport and microcrack healing; right: signal recovery curve\" \\\n --dry-run\n```\n\nUse a fully custom prompt:\n\n```bash\npython skills/nature-figure/scripts/generate_openrouter_schematic.py \\\n --prompt-file schematic_prompt.md \\\n --raw \\\n --outdir outputs/schematic\n```\n\nUse one or more reference images:\n\n```bash\npython skills/nature-figure/scripts/generate_openrouter_schematic.py \\\n --prompt-file schematic_prompt.md \\\n --reference-image draft_layout.png \\\n --reference-image https://example.com/style-reference.png\n```\n\nThe script saves generated files plus `request_metadata.json` in the output directory.\n\nUse another OpenAI-compatible images endpoint (`--api-url`, or `SCHEMATIC_IMAGE_API_URL`) when the user already pays for a gateway instead of OpenRouter. The response shape (`data[].b64_json` or `data[].url`) is the same, so nothing else changes; the endpoint actually called is recorded in `request_metadata.json`. Example with OrcaRouter:\n\n```bash\nexport ORCAROUTER_API_KEY=\"sk-orca-...\"\npython skills/nature-figure/scripts/generate_openrouter_schematic.py \\\n --api-url https://api.orcarouter.ai/v1/images/generations \\\n --api-key-env ORCAROUTER_API_KEY \\\n --model openai/gpt-image-2 \\\n --title \"Paper title\" \\\n --outdir outputs/schematic\n```\n\n## Recommended defaults\n\n- `model`: `openai/gpt-image-2`\n- `aspect_ratio`: `16:9` for graphical abstracts, `4:3` for mechanism figures, `1:1` for cover-like concepts\n- `resolution`: `2K` for review drafts; use higher only when needed\n- `quality`: `high`\n- `output_format`: `png`\n- `background`: `opaque` unless the user asks for transparent\n\n## Follow-up QA\n\nAfter generation:\n\n1. inspect the image visually\n2. check whether labels are legible and spelled correctly\n3. list any scientific hallucinations or unsupported visual claims\n4. recommend which labels or arrows should be redrawn as vector objects\n5. keep the generated image and metadata together for provenance\n6. report separate verdicts for internal design use and submission eligibility\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5928,"content_sha256":"52a26512c9eabd040c95c5c97f6fba9b110cd4be5b41d667d372c489d11526c6"},{"filename":"references/qa-contract.md","content":"# QA Contract\n\n## Contents\n\n- [Current official references to verify](#current-official-references-to-verify)\n- [Pre-submission checklist](#pre-submission-checklist)\n- [Statistics legend minimum](#statistics-legend-minimum)\n- [Image-integrity minimum](#image-integrity-minimum)\n- [Automated source preflight](#automated-source-preflight)\n- [Automatic multi-panel alignment gate](#automatic-multi-panel-alignment-gate)\n- [Automatic rendered collision audit](#automatic-rendered-collision-audit)\n- [Rendered panel-by-panel audit](#rendered-panel-by-panel-audit)\n- [Typography and PDF glyph floor](#typography-and-pdf-glyph-floor)\n- [Printed size in the manuscript](#printed-size-in-the-manuscript)\n- [Multi-line labels](#multi-line-labels)\n- [Uncertainty consistency](#uncertainty-consistency)\n- [Geometry and annotation placement](#geometry-and-annotation-placement)\n- [Color separation and salience](#color-separation-and-salience)\n- [Transformation and paired-effect checks](#transformation-and-paired-effect-checks)\n- [Export checks](#export-checks)\n\n\nUse this before final delivery, before a revision package, and whenever the figure\ncontains microscopy, blots, gels, clinical subgroup analysis, or statistical claims.\nJournal rules change, so verify the latest target journal author guide for final\nsubmission. The values below are conservative defaults for Nature-family style work.\nFor the flagship journal Nature, load `nature-article-requirements.md` and use\nits stage-specific main-figure, Extended Data and legend contracts.\n\n## Current official references to verify\n\n- Nature research figure guide: `https://research-figure-guide.nature.com/`\n- Nature building/exporting panels: `https://research-figure-guide.nature.com/figures/building-and-exporting-figure-panels/`\n- Nature preparing figures/specifications: `https://research-figure-guide.nature.com/figures/preparing-figures-our-specifications/`\n- Nature initial submission and statistics guidance: `https://www.nature.com/nature/for-authors/initial-submission`\n- Nature formatting guide: `https://www.nature.com/nature/for-authors/formatting-guide`\n- Journal of Cell Biology figure/video guidelines for microscopy-oriented image QA: `https://rupress.org/jcb/pages/fig-vid-guidelines`\n- Elsevier/Cell-family image-manipulation baseline: `https://www.sciencedirect.com/journal/the-cell-surface/publish/guide-for-authors`\n\n## Pre-submission checklist\n\n| Check | Pass condition |\n|---|---|\n| Core conclusion | One-sentence claim exists and every panel maps to it |\n| Archetype | Figure has a declared archetype and panel hierarchy |\n| Backend exclusivity | The selected backend produced all plotting, previews, exports, and visual QA renders |\n| Final size | Single-column about 89 mm or double-column about 183 mm, height not above target journal limit |\n| Text size | Body/tick/legend text is readable at final size, usually 5-7 pt for dense journal figures |\n| Rendered glyph floor | Every PDF text run, including math superscripts/subscripts, is at least 5 pt |\n| Panel labels | Lowercase, bold, near top-left, typically 8 pt at final size |\n| Editable text | SVG/PDF text remains editable; no outlined text unless unavoidable for special symbols |\n| Font | Arial/Helvetica/sans-serif fallback is used consistently |\n| Color | No rainbow color maps; red/green is not the only encoding; grayscale print remains interpretable |\n| Legend strategy | Shared or direct labels where possible; no repeated redundant legends |\n| Display terminology | Legend labels use display-style initial capitalization and preserve canonical model names |\n| Statistics | `n`, biological/technical repeat definition, center, spread, test, correction, and exact comparison are documented |\n| Comparable uncertainty | Every comparable seed/fold/split aggregate panel shows the same variability definition or documents an exemption |\n| Annotation clearance | Automatic PDF collision audit has no FAIL findings; every WARN is reviewed at final size and justified or fixed |\n| Panel alignment | Every multi-panel figure has a fresh alignment JSON; comparable row/column edges, dimensions, labels and repeated gutters are within 1.5 pt or carry a reasoned exemption |\n| Visual hierarchy | Hero evidence remains more salient than neutral baselines after rendering |\n| Numerical transforms | Interpolation/normalization direction and monotonicity assumptions are asserted in code |\n| Source data | Quantitative panels can be traced to a clean CSV/TSV/XLSX or script output |\n| Raster resolution | Photos/microscopy are high-resolution enough for final size; line art uses vector where possible |\n| Microscopy scale | Scale bar is present, calibrated, and not only a magnification factor |\n| Image integrity | Crop, contrast, pseudo-color, stitching, reuse, and raw-file provenance are recorded |\n| Export bundle | Script, source data, SVG, PDF, TIFF/PNG preview, and QA notes are delivered together when requested; previews are not mislabeled as accepted main-figure upload formats |\n\n## Statistics legend minimum\n\nFor each quantitative panel, capture:\n\n```text\nn definition:\nbiological replicates:\ntechnical replicates:\ncenter statistic:\nspread/interval:\ntest:\nmultiple-comparison correction:\np-value display:\nsource-data file:\n```\n\nFor machine-learning/model figures, also capture:\n\n```text\ntrain/validation/test split:\nnumber of seeds or folds:\nmetric definition:\nconfidence interval or variability definition:\nbaseline definition:\n```\n\n## Image-integrity minimum\n\nFor each image panel, capture:\n\n```text\nraw file:\nprocessed file:\ncrop:\nbrightness/contrast/gamma:\npseudo-color:\nscale calibration:\nstitching:\nreuse in other figures:\nquantification link:\n```\n\nGlobal adjustments are generally safer than local selective edits. If an adjustment\nchanges the visibility of relevant background or bands, flag it instead of silently\nnormalizing it away.\n\n## Automated source preflight\n\nRun the dependency-free validator on the final plotting source before rendering the delivery bundle:\n\n```bash\n# Python source\npython skills/nature-figure/scripts/validate_figure.py path/to/figure.py\n\n# R, R Markdown, or Quarto source\npython skills/nature-figure/scripts/validate_figure.py path/to/figure.R\n\n# Machine-readable report or stricter warning gate\npython skills/nature-figure/scripts/validate_figure.py path/to/figure.py --json\npython skills/nature-figure/scripts/validate_figure.py path/to/figure.py --strict\n\n# Exported-PDF glyph-size audit\npython skills/nature-figure/scripts/audit_pdf_text.py path/to/figure.pdf --min-pt 5\npython skills/nature-figure/scripts/audit_pdf_text.py path/to/figure.pdf --min-pt 5 --json\n\n# Backend-neutral audit of the render-time Python/R panel-layout manifest\npython skills/nature-figure/scripts/audit_panel_alignment.py path/to/figure.alignment-layout.json \\\n --json-out path/to/figure.alignment.json \\\n --overlay-svg path/to/figure.alignment.svg \\\n --strict\n\n# Mandatory rendered collision audit for Python and R figures\npython skills/nature-figure/scripts/audit_figure_collisions.py path/to/figure.pdf \\\n --json-out path/to/figure.collision-audit.json \\\n --overlay-pdf path/to/figure.collision-audit.pdf\n\n# Optional: make ambiguous WARN findings blocking\npython skills/nature-figure/scripts/audit_figure_collisions.py path/to/figure.pdf --strict\n```\n\nThe source preflight checks syntax, font configuration and size floor, mathtext shrinkage risk, literal legend-label capitalization, unsafe color maps, editable-text settings, vector/raster exports, DPI, common journal widths, potential sampling or unreported missing-data exclusion, simulated-data leakage, log guards, interpolation monotonicity, stochastic uncertainty encoding, rotated-text anchoring, risky annotation workarounds, and obvious cross-backend plotting references. The panel-alignment gate reads final axes/grob rectangles measured by the selected backend and applies one physical-point contract to Python and R. The PDF text audit scans supported content streams for actual `Tf` font-size operators and catches reduced script glyphs that source-level `fontsize` checks miss. The rendered collision audit uses PyMuPDF geometry from the final PDF and therefore applies equally to Python- and R-generated figures.\n\nTreat the result as a deterministic source audit, not as evidence that the analysis or rendered figure is correct. Resolve all `FAIL` findings before delivery. Review every `WARN`, then run the selected backend and inspect the actual SVG/PDF/TIFF/PNG outputs at final size. A warning may be acceptable only when the QA notes state the reason.\n\n## Automatic multi-panel alignment gate\n\nRun the gate for every figure with at least two comparable panels, after the\nfinal layout engine has drawn fonts, legends, colorbars and constrained/tight\nlayout, and before exporting submission files. Rerun it after any change that\ncan affect panel geometry. A single-panel figure is `not applicable`; a\nmulti-panel figure with no declared or inferable comparison groups is `NOT\nAUDITABLE`, not a pass.\n\nThe default tolerance is `1.5 pt` (about `0.53 mm`) at final physical size. The\nauditor checks:\n\n- panels in one row: common top and bottom edges plus equal plot-area height;\n- three or four same-row panels with equal grid spans: equal final plot-area\n widths within `1.5 pt`;\n- panels in one column: common left and right edges plus equal plot-area width;\n- unequal-span grid panels: shared outer start/stop edges, including automatic\n `left two + right one` and `left one + right two` arrangements;\n- three or more comparable panels: uniform repeated horizontal/vertical gutters;\n- detectable bold lowercase top-left panel labels: common y anchors within rows\n and common x anchors within columns;\n- plot-area rectangles do not overlap.\n\nFor Matplotlib, copy `audit_panel_alignment.py` beside the plotting script or\nadd the skill scripts directory to `PYTHONPATH`, then call this after the last\nlayout change:\n\n```python\nfrom audit_panel_alignment import require_matplotlib_panel_alignment\n\nrequire_matplotlib_panel_alignment(\n fig,\n json_out=\"figure.alignment.json\",\n overlay_svg=\"figure.alignment.svg\",\n tolerance_pt=1.5,\n gutter_tolerance_pt=1.5,\n require_panel_labels=True,\n strict=True,\n)\n```\n\nOrdinary `subplots`/`GridSpec` row and column groups are inferred from\n`SubplotSpec`. A panel spanning both grid rows is automatically compared with\nthe upper small panel at their shared top boundary and the lower small panel at\ntheir shared bottom boundary; this works whether the spanning panel is in the\nleft or right column. The spanning panel is not incorrectly required to equal\neither small panel's height. Position outside panel letters with a fixed point\noffset from the axes corner rather than a shared axes-fraction offset, because\n`y=1.02` produces different physical displacements for tall and short panels.\nFor a horizontal row of three or four panels, equal column spans imply equal\nfinal widths. Intentional unequal `width_ratios` must use a narrow exemption\nsuch as `{\"panels\": [\"b\"], \"checks\": [\"panel-width\"], \"reason\": \"middle hero panel\"}`;\ndo not exempt the entire row or increase the tolerance.\nPass explicit `row_groups` and `column_groups` when axes come from nested grids\nor separately created containers. Exclude a\ncolorbar or inset with `exclude_axes=[...]`, or add an exemption containing the\naffected panel, the exact checks and a non-empty scientific/layout reason.\n\nFor R/patchwork, source `panel_alignment.R` and call\n`require_patchwork_panel_alignment()` with the same width/height used for final\nexport. Common patchwork groups and structured unequal-span designs are inferred\nfrom gtable cells, including two stacked panels beside a two-row spanning panel\nin either column. Same-row groups of three or four equal gtable spans must also\nhave equal final widths. Nested or manually positioned designs must provide explicit\n`row_groups` / `column_groups`. The R helper requires `patchwork`, `grid` and\n`jsonlite`; it measures in R and invokes the Python CLI only to audit the\nresulting JSON, so it does not redraw or replace the R figure.\n\n| Result | Required action |\n|---|---|\n| `NOT APPLICABLE`, exit `0` | Single rendered plot-area only; preserve the JSON and continue |\n| `PASS`, exit `0` | Preserve the alignment JSON and continue to export/PDF QA |\n| `FIX BEFORE DELIVERY`, exit `1` | Correct the selected-backend layout and rerun; do not export the delivery bundle |\n| `REVIEW REQUIRED` | Resolve WARN or document it; `--strict` makes WARN blocking |\n| `NOT AUDITABLE`, exit `2` | Supply valid measured geometry and comparison groups; do not claim alignment passed |\n\nThe QA-only alignment SVG shows measured panel rectangles, not scientific\ncontent. It must not replace the selected backend's figure. Do not weaken the\nglobal tolerance to hide one exception. Structured grid spanning is audited\nautomatically; a free-positioned hero panel, inset, legend-only axis or colorbar\nshould be omitted from unrelated groups or carry a specific exemption reason.\n\n## Automatic rendered collision audit\n\nRun `audit_figure_collisions.py` after **every generated figure and every\nrevision that can change layout**, including edits to text, fonts, legend,\nannotations, axes, data, uncertainty, panel size or arrangement. Do not reuse a\nreport from an earlier render. Preserve the JSON report with the delivery QA;\nthe marked PDF is diagnostic only.\n\n| Result | Meaning | Required action |\n|---|---|---|\n| `text-text` FAIL | Two rendered text boxes materially overlap | Separate, shorten, rotate or resize the labels and rerun |\n| `text-stroke` FAIL | A line, curve, marker edge, error bar or other stroked path crosses the interior of text | Move the text or alter the layout; do not hide the path with an opaque white box |\n| `text-page-clipping` FAIL | A rendered text trace extends beyond the final PDF page | Expand/reposition the layout and re-export |\n| `text-fill-edge` WARN | Text only partly overlaps a bar, heatmap cell or other fill | Inspect at final size; fix unless the edge overlap is intentional and legible |\n| `text-image-edge` WARN | Text only partly overlaps a raster image boundary | Inspect panel labels, scale bars and image annotations at final size |\n| contained overlay count | Text is fully inside a fill or image | Informational because in-bar labels, heatmap values and image annotations can be intentional |\n\nExit code `1` and verdict `FIX BEFORE DELIVERY` block delivery. Exit code `2`\nor `NOT AUDITABLE` means the PDF/dependency could not be checked and must not be\nreported as a pass. `REVIEW REQUIRED` does not silently pass: inspect each WARN\nand record the reason, or use `--strict` to make WARN blocking. PyMuPDF is\ndeclared in `requirements.txt`; install it with:\n\n```bash\npython -m pip install -r skills/nature-figure/requirements.txt\n```\n\nThe detector deliberately separates reliable geometry failures from ambiguous\nfill/image overlays. It does not prove visual quality, semantic correctness,\nadequate contrast or accessibility, so the final-size panel audit remains\nmandatory.\n\n## Rendered panel-by-panel audit\n\nDo not approve a figure from a whole-page glance. Inspect each panel at final physical size, then inspect the assembled figure. Record one row per panel:\n\n| Panel | Unique claim | Center/summary | Spread/interval | Replicate unit | Labels/legend | Alignment group/result | Collision check | Pass |\n|---|---|---|---|---|---|---|---|---|\n| a | What question only this panel answers | mean/median/raw | SD/SE/CI/none + reason | seeds/folds/subjects/etc. | exact display labels | row/column group, deviation or exemption | collision report findings + data/error extent + text bbox | yes/no |\n\nCover each panel mentally. If the figure's argument remains complete, merge or remove that panel. Compare repeated panels side by side for consistent terminology, uncertainty, axes, and color mapping. After adding error bars or uncertainty bands, remove arrows, brackets, or fills that encode the same gap and occupy the same geometry.\n\n## Typography and PDF glyph floor\n\n- The 5 pt floor applies to every rendered glyph, not only the parent `fontsize` in source code.\n- Matplotlib mathtext commonly scales superscripts/subscripts to about 0.7 of the parent. A 7 pt `$R^2 nature-figure — Skillopedia can therefore contain a 4.9 pt glyph. Prefer a Unicode glyph such as `R²` when it preserves the intended notation, or increase the parent size and confirm the PDF audit.\n- Measure long labels against their allocated group width at final size. Compare rendered text bounding-box width in millimetres with the available slot; do not rely on the source font number alone.\n- Keep canonical capitalization such as `XGBoost`, `DeepSeek`, `GPT-5.2`, and `RF`. Legend labels start with display-style capitalization, while prose follows normal sentence grammar. Do not use blind string title-casing.\n\n## Printed size in the manuscript\n\nThe size that matters is the size a reader sees on the typeset page, not the\n`fontsize` in the source. Measure it there.\n\n- Read the caption and body sizes from the compiled manuscript (for example the\n document class's `\\f@size` inside a caption) rather than assuming them. Elsevier's\n `elsarticle` preprint at 12 pt sets captions at 10 pt.\n- Set figure text relative to the caption, a little smaller (for example 0.8 to 0.9\n of the caption) unless the journal states a size. Text larger than the caption\n reads as larger than the paper.\n- Three things change the printed size without changing the source number:\n 1. **Rescaling on placement.** A tight bounding box crops the drawing narrower than\n it was drawn; `\\includegraphics[width=\\textwidth]` then enlarges it, and every\n glyph with it. Draw at the text width, save with zero padding, and place the\n figure at its natural size (`\\includegraphics{...}` without a width).\n 2. **Units.** TeX's point is 1/72.27 in; PDF and Matplotlib use 1/72 in. A 390 pt\n text width is 388.5 PDF points. Convert before drawing at the text width.\n 3. **Typeface.** Fonts of one family differ in x-height: Times New Roman at 10 pt\n is visibly larger than the `newtx` Times of the text at 10 pt. A monospace face\n reads a size larger than the text face at the same nominal size.\n- Verify by measurement: render the compiled page at 300 to 600 dpi, take words of\n x-height letters only (a, c, e, m, n, o, r, s, u, v, w, x, z), and compare their\n ink height inside the figure with the caption's and the body's. Adjust the\n source size by the measured ratio, regenerate every figure, and measure again.\n- Check every figure's PDF width against the text width after a style change; one\n figure a few points too wide is an overfull box or an unwanted rescale.\n- Keep the shape consistent across a paper's figures when the author asks for it\n (for example every figure 4:3 or 1:1 at the text width), and rearrange panels to\n fit rather than letting each figure take its own height. Let the content set the\n height within that rule: a row of two bar charts reads too tall at 4:3 and is\n better wider than high (for example 5:3); a schematic with stacked rows may need\n 1:1. Ask before breaking a ratio the author set.\n\n## Multi-line labels\n\n- Matplotlib's `linespacing` does not give one leading for every alignment: the same\n value printed 1.19 times the font size for top-aligned labels and 1.09 times for\n centre-aligned ones in one figure. Where labels of several lines sit side by side,\n set each line separately on its own baseline with one leading (for example 1.2\n times the font size), and measure the baseline distances in the PDF.\n- Break two-line labels where the words break naturally, and give every label of a\n kind the same number of lines when they sit in one row.\n\n## Uncertainty consistency\n\n- If a line or bar is a mean/median across random seeds, folds, splits, subjects, or repeated experiments, encode the requested spread in every comparable panel.\n- State the exact definition, for example `median ± one seed SD`, in the legend or panel notes. Do not infer or invent it.\n- Presence of one `fill_between`, `errorbar`, `yerr`, or `geom_errorbar` call does not prove coverage. Use the panel audit table to verify every comparable panel.\n- Recompute label clearance from the upper uncertainty extent after error bars are added.\n\n## Geometry and annotation placement\n\n- Measure spacing between the actual objects being compared. Use rendered/tight bounding boxes for panel-to-legend or legend-row gaps; scanning an entire raster row can mix unrelated objects at different horizontal positions.\n- Use plot-area rectangles for axes alignment and tight bounding boxes for outer-content clearance; do not confuse unequal tick-label widths with a shifted data rectangle.\n- Derive label positions from data and uncertainty bounds, for example `max(center + spread) + margin`, rather than a fixed `LABEL_Y`.\n- For rotated Matplotlib text, use `rotation_mode=\"anchor\"` and verify the final bounding box.\n- If a curve crosses a label, reposition the label beyond the local data envelope. Avoid opaque white `bbox` masks that cut a conspicuous hole in a line.\n- Equal pixel y-coordinates can still look uneven when bar heights create unequal whitespace. Diagnose the perceived gap before moving already aligned labels.\n- Remove a panel title when the caption and the legend already say what the panel shows; it costs height and repeats the caption.\n- Keep the gaps between panels to what their tick labels, axis titles and panel letters need. Give any space left over to the plot areas, not to the gutters.\n- Use tick values in the data's own steps: whole numbers for counts, and round steps (0, 5, 10) for lengths, never 2.5 or 17.5 that an automatic locator chooses after a resize. Set the ticks explicitly and check them after every change of size.\n- Follow the target journal's convention for labelling panels, (a) in the artwork or sub-captions under each panel. Verify it in the journal's guide for authors before changing it; if the guide is silent, keep the paper's existing convention and say that it could not be confirmed.\n- Place a legend where it reads with its panel, usually directly below or above it and close to the axis; a legend far from the plot area, or one panel's legend floating over another panel, costs eye travel. Prefer one row when it fits; break into two rows rather than shrinking the type.\n- When two panels need colour encodings, keep their colours distinct across panels but at the same saturation and lightness, so the figure reads as one palette.\n\n## Color separation and salience\n\n- Pairwise ΔE and white-background contrast are necessary checks, not a complete design test.\n- Verify hierarchy after rendering: neutral baselines should not appear stronger than the proposed method or primary evidence.\n- Do not repurpose a sequential light-to-dark palette as unrelated categorical colors merely because the hues look attractive.\n- Check grayscale and color-vision robustness, then inspect the actual figure because metrics do not encode which series should dominate attention.\n\n## Transformation and paired-effect checks\n\n- `numpy.interp` requires increasing `xp`. Use `scripts/figure_safety.py::interp_monotone`, or explicitly assert monotonicity and reverse/sort `xp` and `fp` together. Plausible-looking output is not evidence of correctness.\n- Do not plan one figure per source table. Group panels by the distinct claims they support.\n- When repeated units are matched, inspect paired differences. Broad between-dataset or between-subject heterogeneity can make four marginal distributions overlap even when the within-unit effect is strong; use a paired-difference view when the scientific claim is paired.\n\n## Export checks\n\nRun only the export block for the selected backend. If that backend is unavailable,\nstop and report the missing runtime/package instead of producing a substitute export\nwith the other language.\n\n### Python\n\n```python\nimport matplotlib as mpl\nmpl.rcParams[\"svg.fonttype\"] = \"none\"\nmpl.rcParams[\"pdf.fonttype\"] = 42\nfig.savefig(\"figure.svg\", bbox_inches=\"tight\")\nfig.savefig(\"figure.pdf\", bbox_inches=\"tight\")\nfig.savefig(\"figure.tiff\", dpi=600, bbox_inches=\"tight\")\n```\n\n### R\n\n```r\nsvglite::svglite(\"figure.svg\", width = width_mm / 25.4, height = height_mm / 25.4)\nprint(plot)\ndev.off()\n\ngrDevices::cairo_pdf(\"figure.pdf\", width = width_mm / 25.4, height = height_mm / 25.4, family = \"Arial\")\nprint(plot)\ndev.off()\n\nragg::agg_tiff(\"figure.tiff\", width = width_mm / 25.4, height = height_mm / 25.4, units = \"in\", res = 600)\nprint(plot)\ndev.off()\n```\n\nOpen the SVG/PDF after export and verify that text can be selected, labels do not\noverlap, and the figure still reads at final printed size.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":24785,"content_sha256":"4f31375cdb614d74eddb8aa7691c0bdfc8a8f20defcca27ce4a36192308748a8"},{"filename":"references/r-template-index.md","content":"# Private R Template Adaptation\n\nUse this reference when the user chooses R and provides or mentions an existing\nR plotting template collection. Treat such material as private working context.\nDo not reveal absolute paths, folder names, filenames, screenshots, provenance, or\nany identifying labels from the source collection in user-facing output.\n\n## Privacy rules\n\n- Never include absolute local paths in generated code, reports, comments, or final replies.\n- Never mention the original source file, folder, template number, course title, download\n location, chat attachment, or private document name.\n- When a template is useful, describe it generically by chart family: \"a grouped bar\n template\", \"a ComplexHeatmap workflow\", \"a survival plotting workflow\".\n- If a reusable idea is copied from a private template, rewrite the final code as a clean,\n self-contained script with neutral function names and neutral comments.\n- If the user asks where a style came from, say it was adapted from the provided working\n materials without identifying the path or source file.\n\n## Generic search strategy\n\nSearch private materials by chart family and package names, not by exposing paths:\n\n```bash\nfind \u003cprivate-template-root> -type f \\( -name '*.R' -o -name '*.Rmd' -o -name '*.r' \\)\nrg -n \"ggplot|patchwork|ComplexHeatmap|ggrepel|svglite|cairo_pdf|survminer|circlize\" \u003cprivate-template-root>\n```\n\nKeep these commands in internal working notes only. Do not paste the user's private\nroot path into the final answer.\n\n## Chart-family map\n\nUse these generic families to decide what to inspect:\n\n| Need | Search targets |\n|---|---|\n| Bars and grouped comparisons | `geom_col`, `geom_bar`, `position_dodge`, `stat_compare_means` |\n| Error bars and point-interval plots | `geom_errorbar`, `geom_pointrange`, `mean_se`, `stat_summary` |\n| Stacked or bidirectional bars | `position_stack`, `coord_flip`, signed values, paired positive/negative bars |\n| Box, violin, paired, and raincloud-style distributions | `geom_boxplot`, `geom_violin`, `geom_jitter`, paired sample identifiers |\n| Heatmaps and annotated heatmaps | `ComplexHeatmap`, `HeatmapAnnotation`, `pheatmap`, `geom_tile` |\n| Correlation, scatter, bubble, and volcano plots | `geom_point`, `geom_smooth`, `ggrepel`, `logFC`, `pvalue`, bubble size scales |\n| PCA, PCoA, NMDS, tSNE, UMAP | `prcomp`, `cmdscale`, `vegan`, `Rtsne`, `Seurat`, embedding coordinates |\n| Survival, Cox, subgroup, ROC, forest | `survival`, `survminer`, `coxph`, `forestplot`, `timeROC`, hazard ratios |\n| Enrichment and pathway summaries | `clusterProfiler`, `GSEA`, `enrichGO`, `enrichKEGG`, dot plots, ridge plots |\n| Circular, genome, phylogeny, chromosome | `circlize`, `ggtree`, `karyoploteR`, genome interval tracks |\n| Single-cell and omics workflows | `Seurat`, marker genes, differential expression, cell-type annotation |\n| Maps, anatomy, and spatial summaries | `sf`, `maps`, `gganatogram`, spatial coordinates |\n| Radar, lollipop, dumbbell, UpSet, Venn, Sankey | `ggradar`, `geom_segment`, `UpSetR`, `ggalluvial`, set operations |\n\n## Adaptation checklist\n\nWhen adapting a private template:\n\n- Keep useful data wrangling, statistics, and geoms.\n- Replace template-specific colors with the figure-level semantic palette.\n- Normalize fonts to final-size 5-7 pt text and 8 pt bold lowercase panel labels.\n- Convert single-output PNG/PDF scripts to SVG/PDF/TIFF export.\n- Remove decorative elements that do not support the core conclusion.\n- Ensure each statistical comparison has `n`, center, spread, test, and correction\n information in the legend or source-data notes.\n- For image panels, document raw file, crop, contrast, scale-bar calibration, and any\n stitching or pseudo-coloring in private QA notes.\n- Final code should be self-contained and should not require the original private\n folder structure unless the user explicitly asks to keep that workflow.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3903,"content_sha256":"0ebbbd4171896b61639d7969a010401d0aa257bafb99783026a1ba32c91ce19e"},{"filename":"references/r-workflow.md","content":"# R Workflow\n\n## Contents\n\n- [R-only execution rule](#r-only-execution-rule)\n- [Required packages by task](#required-packages-by-task)\n- [Contract scaffold](#contract-scaffold)\n- [Panel labels in R](#panel-labels-in-r)\n- [Patchwork layout patterns](#patchwork-layout-patterns)\n- [Patchwork panel-alignment gate](#patchwork-panel-alignment-gate)\n- [ComplexHeatmap export](#complexheatmap-export)\n- [Template reuse rule](#template-reuse-rule)\n\n\nUse this when the user chooses R, brings R data/scripts, or asks to reuse the local\nR plotting templates. The R track should still follow the same figure contract:\nclaim first, evidence hierarchy second, plotting code third.\n\n## R-only execution rule\n\nWhen the user has selected R, do all figure drawing, previewing, exporting, and\nvisual QA in R. Do not call Python/matplotlib/seaborn/plotly to create a temporary\npreview, fallback export, or layout approximation. If R, `Rscript`, or required R\npackages are missing, pause rendering, continue independent script or data checks, and report the missing dependency. You\nmay still write the R script, provide `install.packages()` commands, or ask permission\nto install dependencies, but do not cross-render the figure in another language.\n\nAllowed non-R utilities are limited to non-visual tasks such as shell file inspection,\nCSV line counts, checksums, archive extraction, or text search. They must not create\nimage/vector outputs or alter visual layout.\n\n## Required packages by task\n\n| Task | Preferred packages |\n|---|---|\n| Bars, boxplots, violins, dot plots, lines, volcano plots | `ggplot2`, `ggrepel`, `dplyr`, `tidyr` |\n| Multi-panel assembly and alignment QA | `patchwork`, `grid`, `jsonlite`; use `cowplot` only when inset alignment requires it |\n| Rich omics heatmaps | `ComplexHeatmap`, `circlize`, `grid` |\n| Survival and clinical subgroup plots | `survival`, `survminer`, `forestplot`, `ggplot2` |\n| Circular/genome plots | `circlize`, `ggtree`, `gggenes`, domain-specific packages |\n| Export | `svglite`, `grDevices::cairo_pdf`, `ragg` |\n\n## Contract scaffold\n\n```r\nlibrary(ggplot2)\nlibrary(patchwork)\nsource(\"skills/nature-figure/scripts/panel_alignment.R\")\n\npalette_contract \u003c- c(\n neutral_dark = \"#272727\",\n neutral_mid = \"#767676\",\n neutral_light = \"#D8D8D8\",\n signal_blue = \"#3182BD\",\n signal_teal = \"#33B5A5\",\n accent_red = \"#D24B40\",\n accent_orange = \"#E28E2C\"\n)\n\ntheme_nature_contract \u003c- function(base_size = 6.5, base_family = \"Arial\") {\n theme_classic(base_size = base_size, base_family = base_family) +\n theme(\n axis.line = element_line(linewidth = 0.35, colour = \"black\"),\n axis.ticks = element_line(linewidth = 0.35, colour = \"black\"),\n axis.title = element_text(size = base_size),\n axis.text = element_text(size = base_size - 0.5),\n legend.title = element_text(size = base_size - 0.3),\n legend.text = element_text(size = base_size - 0.7),\n strip.text = element_text(size = base_size - 0.3, face = \"bold\"),\n plot.title = element_text(size = base_size + 0.5, face = \"bold\"),\n panel.grid = element_blank()\n )\n}\n\ntheme_set(theme_nature_contract())\n\nsave_pub_r \u003c- function(plot, filename, width_mm = 183, height_mm = 120, dpi = 600) {\n w \u003c- width_mm / 25.4\n h \u003c- height_mm / 25.4\n\n if (inherits(plot, \"patchwork\")) {\n require_patchwork_panel_alignment(\n plot,\n manifest_path = paste0(filename, \".alignment-layout.json\"),\n report_path = paste0(filename, \".alignment.json\"),\n overlay_svg = paste0(filename, \".alignment.svg\"),\n width_in = w,\n height_in = h,\n tolerance_pt = 1.5,\n gutter_tolerance_pt = 1.5,\n strict = TRUE\n )\n }\n\n svglite::svglite(paste0(filename, \".svg\"), width = w, height = h)\n print(plot)\n dev.off()\n\n grDevices::cairo_pdf(paste0(filename, \".pdf\"), width = w, height = h, family = \"Arial\")\n print(plot)\n dev.off()\n\n ragg::agg_tiff(paste0(filename, \".tiff\"), width = w, height = h, units = \"in\", res = dpi)\n print(plot)\n dev.off()\n}\n```\n\n## Panel labels in R\n\nUse patchwork tags for most multi-panel figures:\n\n```r\nfig \u003c- (p_a | p_b) / (p_c | p_d) +\n plot_annotation(tag_levels = \"a\") &\n theme(plot.tag = element_text(size = 8, face = \"bold\"))\n```\n\nUse manual labels only when dark image plates or inset geometry make patchwork tags\nmisalign.\n\n## Patchwork layout patterns\n\n### Quantitative grid\n\n```r\nfig \u003c- (p_a | p_b | guide_area()) /\n (p_c | p_d | p_e) +\n plot_layout(guides = \"collect\", widths = c(1, 1, 0.45)) &\n theme(legend.position = \"right\")\n```\n\n### Schematic-led composite\n\n```r\ndesign \u003c- \"\nAAAA\nBBCD\n\"\nfig \u003c- p_schematic + p_b + p_c + p_d +\n plot_layout(design = design, heights = c(1.8, 1))\n```\n\n### Image plate plus quant\n\nKeep black backgrounds inside image panels only. Put scale bars on the image, then\nplace quantification next to or below the representative image.\n\n```r\np_img \u003c- ggplot(img_df, aes(x, y, fill = intensity)) +\n geom_raster() +\n scale_fill_gradient(low = \"black\", high = \"white\") +\n coord_fixed(expand = FALSE) +\n annotate(\"segment\", x = 10, xend = 40, y = 10, yend = 10,\n linewidth = 0.6, colour = \"white\") +\n theme_void() +\n theme(legend.position = \"none\", plot.background = element_rect(fill = \"black\", colour = NA))\n```\n\n## Patchwork panel-alignment gate\n\n`panel_alignment.R` converts the final patchwork gtable cells to physical-point\nrectangles on a temporary R graphics device with the same dimensions as the\nreal export. It then invokes the backend-neutral\n`audit_panel_alignment.py` JSON auditor. Python does not render or modify the R\nfigure.\n\nSimple patchwork grids infer row/column groups from gtable cells. Structured\nunequal-span designs are also automatic: two stacked panels in the left column\nbeside one two-row panel in the right column, and the mirrored arrangement,\nreceive shared top/bottom boundary checks without an invalid equal-height\ncomparison. Horizontal groups of three or four equal gtable spans must also\nhave equal final widths; intentional unequal widths require a reasoned\n`panel-width` exemption. For nested or manually composed figures, declare the intended\ncomparisons explicitly:\n\n```r\nrequire_patchwork_panel_alignment(\n fig,\n manifest_path = \"figures/fig2.alignment-layout.json\",\n report_path = \"figures/fig2.alignment.json\",\n overlay_svg = \"figures/fig2.alignment.svg\",\n width_in = 183 / 25.4,\n height_in = 120 / 25.4,\n panel_ids = c(\"a\", \"b\", \"c\", \"d\"),\n row_groups = list(c(\"a\", \"b\"), c(\"c\", \"d\")),\n column_groups = list(c(\"a\", \"c\"), c(\"b\", \"d\")),\n exemptions = list(\n list(\n panels = c(\"a\"),\n checks = c(\"column\", \"panel-width\"),\n reason = \"hero panel intentionally spans the evidence columns\"\n )\n ),\n tolerance_pt = 1.5,\n gutter_tolerance_pt = 1.5,\n strict = TRUE\n)\n```\n\nEvery exemption must name the panel, the exact check and the reason. Do not\nincrease the global tolerance to hide an inset, colorbar, legend-only cell or\nhero panel. A non-zero audit status stops the R delivery script. Preserve both\nthe measured layout manifest and audit JSON; the alignment SVG is QA-only.\n\n## ComplexHeatmap export\n\n`ComplexHeatmap` objects are grid objects, not ggplot objects. Export them by opening\nthe graphics device, drawing, then closing it.\n\n```r\nlibrary(ComplexHeatmap)\nlibrary(circlize)\n\npdf(\"heatmap.pdf\", width = 7.2, height = 4.8, family = \"Arial\")\ndraw(ht, heatmap_legend_side = \"right\", annotation_legend_side = \"right\")\ndev.off()\n\nsvglite::svglite(\"heatmap.svg\", width = 7.2, height = 4.8)\ndraw(ht, heatmap_legend_side = \"right\", annotation_legend_side = \"right\")\ndev.off()\n```\n\n## Template reuse rule\n\nThe local R materials are examples, not final style. When reusing them:\n\n1. Inspect only the nearest template folder.\n2. Keep useful data wrangling, statistics, and geoms.\n3. Replace ad hoc colors, oversized fonts, dense legends, and PNG-only export.\n4. Rebuild the final script around `theme_nature_contract()` and `save_pub_r()`.\n5. Add source-data output if the figure is manuscript-facing.\n\nOpen `references/r-template-index.md` for the local template atlas.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8070,"content_sha256":"ad5403b890801d8e350976d65462babcab2dc1f525351d20bc6136b92900d9d6"},{"filename":"references/template-catalog.md","content":"# Validated Plot Template Catalog\n\nUse this catalog after the backend is resolved to Python and after `asset-adaptation.md` confirms that the requested panel matches a template semantically. These templates use only NumPy and Matplotlib, require real CSV input for production, and produce SVG, PDF, 600 dpi TIFF, and a count-based QA JSON record.\n\nRun `python scripts/plot_templates.py \u003csubcommand> --help` for the complete interface.\n\n| Subcommand | Required CSV shape | Main safeguards |\n|---|---|---|\n| `volcano` | gene, effect-size, adjusted-p columns | positive p-value check, explicit thresholds, all points retained, large-data rasterization |\n| `roc` | one FPR column plus one or more TPR columns | range check in `[0,1]`, stable FPR sorting, trapezoidal AUC recorded |\n| `dotplot` | row category, column category, size, color | complete category-order validation, no silent category removal |\n| `marginal` | x, y, optional group | preserves all observations, separates 2D joint structure from 1D marginal distributions |\n| `paired` | pair ID, condition, value | exactly two conditions, rejects duplicate or incomplete pairs unless exclusion is explicit |\n\n## Production examples\n\n```bash\npython skills/nature-figure/scripts/plot_templates.py volcano \\\n --input results.csv --gene-col gene --effect-col log2fc --p-col padj \\\n --output figures/volcano\n\npython skills/nature-figure/scripts/plot_templates.py roc \\\n --input roc.csv --fpr-col fpr --tpr-cols model_a,model_b \\\n --output figures/roc\n\npython skills/nature-figure/scripts/plot_templates.py dotplot \\\n --input markers.csv --row-col cell_type --column-col gene \\\n --size-col pct_exp --color-col avg_exp_scaled --output figures/markers\n```\n\nNever use `--demo` for a manuscript deliverable. It exists only for explicit smoke tests and is marked in the QA JSON. If required numeric data are missing or non-finite, the default is to stop. `--drop-incomplete` is an explicit exception that records exclusion counts; it is not permission to hide scientifically important missingness.\n\nThe templates do not compute hypothesis tests. Add statistics only after defining the replicate unit, test, assumptions, multiplicity correction, and legend reporting contract.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2227,"content_sha256":"f3dfe03d7ad6d004d17cb1f576e39020f55d1aa6d26f115a3be9ce6279e7d244"},{"filename":"references/tutorials.md","content":"# Tutorials — Nature Figure Making\n\n## Contents\n\n- [Tutorial 1: Grouped bar chart (multi-metric comparison)](#tutorial-1-grouped-bar-chart-multi-metric-comparison)\n- [Tutorial 2: Ablation bar chart (alpha-graduated, horizontal)](#tutorial-2-ablation-bar-chart-alpha-graduated-horizontal)\n- [Tutorial 3: Multi-panel trend with shared legend](#tutorial-3-multi-panel-trend-with-shared-legend)\n- [Tutorial 4: Heatmap with dual colormaps (positive/negative columns)](#tutorial-4-heatmap-with-dual-colormaps-positivenegative-columns)\n- [Related files](#related-files)\n\n\nEnd-to-end walkthroughs for the most common publication figure types.\nAll examples use helpers from [api.md](api.md) and patterns from [common-patterns.md](common-patterns.md).\nFor the retained third-party demo map, copyright boundary, and original reimplementation guidance, open [demos.md](demos.md).\n\n---\n\n## Tutorial 1: Grouped bar chart (multi-metric comparison)\n\n**Goal**: Several methods compared across multiple metrics. Legend in a dedicated panel.\nWhen methods belong to related families, use one coherent baseline family plus one coherent hero family.\n\n```python\nimport os\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom matplotlib import gridspec\n\n# --- Style ---\nplt.rcParams['font.family'] = 'sans-serif'\nplt.rcParams['font.sans-serif'] = ['Arial']\nplt.rcParams['svg.fonttype'] = 'none'\nplt.rcParams['font.size'] = 24\nplt.rcParams['axes.spines.right'] = False\nplt.rcParams['axes.spines.top'] = False\nplt.rcParams['axes.linewidth'] = 3\n\n# --- Data ---\nmethods = ['ResNet1d18', 'ResNet1d34', 'ECGFounder', 'CSFM-Tiny', 'CSFM-Base', 'CSFM-Large']\ncolors = ['#484878', '#7884B4', '#B4C0E4', '#E4E4F0', '#E4CCD8', '#F0C0CC']\nmetrics = ['Metric 1', 'Metric 2', 'Metric 3']\nmean = {\n 'Metric 1': np.array([0.81, 0.83, 0.86, 0.89, 0.91, 0.92]),\n 'Metric 2': np.array([0.63, 0.67, 0.71, 0.74, 0.77, 0.79]),\n 'Metric 3': np.array([0.41, 0.45, 0.49, 0.53, 0.56, 0.58]),\n}\nstd = {k: v * 0.03 for k, v in mean.items()} # placeholder\n\n# --- Figure ---\nfig = plt.figure(figsize=(28, 6))\ngs = gridspec.GridSpec(1, len(metrics) + 1) # +1 for legend panel\n\nhandles, labels = None, None\nfor col, metric in enumerate(metrics):\n ax = fig.add_subplot(gs[col])\n bars = ax.bar(\n range(len(methods)),\n mean[metric],\n yerr=std[metric],\n capsize=5,\n color=colors,\n label=methods,\n error_kw={'elinewidth': 2, 'capthick': 2},\n )\n if col == 0:\n handles, labels = ax.get_legend_handles_labels()\n ax.set_xticks([])\n y_vals = mean[metric]\n margin = (y_vals.max() - y_vals.min()) * 0.15\n ax.set_ylim([y_vals.min() - margin, y_vals.max() + margin])\n ax.set_ylabel(metric, fontsize=32)\n\n# Legend-only panel\nax_leg = fig.add_subplot(gs[-1])\nax_leg.legend(handles, labels, fontsize=28, loc='center', frameon=False)\nax_leg.set_axis_off()\n\nfig.tight_layout(pad=2)\nos.makedirs('./figures', exist_ok=True)\nfig.savefig('./figures/comparison.png', dpi=300)\nfig.savefig('./figures/comparison.pdf', dpi=300)\nplt.close(fig)\n```\n\n---\n\n## Tutorial 2: Ablation bar chart (alpha-graduated, horizontal)\n\n**Goal**: Same method with components progressively added; alpha encodes completeness.\n\n```python\nimport os\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nplt.rcParams['font.family'] = 'sans-serif'\nplt.rcParams['font.sans-serif'] = ['Arial']\nplt.rcParams['svg.fonttype'] = 'none'\nplt.rcParams['font.size'] = 24\nplt.rcParams['axes.spines.right'] = False\nplt.rcParams['axes.spines.top'] = False\nplt.rcParams['axes.linewidth'] = 3\n\nconfigs = ['None', '+ Module A', '+ Module B', '+ Module C', 'Full']\nvalues = np.array([0.72, 0.78, 0.81, 0.84, 0.88])\nstds = np.array([0.02, 0.02, 0.01, 0.01, 0.01])\n\nn = len(configs)\nblue_rgb = (0.215686, 0.458824, 0.729412) # #3775BA\nalphas = np.linspace(0.2, 1.0, n)\ncolors = [(blue_rgb[0], blue_rgb[1], blue_rgb[2], a) for a in alphas]\n\nfig, ax = plt.subplots(figsize=(12, 6))\nax.barh(range(n), values, xerr=stds,\n color=colors, ecolor='k', capsize=5)\nax.set_yticks(range(n))\nax.set_yticklabels(configs)\nax.set_xlim([values.min() - 0.05, values.max() + 0.03])\nax.set_xlabel('Score', fontsize=32)\n\nfig.tight_layout(pad=2)\nos.makedirs('./figures', exist_ok=True)\nfig.savefig('./figures/ablation.png', dpi=300)\nplt.close(fig)\n```\n\n---\n\n## Tutorial 3: Multi-panel trend with shared legend\n\n**Goal**: Two trend panels (e.g., train/val curves) and a legend-only third panel.\n\n```python\nimport os\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nplt.rcParams['font.family'] = 'sans-serif'\nplt.rcParams['font.sans-serif'] = ['Arial']\nplt.rcParams['svg.fonttype'] = 'none'\nplt.rcParams['font.size'] = 15\nplt.rcParams['axes.spines.right'] = False\nplt.rcParams['axes.spines.top'] = False\nplt.rcParams['axes.linewidth'] = 2\n\nmethods = ['Baseline', 'CSFM-Tiny', 'CSFM-Base', 'CSFM-Large']\ncolors = ['#7884B4', '#E4E4F0', '#E4CCD8', '#F0C0CC']\nx = np.arange(0, 100, 5)\n\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\n\nfor panel_idx, (ax, panel_name) in enumerate(zip(axes[:2], ['Training', 'Validation'])):\n for method, color in zip(methods, colors):\n y = 0.48 + 0.42 * (1 - np.exp(-x / 30)) + np.random.randn(len(x)) * 0.01\n if method == 'Baseline':\n y -= 0.03\n elif method == 'CSFM-Tiny':\n y += 0.00\n elif method == 'CSFM-Base':\n y += 0.02\n elif method == 'CSFM-Large':\n y += 0.03\n ax.plot(x, y, color=color, lw=2.5, marker='o', markersize=6, label=method)\n ax.set_title(panel_name, fontsize=18)\n ax.set_xlabel('Epoch', fontsize=16)\n ax.set_ylabel('Loss', fontsize=16)\n if panel_idx == 0:\n handles, labels = ax.get_legend_handles_labels()\n\n# Legend-only panel\naxes[2].legend(handles, labels, fontsize=14, loc='center', frameon=False)\naxes[2].set_axis_off()\n\nfig.tight_layout(pad=2)\nos.makedirs('./figures', exist_ok=True)\nfig.savefig('./figures/trends.png', dpi=300)\nfig.savefig('./figures/trends.pdf', dpi=300)\nplt.close(fig)\n```\n\n---\n\n## Tutorial 4: Heatmap with dual colormaps (positive/negative columns)\n\n**Goal**: Score matrix where positive = Reds, negative = Blues_r. Cell text auto-contrasted.\n\n```python\nimport os\nimport numpy as np\nimport matplotlib as mpl\nimport matplotlib.pyplot as plt\n\nplt.rcParams['font.family'] = 'sans-serif'\nplt.rcParams['font.sans-serif'] = ['Arial']\nplt.rcParams['svg.fonttype'] = 'none'\nplt.rcParams['font.size'] = 16\nplt.rcParams['axes.spines.right'] = False\nplt.rcParams['axes.spines.top'] = False\nplt.rcParams['axes.linewidth'] = 2\n\n# matrix: rows = methods, cols = metrics (alternating positive/negative directions)\nmethods = ['Method A', 'Method B', 'Method C', 'Method D']\nmetrics = ['Score (+)', 'Error (-)', 'F1 (+)', 'Loss (-)']\nmatrix = np.array([\n [0.88, 0.12, 0.85, 0.20],\n [0.81, 0.18, 0.78, 0.28],\n [0.75, 0.25, 0.72, 0.35],\n [0.70, 0.30, 0.68, 0.40],\n])\n\nfig, ax = plt.subplots(figsize=(10, 6))\nn_rows, n_cols = matrix.shape\nvmin, vmax = matrix.min(0), matrix.max(0)\n\nfor j in range(n_cols):\n is_positive = (j % 2 == 0)\n cmap = plt.cm.Reds if is_positive else plt.cm.Blues_r\n cmap = cmap.copy()\n norm = mpl.colors.Normalize(\n vmin=0 if is_positive else vmax[j],\n vmax=vmax[j] if is_positive else 0\n )\n ax.imshow(matrix[:, j:j+1], cmap=cmap, norm=norm,\n aspect='auto', extent=[j-0.5, j+0.5, 0, n_rows], origin='lower')\n\nfor (i, j), val in np.ndenumerate(matrix):\n is_positive = (j % 2 == 0)\n cmap = plt.cm.Reds if is_positive else plt.cm.Blues_r\n norm = mpl.colors.Normalize(vmin=0 if is_positive else vmax[j],\n vmax=vmax[j] if is_positive else 0)\n r, g, b, _ = cmap(norm(val))\n lum = 0.299*r + 0.587*g + 0.114*b\n color = 'white' if lum \u003c 0.5 else 'black'\n ax.text(j, i + 0.5, f'{val:.2f}', ha='center', va='center',\n fontsize=13, color=color)\n\nax.set_xlim(-0.5, n_cols - 0.5)\nax.set_xticks(np.arange(n_cols))\nax.set_xticklabels(metrics, rotation=30, ha='right', fontsize=14)\nax.tick_params(axis='x', bottom=False, top=False, length=0)\nax.set_yticks(np.arange(n_rows) + 0.5)\nax.set_yticklabels(methods, fontsize=14)\nax.set_frame_on(False)\nax.invert_yaxis()\n\nfig.tight_layout(pad=2)\nos.makedirs('./figures', exist_ok=True)\nfig.savefig('./figures/heatmap.png', dpi=300)\nplt.close(fig)\n```\n\n---\n\n## Related files\n\n- [SKILL.md](../SKILL.md) — When to use this skill\n- [api.md](api.md) — Reusable helper implementations\n- [common-patterns.md](common-patterns.md) — Layout and encoding patterns used above\n- [design-theory.md](design-theory.md) — Why these choices exist\n- [chart-types.md](chart-types.md) — Radar, 3D sphere, scatter, fill_between\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8728,"content_sha256":"0fbd92cedc8da312ec4b8655127d61ce2ba68f218b70a346d6716977ab4861d6"},{"filename":"references/vector-schematic-style-rules.md","content":"# Vector Schematic Style Rules\n\nConventions for architecture, pipeline, and mechanism schematics drawn with the\n[vector schematic workflow](vector-schematic-workflow.md). They bend only when\nthe figure's own story needs it; say so in the brief when a rule is bent.\n\n## Contents\n\n- [Type](#type)\n- [Colour](#colour)\n- [Lines and arrows](#lines-and-arrows)\n- [Structure](#structure)\n- [Legend and numbering](#legend-and-numbering)\n- [Agent and tool diagrams](#agent-and-tool-diagrams)\n- [Common failures](#common-failures)\n\n## Type\n\n- One family, at most three sizes, ordinary text 5 to 7 pt at final size for\n the flagship Nature journal; panel letters follow the journal rule. Nature\n asks for a sans-serif face; for another journal follow its own list (some,\n such as Elsevier's, also accept Times). Whatever the family, use it for every\n label, including mathematics and code names (a matching monospace).\n- Bold only the top-level group or layer names; set box titles and labels in the\n regular weight. Set code and function names in the text face unless the paper\n distinguishes them typographically: a monospace face reads a size larger.\n- All text in one dark ink. Do not set labels or descriptions in grey; colour in\n text only where it carries a meaning the figure states (an error path, a\n highlighted component).\n- Give each box its name and, where the reader needs it, one short line on its\n job. A figure of title-only boxes reads as empty and a box holding a paragraph\n reads as crowded; move the rest to the caption.\n- Set several lines in a narrow box ragged or centred. Justify only when every\n box of that kind has the same text width and the line breaks leave the word\n spacing even; re-word rather than stretch a short line.\n- Labels are the manuscript's own terms, spelled and capitalised as in the text.\n- Text stays live. Do not outline it and do not use a glyph the embedded font\n lacks; draw an ellipsis or arrow as shapes if needed.\n- Centre each label in its box, and size the box from the measured label width\n plus padding, not the other way round.\n\n## Colour\n\n- White canvas. At most two saturated fills, and exactly one accent that marks\n the paper's new component.\n- Pale or grey fills for existing parts, white with a grey outline for external\n systems.\n- Red only for errors, bottlenecks, or a highlighted path.\n- Keep text contrast readable: dark text on pale fills, white text only on dark\n accents. Do not rely on colour alone; pair it with a label, shape, or line\n style so the figure survives greyscale printing and colour-vision differences.\n- No rainbow categorical fills, gradients, 3D, or drop shadows.\n\n## Lines and arrows\n\n- Every directed edge has an arrowhead; the main path is the heaviest line.\n- Solid means data or control that always flows. Dashed carries exactly one\n meaning per figure (for example feedback, or a logical group boundary), never\n two. If groups are dashed, make feedback dotted, or the reverse.\n- Use orthogonal routes for structure and curves only for feedback.\n- Where two unconnected lines must cross, let them cross plainly: a gap at the\n crossing reads as a broken line. Mark a real branch point with a small dot.\n- Attach a connector at the middle of the side it meets. Where that is taken or\n impossible, use a third or a quarter of the side, so attachment points look\n chosen. A connector into a circle aims at its centre.\n- Prefer straight connectors and allow at most one bend. Re-place the boxes so a\n connector can run straight before adding bends to route around them.\n- Never run a connector along a box edge, a band edge or another line; keep a\n visible gap, or the two read as one.\n- No line passes through a label, and arrows do not end on a container\n boundary unless the whole group is the endpoint.\n- Keep arrowheads and tails clear of tier dividers and container titles.\n\n## Structure\n\n- Nest at most three levels. A container is never filled darker than its\n children; outline-only containers are the safe default.\n- Equal gaps in a row, and snap box edges to a common grid; repeated cards (an\n output row, a row of tools) are equally spaced across the same span. Boxes that play the\n same role in different rows or containers share their width and their top and\n bottom edges.\n- Repeat a container's internal order in every container of its kind (for\n example tools above, the functions that call them below).\n- Place a container title in the headroom between the container's top edge and\n its first box, with no overlap.\n- Show repetition with a `×N` mark or a stack of two offset boxes, not N copies.\n- Pick one arrangement direction and keep the main path monotone along it.\n\n## Legend and numbering\n\n- Add a legend only when more than four fills, line styles, or icons carry\n meaning and the caption cannot explain them. Place it within one box-width of\n what it decodes.\n- Use circled numbers at the upper-left of an edge for the main path, and make\n the numbers match the caption's walkthrough.\n\n## Agent and tool diagrams\n\n- Draw an LLM agent as what it is: a loop that iterates. Show its own cycle\n (reason, call a tool, read the result) with the agent at its centre, and step\n marks or a repeat sign so the reader sees many rounds, not one pass.\n- Draw the tools as things the agent calls. Calls leave the cycle's tool-call\n station and results, refusals and returned designs come back to its\n observation station; group the tools by module and connect each module once.\n- A retrieval step (a knowledge library, a search) is a tool call like the\n others, not a separate stage before the agent.\n- Do not chain the agent and its tools as stages of a pipeline. A fixed order\n inside one tool call (check A, then B) may be drawn as a row inside that\n tool's container.\n- Name the model the agent runs on when the paper compares models or harnesses.\n\n## Common failures\n\n| Failure | Fix |\n|---|---|\n| Too many boxes for the column | Merge, show once with `×N`, or split into two figures |\n| Several things in the accent colour | Keep one accent: the new component |\n| Dashed used for two meanings | Give one meaning a dotted line or a solid frame |\n| Text below the journal floor | Reduce content, never the type |\n| Arrow touching a title or crossing a label | Reroute or move the label |\n| Decisions made silently | Record them as assumptions in the brief |\n| The figure implies behaviour the system lacks | Check each component, edge and label against the code or methods |\n| An agent drawn as one stage of a pipeline | Draw its loop; draw the tools as callees |\n| Wide gaps in justified box text | Re-word, or set the box ragged |\n| A gap at a line crossing that reads as a break | Let the lines cross plainly |\n| A connector that winds around boxes | Move the boxes so it runs straight, with at most one bend |\n| Constraints that cannot all hold (a regular shape, straight connectors, alignment) | Say which conflict and ask which to keep |\n| A layout the author describes in words but you read differently | Ask for a sketch or offer two arrangements before redrawing |\n| Grey labels, or boxes of one width in one row and another in the next | One dark ink; shared widths and edges |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7230,"content_sha256":"fa96f0452b67deded2ce0437dc6bde7bedeae9dd438f18e22525d61953b32ee5"},{"filename":"references/vector-schematic-workflow.md","content":"# Vector Schematic Workflow\n\nUse this reference when the user wants a **hand-built, editable vector schematic**\nfor a manuscript: a system or method architecture, a pipeline overview, an\nagent or model block diagram, or a mechanism or process flow. This is the\ndeterministic, non-image-model counterpart of the AI route in\n[ai-graphical-abstract-workflow.md](ai-graphical-abstract-workflow.md). It does\nnot plot data, so it skips the Python/R backend gate.\n\nDo not use it for quantitative panels. Do not invent components, flows, or\nnumbers the author did not supply.\n\n## Contents\n\n- [Why a vector schematic](#why-a-vector-schematic)\n- [1. Write a brief and get it confirmed](#1-write-a-brief-and-get-it-confirmed)\n- [2. Fix the canvas from the journal](#2-fix-the-canvas-from-the-journal)\n- [3. Pick the arrangement](#3-pick-the-arrangement)\n- [4. Draw the SVG](#4-draw-the-svg)\n- [5. Check, look, fix](#5-check-look-fix)\n- [6. Deliver](#6-deliver)\n- [Optional companion and credit](#optional-companion-and-credit)\n\n## Why a vector schematic\n\nA figure drawn as SVG keeps live text and shapes, so authors and production\neditors can change a label without regenerating an image. It also makes the\nfinal-size checks below computable instead of a matter of eyeballing a bitmap.\n\n## 1. Write a brief and get it confirmed\n\nBefore drawing, write a short brief in the user's language and have the user\nconfirm it. Infer first and ask second: ask only what changes the structure and\ncannot be read from the material, at most three points in one message, each\nphrased as your guess plus the alternative.\n\nThe brief states:\n\n1. What the figure is: the system, the scope (whole system, one subsystem, one\n path), where it sits in the paper, single or double column.\n2. Every component with its exact label, one clause on its job, its container,\n and a tag: **new** (the paper's contribution), existing, or external.\n3. The main path as numbered steps (\"from, to, what moves\"), then secondary\n flows, and which of them are control, feedback, or optional.\n4. The one point the figure argues, and where the eye should start.\n5. Layout and style intent, including whether a legend and step numbers are used.\n6. Three to five assumptions to confirm, each with its source.\n7. A caption draft that walks the numbered steps.\n\nKeep component labels identical to the manuscript's own terms. Never add a box\nor arrow that the brief does not contain.\n\nCheck the brief against the implementation, not only the manuscript draft: for\nevery component and edge, find the code, configuration or methods text that\ndoes it, and note under which condition it runs (for example which ablation\narm has the tool). A label such as \"used for every design\" or an arrow from one\ntool to another is a claim; drop it or rephrase it when the source does not\nsupport it, and tell the user.\n\n## 2. Fix the canvas from the journal\n\nRead the target journal's figure rules first. For the flagship Nature journal\nuse [nature-article-requirements.md](nature-article-requirements.md): 89 mm\n(about 252 pt) single column, 183 mm (about 519 pt) double column, text between\n5 pt and 7 pt at final size, a sans-serif face (Arial or Helvetica preferred),\nlive editable text, no outlined text.\n\nDraw at final size: set `width`/`height` with units and a matching `viewBox`,\nso one drawing unit is one point and every size in the file is a print size.\n\n## 3. Pick the arrangement\n\nChoose by the structure of the content, not by taste:\n\n| Content | Arrangement |\n|---|---|\n| Stages in order | left-to-right or top-down pipeline |\n| Tiers or planes (control, execution, resources) | stacked layers, one container per tier |\n| A hub and its workers | central node with fan-out |\n| Cycle or feedback | loop with one dashed return edge |\n| An agent that calls tools | the agent's own loop at the hub; tool modules as callees around it ([style rules](vector-schematic-style-rules.md#agent-and-tool-diagrams)) |\n| Alternatives or before/after | side-by-side panels with identical structure |\n\nBudget: about 13 labelled boxes for a single column, about 20 for a double\ncolumn. If the brief is heavier, merge boxes, show a repeated unit once with a\n`×N` mark, or move detail to a second figure. Never shrink text below the\njournal floor to fit more.\n\n## 4. Draw the SVG\n\n- One group per component with a stable `id` (`n:orchestrator`, `e:submit`,\n `c:control-plane`) wrapping its shape and its `\u003ctext>`, so later edits map\n back to the brief.\n- Text stays `\u003ctext>`. No gradients, filters, drop shadows, scripts, or\n external references; embed any bitmap as a data URI.\n- Apply [vector-schematic-style-rules.md](vector-schematic-style-rules.md) for\n colour, line, arrowhead, container, and legend conventions.\n- Size each box from the measured label width plus padding rather than guessing.\n\n## 5. Check, look, fix\n\n1. Run the audit on the SVG:\n `python skills/nature-figure/scripts/audit_svg_schematic.py figure.svg`.\n It checks final-size text, canvas width, off-canvas content, and forbidden\n constructs. A non-zero exit blocks delivery.\n2. Render the SVG to PNG at final size and look at it. The audit cannot see\n overlaps between boxes, crossing arrows, or a label that touches a line.\n3. Walk the brief: every component and flow is present, the main path reads in\n order, the new component is the visual accent, and the numbered steps match\n the caption.\n4. Fix, then repeat from step 1 until the audit passes and every remaining\n warning is deliberate. Do this after every revision, not only before\n delivery: render the changed figure at final size, crop the region you\n edited, and look for overlaps, clipped text, loose lines and arrows that\n end on the wrong box before reporting the revision as done.\n\n## 6. Deliver\n\nHand over the SVG, a PDF exported from it, a PNG preview, the caption draft,\nthe assumption list from the brief, and one line on how to edit (element ids\nmatch the brief). Embed or outline nothing silently: say which fonts the SVG\nuses. Treat the figure as a draft until the author has reviewed the science.\n\n## Optional companion and credit\n\nThe structure of this workflow (brief gate, final-size drawing, check loop)\nfollows the approach of the MIT-licensed FigGenie skill\n(\u003chttps://github.com/Sleepy-Avacado/FigGenie>), which additionally ships a\nlayout and exemplar library, a browser-based linter, and PowerPoint export for\nsystems-conference papers. It is an independent project with its own corpus\nand licences; install it separately if you want those features. This\nrepository does not redistribute its figures, parts, or fonts.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6629,"content_sha256":"9f96fa703bbe29581e50ddac9756900553b5617f46b4a0b31e8978278fffa329"},{"filename":"requirements.txt","content":"PyMuPDF>=1.24.0,\u003c2.0.0\n","content_type":"text/plain; charset=utf-8","language":null,"size":23,"content_sha256":"e2596dd9f3381fb6280bd18added6c3d0025d954c99a132e3d0bc8d3e8a24d00"},{"filename":"scripts/audit_figure_collisions.py","content":"#!/usr/bin/env python3\n\"\"\"Audit rendered PDF figures for probable text and graphic collisions.\n\nThe audit is backend-neutral: Python and R figures are both checked from their\nfinal PDF geometry. Reliable collisions (text-text, text crossed by a stroked\npath, and text clipped by the page) block delivery. Partial text overlap with a\nfilled shape or raster image is reported for review because in-bar labels,\nheatmap values, microscopy annotations, and scale bars can be intentional.\n\nPyMuPDF is imported lazily so ``--self-test`` can exercise the geometry core in\nminimal CI environments. Install the runtime dependency with\n``python -m pip install -r skills/nature-figure/requirements.txt``.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport math\nimport os\nimport sys\nimport tempfile\nfrom dataclasses import asdict, dataclass, field\nfrom itertools import combinations\nfrom pathlib import Path\nfrom typing import Any, Iterable, Sequence\n\n\nRect = tuple[float, float, float, float]\nPoint = tuple[float, float]\nSegment = tuple[Point, Point]\n\n\n@dataclass(frozen=True)\nclass TextBox:\n index: int\n text: str\n bbox: Rect\n\n\n@dataclass(frozen=True)\nclass StrokePath:\n index: int\n bbox: Rect\n width: float\n segments: tuple[Segment, ...]\n\n\n@dataclass(frozen=True)\nclass FilledRegion:\n index: int\n bbox: Rect\n source: str\n\n\n@dataclass(frozen=True)\nclass TraceBox:\n index: int\n text: str\n bbox: Rect\n\n\n@dataclass\nclass PageGeometry:\n page: int\n bbox: Rect\n texts: list[TextBox] = field(default_factory=list)\n traces: list[TraceBox] = field(default_factory=list)\n strokes: list[StrokePath] = field(default_factory=list)\n fills: list[FilledRegion] = field(default_factory=list)\n images: list[FilledRegion] = field(default_factory=list)\n\n\n@dataclass(frozen=True)\nclass CollisionFinding:\n severity: str\n kind: str\n page: int\n message: str\n text: str\n text_bbox: Rect\n other_text: str | None = None\n other_bbox: Rect | None = None\n object_count: int = 1\n object_indexes: tuple[int, ...] = ()\n\n\ndef normalize_rect(rect: Sequence[float]) -> Rect:\n x0, y0, x1, y1 = (float(value) for value in rect)\n return min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1)\n\n\ndef rect_width(rect: Rect) -> float:\n return max(0.0, rect[2] - rect[0])\n\n\ndef rect_height(rect: Rect) -> float:\n return max(0.0, rect[3] - rect[1])\n\n\ndef rect_area(rect: Rect) -> float:\n return rect_width(rect) * rect_height(rect)\n\n\ndef rect_intersection(first: Rect, second: Rect) -> Rect | None:\n x0 = max(first[0], second[0])\n y0 = max(first[1], second[1])\n x1 = min(first[2], second[2])\n y1 = min(first[3], second[3])\n if x1 \u003c= x0 or y1 \u003c= y0:\n return None\n return x0, y0, x1, y1\n\n\ndef rect_overlap_ratio(first: Rect, second: Rect) -> float:\n intersection = rect_intersection(first, second)\n if intersection is None:\n return 0.0\n denominator = min(rect_area(first), rect_area(second))\n return rect_area(intersection) / denominator if denominator > 0 else 0.0\n\n\ndef rect_contains(outer: Rect, inner: Rect, tolerance: float = 0.0) -> bool:\n return (\n inner[0] >= outer[0] - tolerance\n and inner[1] >= outer[1] - tolerance\n and inner[2] \u003c= outer[2] + tolerance\n and inner[3] \u003c= outer[3] + tolerance\n )\n\n\ndef rect_inset(rect: Rect, amount: float) -> Rect:\n maximum = max(0.0, min(rect_width(rect), rect_height(rect)) * 0.2)\n inset = min(max(0.0, amount), maximum)\n return rect[0] + inset, rect[1] + inset, rect[2] - inset, rect[3] - inset\n\n\ndef rect_expand(rect: Rect, amount: float) -> Rect:\n pad = max(0.0, amount)\n return rect[0] - pad, rect[1] - pad, rect[2] + pad, rect[3] + pad\n\n\ndef union_rects(rectangles: Iterable[Rect]) -> Rect | None:\n rows = list(rectangles)\n if not rows:\n return None\n return (\n min(row[0] for row in rows),\n min(row[1] for row in rows),\n max(row[2] for row in rows),\n max(row[3] for row in rows),\n )\n\n\ndef point_in_rect(point: Point, rect: Rect) -> bool:\n return rect[0] \u003c= point[0] \u003c= rect[2] and rect[1] \u003c= point[1] \u003c= rect[3]\n\n\ndef segment_intersects_rect(segment: Segment, rect: Rect) -> bool:\n \"\"\"Return whether a line segment intersects a rectangle (Liang-Barsky).\"\"\"\n (x0, y0), (x1, y1) = segment\n if point_in_rect((x0, y0), rect) or point_in_rect((x1, y1), rect):\n return True\n dx = x1 - x0\n dy = y1 - y0\n p = (-dx, dx, -dy, dy)\n q = (x0 - rect[0], rect[2] - x0, y0 - rect[1], rect[3] - y0)\n lower, upper = 0.0, 1.0\n for denominator, numerator in zip(p, q):\n if abs(denominator) \u003c 1e-12:\n if numerator \u003c 0:\n return False\n continue\n ratio = numerator / denominator\n if denominator \u003c 0:\n lower = max(lower, ratio)\n else:\n upper = min(upper, ratio)\n if lower > upper:\n return False\n return True\n\n\ndef cubic_segments(points: Sequence[Point], steps: int = 16) -> list[Segment]:\n if len(points) != 4:\n return []\n p0, p1, p2, p3 = points\n samples: list[Point] = []\n for index in range(steps + 1):\n t = index / steps\n u = 1.0 - t\n samples.append(\n (\n u**3 * p0[0] + 3 * u**2 * t * p1[0] + 3 * u * t**2 * p2[0] + t**3 * p3[0],\n u**3 * p0[1] + 3 * u**2 * t * p1[1] + 3 * u * t**2 * p2[1] + t**3 * p3[1],\n )\n )\n return list(zip(samples, samples[1:]))\n\n\ndef rectangle_segments(rect: Rect) -> tuple[Segment, ...]:\n x0, y0, x1, y1 = rect\n return (\n ((x0, y0), (x1, y0)),\n ((x1, y0), (x1, y1)),\n ((x1, y1), (x0, y1)),\n ((x0, y1), (x0, y0)),\n )\n\n\ndef _point(value: Any) -> Point:\n return float(value.x), float(value.y)\n\n\ndef _segments_from_items(items: Sequence[Sequence[Any]]) -> tuple[Segment, ...]:\n segments: list[Segment] = []\n for item in items:\n if not item:\n continue\n operator = item[0]\n if operator == \"l\" and len(item) >= 3:\n segments.append((_point(item[1]), _point(item[2])))\n elif operator == \"c\" and len(item) >= 5:\n segments.extend(cubic_segments([_point(value) for value in item[1:5]]))\n elif operator == \"re\" and len(item) >= 2:\n segments.extend(rectangle_segments(normalize_rect(tuple(item[1]))))\n elif operator == \"qu\" and len(item) >= 2:\n quad = item[1]\n points = [_point(quad.ul), _point(quad.ur), _point(quad.lr), _point(quad.ll)]\n segments.extend(zip(points, points[1:] + points[:1]))\n return tuple(segments)\n\n\ndef _text_from_chars(chars: Sequence[Sequence[Any]]) -> str:\n output: list[str] = []\n for char in chars:\n try:\n codepoint = int(char[0])\n output.append(chr(codepoint) if codepoint >= 0 else \"�\")\n except (TypeError, ValueError, OverflowError):\n output.append(\"�\")\n return \"\".join(output).strip()\n\n\ndef extract_pdf_geometry(path: Path) -> list[PageGeometry]:\n try:\n import pymupdf as fitz # type: ignore\n except ImportError:\n try:\n import fitz # type: ignore\n except ImportError as exc:\n raise RuntimeError(\n \"PyMuPDF is required for rendered collision QA. Install it with: \"\n \"python -m pip install -r skills/nature-figure/requirements.txt\"\n ) from exc\n\n document = fitz.open(path)\n pages: list[PageGeometry] = []\n try:\n for page_index, page in enumerate(document, 1):\n geometry = PageGeometry(page=page_index, bbox=normalize_rect(tuple(page.rect)))\n\n trace_rows: list[TraceBox] = []\n for trace_index, trace in enumerate(page.get_texttrace()):\n text = _text_from_chars(trace.get(\"chars\", ()))\n if not text:\n continue\n trace_rows.append(\n TraceBox(index=trace_index, text=text, bbox=normalize_rect(trace[\"bbox\"]))\n )\n geometry.traces.extend(trace_rows)\n\n text_dict = page.get_text(\"dict\")\n text_index = 0\n used_trace_indexes: set[int] = set()\n for block in text_dict.get(\"blocks\", []):\n if block.get(\"type\") != 0:\n continue\n for line in block.get(\"lines\", []):\n spans = [span for span in line.get(\"spans\", []) if span.get(\"text\", \"\").strip()]\n if not spans:\n continue\n loose_bbox = union_rects(normalize_rect(span[\"bbox\"]) for span in spans)\n if loose_bbox is None:\n continue\n matched_traces = [\n trace\n for trace in trace_rows\n if trace.index not in used_trace_indexes\n and rect_overlap_ratio(trace.bbox, loose_bbox) >= 0.3\n ]\n bbox = union_rects(trace.bbox for trace in matched_traces) or loose_bbox\n used_trace_indexes.update(trace.index for trace in matched_traces)\n text = \"\".join(span.get(\"text\", \"\") for span in spans).strip()\n geometry.texts.append(TextBox(index=text_index, text=text, bbox=bbox))\n text_index += 1\n\n for trace in trace_rows:\n if trace.index in used_trace_indexes:\n continue\n if rect_intersection(trace.bbox, geometry.bbox) is None:\n continue\n geometry.texts.append(\n TextBox(index=text_index, text=trace.text, bbox=trace.bbox)\n )\n text_index += 1\n\n for drawing_index, drawing in enumerate(page.get_drawings()):\n drawing_type = str(drawing.get(\"type\", \"\"))\n drawing_bbox = normalize_rect(tuple(drawing[\"rect\"]))\n if \"s\" in drawing_type and drawing.get(\"stroke_opacity\", 1.0) not in (None, 0):\n segments = _segments_from_items(drawing.get(\"items\", ()))\n if segments:\n geometry.strokes.append(\n StrokePath(\n index=drawing_index,\n bbox=drawing_bbox,\n width=float(drawing.get(\"width\") or 0.0),\n segments=segments,\n )\n )\n if \"f\" in drawing_type and drawing.get(\"fill_opacity\", 1.0) not in (None, 0):\n rectangle_items = [item for item in drawing.get(\"items\", ()) if item and item[0] == \"re\"]\n if rectangle_items:\n for offset, item in enumerate(rectangle_items):\n geometry.fills.append(\n FilledRegion(\n index=drawing_index * 1000 + offset,\n bbox=normalize_rect(tuple(item[1])),\n source=\"fill\",\n )\n )\n else:\n geometry.fills.append(\n FilledRegion(index=drawing_index, bbox=drawing_bbox, source=\"fill\")\n )\n\n image_index = 0\n seen_images: set[tuple[int, int, int, int]] = set()\n for image in page.get_images(full=True):\n xref = int(image[0])\n for image_rect in page.get_image_rects(xref):\n bbox = normalize_rect(tuple(image_rect))\n key = tuple(round(value * 10) for value in bbox)\n if key in seen_images:\n continue\n seen_images.add(key)\n geometry.images.append(\n FilledRegion(index=image_index, bbox=bbox, source=\"image\")\n )\n image_index += 1\n\n pages.append(geometry)\n finally:\n document.close()\n return pages\n\n\ndef _outside_distance(inner: Rect, outer: Rect) -> float:\n return max(\n outer[0] - inner[0],\n outer[1] - inner[1],\n inner[2] - outer[2],\n inner[3] - outer[3],\n 0.0,\n )\n\n\ndef _finding_sort_key(finding: CollisionFinding) -> tuple[Any, ...]:\n severity_order = {\"FAIL\": 0, \"WARN\": 1}\n return severity_order.get(finding.severity, 2), finding.page, finding.kind, finding.text_bbox\n\n\ndef audit_page_geometry(\n page: PageGeometry,\n *,\n text_inset_pt: float = 0.6,\n min_overlap_ratio: float = 0.05,\n clipping_tolerance_pt: float = 0.5,\n background_area_ratio: float = 0.2,\n) -> tuple[list[CollisionFinding], dict[str, int]]:\n findings: list[CollisionFinding] = []\n info = {\"contained_fill_overlays\": 0, \"contained_image_overlays\": 0}\n\n for first, second in combinations(page.texts, 2):\n ratio = rect_overlap_ratio(first.bbox, second.bbox)\n if ratio \u003c min_overlap_ratio:\n continue\n findings.append(\n CollisionFinding(\n severity=\"FAIL\",\n kind=\"text-text\",\n page=page.page,\n message=f\"Text boxes overlap by {ratio:.1%} of the smaller box\",\n text=first.text,\n text_bbox=first.bbox,\n other_text=second.text,\n other_bbox=second.bbox,\n )\n )\n\n for text in page.texts:\n inner = rect_inset(text.bbox, text_inset_pt)\n hit_indexes: list[int] = []\n hit_rectangles: list[Rect] = []\n for stroke in page.strokes:\n expanded = rect_expand(inner, max(0.0, stroke.width) / 2)\n if rect_intersection(expanded, rect_expand(stroke.bbox, stroke.width / 2)) is None:\n continue\n if any(segment_intersects_rect(segment, expanded) for segment in stroke.segments):\n hit_indexes.append(stroke.index)\n hit_rectangles.append(stroke.bbox)\n if hit_indexes:\n findings.append(\n CollisionFinding(\n severity=\"FAIL\",\n kind=\"text-stroke\",\n page=page.page,\n message=f\"Text is crossed by {len(hit_indexes)} stroked path(s)\",\n text=text.text,\n text_bbox=text.bbox,\n other_bbox=union_rects(hit_rectangles),\n object_count=len(hit_indexes),\n object_indexes=tuple(hit_indexes),\n )\n )\n\n page_area = max(rect_area(page.bbox), 1.0)\n for regions, kind, info_key in (\n (page.fills, \"text-fill-edge\", \"contained_fill_overlays\"),\n (page.images, \"text-image-edge\", \"contained_image_overlays\"),\n ):\n for text in page.texts:\n partial_indexes: list[int] = []\n partial_rectangles: list[Rect] = []\n for region in regions:\n if rect_area(region.bbox) / page_area >= background_area_ratio:\n continue\n ratio = rect_overlap_ratio(text.bbox, region.bbox)\n if ratio \u003c min_overlap_ratio:\n continue\n if rect_contains(region.bbox, text.bbox, tolerance=0.25):\n info[info_key] += 1\n continue\n partial_indexes.append(region.index)\n partial_rectangles.append(region.bbox)\n if partial_indexes:\n label = \"filled region\" if kind == \"text-fill-edge\" else \"raster image\"\n findings.append(\n CollisionFinding(\n severity=\"WARN\",\n kind=kind,\n page=page.page,\n message=f\"Text partially overlaps the edge of {len(partial_indexes)} {label}(s)\",\n text=text.text,\n text_bbox=text.bbox,\n other_bbox=union_rects(partial_rectangles),\n object_count=len(partial_indexes),\n object_indexes=tuple(partial_indexes),\n )\n )\n\n clipped_seen: set[tuple[str, tuple[int, int, int, int]]] = set()\n for trace in page.traces:\n if _outside_distance(trace.bbox, page.bbox) \u003c= clipping_tolerance_pt:\n continue\n key = (trace.text, tuple(round(value * 10) for value in trace.bbox))\n if key in clipped_seen:\n continue\n clipped_seen.add(key)\n findings.append(\n CollisionFinding(\n severity=\"FAIL\",\n kind=\"text-page-clipping\",\n page=page.page,\n message=\"Text extends beyond the final PDF page boundary\",\n text=trace.text,\n text_bbox=trace.bbox,\n other_bbox=page.bbox,\n object_indexes=(trace.index,),\n )\n )\n\n return sorted(findings, key=_finding_sort_key), info\n\n\ndef audit_geometries(\n pages: Sequence[PageGeometry],\n *,\n text_inset_pt: float = 0.6,\n min_overlap_ratio: float = 0.05,\n clipping_tolerance_pt: float = 0.5,\n) -> dict[str, Any]:\n findings: list[CollisionFinding] = []\n contained_fill_overlays = 0\n contained_image_overlays = 0\n for page in pages:\n page_findings, info = audit_page_geometry(\n page,\n text_inset_pt=text_inset_pt,\n min_overlap_ratio=min_overlap_ratio,\n clipping_tolerance_pt=clipping_tolerance_pt,\n )\n findings.extend(page_findings)\n contained_fill_overlays += info[\"contained_fill_overlays\"]\n contained_image_overlays += info[\"contained_image_overlays\"]\n\n fail_count = sum(finding.severity == \"FAIL\" for finding in findings)\n warn_count = sum(finding.severity == \"WARN\" for finding in findings)\n visible_text_count = sum(len(page.texts) for page in pages)\n trace_count = sum(len(page.traces) for page in pages)\n auditable = visible_text_count > 0 or trace_count > 0\n verdict = (\n \"NOT AUDITABLE\"\n if not auditable\n else \"FIX BEFORE DELIVERY\"\n if fail_count\n else \"REVIEW REQUIRED\"\n if warn_count\n else \"PASS\"\n )\n return {\n \"auditable\": auditable,\n \"verdict\": verdict,\n \"page_count\": len(pages),\n \"visible_text_box_count\": visible_text_count,\n \"text_trace_count\": trace_count,\n \"stroke_path_count\": sum(len(page.strokes) for page in pages),\n \"filled_region_count\": sum(len(page.fills) for page in pages),\n \"image_region_count\": sum(len(page.images) for page in pages),\n \"summary\": {\n \"fail\": fail_count,\n \"warn\": warn_count,\n \"contained_fill_overlays\": contained_fill_overlays,\n \"contained_image_overlays\": contained_image_overlays,\n },\n \"thresholds\": {\n \"text_inset_pt\": text_inset_pt,\n \"min_overlap_ratio\": min_overlap_ratio,\n \"clipping_tolerance_pt\": clipping_tolerance_pt,\n },\n \"findings\": [asdict(finding) for finding in sorted(findings, key=_finding_sort_key)],\n }\n\n\ndef audit_pdf(\n path: Path,\n *,\n text_inset_pt: float = 0.6,\n min_overlap_ratio: float = 0.05,\n clipping_tolerance_pt: float = 0.5,\n) -> dict[str, Any]:\n pages = extract_pdf_geometry(path)\n return {\n \"pdf\": str(path),\n **audit_geometries(\n pages,\n text_inset_pt=text_inset_pt,\n min_overlap_ratio=min_overlap_ratio,\n clipping_tolerance_pt=clipping_tolerance_pt,\n ),\n }\n\n\ndef write_overlay_pdf(source: Path, destination: Path, findings: Sequence[dict[str, Any]]) -> None:\n try:\n import pymupdf as fitz # type: ignore\n except ImportError:\n try:\n import fitz # type: ignore\n except ImportError as exc:\n raise RuntimeError(\"PyMuPDF is required to write the diagnostic overlay PDF\") from exc\n if source.resolve() == destination.resolve():\n raise ValueError(\"overlay PDF must not overwrite the source PDF\")\n destination.parent.mkdir(parents=True, exist_ok=True)\n descriptor, temporary_name = tempfile.mkstemp(\n prefix=f\".{destination.stem}.\",\n suffix=\".tmp.pdf\",\n dir=destination.parent,\n )\n os.close(descriptor)\n temporary = Path(temporary_name)\n temporary.unlink()\n try:\n document = fitz.open(source)\n try:\n for finding in findings:\n page_number = int(finding[\"page\"]) - 1\n if not 0 \u003c= page_number \u003c len(document):\n continue\n page = document[page_number]\n color = (0.9, 0.05, 0.05) if finding[\"severity\"] == \"FAIL\" else (1.0, 0.55, 0.0)\n page.draw_rect(fitz.Rect(finding[\"text_bbox\"]), color=color, width=1.2, overlay=True)\n other_bbox = finding.get(\"other_bbox\")\n if other_bbox:\n page.draw_rect(\n fitz.Rect(other_bbox),\n color=color,\n width=0.6,\n dashes=\"[2 2] 0\",\n overlay=True,\n )\n document.save(temporary)\n finally:\n document.close()\n temporary.replace(destination)\n finally:\n if temporary.exists():\n temporary.unlink()\n\n\ndef render_text(result: dict[str, Any], strict: bool = False) -> str:\n summary = result[\"summary\"]\n lines = [\n \"Nature Figure Rendered Collision Audit\",\n f\"pdf: {result.get('pdf', '\u003cgeometry fixture>')}\",\n f\"pages: {result['page_count']}\",\n f\"visible text boxes: {result['visible_text_box_count']}\",\n f\"stroke paths: {result['stroke_path_count']}\",\n \"\",\n ]\n for finding in result[\"findings\"]:\n lines.append(\n f\"[{finding['severity']}] page {finding['page']} {finding['kind']}: \"\n f\"{finding['message']} — {finding['text']!r}\"\n )\n if finding.get(\"other_text\"):\n lines.append(f\" other text: {finding['other_text']!r}\")\n lines.append(f\" text bbox: {finding['text_bbox']}\")\n lines.extend(\n [\n \"\",\n f\"summary: {summary['fail']} fail, {summary['warn']} warn, \"\n f\"{summary['contained_fill_overlays']} contained fill overlays, \"\n f\"{summary['contained_image_overlays']} contained image overlays\",\n f\"verdict: {result['verdict']}\",\n \"note: contained overlays can be intentional; WARN findings need final-size visual review\",\n ]\n )\n if strict and summary[\"warn\"]:\n lines.append(\"strict verdict: FIX BEFORE DELIVERY (WARN is blocking)\")\n return \"\\n\".join(lines)\n\n\ndef exit_code(result: dict[str, Any], strict: bool = False) -> int:\n if not result[\"auditable\"]:\n return 2\n summary = result[\"summary\"]\n if summary[\"fail\"] or (strict and summary[\"warn\"]):\n return 1\n return 0\n\n\ndef run_self_tests() -> None:\n clean = PageGeometry(\n page=1,\n bbox=(0, 0, 200, 120),\n texts=[TextBox(0, \"Clear\", (50, 20, 90, 32))],\n traces=[TraceBox(0, \"Clear\", (50, 20, 90, 32))],\n strokes=[StrokePath(0, (20, 70, 180, 70), 1.0, (((20, 70), (180, 70)),))],\n )\n clean_result = audit_geometries([clean])\n assert clean_result[\"verdict\"] == \"PASS\", clean_result\n\n crossed = PageGeometry(\n page=1,\n bbox=(0, 0, 200, 120),\n texts=[TextBox(0, \"Crossed\", (50, 40, 100, 54))],\n traces=[TraceBox(0, \"Crossed\", (50, 40, 100, 54))],\n strokes=[StrokePath(0, (20, 47, 180, 47), 1.0, (((20, 47), (180, 47)),))],\n )\n crossed_result = audit_geometries([crossed])\n assert crossed_result[\"verdict\"] == \"FIX BEFORE DELIVERY\", crossed_result\n assert crossed_result[\"findings\"][0][\"kind\"] == \"text-stroke\", crossed_result\n\n intentional_fill = PageGeometry(\n page=1,\n bbox=(0, 0, 200, 120),\n texts=[TextBox(0, \"Inside\", (60, 40, 90, 52))],\n traces=[TraceBox(0, \"Inside\", (60, 40, 90, 52))],\n fills=[FilledRegion(0, (50, 30, 100, 60), \"fill\")],\n )\n fill_result = audit_geometries([intentional_fill])\n assert fill_result[\"verdict\"] == \"PASS\", fill_result\n assert fill_result[\"summary\"][\"contained_fill_overlays\"] == 1, fill_result\n\n\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\"pdf\", nargs=\"?\", type=Path, help=\"final exported PDF figure\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"print the report as JSON\")\n parser.add_argument(\"--json-out\", type=Path, help=\"write a machine-readable JSON report\")\n parser.add_argument(\"--overlay-pdf\", type=Path, help=\"write a QA-only PDF with collision boxes\")\n parser.add_argument(\"--strict\", action=\"store_true\", help=\"treat WARN findings as blocking\")\n parser.add_argument(\"--text-inset-pt\", type=float, default=0.6)\n parser.add_argument(\"--min-overlap-ratio\", type=float, default=0.05)\n parser.add_argument(\"--clipping-tolerance-pt\", type=float, default=0.5)\n parser.add_argument(\"--self-test\", action=\"store_true\", help=\"run dependency-free geometry tests\")\n return parser\n\n\ndef main(argv: list[str] | None = None) -> int:\n args = build_parser().parse_args(argv)\n if args.self_test:\n run_self_tests()\n print(\"audit_figure_collisions.py self-test: PASS\")\n return 0\n if args.pdf is None:\n print(\"error: PDF path is required unless --self-test is used\", file=sys.stderr)\n return 2\n for name, value in (\n (\"--text-inset-pt\", args.text_inset_pt),\n (\"--min-overlap-ratio\", args.min_overlap_ratio),\n (\"--clipping-tolerance-pt\", args.clipping_tolerance_pt),\n ):\n if value \u003c 0 or not math.isfinite(value):\n print(f\"error: {name} must be a finite non-negative number\", file=sys.stderr)\n return 2\n if not 0 \u003c= args.min_overlap_ratio \u003c= 1:\n print(\"error: --min-overlap-ratio must be between 0 and 1\", file=sys.stderr)\n return 2\n try:\n with args.pdf.open(\"rb\") as handle:\n is_pdf = b\"%PDF-\" in handle.read(1024) # the spec allows junk before the header\n except OSError as exc:\n print(f\"error: {exc}\", file=sys.stderr)\n return 2\n if not is_pdf:\n print(f\"error: not a PDF file (missing %PDF- header): {args.pdf}\", file=sys.stderr)\n return 2\n try:\n result = audit_pdf(\n args.pdf,\n text_inset_pt=args.text_inset_pt,\n min_overlap_ratio=args.min_overlap_ratio,\n clipping_tolerance_pt=args.clipping_tolerance_pt,\n )\n if args.json_out:\n args.json_out.parent.mkdir(parents=True, exist_ok=True)\n args.json_out.write_text(json.dumps(result, indent=2, ensure_ascii=False) + \"\\n\", encoding=\"utf-8\")\n if args.overlay_pdf and result[\"findings\"]:\n write_overlay_pdf(args.pdf, args.overlay_pdf, result[\"findings\"])\n except (OSError, RuntimeError, ValueError) as exc:\n print(f\"error: {exc}\", file=sys.stderr)\n return 2\n print(json.dumps(result, indent=2, ensure_ascii=False) if args.json else render_text(result, args.strict))\n return exit_code(result, args.strict)\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":27709,"content_sha256":"4bd020238a9f207c8a67c61d7209cef5959f2995a1d9cd95911d6fa93de7aa36"},{"filename":"scripts/audit_panel_alignment.py","content":"#!/usr/bin/env python3\n\"\"\"Audit multi-panel figure alignment from rendered layout geometry.\n\nThe core auditor consumes a small backend-neutral JSON manifest. Matplotlib\nfigures can call :func:`require_matplotlib_panel_alignment` directly after the\nfinal layout draw. R/patchwork figures use ``panel_alignment.R`` to export the\nsame manifest and then invoke this CLI.\n\nThis gate checks the plot-area rectangles that readers perceive as panels. It\ndoes not infer scientific equivalence: comparable row and column groups come\nfrom Matplotlib SubplotSpec metadata, patchwork/gtable metadata, or explicit\ngroups supplied by the plotting script. Horizontal rows of three or four equal\ngrid spans must also have equal final physical widths. Asymmetric hero panels,\ninsets and colorbars must be excluded or exempted with a recorded reason rather\nthan silently weakening the tolerance.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport html\nimport json\nimport math\nimport os\nimport re\nimport tempfile\nfrom collections import defaultdict\nfrom collections.abc import Iterable, Mapping, Sequence\nfrom pathlib import Path\nfrom typing import Any\n\n\nSCHEMA_VERSION = 1\nDEFAULT_TOLERANCE_PT = 1.5\nDEFAULT_GUTTER_TOLERANCE_PT = 1.5\n\n\nclass PanelAlignmentError(RuntimeError):\n \"\"\"Raised when a required in-memory alignment gate does not pass.\"\"\"\n\n\ndef _atomic_write_text(path: Path, content: str) -> None:\n path.parent.mkdir(parents=True, exist_ok=True)\n descriptor, temporary_name = tempfile.mkstemp(\n prefix=f\".{path.name}.\", suffix=\".tmp\", dir=path.parent\n )\n temporary = Path(temporary_name)\n try:\n with os.fdopen(descriptor, \"w\", encoding=\"utf-8\") as handle:\n handle.write(content)\n temporary.replace(path)\n except Exception:\n temporary.unlink(missing_ok=True)\n raise\n\n\ndef _bbox(value: Any) -> tuple[float, float, float, float]:\n if isinstance(value, Mapping):\n values = [value.get(key) for key in (\"left\", \"bottom\", \"right\", \"top\")]\n else:\n values = list(value) if isinstance(value, Sequence) else []\n if len(values) != 4:\n raise ValueError(\"panel bbox_pt must contain left, bottom, right and top\")\n bbox = tuple(float(item) for item in values)\n if not all(math.isfinite(item) for item in bbox):\n raise ValueError(\"panel bbox_pt values must be finite\")\n if bbox[2] \u003c= bbox[0] or bbox[3] \u003c= bbox[1]:\n raise ValueError(\"panel bbox_pt must have positive width and height\")\n return bbox\n\n\ndef _group_rows(raw: Any, prefix: str) -> list[dict[str, Any]]:\n groups: list[dict[str, Any]] = []\n if raw is None:\n return groups\n if not isinstance(raw, Sequence) or isinstance(raw, (str, bytes)):\n raise ValueError(f\"{prefix}_groups must be a list\")\n for index, item in enumerate(raw, 1):\n if isinstance(item, Mapping):\n panels = item.get(\"panels\", [])\n group_id = str(item.get(\"id\") or f\"{prefix}-{index}\")\n else:\n panels = item\n group_id = f\"{prefix}-{index}\"\n if not isinstance(panels, Sequence) or isinstance(panels, (str, bytes)):\n raise ValueError(f\"{prefix} group {group_id} must list panel ids\")\n panel_ids = [str(panel) for panel in panels]\n if len(panel_ids) \u003c 2:\n raise ValueError(f\"{prefix} group {group_id} needs at least two panels\")\n if len(panel_ids) != len(set(panel_ids)):\n raise ValueError(f\"{prefix} group {group_id} repeats a panel id\")\n groups.append({\"id\": group_id, \"panels\": panel_ids})\n return groups\n\n\ndef _inferred_groups(panels: Sequence[dict[str, Any]], orientation: str) -> list[dict[str, Any]]:\n grouped: dict[tuple[Any, ...], list[str]] = defaultdict(list)\n for panel in panels:\n if orientation == \"row\":\n fields = (panel.get(\"grid_id\"), panel.get(\"row_start\"), panel.get(\"row_stop\"))\n else:\n fields = (panel.get(\"grid_id\"), panel.get(\"col_start\"), panel.get(\"col_stop\"))\n if any(value is None for value in fields):\n continue\n grouped[fields].append(str(panel[\"id\"]))\n output: list[dict[str, Any]] = []\n for index, (_key, panel_ids) in enumerate(grouped.items(), 1):\n if len(panel_ids) >= 2:\n output.append({\"id\": f\"inferred-{orientation}-{index}\", \"panels\": panel_ids})\n return output\n\n\ndef _inferred_boundary_groups(panels: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:\n \"\"\"Infer shared grid boundaries across panels with unequal spans.\n\n This covers layouts such as two stacked panels beside one panel that spans\n both rows. Equal-span panels are handled by the ordinary row/column groups,\n so boundary groups are emitted only when at least two different spans meet\n at the same grid boundary.\n \"\"\"\n specifications = (\n (\"top\", \"row_start\", \"row_start\", \"row_stop\"),\n (\"bottom\", \"row_stop\", \"row_start\", \"row_stop\"),\n (\"left\", \"col_start\", \"col_start\", \"col_stop\"),\n (\"right\", \"col_stop\", \"col_start\", \"col_stop\"),\n )\n output: list[dict[str, Any]] = []\n for edge, boundary_field, span_start, span_stop in specifications:\n grouped: dict[tuple[Any, Any], list[dict[str, Any]]] = defaultdict(list)\n for panel in panels:\n grid_id = panel.get(\"grid_id\")\n boundary = panel.get(boundary_field)\n start = panel.get(span_start)\n stop = panel.get(span_stop)\n if any(value is None for value in (grid_id, boundary, start, stop)):\n continue\n grouped[(grid_id, boundary)].append(panel)\n edge_index = 0\n for (_grid_id, _boundary), members in grouped.items():\n spans = {(panel.get(span_start), panel.get(span_stop)) for panel in members}\n if len(members) \u003c 2 or len(spans) \u003c 2:\n continue\n edge_index += 1\n output.append(\n {\n \"id\": f\"inferred-shared-{edge}-{edge_index}\",\n \"edge\": edge,\n \"panels\": [str(panel[\"id\"]) for panel in members],\n }\n )\n return output\n\n\ndef _validated_layout(manifest: Mapping[str, Any]) -> tuple[dict[str, Any] | None, list[str]]:\n errors: list[str] = []\n if manifest.get(\"schema_version\") != SCHEMA_VERSION:\n errors.append(f\"schema_version must be {SCHEMA_VERSION}\")\n\n figure = manifest.get(\"figure\")\n if not isinstance(figure, Mapping):\n errors.append(\"figure must be an object with width_pt and height_pt\")\n figure = {}\n try:\n width_pt = float(figure.get(\"width_pt\"))\n height_pt = float(figure.get(\"height_pt\"))\n if not math.isfinite(width_pt) or not math.isfinite(height_pt) or width_pt \u003c= 0 or height_pt \u003c= 0:\n raise ValueError\n except (TypeError, ValueError):\n errors.append(\"figure width_pt and height_pt must be positive finite numbers\")\n width_pt = height_pt = 0.0\n\n raw_panels = manifest.get(\"panels\")\n panels: list[dict[str, Any]] = []\n if not isinstance(raw_panels, Sequence) or isinstance(raw_panels, (str, bytes)):\n errors.append(\"panels must be a list\")\n raw_panels = []\n for index, raw_panel in enumerate(raw_panels, 1):\n if not isinstance(raw_panel, Mapping):\n errors.append(f\"panel {index} must be an object\")\n continue\n panel_id = str(raw_panel.get(\"id\") or \"\").strip()\n if not panel_id:\n errors.append(f\"panel {index} is missing id\")\n continue\n try:\n bbox = _bbox(raw_panel.get(\"bbox_pt\"))\n except (TypeError, ValueError) as exc:\n errors.append(f\"panel {panel_id}: {exc}\")\n continue\n if bbox[0] \u003c -0.01 or bbox[1] \u003c -0.01 or bbox[2] > width_pt + 0.01 or bbox[3] > height_pt + 0.01:\n errors.append(f\"panel {panel_id} bbox_pt extends beyond the declared figure\")\n panel = dict(raw_panel)\n panel[\"id\"] = panel_id\n panel[\"bbox_pt\"] = list(bbox)\n anchor = raw_panel.get(\"panel_label_anchor_pt\")\n if anchor is not None:\n try:\n anchor_values = [float(value) for value in anchor]\n if len(anchor_values) != 2 or not all(math.isfinite(value) for value in anchor_values):\n raise ValueError\n panel[\"panel_label_anchor_pt\"] = anchor_values\n except (TypeError, ValueError):\n errors.append(f\"panel {panel_id} has invalid panel_label_anchor_pt\")\n panels.append(panel)\n\n panel_ids = [panel[\"id\"] for panel in panels]\n if len(panel_ids) != len(set(panel_ids)):\n errors.append(\"panel ids must be unique\")\n if not panels:\n errors.append(\"at least one panel rectangle is required\")\n\n try:\n row_groups = _group_rows(manifest.get(\"row_groups\"), \"row\")\n column_groups = _group_rows(manifest.get(\"column_groups\"), \"column\")\n except ValueError as exc:\n errors.append(str(exc))\n row_groups = column_groups = []\n if not row_groups:\n row_groups = _inferred_groups(panels, \"row\")\n if not column_groups:\n column_groups = _inferred_groups(panels, \"column\")\n boundary_groups = _inferred_boundary_groups(panels)\n\n known_ids = set(panel_ids)\n for group in [*row_groups, *column_groups]:\n missing = [panel for panel in group[\"panels\"] if panel not in known_ids]\n if missing:\n errors.append(f\"group {group['id']} references unknown panels: {', '.join(missing)}\")\n\n exemptions: list[dict[str, Any]] = []\n raw_exemptions = manifest.get(\"exemptions\", [])\n if not isinstance(raw_exemptions, Sequence) or isinstance(raw_exemptions, (str, bytes)):\n errors.append(\"exemptions must be a list\")\n raw_exemptions = []\n for index, exemption in enumerate(raw_exemptions, 1):\n if not isinstance(exemption, Mapping):\n errors.append(f\"exemption {index} must be an object\")\n continue\n exemption_panels = exemption.get(\"panels\", [])\n checks = exemption.get(\"checks\", [])\n reason = str(exemption.get(\"reason\") or \"\").strip()\n if isinstance(exemption_panels, str):\n exemption_panels = [exemption_panels]\n if isinstance(checks, str):\n checks = [checks]\n if not exemption_panels or any(str(panel) not in known_ids for panel in exemption_panels):\n errors.append(f\"exemption {index} must reference known panels\")\n allowed_checks = {\n \"row\",\n \"column\",\n \"panel-width\",\n \"horizontal-gutter\",\n \"vertical-gutter\",\n \"panel-label\",\n \"all\",\n }\n check_names = [str(check) for check in checks]\n if not check_names or any(check not in allowed_checks for check in check_names):\n errors.append(f\"exemption {index} has invalid checks\")\n if not reason:\n errors.append(f\"exemption {index} needs a non-empty reason\")\n exemptions.append(\n {\"panels\": [str(panel) for panel in exemption_panels], \"checks\": check_names, \"reason\": reason}\n )\n\n if errors:\n return None, errors\n return {\n \"schema_version\": SCHEMA_VERSION,\n \"backend\": str(manifest.get(\"backend\") or \"unknown\"),\n \"figure\": {\"width_pt\": width_pt, \"height_pt\": height_pt},\n \"panels\": panels,\n \"row_groups\": row_groups,\n \"column_groups\": column_groups,\n \"boundary_groups\": boundary_groups,\n \"exemptions\": exemptions,\n }, []\n\n\ndef _is_exempt(layout: Mapping[str, Any], panel_id: str, check: str) -> bool:\n for exemption in layout.get(\"exemptions\", []):\n if panel_id in exemption[\"panels\"] and (check in exemption[\"checks\"] or \"all\" in exemption[\"checks\"]):\n return True\n return False\n\n\ndef _metric_spread(values: Mapping[str, float]) -> float:\n return max(values.values()) - min(values.values()) if values else 0.0\n\n\ndef _finding(\n kind: str,\n group: Mapping[str, Any],\n panels: Sequence[str],\n tolerance_pt: float,\n metrics: Mapping[str, Mapping[str, float]],\n message: str,\n severity: str = \"FAIL\",\n) -> dict[str, Any]:\n return {\n \"severity\": severity,\n \"kind\": kind,\n \"group\": group[\"id\"],\n \"panels\": list(panels),\n \"message\": message,\n \"tolerance_pt\": tolerance_pt,\n \"metric_spreads_pt\": {\n metric: round(_metric_spread(values), 6) for metric, values in metrics.items()\n },\n \"values_pt\": {\n metric: {panel: round(value, 6) for panel, value in values.items()}\n for metric, values in metrics.items()\n },\n }\n\n\ndef audit_layout_manifest(\n manifest: Mapping[str, Any],\n *,\n tolerance_pt: float = DEFAULT_TOLERANCE_PT,\n gutter_tolerance_pt: float = DEFAULT_GUTTER_TOLERANCE_PT,\n require_panel_labels: bool = False,\n) -> dict[str, Any]:\n \"\"\"Audit a backend-neutral panel-layout manifest.\"\"\"\n if tolerance_pt \u003c 0 or gutter_tolerance_pt \u003c 0:\n raise ValueError(\"alignment tolerances must be non-negative\")\n layout, errors = _validated_layout(manifest)\n if layout is None:\n return {\n \"schema_version\": SCHEMA_VERSION,\n \"auditable\": False,\n \"verdict\": \"NOT AUDITABLE\",\n \"summary\": {\"fail\": 0, \"warn\": 0, \"comparisons\": 0, \"exemptions\": 0},\n \"errors\": errors,\n \"findings\": [],\n }\n\n if len(layout[\"panels\"]) == 1:\n return {\n \"schema_version\": SCHEMA_VERSION,\n \"applicable\": False,\n \"auditable\": False,\n \"verdict\": \"NOT APPLICABLE\",\n \"backend\": layout[\"backend\"],\n \"summary\": {\"fail\": 0, \"warn\": 0, \"comparisons\": 0, \"exemptions\": 0},\n \"tolerances\": {\n \"alignment_pt\": tolerance_pt,\n \"gutter_pt\": gutter_tolerance_pt,\n },\n \"errors\": [],\n \"findings\": [],\n \"layout\": layout,\n }\n\n panel_map = {panel[\"id\"]: panel for panel in layout[\"panels\"]}\n findings: list[dict[str, Any]] = []\n comparisons = 0\n\n for orientation, group_key, check_name in (\n (\"row\", \"row_groups\", \"row\"),\n (\"column\", \"column_groups\", \"column\"),\n ):\n for group in layout[group_key]:\n group_panel_ids = list(group[\"panels\"])\n panel_ids = [\n panel_id for panel_id in group_panel_ids if not _is_exempt(layout, panel_id, check_name)\n ]\n if len(panel_ids) \u003c 2:\n continue\n comparisons += 1\n boxes = {panel_id: _bbox(panel_map[panel_id][\"bbox_pt\"]) for panel_id in panel_ids}\n if orientation == \"row\":\n metrics = {\n \"top\": {panel: box[3] for panel, box in boxes.items()},\n \"bottom\": {panel: box[1] for panel, box in boxes.items()},\n \"height\": {panel: box[3] - box[1] for panel, box in boxes.items()},\n }\n kind = \"row-axes-misalignment\"\n message = \"Panels intended for one row do not share top/bottom edges and height\"\n else:\n metrics = {\n \"left\": {panel: box[0] for panel, box in boxes.items()},\n \"right\": {panel: box[2] for panel, box in boxes.items()},\n \"width\": {panel: box[2] - box[0] for panel, box in boxes.items()},\n }\n kind = \"column-axes-misalignment\"\n message = \"Panels intended for one column do not share left/right edges and width\"\n if max(_metric_spread(values) for values in metrics.values()) > tolerance_pt:\n findings.append(_finding(kind, group, panel_ids, tolerance_pt, metrics, message))\n\n if orientation == \"row\" and len(group_panel_ids) >= 3:\n width_candidates = [\n panel_id\n for panel_id in group_panel_ids\n if not _is_exempt(layout, panel_id, \"row\")\n and not _is_exempt(layout, panel_id, \"panel-width\")\n ]\n by_column_span: dict[Any, list[str]] = defaultdict(list)\n for panel_id in width_candidates:\n panel = panel_map[panel_id]\n try:\n column_span = float(panel.get(\"col_stop\")) - float(panel.get(\"col_start\"))\n if not math.isfinite(column_span) or column_span \u003c= 0:\n raise ValueError\n span_key: Any = round(column_span, 9)\n except (TypeError, ValueError):\n span_key = \"unspecified\"\n by_column_span[span_key].append(panel_id)\n for width_index, width_ids in enumerate(by_column_span.values(), 1):\n if len(width_ids) \u003c 2:\n continue\n comparisons += 1\n width_metrics = {\n \"width\": {\n panel_id: _bbox(panel_map[panel_id][\"bbox_pt\"])[2]\n - _bbox(panel_map[panel_id][\"bbox_pt\"])[0]\n for panel_id in width_ids\n }\n }\n if _metric_spread(width_metrics[\"width\"]) > tolerance_pt:\n findings.append(\n _finding(\n \"horizontal-panel-width-misalignment\",\n {\"id\": f\"{group['id']}-equal-span-{width_index}\"},\n width_ids,\n tolerance_pt,\n width_metrics,\n \"Three-or-more-panel row contains unequal final plot-area widths for equal grid spans\",\n )\n )\n\n anchors = {\n panel_id: panel_map[panel_id].get(\"panel_label_anchor_pt\") for panel_id in panel_ids\n }\n present_anchors = {panel: anchor for panel, anchor in anchors.items() if anchor is not None}\n missing_labels = [panel for panel, anchor in anchors.items() if anchor is None]\n if require_panel_labels and missing_labels:\n findings.append(\n {\n \"severity\": \"WARN\",\n \"kind\": \"panel-label-not-auditable\",\n \"group\": group[\"id\"],\n \"panels\": missing_labels,\n \"message\": \"Comparable panels are missing detectable top-left panel-label anchors\",\n }\n )\n if len(present_anchors) >= 2:\n label_check = \"panel-label\"\n label_ids = [\n panel for panel in panel_ids if panel in present_anchors and not _is_exempt(layout, panel, label_check)\n ]\n if len(label_ids) >= 2:\n coordinate = 1 if orientation == \"row\" else 0\n metric_name = \"label-y\" if orientation == \"row\" else \"label-x\"\n metrics = {\n metric_name: {panel: float(present_anchors[panel][coordinate]) for panel in label_ids}\n }\n if _metric_spread(metrics[metric_name]) > tolerance_pt:\n findings.append(\n _finding(\n f\"{orientation}-panel-label-misalignment\",\n group,\n label_ids,\n tolerance_pt,\n metrics,\n \"Panel-label anchors are not aligned within their comparable group\",\n )\n )\n\n gutter_check = \"horizontal-gutter\" if orientation == \"row\" else \"vertical-gutter\"\n gutter_ids = [panel for panel in panel_ids if not _is_exempt(layout, panel, gutter_check)]\n if len(gutter_ids) >= 2:\n if orientation == \"row\":\n ordered = sorted(gutter_ids, key=lambda panel: boxes[panel][0])\n gaps = {\n f\"{first}->{second}\": boxes[second][0] - boxes[first][2]\n for first, second in zip(ordered, ordered[1:])\n }\n else:\n ordered = sorted(gutter_ids, key=lambda panel: boxes[panel][3], reverse=True)\n gaps = {\n f\"{first}->{second}\": boxes[first][1] - boxes[second][3]\n for first, second in zip(ordered, ordered[1:])\n }\n if gaps and min(gaps.values()) \u003c -tolerance_pt:\n findings.append(\n _finding(\n \"panel-axes-overlap\",\n group,\n gutter_ids,\n tolerance_pt,\n {\"gutter\": gaps},\n \"Panel plot-area rectangles overlap\",\n )\n )\n elif len(gaps) >= 2 and _metric_spread(gaps) > gutter_tolerance_pt:\n findings.append(\n _finding(\n f\"{gutter_check}-misalignment\",\n group,\n gutter_ids,\n gutter_tolerance_pt,\n {\"gutter\": gaps},\n \"Comparable inter-panel gutters are not uniform\",\n )\n )\n\n edge_coordinates = {\"left\": 0, \"bottom\": 1, \"right\": 2, \"top\": 3}\n for group in layout[\"boundary_groups\"]:\n edge = str(group[\"edge\"])\n check_name = \"row\" if edge in {\"top\", \"bottom\"} else \"column\"\n panel_ids = [\n panel_id\n for panel_id in group[\"panels\"]\n if not _is_exempt(layout, panel_id, check_name)\n ]\n if len(panel_ids) \u003c 2:\n continue\n comparisons += 1\n coordinate = edge_coordinates[edge]\n metrics = {\n edge: {\n panel_id: _bbox(panel_map[panel_id][\"bbox_pt\"])[coordinate]\n for panel_id in panel_ids\n }\n }\n if _metric_spread(metrics[edge]) > tolerance_pt:\n findings.append(\n _finding(\n f\"shared-{edge}-edge-misalignment\",\n group,\n panel_ids,\n tolerance_pt,\n metrics,\n f\"Panels meeting the same grid {edge} boundary are not aligned\",\n )\n )\n\n if edge not in {\"top\", \"left\"}:\n continue\n anchors = {\n panel_id: panel_map[panel_id].get(\"panel_label_anchor_pt\")\n for panel_id in panel_ids\n }\n missing_labels = [panel_id for panel_id, anchor in anchors.items() if anchor is None]\n if require_panel_labels and missing_labels:\n findings.append(\n {\n \"severity\": \"WARN\",\n \"kind\": \"panel-label-not-auditable\",\n \"group\": group[\"id\"],\n \"panels\": missing_labels,\n \"message\": \"Comparable panels are missing detectable top-left panel-label anchors\",\n }\n )\n label_ids = [\n panel_id\n for panel_id, anchor in anchors.items()\n if anchor is not None and not _is_exempt(layout, panel_id, \"panel-label\")\n ]\n if len(label_ids) \u003c 2:\n continue\n label_coordinate = 1 if edge == \"top\" else 0\n metric_name = \"label-y\" if edge == \"top\" else \"label-x\"\n label_metrics = {\n metric_name: {\n panel_id: float(anchors[panel_id][label_coordinate])\n for panel_id in label_ids\n }\n }\n if _metric_spread(label_metrics[metric_name]) > tolerance_pt:\n findings.append(\n _finding(\n f\"shared-{edge}-panel-label-misalignment\",\n group,\n label_ids,\n tolerance_pt,\n label_metrics,\n \"Panel-label anchors are not aligned at the shared grid boundary\",\n )\n )\n fail_count = sum(finding[\"severity\"] == \"FAIL\" for finding in findings)\n warn_count = sum(finding[\"severity\"] == \"WARN\" for finding in findings)\n auditable = comparisons > 0\n verdict = \"FIX BEFORE DELIVERY\" if fail_count else \"REVIEW REQUIRED\" if warn_count else \"PASS\"\n if not auditable:\n verdict = \"NOT AUDITABLE\"\n return {\n \"schema_version\": SCHEMA_VERSION,\n \"applicable\": True,\n \"auditable\": auditable,\n \"verdict\": verdict,\n \"backend\": layout[\"backend\"],\n \"summary\": {\n \"fail\": fail_count,\n \"warn\": warn_count,\n \"comparisons\": comparisons,\n \"exemptions\": len(layout[\"exemptions\"]),\n },\n \"tolerances\": {\n \"alignment_pt\": tolerance_pt,\n \"gutter_pt\": gutter_tolerance_pt,\n },\n \"layout\": layout,\n \"findings\": findings,\n **(\n {\"errors\": [\"No comparable row, column or shared-boundary groups were declared or inferred\"]}\n if not auditable\n else {}\n ),\n }\n\n\ndef exit_code(report: Mapping[str, Any], strict: bool = False) -> int:\n if report.get(\"verdict\") == \"NOT APPLICABLE\":\n return 0\n if not report.get(\"auditable\") or report.get(\"verdict\") == \"NOT AUDITABLE\":\n return 2\n if int(report.get(\"summary\", {}).get(\"fail\", 0)):\n return 1\n if strict and int(report.get(\"summary\", {}).get(\"warn\", 0)):\n return 1\n return 0\n\n\ndef render_text(report: Mapping[str, Any], strict: bool = False) -> str:\n summary = report.get(\"summary\", {})\n lines = [\n f\"Panel alignment: {report.get('verdict', 'UNKNOWN')}\",\n f\" comparisons: {summary.get('comparisons', 0)}\",\n f\" fail: {summary.get('fail', 0)}\",\n f\" warn: {summary.get('warn', 0)}\",\n f\" exemptions: {summary.get('exemptions', 0)}\",\n ]\n for error in report.get(\"errors\", []):\n lines.append(f\" ERROR: {error}\")\n for finding in report.get(\"findings\", []):\n lines.append(\n f\" {finding['severity']} {finding['kind']} [{finding.get('group', 'n/a')}]: \"\n f\"{finding['message']} ({', '.join(finding.get('panels', []))})\"\n )\n if strict and summary.get(\"warn\", 0):\n lines.append(\" strict mode: WARN findings block delivery\")\n return \"\\n\".join(lines)\n\n\ndef write_json_report(report: Mapping[str, Any], path: str | Path) -> None:\n _atomic_write_text(Path(path), json.dumps(report, indent=2, ensure_ascii=False) + \"\\n\")\n\n\ndef write_overlay_svg(report: Mapping[str, Any], path: str | Path) -> None:\n layout = report.get(\"layout\")\n if not isinstance(layout, Mapping):\n raise ValueError(\"an auditable layout is required for the diagnostic SVG\")\n width = float(layout[\"figure\"][\"width_pt\"])\n height = float(layout[\"figure\"][\"height_pt\"])\n failed_panels = {\n panel for finding in report.get(\"findings\", []) if finding.get(\"severity\") == \"FAIL\"\n for panel in finding.get(\"panels\", [])\n }\n rows = [\n f'\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}pt\" height=\"{height}pt\" viewBox=\"0 0 {width} {height}\">',\n '\u003crect width=\"100%\" height=\"100%\" fill=\"white\"/>',\n ]\n for panel in layout[\"panels\"]:\n left, bottom, right, top = _bbox(panel[\"bbox_pt\"])\n y = height - top\n color = \"#d7191c\" if panel[\"id\"] in failed_panels else \"#2c7bb6\"\n rows.append(\n f'\u003crect x=\"{left:.3f}\" y=\"{y:.3f}\" width=\"{right-left:.3f}\" height=\"{top-bottom:.3f}\" '\n f'fill=\"none\" stroke=\"{color}\" stroke-width=\"1.2\"/>'\n )\n rows.append(\n f'\u003ctext x=\"{left+2:.3f}\" y=\"{y+10:.3f}\" font-family=\"Arial, sans-serif\" font-size=\"8\" '\n f'fill=\"{color}\">{html.escape(str(panel[\"id\"]))}\u003c/text>'\n )\n rows.append(\n f'\u003ctext x=\"4\" y=\"{height-5:.3f}\" font-family=\"Arial, sans-serif\" font-size=\"7\" fill=\"#333\">'\n f'{report.get(\"verdict\", \"UNKNOWN\")}: {report.get(\"summary\", {}).get(\"fail\", 0)} fail, '\n f'{report.get(\"summary\", {}).get(\"warn\", 0)} warn\u003c/text>'\n )\n rows.append(\"\u003c/svg>\")\n _atomic_write_text(Path(path), \"\\n\".join(rows) + \"\\n\")\n\n\ndef _alphabetic_id(index: int) -> str:\n output = \"\"\n value = index + 1\n while value:\n value, remainder = divmod(value - 1, 26)\n output = chr(ord(\"a\") + remainder) + output\n return output\n\n\ndef _font_is_bold(weight: Any) -> bool:\n if isinstance(weight, (int, float)):\n return float(weight) >= 600\n return str(weight).lower() in {\"bold\", \"semibold\", \"demibold\", \"heavy\", \"black\"}\n\n\ndef _matplotlib_panel_label_anchor(ax: Any) -> tuple[str, list[float]] | None:\n for artist in ax.texts:\n label = artist.get_text().strip()\n if not re.fullmatch(r\"[a-z]\", label) or not _font_is_bold(artist.get_fontweight()):\n continue\n display = artist.get_transform().transform(artist.get_position())\n axes_xy = ax.transAxes.inverted().transform(display)\n if not (-0.35 \u003c= axes_xy[0] \u003c= 0.2 and 0.8 \u003c= axes_xy[1] \u003c= 1.35):\n continue\n inches = ax.figure.dpi_scale_trans.inverted().transform(display)\n return label, [float(inches[0] * 72), float(inches[1] * 72)]\n return None\n\n\ndef matplotlib_layout_manifest(\n fig: Any,\n *,\n axes: Sequence[Any] | None = None,\n panel_ids: Mapping[Any, str] | Sequence[str] | None = None,\n row_groups: Sequence[Any] | None = None,\n column_groups: Sequence[Any] | None = None,\n exclude_axes: Iterable[Any] = (),\n exemptions: Sequence[Mapping[str, Any]] = (),\n) -> dict[str, Any]:\n \"\"\"Measure final Matplotlib axes rectangles in physical points.\"\"\"\n fig.canvas.draw()\n width_in, height_in = (float(value) for value in fig.get_size_inches())\n width_pt, height_pt = width_in * 72, height_in * 72\n excluded = {id(axis) for axis in exclude_axes}\n candidates = list(axes) if axes is not None else list(fig.axes)\n selected: list[Any] = []\n seen_subplot_cells: set[tuple[int, int, int]] = set()\n for axis in candidates:\n if id(axis) in excluded or not axis.get_visible():\n continue\n axis_label = str(axis.get_label() or \"\")\n if axis_label.startswith(\"\u003ccolorbar\"):\n continue\n try:\n subplot_spec = axis.get_subplotspec()\n except AttributeError:\n subplot_spec = None\n if subplot_spec is not None and panel_ids is None:\n subplot_key = (\n id(subplot_spec.get_gridspec()),\n int(subplot_spec.num1),\n int(subplot_spec.num2),\n )\n if subplot_key in seen_subplot_cells:\n continue\n seen_subplot_cells.add(subplot_key)\n selected.append(axis)\n\n if isinstance(panel_ids, Mapping):\n ids = [str(panel_ids.get(axis) or \"\") for axis in selected]\n elif panel_ids is not None:\n ids = [str(value) for value in panel_ids]\n if len(ids) != len(selected):\n raise ValueError(\"panel_ids length must match the selected Matplotlib axes\")\n else:\n ids = []\n for index, axis in enumerate(selected):\n candidate = str(axis.get_gid() or axis.get_label() or \"\").strip()\n ids.append(candidate if candidate and not candidate.startswith(\"\u003c\") else _alphabetic_id(index))\n\n grid_ids: dict[int, str] = {}\n panels: list[dict[str, Any]] = []\n for axis, panel_id in zip(selected, ids):\n position = axis.get_position(original=False)\n panel: dict[str, Any] = {\n \"id\": panel_id,\n \"bbox_pt\": [\n float(position.x0 * width_pt),\n float(position.y0 * height_pt),\n float(position.x1 * width_pt),\n float(position.y1 * height_pt),\n ],\n }\n try:\n subplot_spec = axis.get_subplotspec()\n except AttributeError:\n subplot_spec = None\n if subplot_spec is not None:\n grid_spec = subplot_spec.get_gridspec()\n grid_key = id(grid_spec)\n if grid_key not in grid_ids:\n grid_ids[grid_key] = f\"matplotlib-grid-{len(grid_ids) + 1}\"\n panel.update(\n {\n \"grid_id\": grid_ids[grid_key],\n \"row_start\": int(subplot_spec.rowspan.start),\n \"row_stop\": int(subplot_spec.rowspan.stop),\n \"col_start\": int(subplot_spec.colspan.start),\n \"col_stop\": int(subplot_spec.colspan.stop),\n }\n )\n label_anchor = _matplotlib_panel_label_anchor(axis)\n if label_anchor is not None:\n panel[\"panel_label\"] = label_anchor[0]\n panel[\"panel_label_anchor_pt\"] = label_anchor[1]\n panels.append(panel)\n\n manifest: dict[str, Any] = {\n \"schema_version\": SCHEMA_VERSION,\n \"backend\": \"python-matplotlib\",\n \"figure\": {\"width_pt\": width_pt, \"height_pt\": height_pt},\n \"panels\": panels,\n \"exemptions\": [dict(exemption) for exemption in exemptions],\n }\n if row_groups is not None:\n manifest[\"row_groups\"] = list(row_groups)\n if column_groups is not None:\n manifest[\"column_groups\"] = list(column_groups)\n return manifest\n\n\ndef require_matplotlib_panel_alignment(\n fig: Any,\n *,\n json_out: str | Path | None = None,\n overlay_svg: str | Path | None = None,\n tolerance_pt: float = DEFAULT_TOLERANCE_PT,\n gutter_tolerance_pt: float = DEFAULT_GUTTER_TOLERANCE_PT,\n require_panel_labels: bool = False,\n strict: bool = False,\n **manifest_options: Any,\n) -> dict[str, Any]:\n \"\"\"Measure and block delivery when a Matplotlib multi-panel layout is misaligned.\"\"\"\n manifest = matplotlib_layout_manifest(fig, **manifest_options)\n report = audit_layout_manifest(\n manifest,\n tolerance_pt=tolerance_pt,\n gutter_tolerance_pt=gutter_tolerance_pt,\n require_panel_labels=require_panel_labels,\n )\n if json_out is not None:\n write_json_report(report, json_out)\n if overlay_svg is not None and report.get(\"layout\"):\n write_overlay_svg(report, overlay_svg)\n code = exit_code(report, strict=strict)\n if code:\n raise PanelAlignmentError(render_text(report, strict=strict))\n return report\n\n\ndef run_self_tests() -> None:\n aligned = {\n \"schema_version\": 1,\n \"backend\": \"self-test\",\n \"figure\": {\"width_pt\": 300, \"height_pt\": 200},\n \"panels\": [\n {\"id\": \"a\", \"bbox_pt\": [20, 110, 130, 180], \"grid_id\": \"g\", \"row_start\": 0, \"row_stop\": 1, \"col_start\": 0, \"col_stop\": 1},\n {\"id\": \"b\", \"bbox_pt\": [170, 110, 280, 180], \"grid_id\": \"g\", \"row_start\": 0, \"row_stop\": 1, \"col_start\": 1, \"col_stop\": 2},\n {\"id\": \"c\", \"bbox_pt\": [20, 20, 130, 90], \"grid_id\": \"g\", \"row_start\": 1, \"row_stop\": 2, \"col_start\": 0, \"col_stop\": 1},\n {\"id\": \"d\", \"bbox_pt\": [170, 20, 280, 90], \"grid_id\": \"g\", \"row_start\": 1, \"row_stop\": 2, \"col_start\": 1, \"col_stop\": 2},\n ],\n }\n if exit_code(audit_layout_manifest(aligned)) != 0:\n raise AssertionError(\"aligned self-test layout did not pass\")\n shifted = json.loads(json.dumps(aligned))\n shifted[\"panels\"][1][\"bbox_pt\"][1] -= 5\n if exit_code(audit_layout_manifest(shifted)) != 1:\n raise AssertionError(\"shifted self-test layout did not fail\")\n unequal_widths = {\n \"schema_version\": 1,\n \"backend\": \"self-test\",\n \"figure\": {\"width_pt\": 320, \"height_pt\": 100},\n \"panels\": [\n {\"id\": \"a\", \"bbox_pt\": [10, 20, 70, 80], \"grid_id\": \"g\", \"row_start\": 0, \"row_stop\": 1, \"col_start\": 0, \"col_stop\": 1},\n {\"id\": \"b\", \"bbox_pt\": [90, 20, 170, 80], \"grid_id\": \"g\", \"row_start\": 0, \"row_stop\": 1, \"col_start\": 1, \"col_stop\": 2},\n {\"id\": \"c\", \"bbox_pt\": [190, 20, 290, 80], \"grid_id\": \"g\", \"row_start\": 0, \"row_stop\": 1, \"col_start\": 2, \"col_stop\": 3},\n ],\n }\n unequal_report = audit_layout_manifest(unequal_widths)\n if exit_code(unequal_report) != 1 or not any(\n finding.get(\"kind\") == \"horizontal-panel-width-misalignment\"\n for finding in unequal_report.get(\"findings\", [])\n ):\n raise AssertionError(\"unequal-width self-test row did not fail\")\n\n\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(\n description=\"Audit rendered multi-panel axes alignment from a JSON layout manifest.\"\n )\n parser.add_argument(\"layout\", nargs=\"?\", help=\"backend-neutral panel-layout JSON\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"print the report as JSON\")\n parser.add_argument(\"--json-out\", help=\"write the report atomically as JSON\")\n parser.add_argument(\"--overlay-svg\", help=\"write a QA-only SVG of measured panel rectangles\")\n parser.add_argument(\"--tolerance-pt\", type=float, default=DEFAULT_TOLERANCE_PT)\n parser.add_argument(\"--gutter-tolerance-pt\", type=float, default=DEFAULT_GUTTER_TOLERANCE_PT)\n parser.add_argument(\"--require-panel-labels\", action=\"store_true\")\n parser.add_argument(\"--strict\", action=\"store_true\", help=\"make WARN findings blocking\")\n parser.add_argument(\"--self-test\", action=\"store_true\", help=\"run dependency-free core tests\")\n return parser\n\n\ndef main(argv: Sequence[str] | None = None) -> int:\n parser = build_parser()\n args = parser.parse_args(argv)\n if args.self_test:\n run_self_tests()\n print(\"Panel alignment self-test: PASS\")\n return 0\n if not args.layout:\n parser.error(\"layout JSON is required unless --self-test is used\")\n try:\n manifest = json.loads(Path(args.layout).read_text(encoding=\"utf-8\"))\n if not isinstance(manifest, dict):\n raise ValueError(\"layout JSON must be an object\")\n report = audit_layout_manifest(\n manifest,\n tolerance_pt=args.tolerance_pt,\n gutter_tolerance_pt=args.gutter_tolerance_pt,\n require_panel_labels=args.require_panel_labels,\n )\n except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:\n report = {\n \"schema_version\": SCHEMA_VERSION,\n \"auditable\": False,\n \"verdict\": \"NOT AUDITABLE\",\n \"summary\": {\"fail\": 0, \"warn\": 0, \"comparisons\": 0, \"exemptions\": 0},\n \"errors\": [str(exc)],\n \"findings\": [],\n }\n if args.json_out:\n write_json_report(report, args.json_out)\n if args.overlay_svg and report.get(\"layout\"):\n write_overlay_svg(report, args.overlay_svg)\n print(json.dumps(report, indent=2, ensure_ascii=False) if args.json else render_text(report, strict=args.strict))\n return exit_code(report, strict=args.strict)\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":39469,"content_sha256":"4ed2d6afe9db45aaf6ca26aeb264cfc251ae92b49ee9fbf117d926e1956acf56"},{"filename":"scripts/audit_pdf_text.py","content":"#!/usr/bin/env python3\n\"\"\"Audit effective text sizes in PDF content streams.\n\nThe size is ``Tf`` size scaled by the text matrix (``Tm``) and ``cm`` transforms.\n\nThis dependency-free check catches reduced mathtext superscripts/subscripts and\nother glyph runs that can fall below a journal font-size floor even when the\nparent matplotlib ``fontsize`` is compliant. It supports plain and FlateDecode\ncontent streams, which covers normal matplotlib PDF output.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport re\nimport sys\nimport zlib\nfrom dataclasses import asdict, dataclass\nfrom pathlib import Path\n\n\nSTREAM_START = re.compile(rb\"stream\\r?\\n\")\nTOKEN = re.compile(\n rb\"%[^\\r\\n]*\" # comment\n rb\"|\\((?:\\\\.|[^\\\\()])*\\)\" # literal string (no nested parentheses)\n rb\"|\u003c\u003c|>>|\u003c[0-9A-Fa-f\\s]*>\" # dict delimiters / hex string\n rb\"|[\\[\\]]\"\n rb\"|/[^\\s/\u003c>\\[\\]()%]*\" # name\n rb\"|[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[Ee][-+]?\\d+)?\" # number\n rb\"|[^\\s/\u003c>\\[\\]()%]+\", # operator\n re.S,\n)\nNUMBER = re.compile(rb\"[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[Ee][-+]?\\d+)?$\")\nSHOW_OPERATORS = {b\"Tj\", b\"TJ\", b\"'\", b'\"'}\nIDENTITY = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)\n\n\ndef multiply(first: tuple[float, ...], second: tuple[float, ...]) -> tuple[float, ...]:\n \"\"\"Return ``first x second`` for PDF row-vector matrices ``[a b c d e f]``.\"\"\"\n a1, b1, c1, d1, e1, f1 = first\n a2, b2, c2, d2, e2, f2 = second\n return (\n a1 * a2 + b1 * c2,\n a1 * b2 + b1 * d2,\n c1 * a2 + d1 * c2,\n c1 * b2 + d1 * d2,\n e1 * a2 + f1 * c2 + e2,\n e1 * b2 + f1 * d2 + f2,\n )\n\n\ndef matrix_scale(matrix: tuple[float, ...]) -> float:\n \"\"\"Isotropic scale factor of the linear part (square root of |det|).\"\"\"\n return abs(matrix[0] * matrix[3] - matrix[1] * matrix[2]) ** 0.5\n\n\n@dataclass(frozen=True)\nclass TextRun:\n stream: int\n font: str\n size_pt: float\n\n\ndef scan_text_runs(stream: bytes, stream_index: int) -> list[TextRun]:\n \"\"\"Return glyph runs with the effective size ``Tf size x Tm x CTM``.\n\n Runs are recorded when text is shown, so a ``Tm`` that follows ``Tf`` (as in\n Cairo output using ``1 Tf``) is applied to the size.\n \"\"\"\n runs: list[TextRun] = []\n operands: list[bytes] = []\n ctm = IDENTITY\n ctm_stack: list[tuple[float, ...]] = []\n text_matrix = IDENTITY\n font = \"\"\n font_size = 0.0\n last: tuple[str, float] | None = None\n for match in TOKEN.finditer(stream):\n token = match.group(0)\n if token.startswith(b\"%\"):\n continue\n if token.startswith(b\"(\"):\n operands.append(b\"(\")\n continue\n if token[:1] in (b\"/\", b\"[\", b\"]\", b\"\u003c\") or NUMBER.match(token):\n operands.append(token)\n continue\n operator = token\n if operator == b\"q\":\n ctm_stack.append(ctm)\n elif operator == b\"Q\":\n if ctm_stack:\n ctm = ctm_stack.pop()\n elif operator == b\"cm\" and len(operands) >= 6:\n try:\n values = tuple(float(value) for value in operands[-6:])\n except ValueError:\n values = None\n if values:\n ctm = multiply(values, ctm)\n elif operator == b\"BT\":\n text_matrix = IDENTITY\n elif operator == b\"Tm\" and len(operands) >= 6:\n try:\n text_matrix = tuple(float(value) for value in operands[-6:])\n except ValueError:\n pass\n elif operator == b\"Tf\" and len(operands) >= 2:\n try:\n font = operands[-2].lstrip(b\"/\").decode(\"ascii\", errors=\"replace\")\n font_size = float(operands[-1])\n except ValueError:\n pass\n elif operator in SHOW_OPERATORS:\n size = abs(font_size) * matrix_scale(multiply(text_matrix, ctm))\n if size > 0 and last != (font, size):\n runs.append(TextRun(stream=stream_index, font=font, size_pt=size))\n last = (font, size)\n operands.clear()\n return runs\n\n\ndef decoded_streams(data: bytes) -> tuple[list[bytes], list[str]]:\n streams: list[bytes] = []\n warnings: list[str] = []\n cursor = 0\n stream_number = 0\n while True:\n match = STREAM_START.search(data, cursor)\n if not match:\n break\n stream_number += 1\n end = data.find(b\"endstream\", match.end())\n if end \u003c 0:\n warnings.append(f\"stream {stream_number} has no endstream marker\")\n break\n # Keep the raw stream bytes. zlib accepts PDF's trailing line break,\n # while stripping could accidentally remove a legitimate compressed\n # byte that happens to equal CR or LF.\n payload = data[match.end() : end]\n header = data[max(0, match.start() - 2048) : match.start()]\n dictionary_start = header.rfind(b\"\u003c\u003c\")\n dictionary = header[dictionary_start:] if dictionary_start >= 0 else header\n if b\"/FlateDecode\" in dictionary:\n try:\n payload = zlib.decompress(payload)\n except zlib.error as exc:\n warnings.append(f\"stream {stream_number} FlateDecode failed: {exc}\")\n cursor = end + len(b\"endstream\")\n continue\n elif b\"/Filter\" in dictionary:\n warnings.append(f\"stream {stream_number} uses an unsupported PDF filter\")\n cursor = end + len(b\"endstream\")\n continue\n streams.append(payload)\n cursor = end + len(b\"endstream\")\n return streams, warnings\n\n\ndef audit_pdf(data: bytes, minimum_pt: float = 5.0) -> dict[str, object]:\n streams, warnings = decoded_streams(data)\n runs: list[TextRun] = []\n for stream_index, stream in enumerate(streams, 1):\n runs.extend(scan_text_runs(stream, stream_index))\n below = [run for run in runs if run.size_pt \u003c minimum_pt]\n return {\n \"auditable\": bool(runs),\n \"minimum_required_pt\": minimum_pt,\n \"minimum_found_pt\": min((run.size_pt for run in runs), default=None),\n \"text_run_count\": len(runs),\n \"below_minimum_count\": len(below),\n \"below_minimum\": [asdict(run) for run in below],\n \"warnings\": warnings,\n }\n\n\ndef render_text(path: Path, result: dict[str, object]) -> str:\n lines = [\n \"Nature Figure PDF Text Audit\",\n f\"pdf: {path}\",\n f\"minimum required: {result['minimum_required_pt']:g} pt\",\n ]\n if not result[\"auditable\"]:\n lines.append(\"verdict: NOT AUDITABLE — no supported text-showing operators were found\")\n else:\n lines.extend(\n [\n f\"minimum found: {result['minimum_found_pt']:g} pt\",\n f\"text runs: {result['text_run_count']}\",\n f\"below minimum: {result['below_minimum_count']}\",\n f\"verdict: {'FAIL' if result['below_minimum_count'] else 'PASS'}\",\n ]\n )\n for run in result[\"below_minimum\"]:\n lines.append(f\" - stream {run['stream']}: /{run['font']} {run['size_pt']:g} pt\")\n for warning in result[\"warnings\"]:\n lines.append(f\"warning: {warning}\")\n lines.append(\"note: content-stream scanning does not replace final-size visual inspection or account for every PDF transform (e.g. Form XObject matrices)\")\n return \"\\n\".join(lines)\n\n\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\"pdf\", type=Path, help=\"exported PDF figure\")\n parser.add_argument(\"--min-pt\", type=float, default=5.0, help=\"minimum allowed effective font size in points\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"emit machine-readable JSON\")\n return parser\n\n\ndef main(argv: list[str] | None = None) -> int:\n args = build_parser().parse_args(argv)\n if args.min_pt \u003c= 0:\n print(\"error: --min-pt must be positive\", file=sys.stderr)\n return 2\n try:\n data = args.pdf.read_bytes()\n except OSError as exc:\n print(f\"error: {exc}\", file=sys.stderr)\n return 2\n if not data.startswith(b\"%PDF-\"):\n print(f\"error: not a PDF file: {args.pdf}\", file=sys.stderr)\n return 2\n result = audit_pdf(data, minimum_pt=args.min_pt)\n if args.json:\n print(json.dumps({\"pdf\": str(args.pdf), **result}, indent=2, ensure_ascii=False))\n else:\n print(render_text(args.pdf, result))\n if not result[\"auditable\"]:\n return 2\n return 1 if result[\"below_minimum_count\"] else 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8586,"content_sha256":"0b3d3e30ce9ec7191b8ba6dd053226487ab41594297005afcd127acda3b08281"},{"filename":"scripts/audit_svg_schematic.py","content":"#!/usr/bin/env python3\n\"\"\"Audit a hand-built SVG schematic against final-size journal rules.\n\nChecks, using only the standard library:\n\n* effective text size at final physical size (font-size, ``scale``/``matrix``\n transforms, and the ``viewBox`` to ``width`` scale) against a floor and ceiling;\n* canvas width against a journal maximum;\n* text anchored outside the canvas;\n* constructs that do not survive production: scripts, ``foreignObject``,\n gradients, filters, and external references.\n\nIt does not detect overlaps between boxes, crossing arrows, or outlined text, so\nit never replaces looking at the rendered figure at final size.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport re\nimport sys\nfrom dataclasses import asdict, dataclass\nfrom pathlib import Path\nfrom xml.etree import ElementTree as ET # nosec B405 - DTDs and entities are rejected before parsing\n\nPT_PER_UNIT = {\"pt\": 1.0, \"px\": 0.75, \"mm\": 72 / 25.4, \"cm\": 72 / 2.54, \"in\": 72.0, \"\": None}\nNUMBER = r\"[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[Ee][-+]?\\d+)?\"\nLENGTH = re.compile(rf\"^\\s*({NUMBER})\\s*(pt|px|mm|cm|in)?\\s*$\")\nTRANSFORM = re.compile(rf\"(translate|scale|rotate|matrix)\\(([^)]*)\\)\")\nFORBIDDEN = {\n \"script\": \"FAIL\",\n \"foreignObject\": \"FAIL\",\n \"linearGradient\": \"WARN\",\n \"radialGradient\": \"WARN\",\n \"filter\": \"WARN\",\n}\nMM_SINGLE, MM_DOUBLE = 89.0, 183.0\n\n\n@dataclass(frozen=True)\nclass Finding:\n severity: str\n kind: str\n message: str\n\n\ndef local(tag: str) -> str:\n return tag.rsplit(\"}\", 1)[-1]\n\n\ndef parse_length(value: str | None) -> tuple[float, str] | None:\n match = LENGTH.match(value or \"\")\n return (float(match.group(1)), match.group(2) or \"\") if match else None\n\n\ndef style_value(element: ET.Element, name: str) -> str | None:\n style = element.get(\"style\") or \"\"\n for part in style.split(\";\"):\n key, _, value = part.partition(\":\")\n if key.strip() == name:\n return value.strip()\n return element.get(name)\n\n\nMatrix = tuple[float, float, float, float, float, float]\nIDENTITY: Matrix = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)\n\n\ndef multiply(first: Matrix, second: Matrix) -> Matrix:\n \"\"\"Return ``first x second`` for SVG ``[a b c d e f]`` matrices.\"\"\"\n a1, b1, c1, d1, e1, f1 = first\n a2, b2, c2, d2, e2, f2 = second\n return (\n a1 * a2 + c1 * b2,\n b1 * a2 + d1 * b2,\n a1 * c2 + c1 * d2,\n b1 * c2 + d1 * d2,\n a1 * e2 + c1 * f2 + e1,\n b1 * e2 + d1 * f2 + f1,\n )\n\n\ndef transform_matrix(element: ET.Element) -> Matrix:\n \"\"\"Matrix for this element's own ``transform`` (translate, scale, rotate, matrix).\"\"\"\n result = IDENTITY\n for kind, args in TRANSFORM.findall(element.get(\"transform\") or \"\"):\n n = [float(v) for v in re.findall(NUMBER, args)]\n if kind == \"translate\" and n:\n local_matrix: Matrix = (1.0, 0.0, 0.0, 1.0, n[0], n[1] if len(n) > 1 else 0.0)\n elif kind == \"scale\" and n:\n local_matrix = (n[0], 0.0, 0.0, n[1] if len(n) > 1 else n[0], 0.0, 0.0)\n elif kind == \"rotate\" and n:\n cos, sin = math.cos(math.radians(n[0])), math.sin(math.radians(n[0]))\n local_matrix = (cos, sin, -sin, cos, 0.0, 0.0)\n if len(n) >= 3:\n shift: Matrix = (1.0, 0.0, 0.0, 1.0, n[1], n[2])\n back: Matrix = (1.0, 0.0, 0.0, 1.0, -n[1], -n[2])\n local_matrix = multiply(multiply(shift, local_matrix), back)\n elif kind == \"matrix\" and len(n) >= 6:\n local_matrix = (n[0], n[1], n[2], n[3], n[4], n[5])\n else:\n continue\n result = multiply(result, local_matrix)\n return result\n\n\ndef matrix_scale(matrix: Matrix) -> float:\n \"\"\"Isotropic scale factor (square root of |det|) of the linear part.\"\"\"\n return abs(matrix[0] * matrix[3] - matrix[1] * matrix[2]) ** 0.5\n\n\ndef first_number(value: str | None) -> float | None:\n \"\"\"First number of an SVG coordinate list such as ``\"10 20 30\"``.\"\"\"\n found = re.findall(NUMBER, value or \"\")\n return float(found[0]) if found else None\n\n\ndef canvas_scale(root: ET.Element) -> tuple[float, float | None, bool]:\n \"\"\"Return (points per user unit, width in pt or None, units_known).\"\"\"\n view_box = [float(n) for n in re.findall(NUMBER, root.get(\"viewBox\") or \"\")]\n width = parse_length(root.get(\"width\"))\n view_width = view_box[2] if len(view_box) == 4 and view_box[2] > 0 else None\n if width and PT_PER_UNIT.get(width[1]):\n width_pt = width[0] * PT_PER_UNIT[width[1]]\n return (width_pt / view_width if view_width else PT_PER_UNIT[width[1]]), width_pt, True\n if view_width:\n return 1.0, view_width, False # unitless: assume 1 user unit = 1 pt\n return 1.0, None, False\n\n\ndef audit_svg(\n data: bytes,\n min_pt: float = 5.0,\n max_pt: float = 7.0,\n max_width_mm: float = MM_DOUBLE,\n) -> dict[str, object]:\n findings: list[Finding] = []\n head = data[:4096].lower()\n if b\"\u003c!doctype\" in head or b\"\u003c!entity\" in data.lower():\n return {\"auditable\": False, \"error\": \"DTD or entity declarations are not supported\", \"findings\": []}\n try:\n root = ET.fromstring(data) # nosec B314 - DTDs and entities rejected above\n except ET.ParseError as exc:\n return {\"auditable\": False, \"error\": f\"not well-formed XML: {exc}\", \"findings\": []}\n if local(root.tag) != \"svg\":\n return {\"auditable\": False, \"error\": \"root element is not \u003csvg>\", \"findings\": []}\n\n unit_scale, width_pt, units_known = canvas_scale(root)\n if not units_known:\n findings.append(\n Finding(\"WARN\", \"canvas.units\", \"width has no physical unit; assuming 1 user unit = 1 pt\")\n )\n width_mm = width_pt * 25.4 / 72 if width_pt else None\n if width_mm is None:\n findings.append(Finding(\"WARN\", \"canvas.size\", \"no width or viewBox; final size unknown\"))\n else:\n if width_mm > max_width_mm + 0.5:\n findings.append(\n Finding(\"FAIL\", \"canvas.width\", f\"width {width_mm:.1f} mm exceeds {max_width_mm:g} mm\")\n )\n elif min(abs(width_mm - MM_SINGLE), abs(width_mm - MM_DOUBLE)) > 2.0:\n findings.append(\n Finding(\"WARN\", \"canvas.width\", f\"width {width_mm:.1f} mm is not 89 mm or 183 mm\")\n )\n\n view_box = [float(n) for n in re.findall(NUMBER, root.get(\"viewBox\") or \"\")]\n sizes: list[float] = []\n texts = 0\n\n def check_text(element: ET.Element, label: str, matrix: Matrix, font_size: float | None, anchor: bool) -> None:\n nonlocal texts\n texts += 1\n if font_size is None:\n findings.append(\n Finding(\"WARN\", \"text.size\", f\"font-size not set for {label[:30]!r}; SVG default is 16 units\")\n )\n else:\n effective = font_size * matrix_scale(matrix) * unit_scale\n sizes.append(effective)\n if effective \u003c min_pt - 1e-6:\n findings.append(Finding(\"FAIL\", \"text.small\", f\"{label[:30]!r} is {effective:.2f} pt (\u003c {min_pt:g} pt)\"))\n elif effective > max_pt + 1e-6:\n findings.append(Finding(\"WARN\", \"text.large\", f\"{label[:30]!r} is {effective:.2f} pt (> {max_pt:g} pt)\"))\n if anchor and len(view_box) == 4:\n x, y = first_number(element.get(\"x\")), first_number(element.get(\"y\"))\n if x is not None and y is not None:\n px = matrix[0] * x + matrix[2] * y + matrix[4]\n py = matrix[1] * x + matrix[3] * y + matrix[5]\n if not (\n view_box[0] \u003c= px \u003c= view_box[0] + view_box[2]\n and view_box[1] \u003c= py \u003c= view_box[1] + view_box[3]\n ):\n findings.append(Finding(\"FAIL\", \"text.off-canvas\", f\"{label[:30]!r} is anchored outside the viewBox\"))\n\n def declared_size(element: ET.Element, inherited: float | None) -> float | None:\n length = parse_length(style_value(element, \"font-size\"))\n if not length:\n return inherited\n return length[0] * (PT_PER_UNIT[length[1]] / unit_scale if PT_PER_UNIT.get(length[1]) else 1.0)\n\n def walk(element: ET.Element, matrix: Matrix, font_size: float | None, parent_tag: str) -> None:\n tag = local(element.tag)\n if tag in FORBIDDEN:\n findings.append(Finding(FORBIDDEN[tag], \"forbidden\", f\"\u003c{tag}> is not allowed in a production schematic\"))\n for key, value in element.attrib.items():\n if local(key) == \"href\" and not (value.startswith(\"#\") or value.startswith(\"data:\")):\n findings.append(Finding(\"FAIL\", \"external-ref\", f\"external reference: {value[:60]}\"))\n matrix = multiply(matrix, transform_matrix(element))\n own_size = declared_size(element, font_size)\n label = \"\".join(element.itertext()).strip()\n if tag == \"text\" and label:\n # A size set only on descendant tspans is checked per tspan below.\n sized_children = any(\n local(child.tag) == \"tspan\" and style_value(child, \"font-size\") for child in element.iter()\n )\n if own_size is not None or not sized_children:\n check_text(element, label, matrix, own_size, anchor=True)\n elif tag == \"tspan\" and label:\n if parent_tag != \"text\":\n check_text(element, label, matrix, own_size, anchor=True)\n elif style_value(element, \"font-size\"):\n check_text(element, label, matrix, own_size, anchor=False)\n for child in element:\n walk(child, matrix, own_size, tag)\n\n walk(root, IDENTITY, None, \"\")\n if not texts:\n findings.append(Finding(\"WARN\", \"text.none\", \"no \u003ctext> found; text may be outlined, which journals reject\"))\n return {\n \"auditable\": True,\n \"width_mm\": width_mm,\n \"text_count\": texts,\n \"minimum_found_pt\": min(sizes) if sizes else None,\n \"maximum_found_pt\": max(sizes) if sizes else None,\n \"thresholds\": {\"min_pt\": min_pt, \"max_pt\": max_pt, \"max_width_mm\": max_width_mm},\n \"findings\": [asdict(f) for f in findings],\n }\n\n\ndef verdict(result: dict[str, object]) -> str:\n if not result[\"auditable\"]:\n return \"NOT AUDITABLE\"\n severities = {f[\"severity\"] for f in result[\"findings\"]} # type: ignore[index]\n return \"FAIL\" if \"FAIL\" in severities else \"PASS WITH WARNINGS\" if \"WARN\" in severities else \"PASS\"\n\n\ndef render_text(path: Path, result: dict[str, object]) -> str:\n lines = [\"Nature Figure SVG Schematic Audit\", f\"svg: {path}\"]\n if not result[\"auditable\"]:\n lines.append(f\"error: {result['error']}\")\n else:\n width = result[\"width_mm\"]\n lines.append(f\"width: {width:.1f} mm\" if width else \"width: unknown\")\n lines.append(f\"text elements: {result['text_count']}\")\n if result[\"minimum_found_pt\"] is not None:\n lines.append(f\"text size: {result['minimum_found_pt']:.2f} to {result['maximum_found_pt']:.2f} pt\")\n for f in result[\"findings\"]: # type: ignore[union-attr]\n lines.append(f\"[{f['severity']}] {f['kind']}: {f['message']}\")\n lines.append(f\"verdict: {verdict(result)}\")\n lines.append(\"note: overlaps, crossings and outlined text are not detected; inspect the render at final size\")\n return \"\\n\".join(lines)\n\n\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\"svg\", type=Path, help=\"SVG schematic\")\n parser.add_argument(\"--min-pt\", type=float, default=5.0, help=\"minimum text size in points\")\n parser.add_argument(\"--max-pt\", type=float, default=7.0, help=\"maximum ordinary text size in points\")\n parser.add_argument(\"--max-width-mm\", type=float, default=MM_DOUBLE, help=\"maximum canvas width in mm\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"emit machine-readable JSON\")\n return parser\n\n\ndef main(argv: list[str] | None = None) -> int:\n args = build_parser().parse_args(argv)\n if args.min_pt \u003c= 0 or args.max_pt \u003c args.min_pt or args.max_width_mm \u003c= 0:\n print(\"error: thresholds must be positive and --max-pt >= --min-pt\", file=sys.stderr)\n return 2\n try:\n data = args.svg.read_bytes()\n except OSError as exc:\n print(f\"error: {exc}\", file=sys.stderr)\n return 2\n result = audit_svg(data, args.min_pt, args.max_pt, args.max_width_mm)\n if args.json:\n print(json.dumps({\"svg\": str(args.svg), \"verdict\": verdict(result), **result}, indent=2, ensure_ascii=False))\n else:\n print(render_text(args.svg, result))\n if not result[\"auditable\"]:\n return 2\n return 1 if verdict(result) == \"FAIL\" else 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12722,"content_sha256":"8776cb9da07dc2ef6edf65c1ad23184567172b03685b880c78f3ef74d7241db8"},{"filename":"scripts/figure_safety.py","content":"#!/usr/bin/env python3\n\"\"\"Small numerical/layout safety helpers for Python publication figures.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport numpy as np\n\n\ndef interp_monotone(target: Any, xp: Any, fp: Any) -> Any:\n \"\"\"Interpolate on a strictly monotone grid without silent direction errors.\n\n ``numpy.interp`` requires increasing ``xp``. This helper accepts a strictly\n increasing or decreasing grid, reverses decreasing ``xp`` and ``fp``\n together, and rejects duplicate/non-monotone coordinates.\n \"\"\"\n xp_array = np.asarray(xp, dtype=float)\n fp_array = np.asarray(fp, dtype=float)\n if xp_array.ndim != 1 or fp_array.ndim != 1 or xp_array.size != fp_array.size:\n raise ValueError(\"xp and fp must be one-dimensional arrays of equal length\")\n if xp_array.size \u003c 2:\n raise ValueError(\"at least two interpolation points are required\")\n if not np.all(np.isfinite(xp_array)) or not np.all(np.isfinite(fp_array)):\n raise ValueError(\"xp and fp must contain only finite values\")\n differences = np.diff(xp_array)\n if np.all(differences > 0):\n ordered_xp, ordered_fp = xp_array, fp_array\n elif np.all(differences \u003c 0):\n ordered_xp, ordered_fp = xp_array[::-1], fp_array[::-1]\n else:\n raise ValueError(\"xp must be strictly monotone; duplicates or direction changes are unsafe\")\n return np.interp(target, ordered_xp, ordered_fp)\n\n\ndef label_y_above(values: Any, spread: Any | None = None, pad_fraction: float = 0.04) -> float:\n \"\"\"Return a data-driven label y-position above values and optional spread.\"\"\"\n centers = np.asarray(values, dtype=float)\n if centers.size == 0 or not np.all(np.isfinite(centers)):\n raise ValueError(\"values must contain at least one finite value\")\n upper = centers if spread is None else centers + np.asarray(spread, dtype=float)\n if not np.all(np.isfinite(upper)):\n raise ValueError(\"spread must contain only finite values\")\n data_min = float(np.min(centers))\n data_max = float(np.max(upper))\n scale = max(data_max - data_min, abs(data_max), 1.0)\n return data_max + max(0.0, pad_fraction) * scale\n\n\n__all__ = [\"interp_monotone\", \"label_y_above\"]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2216,"content_sha256":"0e3e9601aacdafca32ef5fbf1967bdeec0abbf7f564edfd711875432bd47c212"},{"filename":"scripts/generate_openrouter_schematic.py","content":"#!/usr/bin/env python3\n\"\"\"Generate manuscript schematic drafts with OpenRouter's Images API.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport base64\nimport ipaddress\nimport json\nimport mimetypes\nimport os\nimport sys\nimport time\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\nfrom typing import Any\n\n\nAPI_URL = \"https://openrouter.ai/api/v1/images\"\nDEFAULT_MODEL = \"openai/gpt-image-2\"\n\n\ndef read_optional_text(text: str | None, path: str | None) -> str:\n parts: list[str] = []\n if text:\n parts.append(text.strip())\n if path:\n parts.append(Path(path).read_text(encoding=\"utf-8\").strip())\n return \"\\n\\n\".join(part for part in parts if part)\n\n\ndef build_prompt(args: argparse.Namespace) -> str:\n custom_prompt = read_optional_text(args.prompt, args.prompt_file)\n if custom_prompt and args.raw:\n return custom_prompt\n\n title = args.title.strip() if args.title else \"\"\n abstract = read_optional_text(args.abstract, args.abstract_file)\n panel_map = args.panel_map.strip() if args.panel_map else \"\"\n\n content_blocks = []\n if title:\n content_blocks.append(f\"Title: {title}\")\n if abstract:\n content_blocks.append(f\"Article summary:\\n{abstract}\")\n if panel_map:\n content_blocks.append(f\"Desired panel flow:\\n{panel_map}\")\n if custom_prompt:\n content_blocks.append(f\"Additional instructions:\\n{custom_prompt}\")\n\n if not content_blocks:\n raise SystemExit(\n \"Provide --prompt/--prompt-file or at least one of --title, \"\n \"--abstract/--abstract-file, or --panel-map.\"\n )\n\n style = args.style or (\n \"Create a clean Nature-style scientific graphical abstract / mechanism \"\n \"schematic for a research paper. Use a flat vector-like visual language, \"\n \"restrained journal palette, clear hierarchy, simple arrows, and minimal \"\n \"short labels. Keep the background uncluttered.\"\n )\n\n constraints = (\n \"Scientific constraints: show only the mechanisms and entities described \"\n \"below; do not invent quantitative values, p-values, microscopy results, \"\n \"institutional logos, journal marks, or unsupported experimental claims. \"\n \"Use conceptual visual elements rather than fake data panels. Text labels \"\n \"must be short and easy to redraw later.\"\n )\n\n return \"\\n\\n\".join([style, constraints, *content_blocks])\n\n\ndef reference_to_payload(value: str) -> dict[str, Any]:\n if value.startswith((\"http://\", \"https://\", \"data:\")):\n url = value\n else:\n path = Path(value)\n if not path.exists():\n raise SystemExit(f\"Reference image not found: {value}\")\n media_type = mimetypes.guess_type(path.name)[0] or \"image/png\"\n encoded = base64.b64encode(path.read_bytes()).decode(\"ascii\")\n url = f\"data:{media_type};base64,{encoded}\"\n return {\"type\": \"image_url\", \"image_url\": {\"url\": url}}\n\n\ndef build_payload(args: argparse.Namespace) -> dict[str, Any]:\n payload: dict[str, Any] = {\n \"model\": args.model,\n \"prompt\": build_prompt(args),\n }\n\n optional_fields = {\n \"aspect_ratio\": args.aspect_ratio,\n \"resolution\": args.resolution,\n \"quality\": args.quality,\n \"output_format\": args.output_format,\n \"background\": args.background,\n \"size\": args.size,\n \"n\": args.n,\n }\n for key, value in optional_fields.items():\n if value is not None:\n payload[key] = value\n\n if args.reference_image:\n payload[\"input_references\"] = [\n reference_to_payload(value) for value in args.reference_image\n ]\n\n return payload\n\n\ndef _require_downloadable_url(url: str, source: str) -> None:\n \"\"\"Response-supplied URLs must be plain http(s) to non-local hosts before download.\"\"\"\n parsed = urllib.parse.urlparse(url)\n if parsed.scheme not in {\"http\", \"https\"}:\n raise SystemExit(f\"Refusing non-http(s) {source}: {url}\")\n hostname = (parsed.hostname or \"\").lower()\n try:\n ip = ipaddress.ip_address(hostname)\n except ValueError:\n ip = None\n if hostname == \"localhost\" or (ip is not None and (ip.is_private or ip.is_loopback or ip.is_link_local)):\n raise SystemExit(f\"Refusing local {source} host: {url}\")\n\n\ndef media_extension(media_type: str | None, output_format: str | None) -> str:\n if media_type == \"image/svg+xml\":\n return \".svg\"\n if media_type == \"image/jpeg\":\n return \".jpg\"\n if media_type == \"image/webp\":\n return \".webp\"\n if media_type == \"image/png\":\n return \".png\"\n if output_format == \"jpeg\":\n return \".jpg\"\n if output_format in {\"png\", \"webp\"}:\n return f\".{output_format}\"\n return \".png\"\n\n\ndef decode_b64_image(value: str) -> bytes:\n if value.startswith(\"data:\"):\n value = value.split(\",\", 1)[1]\n return base64.b64decode(value)\n\n\ndef request_images(payload: dict[str, Any], args: argparse.Namespace) -> dict[str, Any]:\n api_key = os.environ.get(args.api_key_env)\n if not api_key:\n raise SystemExit(f\"Missing API key: set {args.api_key_env}.\")\n\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n site_url = args.site_url or os.environ.get(\"OPENROUTER_SITE_URL\")\n app_name = args.app_name or os.environ.get(\"OPENROUTER_APP_NAME\")\n if site_url:\n headers[\"HTTP-Referer\"] = site_url\n if app_name:\n headers[\"X-Title\"] = app_name\n\n request = urllib.request.Request(\n args.api_url,\n data=json.dumps(payload).encode(\"utf-8\"),\n headers=headers,\n method=\"POST\",\n )\n try:\n with urllib.request.urlopen(request, timeout=args.timeout) as response:\n return json.loads(response.read().decode(\"utf-8\"))\n except urllib.error.HTTPError as exc:\n body = exc.read().decode(\"utf-8\", errors=\"replace\")\n raise SystemExit(f\"Image request failed ({exc.code}) at {args.api_url}: {body}\") from exc\n except (urllib.error.URLError, TimeoutError, OSError) as exc:\n # A timeout while waiting for headers or reading the body raises a bare TimeoutError/OSError.\n raise SystemExit(f\"Image request failed at {args.api_url}: {exc}\") from exc\n\n\ndef save_outputs(response: dict[str, Any], payload: dict[str, Any], args: argparse.Namespace) -> None:\n outdir = Path(args.outdir)\n outdir.mkdir(parents=True, exist_ok=True)\n basename = args.basename or time.strftime(\"openrouter_schematic_%Y%m%d_%H%M%S\")\n\n saved: list[str] = []\n for index, item in enumerate(response.get(\"data\", []), start=1):\n media_type = item.get(\"media_type\")\n ext = media_extension(media_type, args.output_format)\n suffix = \"\" if len(response.get(\"data\", [])) == 1 else f\"_{index:02d}\"\n outpath = outdir / f\"{basename}{suffix}{ext}\"\n\n if \"b64_json\" in item:\n outpath.write_bytes(decode_b64_image(item[\"b64_json\"]))\n saved.append(str(outpath))\n elif \"url\" in item:\n _require_downloadable_url(item[\"url\"], f\"image URL in response item {index}\")\n try:\n with urllib.request.urlopen(item[\"url\"], timeout=args.timeout) as image_response:\n outpath.write_bytes(image_response.read())\n except OSError as exc: # URLError, HTTPError and timeouts are all OSError subclasses\n raise SystemExit(f\"Image download failed for response item {index} ({item['url']}): {exc}\") from exc\n saved.append(str(outpath))\n else:\n raise SystemExit(f\"No image bytes or URL in response item {index}: {item}\")\n\n metadata = {\n \"api_url\": args.api_url,\n \"request\": payload,\n \"response_usage\": response.get(\"usage\"),\n \"created\": response.get(\"created\"),\n \"saved_files\": saved,\n }\n metadata_path = outdir / f\"{basename}_request_metadata.json\"\n metadata_path.write_text(json.dumps(metadata, indent=2, ensure_ascii=False), encoding=\"utf-8\")\n\n print(\"Saved:\")\n for path in saved:\n print(f\" {path}\")\n print(f\" {metadata_path}\")\n\n\ndef parse_args() -> argparse.Namespace:\n parser = argparse.ArgumentParser(\n description=\"Generate manuscript schematic drafts with OpenRouter's Images API.\"\n )\n parser.add_argument(\"--model\", default=os.environ.get(\"OPENROUTER_IMAGE_MODEL\", DEFAULT_MODEL))\n parser.add_argument(\"--title\")\n parser.add_argument(\"--abstract\")\n parser.add_argument(\"--abstract-file\")\n parser.add_argument(\"--panel-map\")\n parser.add_argument(\"--prompt\")\n parser.add_argument(\"--prompt-file\")\n parser.add_argument(\"--style\")\n parser.add_argument(\"--raw\", action=\"store_true\", help=\"Use prompt text without the schematic scaffold.\")\n parser.add_argument(\"--reference-image\", action=\"append\", help=\"Path, URL, or data URL for image-to-image guidance.\")\n parser.add_argument(\"--outdir\", default=\"openrouter_schematic\")\n parser.add_argument(\"--basename\")\n parser.add_argument(\"--aspect-ratio\", default=\"16:9\")\n parser.add_argument(\"--resolution\", default=\"2K\")\n parser.add_argument(\"--size\", help=\"Optional OpenRouter size shorthand, such as 2048x2048.\")\n parser.add_argument(\"--quality\", default=\"high\", choices=[\"auto\", \"low\", \"medium\", \"high\"])\n parser.add_argument(\"--output-format\", default=\"png\", choices=[\"png\", \"jpeg\", \"webp\"])\n parser.add_argument(\"--background\", choices=[\"auto\", \"transparent\", \"opaque\"])\n parser.add_argument(\"--n\", type=int, default=1)\n parser.add_argument(\"--api-key-env\", default=\"OPENROUTER_API_KEY\")\n parser.add_argument(\n \"--api-url\",\n default=os.environ.get(\"SCHEMATIC_IMAGE_API_URL\", API_URL),\n help=(\n \"Images endpoint to call. Defaults to OpenRouter; point it at any \"\n \"OpenAI-compatible images endpoint to use that service instead.\"\n ),\n )\n parser.add_argument(\"--site-url\")\n parser.add_argument(\"--app-name\")\n parser.add_argument(\"--timeout\", type=int, default=180)\n parser.add_argument(\"--dry-run\", action=\"store_true\", help=\"Print request payload without calling the API.\")\n return parser.parse_args()\n\n\ndef main() -> int:\n args = parse_args()\n payload = build_payload(args)\n\n if args.dry_run:\n print(json.dumps(payload, indent=2, ensure_ascii=False))\n return 0\n\n response = request_images(payload, args)\n save_outputs(response, payload, args)\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10586,"content_sha256":"0b8a9ac399be326fa1b575e800f5d7541516a7f1d0d7696dcc81096a3f4edb4b"},{"filename":"scripts/nature_figure_backend.py","content":"#!/usr/bin/env python3\n\"\"\"Read or write the user's default nature-figure plotting backend.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport os\nimport sys\nfrom pathlib import Path\n\n\nVALID_BACKENDS = {\"python\", \"r\"}\n\n\ndef config_path() -> Path:\n override = os.environ.get(\"NATURE_FIGURE_CONFIG\")\n if override:\n return Path(override).expanduser()\n return Path(\"~/.config/nature-skills/nature-figure.json\").expanduser()\n\n\ndef read_config(path: Path) -> dict:\n if not path.exists():\n return {}\n try:\n data = json.loads(path.read_text(encoding=\"utf-8\"))\n except json.JSONDecodeError as exc:\n raise SystemExit(f\"Invalid nature-figure config JSON at {path}: {exc}\") from exc\n if not isinstance(data, dict):\n raise SystemExit(f\"Invalid nature-figure config at {path}: expected object\")\n return data\n\n\ndef write_config(path: Path, data: dict) -> None:\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + \"\\n\", encoding=\"utf-8\")\n\n\ndef get_backend(path: Path) -> int:\n backend = read_config(path).get(\"backend\")\n if isinstance(backend, str) and backend in VALID_BACKENDS:\n print(backend)\n return 0\n return 1\n\n\ndef set_backend(path: Path, backend: str) -> int:\n backend = backend.lower()\n if backend not in VALID_BACKENDS:\n raise SystemExit(\"backend must be one of: python, r\")\n data = read_config(path)\n data[\"backend\"] = backend\n write_config(path, data)\n print(backend)\n return 0\n\n\ndef clear_backend(path: Path) -> int:\n data = read_config(path)\n data.pop(\"backend\", None)\n write_config(path, data)\n return 0\n\n\ndef parse_args() -> argparse.Namespace:\n parser = argparse.ArgumentParser(description=__doc__)\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n subparsers.add_parser(\"get\", help=\"Print the saved backend; exit 1 if unset.\")\n set_parser = subparsers.add_parser(\"set\", help=\"Save the default backend.\")\n set_parser.add_argument(\"backend\", choices=sorted(VALID_BACKENDS))\n subparsers.add_parser(\"clear\", help=\"Remove the saved backend preference.\")\n subparsers.add_parser(\"path\", help=\"Print the config path.\")\n return parser.parse_args()\n\n\ndef main() -> int:\n args = parse_args()\n path = config_path()\n if args.command == \"get\":\n return get_backend(path)\n if args.command == \"set\":\n return set_backend(path, args.backend)\n if args.command == \"clear\":\n return clear_backend(path)\n if args.command == \"path\":\n print(path)\n return 0\n raise AssertionError(args.command)\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2737,"content_sha256":"ae15bc3f19217250edbbb8bfcb33227d185469f67e81071780369224e63d0dca"},{"filename":"scripts/panel_alignment.R","content":"# Render-time panel-alignment helper for R patchwork figures.\n#\n# Source this file from the selected R plotting script. It measures patchwork's\n# final panel cells on a device with the same physical dimensions as the export,\n# writes the backend-neutral JSON manifest consumed by audit_panel_alignment.py,\n# and can run the blocking audit in one call.\n\n.nature_alignment_groups \u003c- function(groups) {\n if (is.null(groups)) {\n return(NULL)\n }\n lapply(groups, function(group) as.character(unlist(group, use.names = FALSE)))\n}\n\n.nature_alignment_panel_rows \u003c- function(grob) {\n rows \u003c- grob$layout[grepl(\"^panel(?:-[0-9]+)?$\", grob$layout$name, perl = TRUE), , drop = FALSE]\n if (nrow(rows) \u003c 2) {\n stop(\"At least two patchwork panel cells are required for alignment QA\", call. = FALSE)\n }\n rows[order(rows$t, rows$l), , drop = FALSE]\n}\n\nwrite_patchwork_panel_layout \u003c- function(\n plot,\n manifest_path,\n width_in,\n height_in,\n panel_ids = NULL,\n row_groups = NULL,\n column_groups = NULL,\n exemptions = list()\n) {\n for (package in c(\"patchwork\", \"grid\", \"jsonlite\")) {\n if (!requireNamespace(package, quietly = TRUE)) {\n stop(sprintf(\"Package '%s' is required for patchwork alignment QA\", package), call. = FALSE)\n }\n }\n if (!is.numeric(width_in) || length(width_in) != 1 || !is.finite(width_in) || width_in \u003c= 0 ||\n !is.numeric(height_in) || length(height_in) != 1 || !is.finite(height_in) || height_in \u003c= 0) {\n stop(\"width_in and height_in must be positive finite numbers\", call. = FALSE)\n }\n\n grob \u003c- patchwork::patchworkGrob(plot)\n panel_rows \u003c- .nature_alignment_panel_rows(grob)\n if (is.null(panel_ids)) {\n panel_ids \u003c- vapply(\n seq_len(nrow(panel_rows)),\n function(index) if (index \u003c= length(letters)) letters[index] else paste0(\"panel-\", index),\n character(1)\n )\n }\n panel_ids \u003c- as.character(panel_ids)\n if (length(panel_ids) != nrow(panel_rows) || anyDuplicated(panel_ids) || any(!nzchar(panel_ids))) {\n stop(\"panel_ids must be unique, non-empty, and match the measured patchwork panels\", call. = FALSE)\n }\n\n probe_path \u003c- tempfile(fileext = \".pdf\")\n grDevices::pdf(probe_path, width = width_in, height = height_in, useDingbats = FALSE)\n device_open \u003c- TRUE\n on.exit({\n if (device_open) {\n grDevices::dev.off()\n }\n unlink(probe_path)\n }, add = TRUE)\n grid::grid.newpage()\n grid::grid.draw(grob)\n grid::grid.force()\n\n widths_pt \u003c- grid::convertWidth(grob$widths, \"pt\", valueOnly = TRUE)\n heights_pt \u003c- grid::convertHeight(grob$heights, \"pt\", valueOnly = TRUE)\n if (any(!is.finite(widths_pt)) || any(!is.finite(heights_pt))) {\n stop(\"Patchwork panel dimensions could not be converted to physical points\", call. = FALSE)\n }\n x_edges \u003c- c(0, cumsum(widths_pt))\n top_edges \u003c- c(0, cumsum(heights_pt))\n total_height_pt \u003c- sum(heights_pt)\n\n panels \u003c- lapply(seq_len(nrow(panel_rows)), function(index) {\n row \u003c- panel_rows[index, ]\n left \u003c- x_edges[row$l]\n right \u003c- x_edges[row$r + 1]\n top \u003c- total_height_pt - top_edges[row$t]\n bottom \u003c- total_height_pt - top_edges[row$b + 1]\n list(\n id = panel_ids[index],\n bbox_pt = unname(c(left, bottom, right, top)),\n grid_id = \"patchwork-grid-1\",\n row_start = as.integer(row$t - 1),\n row_stop = as.integer(row$b),\n col_start = as.integer(row$l - 1),\n col_stop = as.integer(row$r)\n )\n })\n\n manifest \u003c- list(\n schema_version = 1L,\n backend = \"r-patchwork\",\n figure = list(width_pt = width_in * 72, height_pt = height_in * 72),\n panels = panels,\n exemptions = exemptions\n )\n normalized_rows \u003c- .nature_alignment_groups(row_groups)\n normalized_columns \u003c- .nature_alignment_groups(column_groups)\n if (!is.null(normalized_rows)) {\n manifest$row_groups \u003c- normalized_rows\n }\n if (!is.null(normalized_columns)) {\n manifest$column_groups \u003c- normalized_columns\n }\n\n dir.create(dirname(manifest_path), recursive = TRUE, showWarnings = FALSE)\n temporary_path \u003c- tempfile(pattern = \".panel-layout-\", tmpdir = dirname(manifest_path), fileext = \".json\")\n jsonlite::write_json(manifest, temporary_path, auto_unbox = TRUE, pretty = TRUE, digits = NA)\n if (!file.rename(temporary_path, manifest_path)) {\n unlink(temporary_path)\n stop(\"Could not activate the patchwork alignment manifest atomically\", call. = FALSE)\n }\n\n grDevices::dev.off()\n device_open \u003c- FALSE\n unlink(probe_path)\n invisible(manifest)\n}\n\nrequire_patchwork_panel_alignment \u003c- function(\n plot,\n manifest_path,\n report_path,\n width_in,\n height_in,\n panel_ids = NULL,\n row_groups = NULL,\n column_groups = NULL,\n exemptions = list(),\n audit_script = \"skills/nature-figure/scripts/audit_panel_alignment.py\",\n python = Sys.which(\"python\"),\n overlay_svg = NULL,\n tolerance_pt = 1.5,\n gutter_tolerance_pt = 1.5,\n strict = TRUE\n) {\n if (!nzchar(python)) {\n stop(\"Python is required for the backend-neutral panel-alignment JSON audit\", call. = FALSE)\n }\n if (!file.exists(audit_script)) {\n stop(sprintf(\"Panel-alignment audit script not found: %s\", audit_script), call. = FALSE)\n }\n write_patchwork_panel_layout(\n plot = plot,\n manifest_path = manifest_path,\n width_in = width_in,\n height_in = height_in,\n panel_ids = panel_ids,\n row_groups = row_groups,\n column_groups = column_groups,\n exemptions = exemptions\n )\n\n args \u003c- c(\n shQuote(audit_script),\n shQuote(manifest_path),\n \"--json-out\", shQuote(report_path),\n \"--tolerance-pt\", format(tolerance_pt, scientific = FALSE),\n \"--gutter-tolerance-pt\", format(gutter_tolerance_pt, scientific = FALSE)\n )\n if (!is.null(overlay_svg)) {\n args \u003c- c(args, \"--overlay-svg\", shQuote(overlay_svg))\n }\n if (isTRUE(strict)) {\n args \u003c- c(args, \"--strict\")\n }\n output \u003c- suppressWarnings(system2(python, args = args, stdout = TRUE, stderr = TRUE))\n status \u003c- attr(output, \"status\")\n if (is.null(status)) {\n status \u003c- 0L\n }\n if (length(output)) {\n message(paste(output, collapse = \"\\n\"))\n }\n if (status != 0L) {\n stop(\n sprintf(\"Panel alignment gate failed with exit code %d; inspect %s\", status, report_path),\n call. = FALSE\n )\n }\n invisible(jsonlite::read_json(report_path, simplifyVector = TRUE))\n}\n","content_type":"text/plain; charset=utf-8","language":"r","size":6233,"content_sha256":"aeb2db8355e2026e2f2640d25bf18edfa57fb0165ec2c7811f97bb75ba53d26b"},{"filename":"scripts/plot_templates.py","content":"#!/usr/bin/env python3\n\"\"\"Validated Python templates for five common manuscript-figure families.\n\nSubcommands: volcano, roc, dotplot, marginal, and paired. Production runs\nrequire a CSV input. Simulated data is available only through the explicit\n--demo flag and is marked as such in the generated QA record.\n\nThe template families and adaptation safeguards were informed by the\nApache-2.0 academic-figure-skill asset collection. This implementation is\nportable, path-independent, and designed for the nature-figure contract.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport csv\nimport json\nimport math\nimport sys\nfrom pathlib import Path\nfrom typing import Any, Iterable\n\n\nDEFAULT_WIDTH_MM = 183.0\nDEFAULT_HEIGHT_MM = 120.0\nMM_PER_INCH = 25.4\nPALETTE = [\"#2166AC\", \"#B2182B\", \"#1B7837\", \"#F1A340\", \"#762A83\", \"#666666\"]\nDEPENDENCY_ERROR: Exception | None = None\n\ntry:\n import numpy as np\n import matplotlib as mpl\n\n mpl.use(\"Agg\")\n import matplotlib.pyplot as plt\n from matplotlib.colors import LinearSegmentedColormap\n from matplotlib.lines import Line2D\nexcept ImportError as exc: # Keep --help usable in a minimal environment.\n DEPENDENCY_ERROR = exc\n\n\ndef require_dependencies() -> None:\n if DEPENDENCY_ERROR is not None:\n raise RuntimeError(\n \"plot_templates.py requires numpy and matplotlib in the selected Python environment\"\n ) from DEPENDENCY_ERROR\n\n\ndef configure_style() -> None:\n mpl.rcParams.update(\n {\n \"font.family\": \"sans-serif\",\n \"font.sans-serif\": [\"Arial\", \"Helvetica\", \"DejaVu Sans\", \"Liberation Sans\"],\n \"font.size\": 7,\n \"axes.labelsize\": 7,\n \"axes.titlesize\": 8,\n \"xtick.labelsize\": 6,\n \"ytick.labelsize\": 6,\n \"legend.fontsize\": 6,\n \"axes.spines.top\": False,\n \"axes.spines.right\": False,\n \"axes.linewidth\": 0.6,\n \"xtick.direction\": \"out\",\n \"ytick.direction\": \"out\",\n \"xtick.major.width\": 0.6,\n \"ytick.major.width\": 0.6,\n \"legend.frameon\": False,\n \"svg.fonttype\": \"none\",\n \"pdf.fonttype\": 42,\n }\n )\n\n\ndef normalize_prefix(raw: Path) -> Path:\n if raw.suffix.lower() in {\".svg\", \".pdf\", \".tif\", \".tiff\", \".png\"}:\n raw = raw.with_suffix(\"\")\n raw.parent.mkdir(parents=True, exist_ok=True)\n return raw\n\n\ndef with_extension(prefix: Path, extension: str) -> Path:\n \"\"\"Append ``extension`` to the prefix; ``Path.with_suffix`` would clip a dotted stem like ``Fig.2``.\"\"\"\n return prefix.with_name(prefix.name + extension)\n\n\ndef save_bundle(fig: Any, output: Path) -> dict[str, str]:\n output = normalize_prefix(output)\n paths = {\n \"svg\": str(with_extension(output, \".svg\")),\n \"pdf\": str(with_extension(output, \".pdf\")),\n \"tiff\": str(with_extension(output, \".tiff\")),\n }\n fig.savefig(paths[\"svg\"], bbox_inches=\"tight\", facecolor=\"white\")\n fig.savefig(paths[\"pdf\"], bbox_inches=\"tight\", facecolor=\"white\")\n fig.savefig(paths[\"tiff\"], dpi=600, bbox_inches=\"tight\", facecolor=\"white\")\n plt.close(fig)\n return paths\n\n\ndef write_qa(output: Path, qa: dict[str, Any]) -> Path:\n path = with_extension(normalize_prefix(output), \".qa.json\")\n path.write_text(json.dumps(qa, indent=2, ensure_ascii=False) + \"\\n\", encoding=\"utf-8\")\n return path\n\n\ndef read_csv_rows(path: Path) -> tuple[list[dict[str, Any]], list[str]]:\n with path.open(\"r\", encoding=\"utf-8-sig\", newline=\"\") as handle:\n reader = csv.DictReader(handle)\n if reader.fieldnames is None:\n raise ValueError(f\"CSV has no header: {path}\")\n rows = [dict(row) for row in reader]\n return rows, list(reader.fieldnames)\n\n\ndef require_columns(fieldnames: Iterable[str], required: Iterable[str]) -> None:\n available = set(fieldnames)\n missing = [name for name in required if name not in available]\n if missing:\n raise ValueError(f\"missing required CSV columns: {', '.join(missing)}\")\n\n\ndef coerce_numeric_rows(\n rows: list[dict[str, Any]],\n columns: Iterable[str],\n drop_incomplete: bool,\n) -> tuple[list[dict[str, Any]], list[int]]:\n columns = list(columns)\n valid: list[dict[str, Any]] = []\n invalid: list[int] = []\n for row_number, row in enumerate(rows, start=2):\n converted = dict(row)\n try:\n for column in columns:\n value = float(row[column])\n if not math.isfinite(value):\n raise ValueError\n converted[column] = value\n except (KeyError, TypeError, ValueError):\n invalid.append(row_number)\n continue\n valid.append(converted)\n if invalid and not drop_incomplete:\n shown = \", \".join(str(value) for value in invalid[:10])\n suffix = \" ...\" if len(invalid) > 10 else \"\"\n raise ValueError(\n f\"non-finite or missing numeric values at CSV rows {shown}{suffix}; \"\n \"fix the data or rerun with explicit --drop-incomplete\"\n )\n if not valid:\n raise ValueError(\"no valid observations remain\")\n return valid, invalid\n\n\ndef first_seen(values: Iterable[str]) -> list[str]:\n result: list[str] = []\n seen: set[str] = set()\n for value in values:\n if value not in seen:\n seen.add(value)\n result.append(value)\n return result\n\n\ndef parse_order(raw: str | None, observed: Iterable[str], label: str) -> list[str]:\n observed_order = first_seen(str(value) for value in observed)\n if not raw:\n return observed_order\n requested = [value.strip() for value in raw.split(\",\") if value.strip()]\n missing = [value for value in observed_order if value not in requested]\n extra = [value for value in requested if value not in observed_order]\n if missing or extra:\n details = []\n if missing:\n details.append(f\"missing observed {label}: {missing}\")\n if extra:\n details.append(f\"unknown {label}: {extra}\")\n raise ValueError(\"; \".join(details))\n return requested\n\n\ndef nice_reference_values(maximum: float) -> list[float]:\n if maximum \u003c= 0:\n return []\n magnitude = 10 ** math.floor(math.log10(maximum))\n quantum = magnitude / 2\n values = [round((maximum * fraction) / quantum) * quantum for fraction in (0.25, 0.5, 1.0)]\n return sorted({value for value in values if value > 0})\n\n\ndef base_qa(args: argparse.Namespace, kind: str, rows_input: int) -> dict[str, Any]:\n return {\n \"template\": kind,\n \"backend\": \"python\",\n \"demo\": bool(args.demo),\n \"input\": \"\u003cexplicit-demo-data>\" if args.demo else args.input.name,\n \"output_prefix\": normalize_prefix(args.output).name,\n \"rows_input\": rows_input,\n \"rows_plotted\": rows_input,\n \"excluded_rows\": 0,\n \"excluded_entities\": 0,\n \"mapping\": {},\n \"parameters\": {},\n \"notes\": [\n \"All supplied observations are used unless excluded through explicit --drop-incomplete.\",\n \"The QA record stores input basenames rather than private absolute paths.\",\n ],\n }\n\n\ndef load_rows(args: argparse.Namespace, kind: str) -> tuple[list[dict[str, Any]], list[str]]:\n if args.demo:\n rows = demo_rows(kind)\n return rows, list(rows[0])\n if args.input is None:\n raise ValueError(\"production runs require --input CSV; use --demo only for an explicit example\")\n return read_csv_rows(args.input)\n\n\ndef demo_rows(kind: str) -> list[dict[str, Any]]:\n rng = np.random.default_rng(20260715)\n if kind == \"volcano\":\n rows = []\n for index in range(1200):\n effect = float(rng.normal(0, 0.65))\n pvalue = float(rng.uniform(0.02, 1.0))\n if index \u003c 55:\n effect = float(rng.choice([-1, 1]) * rng.uniform(1.2, 3.2))\n pvalue = float(10 ** (-rng.uniform(2, 9)))\n rows.append({\"gene\": f\"Gene_{index + 1}\", \"log2fc\": effect, \"padj\": pvalue})\n return rows\n if kind == \"roc\":\n rows = []\n for fpr in np.linspace(0, 1, 101):\n rows.append(\n {\n \"fpr\": float(fpr),\n \"model_a\": float(np.clip(fpr ** 0.42, 0, 1)),\n \"model_b\": float(np.clip(fpr ** 0.58, 0, 1)),\n \"model_c\": float(np.clip(fpr ** 0.72, 0, 1)),\n }\n )\n return rows\n if kind == \"dotplot\":\n rows = []\n row_names = [\"VCT\", \"EVT\", \"SCT\", \"FB\", \"T\", \"dNK\"]\n column_names = [\"TP63\", \"HLA-G\", \"ERVW-1\", \"VIM\", \"ACTA2\", \"CD3D\", \"GNLY\", \"NKG7\"]\n for row_index, row_name in enumerate(row_names):\n for col_index, column_name in enumerate(column_names):\n distance = abs((row_index * 1.3) - (col_index * 0.7))\n rows.append(\n {\n \"cell_type\": row_name,\n \"gene\": column_name,\n \"pct_exp\": float(np.clip(70 - 14 * distance + rng.normal(0, 5), 1, 90)),\n \"avg_exp_scaled\": float(np.clip(1 - distance / 6 + rng.normal(0, 0.08), 0, 1)),\n }\n )\n return rows\n if kind == \"marginal\":\n rows = []\n for group_index, group in enumerate((\"Control\", \"Treatment A\", \"Treatment B\")):\n x = rng.normal(group_index * 0.7, 0.85, 220)\n y = 0.65 * x + rng.normal(group_index * 0.35, 0.65, 220)\n rows.extend({\"x\": float(a), \"y\": float(b), \"group\": group} for a, b in zip(x, y))\n return rows\n if kind == \"paired\":\n rows = []\n for index in range(28):\n before = float(rng.normal(1.0, 0.18))\n after = float(before + rng.normal(0.20, 0.10))\n rows.extend(\n [\n {\"subject\": f\"S{index + 1:02d}\", \"condition\": \"Before\", \"value\": before},\n {\"subject\": f\"S{index + 1:02d}\", \"condition\": \"After\", \"value\": after},\n ]\n )\n return rows\n raise ValueError(f\"unknown demo kind: {kind}\")\n\n\ndef plot_volcano(args: argparse.Namespace) -> tuple[dict[str, str], dict[str, Any]]:\n rows, fields = load_rows(args, \"volcano\")\n required = [args.gene_col, args.effect_col, args.p_col]\n require_columns(fields, required)\n rows, invalid = coerce_numeric_rows(rows, [args.effect_col, args.p_col], args.drop_incomplete)\n bad_p = [index for index, row in enumerate(rows, start=1) if not 0 \u003c row[args.p_col] \u003c= 1]\n if bad_p:\n raise ValueError(\"adjusted p values must be strictly positive and no greater than 1\")\n\n effects = np.asarray([row[args.effect_col] for row in rows], dtype=float)\n pvalues = np.asarray([row[args.p_col] for row in rows], dtype=float)\n genes = [str(row[args.gene_col]) for row in rows]\n scores = -np.log10(pvalues)\n up = (pvalues \u003c args.p_threshold) & (effects >= args.effect_threshold)\n down = (pvalues \u003c args.p_threshold) & (effects \u003c= -args.effect_threshold)\n neutral = ~(up | down)\n\n fig, ax = plt.subplots(figsize=(args.width_mm / MM_PER_INCH, args.height_mm / MM_PER_INCH))\n rasterized = len(rows) > 50000\n ax.scatter(effects[neutral], scores[neutral], s=5, color=\"#B3B3B3\", alpha=0.45, edgecolors=\"none\", rasterized=rasterized, label=f\"Not significant ({neutral.sum()})\")\n ax.scatter(effects[down], scores[down], s=7, color=\"#2166AC\", alpha=0.72, edgecolors=\"none\", rasterized=rasterized, label=f\"Down ({down.sum()})\")\n ax.scatter(effects[up], scores[up], s=7, color=\"#B2182B\", alpha=0.72, edgecolors=\"none\", rasterized=rasterized, label=f\"Up ({up.sum()})\")\n ax.axhline(-math.log10(args.p_threshold), color=\"#666666\", linestyle=\"--\", linewidth=0.6)\n ax.axvline(-args.effect_threshold, color=\"#666666\", linestyle=\"--\", linewidth=0.6)\n ax.axvline(args.effect_threshold, color=\"#666666\", linestyle=\"--\", linewidth=0.6)\n significant = np.flatnonzero(up | down)\n if args.top_labels > 0 and len(significant):\n selected = significant[np.argsort(pvalues[significant])[: args.top_labels]]\n for is_positive in (False, True):\n side = [index for index in selected if (effects[index] >= 0) == is_positive]\n last_label_y = -math.inf\n for index in sorted(side, key=lambda value: scores[value]):\n label_y = max(float(scores[index]) + 0.12, last_label_y + 0.30)\n last_label_y = label_y\n direction = 1 if is_positive else -1\n ax.annotate(\n genes[index],\n (effects[index], scores[index]),\n xytext=(effects[index] + 0.07 * direction, label_y),\n textcoords=\"data\",\n ha=\"left\" if is_positive else \"right\",\n va=\"bottom\",\n fontsize=5,\n color=\"#333333\",\n arrowprops={\"arrowstyle\": \"-\", \"color\": \"#777777\", \"linewidth\": 0.3},\n )\n ax.set_xlabel(args.effect_label)\n # Keep the subscript as Unicode so the rendered glyph does not shrink below\n # the journal's 5 pt floor when the surrounding label is already compact.\n ax.set_ylabel(\"−log₁₀(adjusted p value)\")\n if args.title:\n ax.set_title(args.title)\n ax.legend(loc=\"upper center\", bbox_to_anchor=(0.5, -0.19), ncol=3)\n paths = save_bundle(fig, args.output)\n\n qa = base_qa(args, \"volcano\", len(rows) + len(invalid))\n qa.update({\"rows_plotted\": len(rows), \"excluded_rows\": len(invalid)})\n qa[\"mapping\"] = {\"gene\": args.gene_col, \"effect\": args.effect_col, \"adjusted_p\": args.p_col}\n qa[\"parameters\"] = {\"p_threshold\": args.p_threshold, \"effect_threshold\": args.effect_threshold, \"top_labels\": args.top_labels, \"rasterized_marks\": rasterized, \"category_counts\": {\"up\": int(up.sum()), \"down\": int(down.sum()), \"neutral\": int(neutral.sum())}}\n return paths, qa\n\n\ndef plot_roc(args: argparse.Namespace) -> tuple[dict[str, str], dict[str, Any]]:\n rows, fields = load_rows(args, \"roc\")\n tpr_columns = [value.strip() for value in args.tpr_cols.split(\",\") if value.strip()] if args.tpr_cols else [field for field in fields if field != args.fpr_col]\n if not tpr_columns:\n raise ValueError(\"no TPR columns selected\")\n require_columns(fields, [args.fpr_col, *tpr_columns])\n rows, invalid = coerce_numeric_rows(rows, [args.fpr_col, *tpr_columns], args.drop_incomplete)\n for row in rows:\n values = [row[args.fpr_col], *(row[column] for column in tpr_columns)]\n if any(value \u003c 0 or value > 1 for value in values):\n raise ValueError(\"FPR and TPR values must stay within [0, 1]\")\n\n fpr = np.asarray([row[args.fpr_col] for row in rows], dtype=float)\n order = np.argsort(fpr, kind=\"stable\")\n sorted_input = bool(np.all(order == np.arange(len(fpr))))\n fig, ax = plt.subplots(figsize=(args.width_mm / MM_PER_INCH, args.height_mm / MM_PER_INCH))\n aucs: dict[str, float] = {}\n for index, column in enumerate(tpr_columns):\n tpr = np.asarray([row[column] for row in rows], dtype=float)[order]\n auc = float(np.trapezoid(tpr, fpr[order]))\n aucs[column] = auc\n ax.plot(fpr[order], tpr, color=PALETTE[index % len(PALETTE)], linewidth=1.3, label=f\"{column} (AUC={auc:.3f})\")\n ax.plot([0, 1], [0, 1], color=\"#888888\", linestyle=\"--\", linewidth=0.7, label=\"Chance\")\n ax.set(xlim=(0, 1), ylim=(0, 1), xlabel=\"False-positive rate\", ylabel=\"True-positive rate\")\n ax.set_aspect(\"equal\", adjustable=\"box\")\n if args.title:\n ax.set_title(args.title)\n ax.legend(loc=\"lower right\")\n paths = save_bundle(fig, args.output)\n\n qa = base_qa(args, \"roc\", len(rows) + len(invalid))\n qa.update({\"rows_plotted\": len(rows), \"excluded_rows\": len(invalid)})\n qa[\"mapping\"] = {\"fpr\": args.fpr_col, \"tpr_series\": tpr_columns}\n qa[\"parameters\"] = {\"auc_method\": \"trapezoidal\", \"auc\": aucs, \"input_already_sorted_by_fpr\": sorted_input}\n return paths, qa\n\n\ndef plot_dotplot(args: argparse.Namespace) -> tuple[dict[str, str], dict[str, Any]]:\n rows, fields = load_rows(args, \"dotplot\")\n required = [args.row_col, args.column_col, args.size_col, args.color_col]\n require_columns(fields, required)\n rows, invalid = coerce_numeric_rows(rows, [args.size_col, args.color_col], args.drop_incomplete)\n if any(row[args.size_col] \u003c 0 for row in rows):\n raise ValueError(\"dot size values must be non-negative\")\n row_order = parse_order(args.row_order, (str(row[args.row_col]) for row in rows), \"row categories\")\n column_order = parse_order(args.column_order, (str(row[args.column_col]) for row in rows), \"column categories\")\n row_map = {value: index for index, value in enumerate(row_order)}\n column_map = {value: index for index, value in enumerate(column_order)}\n\n size_values = np.asarray([row[args.size_col] for row in rows], dtype=float)\n color_values = np.asarray([row[args.color_col] for row in rows], dtype=float)\n size_max = float(size_values.max())\n sizes = np.full_like(size_values, 30.0) if size_max == 0 else 12 + 160 * np.sqrt(size_values / size_max)\n color_min = float(color_values.min())\n color_max = float(color_values.max())\n color_norm = np.full_like(color_values, 0.5) if color_max == color_min else (color_values - color_min) / (color_max - color_min)\n xs = np.asarray([column_map[str(row[args.column_col])] for row in rows])\n ys = np.asarray([row_map[str(row[args.row_col])] for row in rows])\n cmap = LinearSegmentedColormap.from_list(\"nature_expression\", [\"#2166AC\", \"#F7F7F7\", \"#B2182B\"])\n\n fig, ax = plt.subplots(figsize=(args.width_mm / MM_PER_INCH, args.height_mm / MM_PER_INCH))\n scatter = ax.scatter(xs, ys, s=sizes, c=color_norm, cmap=cmap, vmin=0, vmax=1, edgecolors=\"#666666\", linewidths=0.35)\n ax.set_xticks(\n range(len(column_order)),\n column_order,\n rotation=90,\n rotation_mode=\"anchor\",\n ha=\"right\",\n )\n ax.set_yticks(range(len(row_order)), row_order)\n ax.set_xlim(-0.7, len(column_order) - 0.3)\n ax.set_ylim(-0.7, len(row_order) - 0.3)\n ax.invert_yaxis()\n ax.spines[\"top\"].set_visible(True)\n ax.spines[\"right\"].set_visible(True)\n ax.grid(color=\"#E6E6E6\", linewidth=0.35)\n ax.set_axisbelow(True)\n colorbar = fig.colorbar(scatter, ax=ax, fraction=0.025, pad=0.02)\n colorbar.set_label(args.color_label)\n reference_values = nice_reference_values(size_max)\n handles = [Line2D([], [], marker=\"o\", linestyle=\"\", markerfacecolor=\"#999999\", markeredgecolor=\"#666666\", markersize=math.sqrt(12 + 160 * math.sqrt(value / size_max))) for value in reference_values]\n if handles:\n ax.legend(handles, [f\"{value:g}\" for value in reference_values], title=args.size_label, bbox_to_anchor=(1.18, 1), loc=\"upper left\")\n if args.title:\n ax.set_title(args.title)\n paths = save_bundle(fig, args.output)\n\n qa = base_qa(args, \"dotplot\", len(rows) + len(invalid))\n qa.update({\"rows_plotted\": len(rows), \"excluded_rows\": len(invalid)})\n qa[\"mapping\"] = {\"row_category\": args.row_col, \"column_category\": args.column_col, \"dot_size\": args.size_col, \"dot_color\": args.color_col}\n qa[\"parameters\"] = {\"row_order\": row_order, \"column_order\": column_order, \"size_range\": [float(size_values.min()), size_max], \"color_range\": [color_min, color_max]}\n return paths, qa\n\n\ndef plot_marginal(args: argparse.Namespace) -> tuple[dict[str, str], dict[str, Any]]:\n rows, fields = load_rows(args, \"marginal\")\n required = [args.x_col, args.y_col] + ([args.group_col] if args.group_col else [])\n require_columns(fields, required)\n rows, invalid = coerce_numeric_rows(rows, [args.x_col, args.y_col], args.drop_incomplete)\n groups = [str(row[args.group_col]) if args.group_col else \"All observations\" for row in rows]\n group_order = parse_order(args.group_order, groups, \"groups\")\n\n fig = plt.figure(figsize=(args.width_mm / MM_PER_INCH, args.height_mm / MM_PER_INCH))\n grid = fig.add_gridspec(4, 4, hspace=0.05, wspace=0.05)\n ax_joint = fig.add_subplot(grid[1:, :3])\n ax_top = fig.add_subplot(grid[0, :3], sharex=ax_joint)\n ax_right = fig.add_subplot(grid[1:, 3], sharey=ax_joint)\n counts: dict[str, int] = {}\n for index, group in enumerate(group_order):\n mask = np.asarray([value == group for value in groups])\n x = np.asarray([row[args.x_col] for row in rows], dtype=float)[mask]\n y = np.asarray([row[args.y_col] for row in rows], dtype=float)[mask]\n counts[group] = len(x)\n color = PALETTE[index % len(PALETTE)]\n ax_joint.scatter(x, y, s=8, alpha=0.42, color=color, edgecolors=\"none\", rasterized=len(rows) > 50000, label=f\"{group} (n={len(x)})\")\n ax_top.hist(x, bins=args.bins, density=True, histtype=\"stepfilled\", alpha=0.20, color=color)\n ax_top.hist(x, bins=args.bins, density=True, histtype=\"step\", linewidth=0.8, color=color)\n ax_right.hist(y, bins=args.bins, density=True, orientation=\"horizontal\", histtype=\"stepfilled\", alpha=0.20, color=color)\n ax_right.hist(y, bins=args.bins, density=True, orientation=\"horizontal\", histtype=\"step\", linewidth=0.8, color=color)\n ax_joint.set_xlabel(args.x_label or args.x_col)\n ax_joint.set_ylabel(args.y_label or args.y_col)\n ax_joint.legend(loc=\"best\")\n ax_top.tick_params(labelbottom=False)\n ax_right.tick_params(labelleft=False)\n ax_top.set_ylabel(\"Density\")\n ax_right.set_xlabel(\"Density\")\n if args.title:\n ax_top.set_title(args.title)\n paths = save_bundle(fig, args.output)\n\n qa = base_qa(args, \"marginal\", len(rows) + len(invalid))\n qa.update({\"rows_plotted\": len(rows), \"excluded_rows\": len(invalid)})\n qa[\"mapping\"] = {\"x\": args.x_col, \"y\": args.y_col, \"group\": args.group_col}\n qa[\"parameters\"] = {\"group_order\": group_order, \"group_counts\": counts, \"histogram_bins\": args.bins, \"rasterized_marks\": len(rows) > 50000}\n return paths, qa\n\n\ndef plot_paired(args: argparse.Namespace) -> tuple[dict[str, str], dict[str, Any]]:\n rows, fields = load_rows(args, \"paired\")\n required = [args.id_col, args.condition_col, args.value_col]\n require_columns(fields, required)\n rows, invalid = coerce_numeric_rows(rows, [args.value_col], args.drop_incomplete)\n condition_order = parse_order(args.condition_order, (str(row[args.condition_col]) for row in rows), \"conditions\")\n if len(condition_order) != 2:\n raise ValueError(f\"paired template requires exactly two conditions, found {len(condition_order)}\")\n\n subjects: dict[str, dict[str, list[float]]] = {}\n for row in rows:\n subject = str(row[args.id_col])\n condition = str(row[args.condition_col])\n subjects.setdefault(subject, {}).setdefault(condition, []).append(float(row[args.value_col]))\n duplicates = [subject for subject, values in subjects.items() if any(len(values.get(condition, [])) > 1 for condition in condition_order)]\n if duplicates:\n raise ValueError(\"multiple values per subject-condition pair found; aggregate only with an explicit scientific rule before plotting\")\n incomplete = [subject for subject, values in subjects.items() if any(condition not in values for condition in condition_order)]\n if incomplete and not args.drop_incomplete:\n raise ValueError(f\"{len(incomplete)} incomplete subject pairs found; fix the data or rerun with explicit --drop-incomplete\")\n complete = [subject for subject in subjects if subject not in incomplete]\n if not complete:\n raise ValueError(\"no complete subject pairs remain\")\n values_a = np.asarray([subjects[subject][condition_order[0]][0] for subject in complete])\n values_b = np.asarray([subjects[subject][condition_order[1]][0] for subject in complete])\n\n fig, ax = plt.subplots(figsize=(args.width_mm / MM_PER_INCH, args.height_mm / MM_PER_INCH))\n for value_a, value_b in zip(values_a, values_b):\n ax.plot([0, 1], [value_a, value_b], color=\"#B0B0B0\", linewidth=0.55, alpha=0.65, zorder=1)\n boxes = ax.boxplot([values_a, values_b], positions=[0, 1], widths=0.34, patch_artist=True, showfliers=False, medianprops={\"color\": \"#222222\", \"linewidth\": 1.0}, whiskerprops={\"linewidth\": 0.7}, capprops={\"linewidth\": 0.7})\n for patch, color in zip(boxes[\"boxes\"], (\"#9ECAE1\", \"#FC9272\")):\n patch.set(facecolor=color, edgecolor=\"#555555\", alpha=0.55, linewidth=0.7)\n ax.scatter(np.zeros_like(values_a), values_a, s=13, color=\"#2166AC\", edgecolors=\"white\", linewidths=0.35, zorder=3)\n ax.scatter(np.ones_like(values_b), values_b, s=13, color=\"#B2182B\", edgecolors=\"white\", linewidths=0.35, zorder=3)\n ax.set_xticks([0, 1], condition_order)\n ax.set_ylabel(args.value_label or args.value_col)\n if args.title:\n ax.set_title(args.title)\n paths = save_bundle(fig, args.output)\n\n qa = base_qa(args, \"paired\", len(rows) + len(invalid))\n qa.update({\"rows_plotted\": len(complete) * 2, \"excluded_rows\": len(invalid), \"excluded_entities\": len(incomplete)})\n qa[\"mapping\"] = {\"pair_id\": args.id_col, \"condition\": args.condition_col, \"value\": args.value_col}\n qa[\"parameters\"] = {\"condition_order\": condition_order, \"complete_pairs\": len(complete), \"incomplete_pairs_excluded\": len(incomplete), \"statistical_test\": None}\n qa[\"notes\"].append(\"No inferential test is computed; add one only after choosing a justified paired analysis.\")\n return paths, qa\n\n\ndef add_common_arguments(parser: argparse.ArgumentParser, width_mm: float, height_mm: float) -> None:\n source = parser.add_mutually_exclusive_group()\n source.add_argument(\"--input\", type=Path, help=\"production CSV input\")\n source.add_argument(\"--demo\", action=\"store_true\", help=\"render explicit deterministic demo data\")\n parser.add_argument(\"--output\", type=Path, required=True, help=\"output prefix for SVG/PDF/TIFF/QA JSON\")\n parser.add_argument(\"--width-mm\", type=float, default=width_mm)\n parser.add_argument(\"--height-mm\", type=float, default=height_mm)\n parser.add_argument(\"--title\")\n parser.add_argument(\"--drop-incomplete\", action=\"store_true\", help=\"explicitly exclude rows with missing/non-finite required values and record counts\")\n\n\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(description=__doc__)\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n volcano = subparsers.add_parser(\"volcano\", help=\"effect size versus adjusted p value\")\n add_common_arguments(volcano, 89.0, 82.0)\n volcano.add_argument(\"--gene-col\", default=\"gene\")\n volcano.add_argument(\"--effect-col\", default=\"log2fc\")\n volcano.add_argument(\"--p-col\", default=\"padj\")\n volcano.add_argument(\"--effect-label\", default=\"log₂(fold change)\")\n volcano.add_argument(\"--p-threshold\", type=float, default=0.05)\n volcano.add_argument(\"--effect-threshold\", type=float, default=1.0)\n volcano.add_argument(\"--top-labels\", type=int, default=10)\n volcano.set_defaults(plotter=plot_volcano)\n\n roc = subparsers.add_parser(\"roc\", help=\"one FPR column and one or more TPR columns\")\n add_common_arguments(roc, 89.0, 89.0)\n roc.add_argument(\"--fpr-col\", default=\"fpr\")\n roc.add_argument(\"--tpr-cols\", help=\"comma-separated TPR columns; defaults to every non-FPR column\")\n roc.set_defaults(plotter=plot_roc)\n\n dotplot = subparsers.add_parser(\"dotplot\", help=\"marker-gene or other size/color matrix dot plot\")\n add_common_arguments(dotplot, 183.0, 105.0)\n dotplot.add_argument(\"--row-col\", default=\"cell_type\")\n dotplot.add_argument(\"--column-col\", default=\"gene\")\n dotplot.add_argument(\"--size-col\", default=\"pct_exp\")\n dotplot.add_argument(\"--color-col\", default=\"avg_exp_scaled\")\n dotplot.add_argument(\"--row-order\", help=\"comma-separated complete row order\")\n dotplot.add_argument(\"--column-order\", help=\"comma-separated complete column order\")\n dotplot.add_argument(\"--size-label\", default=\"Fraction (%)\")\n dotplot.add_argument(\"--color-label\", default=\"Mean expression\")\n dotplot.set_defaults(plotter=plot_dotplot)\n\n marginal = subparsers.add_parser(\"marginal\", help=\"2D scatter with marginal distributions\")\n add_common_arguments(marginal, 120.0, 105.0)\n marginal.add_argument(\"--x-col\", default=\"x\")\n marginal.add_argument(\"--y-col\", default=\"y\")\n marginal.add_argument(\"--group-col\", default=\"group\")\n marginal.add_argument(\"--group-order\", help=\"comma-separated complete group order\")\n marginal.add_argument(\"--x-label\")\n marginal.add_argument(\"--y-label\")\n marginal.add_argument(\"--bins\", type=int, default=24)\n marginal.set_defaults(plotter=plot_marginal)\n\n paired = subparsers.add_parser(\"paired\", help=\"paired box-and-point plot for exactly two conditions\")\n add_common_arguments(paired, 89.0, 86.0)\n paired.add_argument(\"--id-col\", default=\"subject\")\n paired.add_argument(\"--condition-col\", default=\"condition\")\n paired.add_argument(\"--value-col\", default=\"value\")\n paired.add_argument(\"--condition-order\", help=\"comma-separated two-condition order\")\n paired.add_argument(\"--value-label\")\n paired.set_defaults(plotter=plot_paired)\n return parser\n\n\ndef main(argv: list[str] | None = None) -> int:\n args = build_parser().parse_args(argv)\n try:\n require_dependencies()\n configure_style()\n paths, qa = args.plotter(args)\n qa_path = write_qa(args.output, qa)\n except (OSError, RuntimeError, ValueError) as exc:\n print(f\"error: {exc}\", file=sys.stderr)\n return 2\n print(json.dumps({\"outputs\": paths, \"qa\": str(qa_path)}, indent=2, ensure_ascii=False))\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":29761,"content_sha256":"c5c042bc09464d58cc0363bc8542db946acb4c7efb53360a70386e1a6704691a"},{"filename":"scripts/validate_figure.py","content":"#!/usr/bin/env python3\n\"\"\"Static preflight for publication-figure Python and R source files.\n\nThe validator is intentionally dependency-free. It checks portable source-level\nrequirements before the selected backend renders the figure; it does not claim\nto validate statistics or replace visual inspection.\n\nThe rule structure incorporates and refactors ideas from the Apache-2.0\nacademic-figure-skill QA workflow without retaining its path-bound runners.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport ast\nimport json\nimport re\nimport sys\nfrom dataclasses import asdict, dataclass\nfrom pathlib import Path\nfrom typing import Callable, Iterable\n\n\nLEVEL_ORDER = {\"PASS\": 0, \"WARN\": 1, \"FAIL\": 2}\nPYTHON_SUFFIXES = {\".py\"}\nR_SUFFIXES = {\".r\", \".rmd\", \".qmd\"}\n\n\n@dataclass(frozen=True)\nclass Finding:\n check_id: str\n level: str\n message: str\n evidence: list[str]\n\n @property\n def passed(self) -> bool:\n return self.level == \"PASS\"\n\n\ndef finding(check_id: str, level: str, message: str, evidence: Iterable[str] = ()) -> Finding:\n if level not in LEVEL_ORDER:\n raise ValueError(f\"unknown finding level: {level}\")\n return Finding(check_id, level, message, list(evidence))\n\n\ndef detect_backend(path: Path, requested: str) -> str:\n if requested != \"auto\":\n return requested\n suffix = path.suffix.lower()\n if suffix in PYTHON_SUFFIXES:\n return \"python\"\n if suffix in R_SUFFIXES:\n return \"r\"\n raise ValueError(f\"cannot infer backend from {path.name}; pass --backend python or --backend r\")\n\n\ndef regex_hits(patterns: Iterable[str], source: str, flags: int = re.IGNORECASE) -> list[str]:\n hits: list[str] = []\n for pattern in patterns:\n match = re.search(pattern, source, flags)\n if match:\n hits.append(match.group(0).strip())\n return hits\n\n\ndef check_syntax(source: str, backend: str) -> Finding:\n if backend == \"python\":\n try:\n ast.parse(source)\n except SyntaxError as exc:\n location = f\"line {exc.lineno}\" if exc.lineno else \"unknown line\"\n return finding(\"SOURCE-SYNTAX\", \"FAIL\", f\"Python syntax error at {location}: {exc.msg}\")\n return finding(\"SOURCE-SYNTAX\", \"PASS\", \"Python source parses successfully\")\n\n pairs = {\"(\": \")\", \"[\": \"]\", \"{\": \"}\"}\n stack: list[str] = []\n quote: str | None = None\n escaped = False\n comment = False\n for char in source:\n if comment:\n if char == \"\\n\":\n comment = False\n continue\n if escaped:\n escaped = False\n continue\n if char == \"\\\\\":\n escaped = True\n continue\n if quote:\n if char == quote:\n quote = None\n continue\n if char == \"#\":\n comment = True\n elif char in {'\"', \"'\"}:\n quote = char\n elif char in pairs:\n stack.append(char)\n elif char in pairs.values():\n if not stack or pairs[stack.pop()] != char:\n return finding(\"SOURCE-SYNTAX\", \"FAIL\", \"R source has unbalanced brackets\")\n if stack or quote:\n return finding(\"SOURCE-SYNTAX\", \"FAIL\", \"R source has unbalanced brackets or quotes\")\n return finding(\n \"SOURCE-SYNTAX\",\n \"WARN\",\n \"Basic R delimiter check passed; run Rscript parse() when R is available\",\n )\n\n\ndef check_font_family(source: str, backend: str) -> Finding:\n families = regex_hits(\n [r\"Arial\", r\"Helvetica\", r\"Liberation Sans\", r\"sans-serif\", r\"base_family\\s*=\\s*['\\\"]sans['\\\"]\"],\n source,\n )\n if families:\n return finding(\"FONT-FAMILY\", \"PASS\", \"A publication-safe sans-serif family is configured\", families)\n label = \"matplotlib rcParams\" if backend == \"python\" else \"ggplot/theme or graphics device\"\n return finding(\"FONT-FAMILY\", \"FAIL\", f\"No explicit publication-safe font family found in {label}\")\n\n\ndef explicit_font_sizes(source: str) -> list[float]:\n patterns = [\n r\"(?:font\\.size|fontsize|labelsize|titlesize|legend\\.fontsize)\\s*['\\\"]?\\s*[:=]\\s*(\\d+(?:\\.\\d+)?)\",\n r\"base_size\\s*=\\s*(\\d+(?:\\.\\d+)?)\",\n r\"element_text\\s*\\([^)]*?size\\s*=\\s*(\\d+(?:\\.\\d+)?)\",\n ]\n values: list[float] = []\n for pattern in patterns:\n values.extend(float(value) for value in re.findall(pattern, source, re.IGNORECASE | re.DOTALL))\n return values\n\n\ndef check_font_sizes(source: str, _backend: str) -> Finding:\n sizes = explicit_font_sizes(source)\n if not sizes:\n return finding(\"FONT-SIZE\", \"WARN\", \"No explicit text sizes found; verify readability at final physical size\")\n minimum = min(sizes)\n if minimum \u003c 5:\n return finding(\"FONT-SIZE\", \"FAIL\", f\"Explicit text size falls below the 5 pt floor: {minimum:g} pt\")\n return finding(\"FONT-SIZE\", \"PASS\", f\"All detected text sizes are at least 5 pt (minimum {minimum:g} pt)\")\n\n\ndef check_mathtext_glyph_floor(source: str, backend: str) -> Finding:\n if backend != \"python\":\n return finding(\"FONT-GLYPH-FLOOR\", \"PASS\", \"Python mathtext superscript scaling is not applicable\")\n hits = regex_hits([r\"\\$[^$\\n]*[\\^_](?:\\{[^}]+\\}|[^$\\s])[^$\\n]*\\$\"], source, flags=0)\n if not hits:\n return finding(\"FONT-GLYPH-FLOOR\", \"PASS\", \"No mathtext superscript/subscript expression detected\")\n sizes = explicit_font_sizes(source)\n if not sizes:\n return finding(\n \"FONT-GLYPH-FLOOR\",\n \"WARN\",\n \"Mathtext superscript/subscript detected without an auditable parent font size; inspect the exported PDF\",\n hits,\n )\n minimum_parent = min(sizes)\n estimated_script = minimum_parent * 0.7\n if estimated_script \u003c 5:\n return finding(\n \"FONT-GLYPH-FLOOR\",\n \"WARN\",\n f\"Mathtext may shrink script glyphs below 5 pt (about {estimated_script:.2f} pt from a {minimum_parent:g} pt parent); use a Unicode glyph or audit PDF Tf operators\",\n hits,\n )\n return finding(\n \"FONT-GLYPH-FLOOR\",\n \"PASS\",\n f\"Detected mathtext parent sizes imply script glyphs of about {estimated_script:.2f} pt or larger; confirm in the PDF\",\n hits,\n )\n\n\ndef literal_legend_labels(source: str, backend: str) -> list[str]:\n if backend == \"python\":\n try:\n tree = ast.parse(source)\n except SyntaxError:\n return []\n labels: list[str] = []\n for node in ast.walk(tree):\n if not isinstance(node, ast.Call):\n continue\n call_name = \"\"\n if isinstance(node.func, ast.Attribute):\n call_name = node.func.attr\n elif isinstance(node.func, ast.Name):\n call_name = node.func.id\n label_calls = {\n \"plot\",\n \"bar\",\n \"barh\",\n \"scatter\",\n \"errorbar\",\n \"fill_between\",\n \"hist\",\n \"axhline\",\n \"axvline\",\n \"legend\",\n }\n for keyword in node.keywords:\n if (\n call_name in label_calls\n and keyword.arg == \"label\"\n and isinstance(keyword.value, ast.Constant)\n and isinstance(keyword.value.value, str)\n ):\n labels.append(keyword.value.value)\n elif call_name == \"legend\" and keyword.arg == \"labels\" and isinstance(keyword.value, (ast.List, ast.Tuple)):\n labels.extend(\n item.value\n for item in keyword.value.elts\n if isinstance(item, ast.Constant) and isinstance(item.value, str)\n )\n return labels\n\n labels: list[str] = []\n for content in re.findall(r\"labels\\s*=\\s*c\\(([^)]*)\\)\", source, re.IGNORECASE | re.DOTALL):\n labels.extend(re.findall(r\"['\\\"]([^'\\\"]+)['\\\"]\", content))\n return labels\n\n\ndef check_legend_label_case(source: str, backend: str) -> Finding:\n labels = [label for label in literal_legend_labels(source, backend) if label and label != \"_nolegend_\"]\n lower_initial = []\n for label in labels:\n first_alpha = re.search(r\"[A-Za-z]\", re.sub(r\"^\\s*\\+\\s*\", \"\", label))\n if first_alpha and first_alpha.group(0).islower():\n lower_initial.append(label)\n if lower_initial:\n return finding(\n \"LEGEND-LABEL-CASE\",\n \"WARN\",\n \"Literal legend labels should start with display-style capitalization while preserving canonical model names\",\n lower_initial,\n )\n if labels:\n return finding(\"LEGEND-LABEL-CASE\", \"PASS\", \"Detected literal legend labels start with display-style capitalization\")\n return finding(\"LEGEND-LABEL-CASE\", \"PASS\", \"No auditable literal legend labels detected\")\n\n\ndef check_interpolation_monotonicity(source: str, backend: str) -> Finding:\n if backend != \"python\" or not re.search(r\"\\bnp\\.interp\\s*\\(\", source):\n return finding(\"INTERP-MONOTONIC\", \"PASS\", \"No NumPy interpolation requiring an increasing x-grid detected\")\n guards = regex_hits(\n [\n r\"interp_(?:need_dec|monotone|strict)\",\n r\"np\\.diff\\s*\\([^)]*\\)\\s*[>\u003c]=?\\s*0\",\n r\"np\\.argsort\\s*\\(\",\n r\"\\[\\s*::\\s*-1\\s*\\]\",\n ],\n source,\n )\n if guards:\n return finding(\n \"INTERP-MONOTONIC\",\n \"PASS\",\n \"NumPy interpolation has an explicit monotonicity/reordering safeguard\",\n guards,\n )\n return finding(\n \"INTERP-MONOTONIC\",\n \"WARN\",\n \"np.interp requires an increasing xp grid and can silently return plausible wrong values for decreasing curves; assert monotonicity or reverse/sort xp and fp together\",\n [\"np.interp(\"],\n )\n\n\ndef check_uncertainty_encoding(source: str, _backend: str) -> Finding:\n stochastic = regex_hits(\n [\n r\"\\bseed(?:s|_[A-Za-z0-9_]+)?\\b\",\n r\"random_state\",\n r\"\\b(?:cv|cross_validation|validation)_folds?\\b\",\n r\"\\bfold_(?:id|index|score|scores|metric|metrics|mean|median)\\b\",\n r\"\\bn_folds?\\b\",\n r\"\\bn_splits\\b\",\n r\"\\b(?:train|test|validation|data|random)_split\\b\",\n ],\n source,\n )\n summaries = regex_hits([r\"\\bmedian\\b\", r\"\\bmean\\b\", r\"\\baverage\\b\"], source)\n if not stochastic or not summaries:\n return finding(\"UNCERTAINTY-ENCODING\", \"PASS\", \"No stochastic aggregate requiring a variability audit was inferred\")\n encodings = regex_hits(\n [\n r\"\\b(?:xerr|yerr)\\s*=\",\n r\"\\.errorbar\\s*\\(\",\n r\"\\.fill_between\\s*\\(\",\n r\"geom_(?:errorbar|ribbon|linerange|pointrange)\\s*\\(\",\n ],\n source,\n )\n if encodings:\n return finding(\n \"UNCERTAINTY-ENCODING\",\n \"PASS\",\n \"A stochastic aggregate and uncertainty encoding are both present; still verify every comparable panel\",\n encodings,\n )\n return finding(\n \"UNCERTAINTY-ENCODING\",\n \"WARN\",\n \"Seed/fold/split aggregates are present without an obvious error bar or uncertainty band\",\n [*stochastic, *summaries],\n )\n\n\ndef check_rotation_anchor(source: str, backend: str) -> Finding:\n if backend != \"python\":\n return finding(\"ROTATION-ANCHOR\", \"PASS\", \"Matplotlib rotation anchoring is not applicable\")\n try:\n tree = ast.parse(source)\n except SyntaxError:\n return finding(\"ROTATION-ANCHOR\", \"WARN\", \"Rotation anchoring could not be audited until Python syntax is valid\")\n\n anchored_receivers: set[str] = set()\n for node in ast.walk(tree):\n if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):\n continue\n if node.func.attr != \"set_rotation_mode\" or not node.args:\n continue\n argument = node.args[0]\n if isinstance(argument, ast.Constant) and argument.value == \"anchor\":\n anchored_receivers.add(ast.dump(node.func.value, include_attributes=False))\n\n rotated: list[str] = []\n unanchored: list[str] = []\n for node in ast.walk(tree):\n if not isinstance(node, ast.Call):\n continue\n line = getattr(node, \"lineno\", \"?\")\n rotation_keyword = next((keyword for keyword in node.keywords if keyword.arg == \"rotation\"), None)\n if rotation_keyword is not None:\n evidence = f\"line {line}: rotation=...\"\n rotated.append(evidence)\n mode_keyword = next((keyword for keyword in node.keywords if keyword.arg == \"rotation_mode\"), None)\n anchored = (\n mode_keyword is not None\n and isinstance(mode_keyword.value, ast.Constant)\n and mode_keyword.value.value == \"anchor\"\n )\n if not anchored:\n unanchored.append(evidence)\n if isinstance(node.func, ast.Attribute) and node.func.attr == \"set_rotation\":\n evidence = f\"line {line}: set_rotation(...)\"\n rotated.append(evidence)\n receiver = ast.dump(node.func.value, include_attributes=False)\n if receiver not in anchored_receivers:\n unanchored.append(evidence)\n\n if not rotated:\n return finding(\"ROTATION-ANCHOR\", \"PASS\", \"No rotated Matplotlib text detected\")\n if not unanchored:\n return finding(\"ROTATION-ANCHOR\", \"PASS\", \"Every detected rotated text call uses anchor-based placement\", rotated)\n return finding(\n \"ROTATION-ANCHOR\",\n \"WARN\",\n \"Some rotated text calls lack rotation_mode='anchor' and may cross their intended anchors after rotation\",\n unanchored,\n )\n\n\ndef check_annotation_workarounds(source: str, backend: str) -> Finding:\n if backend != \"python\":\n return finding(\"ANNOTATION-WORKAROUND\", \"PASS\", \"No Matplotlib annotation workaround audit required\")\n hits = regex_hits(\n [\n r\"(?:LABEL|ANNOTATION)_Y\\s*=\\s*[-+]?\\d+(?:\\.\\d+)?\",\n r\"bbox\\s*=\\s*dict\\s*\\([^)]*(?:facecolor|fc)\\s*=\\s*['\\\"](?:white|#fff(?:fff)?)['\\\"]\",\n ],\n source,\n )\n if hits:\n return finding(\n \"ANNOTATION-WORKAROUND\",\n \"WARN\",\n \"Hard-coded annotation heights or opaque white text masks can collide with uncertainty or cut visible gaps in curves; position from rendered/data bounds instead\",\n hits,\n )\n return finding(\"ANNOTATION-WORKAROUND\", \"PASS\", \"No high-risk hard-coded label height or opaque white text mask detected\")\n\n\ndef check_colormaps(source: str, _backend: str) -> Finding:\n hits = regex_hits(\n [\n r\"(?:cmap|palette|colormap)\\s*=\\s*['\\\"](?:jet|rainbow|hsv)['\\\"]\",\n r\"plt\\.cm\\.(?:jet|rainbow|hsv)\\b\",\n r\"scale_(?:color|fill)_(?:gradientn|distiller)\\s*\\([^)]*(?:rainbow|jet|hsv)\",\n r\"\\brainbow\\s*\\(\",\n ],\n source,\n )\n if hits:\n return finding(\"COLOR-MAP\", \"FAIL\", \"Rainbow/jet/hsv color mapping is not publication-safe\", hits)\n return finding(\"COLOR-MAP\", \"PASS\", \"No rainbow/jet/hsv color mapping detected\")\n\n\ndef check_editable_text(source: str, backend: str) -> Finding:\n if backend == \"python\":\n has_svg = bool(re.search(r\"svg\\.fonttype['\\\"]?\\s*[:=]\\s*['\\\"]none['\\\"]\", source, re.IGNORECASE))\n has_pdf = bool(re.search(r\"pdf\\.fonttype['\\\"]?\\s*[:=]\\s*42\\b\", source, re.IGNORECASE))\n if has_svg and has_pdf:\n return finding(\"EDITABLE-TEXT\", \"PASS\", \"SVG and PDF editable-text settings are configured\")\n missing = []\n if not has_svg:\n missing.append(\"svg.fonttype='none'\")\n if not has_pdf:\n missing.append(\"pdf.fonttype=42\")\n return finding(\"EDITABLE-TEXT\", \"FAIL\", \"Missing editable-text settings\", missing)\n\n has_svg = bool(re.search(r\"(?:svglite::)?svglite\\s*\\(\", source))\n has_pdf = bool(\n re.search(r\"(?:grDevices::)?cairo_pdf\\s*\\(\", source)\n or re.search(r\"\\bdevice\\s*=\\s*['\\\"]?(?:grDevices::)?cairo_pdf\\b\", source)\n )\n if has_svg and has_pdf:\n return finding(\"EDITABLE-TEXT\", \"PASS\", \"svglite and cairo_pdf editable-text devices are configured\")\n missing = []\n if not has_svg:\n missing.append(\"svglite\")\n if not has_pdf:\n missing.append(\"cairo_pdf\")\n return finding(\"EDITABLE-TEXT\", \"FAIL\", \"Missing preferred editable-vector devices\", missing)\n\n\ndef check_vector_exports(source: str, _backend: str) -> Finding:\n has_svg = bool(re.search(r\"\\.svg\\b|svglite\\s*\\(\", source, re.IGNORECASE))\n has_pdf = bool(re.search(r\"\\.pdf\\b|cairo_pdf\\s*\\(|\\bpdf\\s*\\(\", source, re.IGNORECASE))\n if has_svg and has_pdf:\n return finding(\"EXPORT-VECTOR\", \"PASS\", \"Both SVG and PDF exports are present\")\n missing = [name for name, present in ((\"SVG\", has_svg), (\"PDF\", has_pdf)) if not present]\n return finding(\"EXPORT-VECTOR\", \"FAIL\", f\"Missing required vector export: {', '.join(missing)}\")\n\n\ndef check_raster_exports(source: str, _backend: str) -> Finding:\n has_tiff = bool(re.search(r\"\\.tiff?\\b|agg_tiff\\s*\\(|\\btiff\\s*\\(\", source, re.IGNORECASE))\n has_png = bool(re.search(r\"\\.png\\b|agg_png\\s*\\(|\\bpng\\s*\\(\", source, re.IGNORECASE))\n if has_tiff:\n return finding(\"EXPORT-RASTER\", \"PASS\", \"TIFF raster export is present\")\n if has_png:\n return finding(\"EXPORT-RASTER\", \"WARN\", \"PNG preview is present but no TIFF submission raster was found\")\n return finding(\"EXPORT-RASTER\", \"FAIL\", \"No TIFF or PNG raster export found\")\n\n\ndef check_resolution(source: str, _backend: str) -> Finding:\n values = [\n int(value)\n for value in re.findall(r\"(?:dpi|res)\\s*[:=]\\s*(\\d+)\", source, re.IGNORECASE)\n ]\n if not values:\n return finding(\"RASTER-DPI\", \"WARN\", \"No explicit raster DPI/resolution found\")\n below_minimum = sorted({value for value in values if value \u003c 300})\n if below_minimum:\n return finding(\"RASTER-DPI\", \"FAIL\", \"Raster resolution below 300 dpi\", [str(v) for v in below_minimum])\n if max(values) \u003c 600:\n return finding(\"RASTER-DPI\", \"WARN\", \"Raster export meets the 300 dpi floor but not the 600 dpi default\", [str(max(values))])\n return finding(\"RASTER-DPI\", \"PASS\", \"A raster export of at least 600 dpi is configured\", [str(max(values))])\n\n\ndef candidate_widths_mm(source: str, backend: str) -> list[float]:\n widths: list[float] = []\n for value in re.findall(r\"(?:fig_)?width_mm\\s*(?:[:=]|\u003c-)\\s*(\\d+(?:\\.\\d+)?)\", source, re.IGNORECASE):\n widths.append(float(value))\n if backend == \"python\":\n # ``figsize=(183 / 25.4, ...)`` already states millimetres; a bare number is inches.\n for value, per_mm in re.findall(\n r\"figsize\\s*=\\s*\\(\\s*(\\d+(?:\\.\\d+)?)(\\s*/\\s*25\\.4)?\", source, re.IGNORECASE\n ):\n widths.append(float(value) if per_mm else float(value) * 25.4)\n else:\n mm_calls = re.findall(\n r\"(?:ggsave|agg_tiff|svglite|cairo_pdf)\\s*\\([^)]*?width\\s*=\\s*(\\d+(?:\\.\\d+)?)[^)]*?units\\s*=\\s*['\\\"]mm['\\\"]\",\n source,\n re.IGNORECASE | re.DOTALL,\n )\n widths.extend(float(value) for value in mm_calls)\n return widths\n\n\ndef check_dimensions(source: str, backend: str) -> Finding:\n widths = candidate_widths_mm(source, backend)\n if not widths:\n return finding(\"FINAL-WIDTH\", \"WARN\", \"No static final width detected; verify the target journal's current specification\")\n width = widths[0]\n if abs(width - 89) \u003c= 4 or abs(width - 183) \u003c= 6:\n return finding(\"FINAL-WIDTH\", \"PASS\", f\"Detected width {width:.1f} mm matches a common journal column width\")\n return finding(\n \"FINAL-WIDTH\",\n \"WARN\",\n f\"Detected width {width:.1f} mm is not near the common 89/183 mm defaults; verify the target journal\",\n )\n\n\ndef check_sampling(source: str, _backend: str) -> Finding:\n hits = regex_hits(\n [\n r\"np\\.random\\.choice\\s*\\(\",\n r\"\\.sample\\s*\\(\",\n r\"\\bsample_n\\s*\\(\",\n r\"\\bsample_frac\\s*\\(\",\n r\"\\bslice_sample\\s*\\(\",\n ],\n source,\n )\n if not hits:\n return finding(\"DATA-SAMPLING\", \"PASS\", \"No high-confidence row-sampling operation detected\")\n documented = bool(re.search(r\"sampling_(?:reason|rationale)|sampling\\s+rationale|sample_size|random_state|set\\.seed\", source, re.IGNORECASE))\n if documented:\n return finding(\"DATA-SAMPLING\", \"WARN\", \"Sampling is present; confirm it is user-requested or scientifically justified and report its effect\", hits)\n return finding(\"DATA-SAMPLING\", \"WARN\", \"Potential silent sampling detected; do not reduce data for rendering convenience\", hits)\n\n\ndef check_exclusions(source: str, _backend: str) -> Finding:\n hits = regex_hits(\n [r\"\\.dropna\\s*\\(\", r\"\\bna\\.omit\\s*\\(\", r\"\\bcomplete\\.cases\\s*\\(\", r\"drop_na\\s*\\(\"],\n source,\n )\n if not hits:\n return finding(\"DATA-EXCLUSION\", \"PASS\", \"No high-confidence missing-data exclusion detected\")\n reported = bool(re.search(r\"n_before|n_after|before_count|after_count|excluded_count|dropped_count\", source, re.IGNORECASE))\n if reported:\n return finding(\"DATA-EXCLUSION\", \"PASS\", \"Missing-data exclusion includes count-tracking markers\", hits)\n return finding(\"DATA-EXCLUSION\", \"WARN\", \"Missing-data exclusion found without explicit before/after count tracking\", hits)\n\n\ndef check_demo_data(source: str, _backend: str) -> Finding:\n hits = regex_hits(\n [\n r\"np\\.random\\.(?:normal|uniform|rand|randn|poisson)\\s*\\(\",\n r\"(?:np\\.random\\.)?default_rng\\s*\\(\",\n r\"\\brng\\.(?:normal|uniform|poisson|choice)\\s*\\(\",\n r\"\\b(?:rnorm|runif|rpois)\\s*\\(\",\n r\"make_(?:classification|regression|blobs)\\s*\\(\",\n ],\n source,\n )\n if not hits:\n return finding(\"DEMO-DATA\", \"PASS\", \"No obvious simulated-data generator detected\")\n isolated = bool(re.search(r\"--demo|demo_mode|if\\s*\\([^)]*demo|if\\s+.*demo\", source, re.IGNORECASE))\n if isolated:\n return finding(\"DEMO-DATA\", \"WARN\", \"Simulated data is present behind an apparent demo path; keep it out of production runs\", hits)\n return finding(\"DEMO-DATA\", \"WARN\", \"Simulated data appears in the plotting source without an explicit demo boundary\", hits)\n\n\ndef check_log_guards(source: str, _backend: str) -> Finding:\n log_hits = regex_hits(\n [r\"\\bnp\\.log(?:2|10)?\\s*\\(\", r\"\\blog(?:2|10)?\\s*\\(\", r\"set_[xy]scale\\s*\\(\\s*['\\\"]log\", r\"scale_[xy]_log10\\s*\\(\"],\n source,\n )\n if not log_hits:\n return finding(\"LOG-GUARD\", \"PASS\", \"No logarithmic transform or axis detected\")\n guards = regex_hits(\n [\n r\"clip\\s*\\([^)]*lower\\s*=\",\n r\"pmax\\s*\\(\",\n r\"not\\s+0\\s*\u003c[^\\n]{0,80}\",\n r\"np\\.any\\s*\\([^)]*\u003c=\\s*0\",\n r\"(?:padj|pval|p_value|pvalue)[^\\n]{0,50}\u003c=\\s*0\",\n r\"non.?positive\",\n r\"strictly\\s+positive|pseudocount|epsilon|eps\\b\",\n ],\n source,\n )\n if guards:\n return finding(\"LOG-GUARD\", \"PASS\", \"A logarithmic transform/axis and a positivity or pseudocount guard were both detected\", guards)\n return finding(\"LOG-GUARD\", \"WARN\", \"Logarithmic operation found without an obvious positivity/pseudocount guard\", log_hits)\n\n\ndef _python_multipanel_signals(source: str) -> list[str]:\n try:\n tree = ast.parse(source)\n except SyntaxError:\n return []\n signals: list[str] = []\n repeated_axis_calls = {name: 0 for name in (\"add_subplot\", \"add_axes\", \"subplot\", \"subplot2grid\")}\n for node in ast.walk(tree):\n if not isinstance(node, ast.Call):\n continue\n if isinstance(node.func, ast.Attribute):\n name = node.func.attr\n elif isinstance(node.func, ast.Name):\n name = node.func.id\n else:\n name = \"\"\n if name in repeated_axis_calls:\n repeated_axis_calls[name] += 1\n if name == \"subplot_mosaic\":\n signals.append(\"subplot_mosaic(...)\")\n if name not in {\"subplots\", \"GridSpec\", \"add_gridspec\"}:\n continue\n values: dict[str, int] = {}\n for index, argument in enumerate(node.args[:2]):\n if isinstance(argument, ast.Constant) and isinstance(argument.value, int):\n values[\"nrows\" if index == 0 else \"ncols\"] = int(argument.value)\n for keyword in node.keywords:\n if keyword.arg in {\"nrows\", \"ncols\"} and isinstance(keyword.value, ast.Constant) and isinstance(keyword.value.value, int):\n values[keyword.arg] = int(keyword.value.value)\n if values.get(\"nrows\", 1) * values.get(\"ncols\", 1) >= 2:\n signals.append(f\"{name}({values.get('nrows', 1)}, {values.get('ncols', 1)})\")\n for name, count in repeated_axis_calls.items():\n if count >= 2:\n signals.append(f\"{count} {name} calls\")\n return signals\n\n\ndef check_panel_alignment_gate(source: str, backend: str) -> Finding:\n if backend == \"python\":\n signals = _python_multipanel_signals(source)\n gate = regex_hits(\n [r\"require_matplotlib_panel_alignment\\s*\\(\", r\"audit_layout_manifest\\s*\\(\"],\n source,\n )\n else:\n signals = regex_hits(\n [\n r\"plot_layout\\s*\\(\",\n r\"wrap_plots\\s*\\(\",\n r\"patchworkGrob\\s*\\(\",\n r\"[A-Za-z][A-Za-z0-9_.]*\\s*\\|\\s*[A-Za-z][A-Za-z0-9_.]*\",\n ],\n source,\n )\n gate = regex_hits(\n [r\"require_patchwork_panel_alignment\\s*\\(\", r\"audit_panel_alignment\\.py\"],\n source,\n )\n if signals and not gate:\n return finding(\n \"PANEL-ALIGNMENT-GATE\",\n \"FAIL\",\n \"Multi-panel layout detected without the mandatory render-time panel-alignment gate\",\n signals,\n )\n if gate:\n return finding(\n \"PANEL-ALIGNMENT-GATE\",\n \"PASS\",\n \"Render-time panel-alignment gate is wired into the plotting source\",\n gate,\n )\n return finding(\"PANEL-ALIGNMENT-GATE\", \"PASS\", \"No statically auditable multi-panel construct detected\")\n\n\ndef check_backend_exclusivity(source: str, backend: str) -> Finding:\n if backend == \"python\":\n hits = regex_hits([r\"\\bRscript\\b\", r\"\\brpy2\\b\", r\"ggplot2|ComplexHeatmap|patchwork\"], source)\n else:\n hits = regex_hits([r\"matplotlib|seaborn|plotly|reticulate::py_run\"], source)\n if hits:\n return finding(\"BACKEND-EXCLUSIVE\", \"WARN\", f\"Possible cross-backend plotting reference found in {backend} source\", hits)\n return finding(\"BACKEND-EXCLUSIVE\", \"PASS\", f\"No obvious cross-backend plotting reference found for {backend}\")\n\n\nCHECKS: tuple[Callable[[str, str], Finding], ...] = (\n check_syntax,\n check_font_family,\n check_font_sizes,\n check_mathtext_glyph_floor,\n check_legend_label_case,\n check_colormaps,\n check_editable_text,\n check_vector_exports,\n check_raster_exports,\n check_resolution,\n check_dimensions,\n check_sampling,\n check_exclusions,\n check_demo_data,\n check_log_guards,\n check_interpolation_monotonicity,\n check_uncertainty_encoding,\n check_rotation_anchor,\n check_annotation_workarounds,\n check_panel_alignment_gate,\n check_backend_exclusivity,\n)\n\n\ndef validate_source(source: str, backend: str) -> list[Finding]:\n return [check(source, backend) for check in CHECKS]\n\n\ndef summarize(findings: Iterable[Finding], strict: bool = False) -> dict[str, object]:\n rows = list(findings)\n counts = {level: sum(row.level == level for row in rows) for level in (\"PASS\", \"WARN\", \"FAIL\")}\n ready = counts[\"FAIL\"] == 0 and (not strict or counts[\"WARN\"] == 0)\n return {\"ready\": ready, \"strict\": strict, \"counts\": counts}\n\n\ndef render_text(path: Path, backend: str, findings: list[Finding], strict: bool) -> str:\n summary = summarize(findings, strict)\n lines = [\n \"Nature Figure Static Preflight\",\n f\"source: {path}\",\n f\"backend: {backend}\",\n \"\",\n ]\n for row in findings:\n lines.append(f\"[{row.level}] {row.check_id}: {row.message}\")\n for item in row.evidence:\n lines.append(f\" - {item}\")\n counts = summary[\"counts\"]\n lines.extend(\n [\n \"\",\n f\"summary: {counts['PASS']} pass, {counts['WARN']} warn, {counts['FAIL']} fail\",\n f\"verdict: {'READY FOR VISUAL QA' if summary['ready'] else 'FIX BEFORE DELIVERY'}\",\n \"note: static preflight does not validate statistics, source-data truth, or rendered appearance\",\n ]\n )\n return \"\\n\".join(lines)\n\n\ndef run_self_tests() -> None:\n good_python = '''\nimport matplotlib as mpl\nimport matplotlib.pyplot as plt\nmpl.rcParams.update({\n \"font.family\": \"sans-serif\",\n \"font.sans-serif\": [\"Arial\", \"Helvetica\"],\n \"font.size\": 7,\n \"svg.fonttype\": \"none\",\n \"pdf.fonttype\": 42,\n})\nwidth_mm = 183\nvalues = values[values > 0]\nfig, ax = plt.subplots(figsize=(width_mm / 25.4, 120 / 25.4))\nax.set_yscale(\"log\")\nfig.savefig(\"figure.svg\", bbox_inches=\"tight\")\nfig.savefig(\"figure.pdf\", bbox_inches=\"tight\")\nfig.savefig(\"figure.tiff\", dpi=600, bbox_inches=\"tight\")\n'''\n good_r = '''\nlibrary(ggplot2)\nwidth_mm \u003c- 183\np \u003c- ggplot(df, aes(x, y)) + theme_classic(base_size = 7, base_family = \"Arial\")\nsvglite::svglite(\"figure.svg\", width = width_mm / 25.4, height = 4)\nprint(p)\ndev.off()\ngrDevices::cairo_pdf(\"figure.pdf\", width = width_mm / 25.4, height = 4, family = \"Arial\")\nprint(p)\ndev.off()\nragg::agg_tiff(\"figure.tiff\", width = width_mm / 25.4, height = 4, units = \"in\", res = 600)\nprint(p)\ndev.off()\n'''\n bad_python = '''\nimport numpy as np\nimport matplotlib.pyplot as plt\nx = np.random.normal(size=100)\nx = np.random.choice(x, 12)\nfig, ax = plt.subplots(figsize=(4, 3))\nax.scatter(x, np.log(x), c=x, cmap=\"jet\")\nax.tick_params(labelsize=4)\nfig.savefig(\"figure.png\", dpi=72)\n'''\n risky_rendered_python = good_python + '''\nseed_values = np.median(seed_scores, axis=0)\nequivalent_n = np.interp(target_error, errors_decreasing, sample_counts)\nax.plot(x, seed_values, label=\"+ semantic guidance\")\nax.text(0.5, 0.5, \"$R^2$\", fontsize=7, rotation=45,\n bbox=dict(facecolor=\"white\", edgecolor=\"none\"))\nLABEL_Y = 96.4\n'''\n\n good_py_findings = validate_source(good_python, \"python\")\n good_py_failures = [row for row in good_py_findings if row.level == \"FAIL\"]\n assert not good_py_failures, good_py_failures\n\n good_r_findings = validate_source(good_r, \"r\")\n good_r_failures = [row for row in good_r_findings if row.level == \"FAIL\"]\n assert not good_r_failures, good_r_failures\n\n bad = {row.check_id: row for row in validate_source(bad_python, \"python\")}\n for check_id in (\"FONT-FAMILY\", \"FONT-SIZE\", \"COLOR-MAP\", \"EDITABLE-TEXT\", \"EXPORT-VECTOR\", \"RASTER-DPI\"):\n assert bad[check_id].level == \"FAIL\", (check_id, bad[check_id])\n for check_id in (\"DATA-SAMPLING\", \"DEMO-DATA\", \"LOG-GUARD\"):\n assert bad[check_id].level == \"WARN\", (check_id, bad[check_id])\n\n risky = {row.check_id: row for row in validate_source(risky_rendered_python, \"python\")}\n for check_id in (\n \"FONT-GLYPH-FLOOR\",\n \"LEGEND-LABEL-CASE\",\n \"INTERP-MONOTONIC\",\n \"UNCERTAINTY-ENCODING\",\n \"ROTATION-ANCHOR\",\n \"ANNOTATION-WORKAROUND\",\n ):\n assert risky[check_id].level == \"WARN\", (check_id, risky[check_id])\n\n\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\"source\", nargs=\"?\", type=Path, help=\"Python or R plotting source\")\n parser.add_argument(\"--backend\", choices=(\"auto\", \"python\", \"r\"), default=\"auto\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"emit machine-readable JSON\")\n parser.add_argument(\"--strict\", action=\"store_true\", help=\"treat warnings as not ready\")\n parser.add_argument(\"--self-test\", action=\"store_true\", help=\"run dependency-free validator tests\")\n return parser\n\n\ndef main(argv: list[str] | None = None) -> int:\n args = build_parser().parse_args(argv)\n if args.self_test:\n run_self_tests()\n print(\"validate_figure.py self-test: PASS\")\n return 0\n if args.source is None:\n build_parser().error(\"source is required unless --self-test is used\")\n if not args.source.is_file():\n print(f\"error: source file not found: {args.source}\", file=sys.stderr)\n return 2\n try:\n backend = detect_backend(args.source, args.backend)\n source = args.source.read_text(encoding=\"utf-8-sig\")\n except (OSError, UnicodeError, ValueError) as exc:\n print(f\"error: {exc}\", file=sys.stderr)\n return 2\n\n findings = validate_source(source, backend)\n summary = summarize(findings, args.strict)\n if args.json:\n payload = {\n \"source\": str(args.source),\n \"backend\": backend,\n \"summary\": summary,\n \"findings\": [asdict(row) for row in findings],\n }\n print(json.dumps(payload, indent=2, ensure_ascii=False))\n else:\n print(render_text(args.source, backend, findings, args.strict))\n return 0 if summary[\"ready\"] else 1\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":33447,"content_sha256":"b9800e93dfa289eddd2e9438af755fcc6dd4a5f0f1da7778e4f999a91b69b042"},{"filename":"static/core/contract.md","content":"# Figure contract before plotting\n\nA publication-quality scientific figure is a visual argument, not an isolated pretty plot. Every figure starts from a claim, an evidence hierarchy, and a review-risk check before code or aesthetics. Before generating or editing code, establish the contract below.\n\n## Backend selection uses the task context and saved preference\n\nBackend selection applies only to rendering or editing plotting code. Reuse a choice already established in the same task and its follow-ups; do not ask again merely because a new message omits the language. Read-only figure review and backend-independent data inspection may proceed without this choice. If the backend remains unresolved, retain the one-time Python/R question and pause only dependent plotting steps. Explicit approval requirements and backend exclusivity remain in force.\n\nFor plotting tasks, first honor an explicit Python/R choice in the current request or a clearly language-specific input file/workflow. Save that backend as the user's default with `scripts/nature_figure_backend.py set python` or `scripts/nature_figure_backend.py set r`.\n\nIf the current request does not specify a backend, first reuse the choice established in this task; otherwise check the saved preference with `scripts/nature_figure_backend.py get`. If it returns `python` or `r`, use that backend without asking again.\n\nIf no saved preference exists, ask one concise question: **Python or R? I will remember this as your default.** Pause dependent plotting steps and wait for the user's answer; continue independent inspection. Do not generate mock data, write scripts, create figures, or choose Python/R by default before this first preference is established. After the user answers, save it and proceed.\n\nOnly recommend a backend when the user explicitly asks you to choose or recommend one. In that case, use `references/backend-selection.md`, state the reason, save the selected backend, and then proceed with the recommended backend.\n\n## The selected backend is exclusive\n\nOnce Python or R is selected, every plotting script, preview image, SVG/PDF/TIFF/PNG export, QA render, and visual workaround must be produced by that same backend. Do not use Python to draw a preview for an R figure, and do not use R to draw a preview for a Python figure, even if the selected runtime or packages are missing locally. The non-selected language may only be used for non-visual file inspection or data conversion when it does not open a graphics device, import plotting libraries, create image/vector files, or change the final visual appearance.\n\nThe backend-neutral `audit_panel_alignment.py`, `audit_pdf_text.py` and\n`audit_figure_collisions.py` tools may inspect a layout manifest or final PDF\nfrom either backend. They do not redraw the scientific content. The selected\nbackend must measure its own final axes/grob geometry before the alignment JSON\nis audited. Alignment and collision overlays are QA-only, not previews,\nsubstitute exports or submission assets; keep the original selected-backend\nfigure authoritative.\n\n## Missing runtime/package rule\n\nAfter the backend is selected, check the selected runtime early (`Rscript`/R for R; Python and required plotting packages for Python). If the selected runtime or required packages are unavailable, pause rendering, continue independent script or data checks, and report the exact blocker. You may provide a selected-backend script and installation commands, or ask permission to install dependencies, but you must not fall back to the other language to make a substitute figure.\n\n## Data-integrity gate\n\nUse all user-provided observations and requested variables unless an exclusion has a scientific or statistical justification or the user explicitly requests a subset. Never reduce data merely to make a plot easier or faster to render. For large point clouds, prefer rasterized marks, hexbin/density representations, aggregation with a stated rule, or another backend-native rendering strategy.\n\nIf any row, column, replicate, image, or category is excluded, record the before/after counts, the exact rule, and the reason in the QA notes. Preserve the unmodified source data and never silently select convenient columns to satisfy a template.\n\nPlan figures by scientific claims, not by source tables. Do not turn each table into a separate figure when several tables answer the same question. If an effect is defined within matched datasets, subjects, seeds, or tasks, inspect and visualize paired differences rather than relying only on overlapping marginal distributions; large between-unit heterogeneity can hide a strong paired effect.\n\n## The five-point contract\n\n1. **Core conclusion**: write the one-sentence claim the figure must defend and the Results-level scientific question it answers.\n2. **Evidence chain**: map each planned panel to one distinct inferential role in that claim, and drop, merge, or demote panels that only redraw another panel's evidence or repeat it under a secondary metric.\n3. **Archetype**: classify the figure as `quantitative grid`, `schematic-led composite`, `image plate + quant`, or `asymmetric mixed-modality figure`.\n4. **Backend**: use the explicit or saved Python/R track exclusively for all figure drawing, previewing, exporting, and visual QA. Do not cross-render with the other language.\n5. **Journal/export contract**: set final dimensions, a 5 pt floor for every rendered glyph, editable text, source data, statistics, image-integrity notes, export formats, a blocking multi-panel alignment gate, and automatic rendered collision QA before styling.\n\nThe highest-priority rule is: **the chart serves the scientific logic**. Aesthetic polish, template matching, and complex layout are subordinate to making the core conclusion clear, defensible, and reviewable.\n\nFor the full method to convert a request into core conclusion, evidence hierarchy, panel map, and review-risk checks, open `references/figure-contract.md`.\nFor a labelled multi-panel figure or a manuscript-level figure sequence, also\nopen `references/multipanel-evidence-architecture.md`: use one figure-level\nclaim as the planning default, give panels complementary evidence roles, and\nalign successive figures with the Results claim escalation.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6274,"content_sha256":"a2b6523a48192cf1dff0d194264c5662c832f4d1da6e9e4763ccd98cc0664d4c"},{"filename":"static/core/stance.md","content":"# Default operating stance\n\nThe older Python/matplotlib rules in this skill remain valid. The skill also supports R, especially `ggplot2 + patchwork + ComplexHeatmap + ggrepel + svglite/cairo_pdf + ragg`.\n\n## Color policy\n\nPrefer **unified method families across all panels** over maximal hue separation. For dense Nature Machine Intelligence-style figure pages, use the low-saturation `NMI pastel` family described in `references/api.md` and reserve green/red mainly for gains, drops, and other directional cues.\n\n## Stance\n\n- Start by classifying the requested figure into one of four archetypes: `quantitative grid`, `schematic-led composite`, `image plate + quant`, or `asymmetric mixed-modality figure`.\n- Prefer one **hero panel** plus subordinate evidence panels over filling the canvas with equal-sized subplots.\n- If the user asks for a single chart, still identify its role in the manuscript claim: discovery, mechanism, validation, comparison, robustness, or clinical/biological relevance.\n- Keep the background white for plots and diagrams; switch to black only for microscopy / volume-rendering image plates.\n- Prefer direct labels over legends when categories are spatially fixed or the legend would force unnecessary eye travel.\n- Do not print a value on every bar, box, point or cell. Let the axis carry the values and print only the numbers the panel argues (for example the one count the claim rests on); move the rest to the legend, the caption or the text. Fewer, larger labels beat many small ones.\n- Keep one restrained palette per figure: usually one neutral family, one signal family, and one accent family.\n- Treat perceptual separation as necessary but not sufficient: verify that the intended hero series is more salient than baselines after rendering, and do not reuse a sequential light-to-dark scale as unrelated categorical colors.\n- Treat statistics, `n`, error-bar definitions, source-data traceability, and image-integrity notes as part of the figure, not as optional caption cleanup.\n- When panels show comparable seed/fold/split aggregates, use the same uncertainty definition in every comparable panel or state why a panel is exempt.\n- Preserve canonical model capitalization in display labels. Legend entries use display-style initial capitalization; prose follows normal sentence grammar. Never apply blind `.title()` transformations to names such as `XGBoost`, `DeepSeek`, `GPT-5.2`, or `RF`.\n- Require the render-time alignment gate for every multi-panel layout, followed by a final-size, panel-by-panel visual inspection. Source validation cannot prove color hierarchy, label clearance, legend spacing, or complete uncertainty coverage.\n- When the user asks for broad `Nature` style rather than ML/NMI-specific style, read `references/nature-2026-observations.md` before choosing layout.\n- When the user references `figures4papers` or the older `scientific-figure-making` skill, treat this skill as the successor and open `references/demos.md` for the third-party demo map, copyright boundary, and original reimplementation guidance.\n\n## User-facing privacy rule\n\nDo not disclose private local paths, private filenames, chat-attachment names, internal reference filenames, template identifiers, or the provenance of private working materials in user-facing replies, generated code comments, figure legends, reports, or manuscript text. Use generic descriptions such as \"the provided R template collection\", \"a private working draft\", or \"the internal figure contract\". If the user provides a private plotting template collection, use it only as an internal adaptation source and do not reveal its path, filenames, or provenance. Only reveal an exact path or source file when the user explicitly asks for that audit trail.\n\n## When to load this skill\n\n- Python or R figures for **papers, slides, or reports** targeting Nature, Science, Cell, NeurIPS, ICLR, or similar venues.\n- Requests involving **grouped bars, trend lines, heatmaps, radar plots, multi-panel grids**, or **PDF/SVG/high-DPI** output.\n- Any mention of \"Nature style\", \"publication figure\", \"paper figure\", \"SCI figure\", \"figures4papers\", \"scientific-figure-making\", \"R plotting template\", or \"high-quality scientific plot\".\n- Requests to improve a figure's logic, aesthetics, panel layout, figure legend, export quality, or journal-readiness.\n\n## When NOT to load\n\n- Plotly, Altair, Bokeh, or other interactive/web-first plotting.\n- EDA-only plots without a publication target.\n- Primary workflow is 3D, GIS, or non-scientific illustration tooling.\n- Illustrator / Figma–first layout.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4595,"content_sha256":"c7e6860e955a2ee9c24a30dbe88e28741da4eef8290c019e849c6b220ea0f07a"},{"filename":"static/fragments/backend/python.md","content":"# Backend: Python (matplotlib / seaborn)\n\n**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, pause rendering, continue independent script or data checks, 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.\n\n## Python quick-start\n\n```python\nimport matplotlib as mpl\nimport matplotlib.pyplot as plt\n# Copy audit_panel_alignment.py beside the plotting source or add the skill's\n# scripts directory to PYTHONPATH before importing it.\nfrom audit_panel_alignment import require_matplotlib_panel_alignment\n\nmpl.rcParams.update({\n \"font.family\": \"sans-serif\",\n \"font.sans-serif\": [\"Arial\", \"Helvetica\", \"DejaVu Sans\", \"sans-serif\"],\n \"svg.fonttype\": \"none\", # editable text in SVG\n \"pdf.fonttype\": 42, # editable TrueType text in PDF\n \"font.size\": 7, # use 15-24 only for large slide-sized panels\n \"axes.spines.right\": False,\n \"axes.spines.top\": False,\n \"axes.linewidth\": 0.8,\n \"legend.frameon\": False,\n})\n\ndef save_pub_py(fig, filename, dpi=600):\n # Run after every layout-affecting change, before export. Single-panel\n # figures record NOT APPLICABLE; unresolved multi-panel geometry blocks.\n require_matplotlib_panel_alignment(\n fig,\n json_out=f\"{filename}.alignment.json\",\n overlay_svg=f\"{filename}.alignment.svg\",\n tolerance_pt=1.5,\n gutter_tolerance_pt=1.5,\n strict=True,\n )\n fig.savefig(f\"{filename}.svg\", bbox_inches=\"tight\")\n fig.savefig(f\"{filename}.pdf\", bbox_inches=\"tight\")\n fig.savefig(f\"{filename}.tiff\", dpi=dpi, bbox_inches=\"tight\")\n```\n\nUse `text.usetex = True` only when LaTeX is installed and math-rich labels are required.\n\n## Going deeper\n\n- `references/api.md` — Python PALETTE, helper function signatures, validation rules.\n- `references/template-catalog.md` — validated CSV-driven volcano, ROC, dot-plot, marginal, and paired templates backed by `scripts/plot_templates.py`.\n- `references/common-patterns.md` — hero panels, legend-only axes, dark image plates, asymmetric layouts.\n- `references/chart-types.md` — radar, 3D sphere, fill_between, scatter patterns.\n- `references/tutorials.md` — end-to-end walkthroughs for bars, trends, heatmaps.\n- `references/demos.md` — third-party figures4papers demo map, copyright boundary, and original Python reimplementation guidance.\n- `scripts/validate_figure.py` — dependency-free source preflight before rendering and visual QA.\n- `scripts/audit_pdf_text.py` — dependency-free `Tf` scan of exported PDF text runs; use it to enforce the 5 pt glyph floor after rendering.\n- `scripts/audit_panel_alignment.py` — mandatory render-time Matplotlib axes alignment gate for every multi-panel figure; call `require_matplotlib_panel_alignment()` after the final layout draw and before export.\n- `scripts/audit_figure_collisions.py` — mandatory PyMuPDF geometry audit after every generated or layout-affecting revision; fix text-text, text-stroke and clipping FAIL findings before delivery.\n- `scripts/figure_safety.py` — strict monotone interpolation and annotation placement above uncertainty extents.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3535,"content_sha256":"1d4809e4ec4ed2d40995d74828732012a6a72c941dfec341745a3118e9312865"},{"filename":"static/fragments/backend/r.md","content":"# Backend: R (ggplot2 / patchwork / ComplexHeatmap)\n\n**R-only execution rule.** When the user has selected R, do all figure drawing, previewing, exporting, and visual QA in R. Do not call matplotlib/seaborn or any Python graphics device to create a temporary preview, fallback export, or layout approximation. If `Rscript`/R or required R packages are missing, pause rendering, continue independent script or data checks, and report the exact blocker. You may still write the R script, provide install commands (for example `install.packages(...)`), or ask permission to install dependencies, but do not cross-render the figure in Python.\n\n## R quick-start\n\n```r\nlibrary(ggplot2)\nlibrary(patchwork)\nsource(\"skills/nature-figure/scripts/panel_alignment.R\")\n\ntheme_set(\n theme_classic(base_size = 6.5, base_family = \"Arial\") +\n theme(\n axis.line = element_line(linewidth = 0.35, colour = \"black\"),\n axis.ticks = element_line(linewidth = 0.35, colour = \"black\"),\n legend.title = element_text(size = 6.2),\n legend.text = element_text(size = 5.8),\n strip.text = element_text(size = 6.2, face = \"bold\"),\n plot.title = element_text(size = 7, face = \"bold\"),\n panel.grid = element_blank()\n )\n)\n\nsave_pub_r \u003c- function(plot, filename, width_mm = 183, height_mm = 120, dpi = 600) {\n w \u003c- width_mm / 25.4\n h \u003c- height_mm / 25.4\n if (inherits(plot, \"patchwork\")) {\n # Run after every layout-affecting change, before export.\n require_patchwork_panel_alignment(\n plot,\n manifest_path = paste0(filename, \".alignment-layout.json\"),\n report_path = paste0(filename, \".alignment.json\"),\n overlay_svg = paste0(filename, \".alignment.svg\"),\n width_in = w,\n height_in = h,\n tolerance_pt = 1.5,\n gutter_tolerance_pt = 1.5,\n strict = TRUE\n )\n }\n svglite::svglite(paste0(filename, \".svg\"), width = w, height = h)\n print(plot)\n dev.off()\n grDevices::cairo_pdf(paste0(filename, \".pdf\"), width = w, height = h, family = \"Arial\")\n print(plot)\n dev.off()\n ragg::agg_tiff(paste0(filename, \".tiff\"), width = w, height = h, units = \"in\", res = dpi)\n print(plot)\n dev.off()\n}\n```\n\n## Going deeper\n\n- `references/r-workflow.md` — the R plotting workflow when the user provides R scripts, templates, or data.\n- `references/r-template-index.md` — adapt a user-provided or private R template collection without exposing source paths.\n- `references/design-theory.md` — typography, color theory, layout rationale, export policy (backend-agnostic).\n- `references/nature-2026-observations.md` — real Nature page archetypes to match before choosing layout.\n- `scripts/validate_figure.py` — dependency-free static R source preflight before running R and inspecting the rendered outputs.\n- `scripts/audit_pdf_text.py` — inspect the final R-generated PDF's rendered glyph floor without redrawing it.\n- `scripts/panel_alignment.R` plus `scripts/audit_panel_alignment.py` — mandatory render-time patchwork/gtable alignment gate for every multi-panel figure; declare comparable groups explicitly for asymmetric layouts.\n- `scripts/audit_figure_collisions.py` — mandatory backend-neutral geometry audit after every generated or layout-affecting revision; its optional marked PDF is QA-only and does not replace the R export.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3299,"content_sha256":"4839a77846f0f71a7a44f077ee8fc665460033e7006cb18a884562712d667e2d"},{"filename":"tests/test_figure_safety.py","content":"from __future__ import annotations\n\nimport importlib.util\nimport sys\nimport unittest\nimport zlib\nfrom pathlib import Path\n\n\nSKILL = Path(__file__).parents[1]\n\n\ndef load_module(name: str, path: Path):\n spec = importlib.util.spec_from_file_location(name, path)\n assert spec and spec.loader\n module = importlib.util.module_from_spec(spec)\n sys.modules[name] = module\n spec.loader.exec_module(module)\n return module\n\n\nVALIDATOR = load_module(\"nature_figure_validator_test\", SKILL / \"scripts\" / \"validate_figure.py\")\nPDF_AUDIT = load_module(\"nature_figure_pdf_audit_test\", SKILL / \"scripts\" / \"audit_pdf_text.py\")\nFIGURE_SAFETY = load_module(\"nature_figure_safety_test\", SKILL / \"scripts\" / \"figure_safety.py\")\n\n\ndef fake_pdf(stream: bytes, compressed: bool = False) -> bytes:\n payload = zlib.compress(stream) if compressed else stream\n filter_entry = b\" /Filter /FlateDecode\" if compressed else b\"\"\n return (\n b\"%PDF-1.4\\n1 0 obj\\n\u003c\u003c /Length \"\n + str(len(payload)).encode(\"ascii\")\n + filter_entry\n + b\" >>\\nstream\\n\"\n + payload\n + b\"\\nendstream\\nendobj\\n%%EOF\\n\"\n )\n\n\nclass PdfTextAuditTests(unittest.TestCase):\n def test_plain_pdf_passes_when_all_tf_sizes_meet_floor(self):\n result = PDF_AUDIT.audit_pdf(fake_pdf(b\"BT /F1 7 Tf (Label) Tj ET\"))\n self.assertTrue(result[\"auditable\"])\n self.assertEqual(result[\"minimum_found_pt\"], 7.0)\n self.assertEqual(result[\"below_minimum_count\"], 0)\n\n def test_flate_pdf_catches_mathtext_sized_run(self):\n result = PDF_AUDIT.audit_pdf(\n fake_pdf(b\"BT /F1 7 Tf (R) Tj /F2 4.9 Tf (2) Tj ET\", compressed=True)\n )\n self.assertTrue(result[\"auditable\"])\n self.assertEqual(result[\"minimum_found_pt\"], 4.9)\n self.assertEqual(result[\"below_minimum_count\"], 1)\n self.assertEqual(result[\"below_minimum\"][0][\"font\"], \"F2\")\n\n def test_pdf_without_supported_tf_is_not_auditable(self):\n result = PDF_AUDIT.audit_pdf(fake_pdf(b\"0 0 m 10 10 l S\"))\n self.assertFalse(result[\"auditable\"])\n\n\nclass SourceSafetyTests(unittest.TestCase):\n def findings(self, source: str):\n return {row.check_id: row for row in VALIDATOR.validate_source(source, \"python\")}\n\n def test_risky_patterns_are_reported(self):\n source = '''\nimport numpy as np\nseed_scores = np.median(scores, axis=0)\nn_equiv = np.interp(target, decreasing_error, n_samples)\nax.plot(x, seed_scores, label=\"+ semantic guidance\")\nax.text(0.5, 0.5, \"$R^2$\", fontsize=7, rotation=45,\n bbox=dict(facecolor=\"white\", edgecolor=\"none\"))\nLABEL_Y = 96.4\n'''\n rows = self.findings(source)\n for check_id in (\n \"FONT-GLYPH-FLOOR\",\n \"LEGEND-LABEL-CASE\",\n \"INTERP-MONOTONIC\",\n \"UNCERTAINTY-ENCODING\",\n \"ROTATION-ANCHOR\",\n \"ANNOTATION-WORKAROUND\",\n ):\n self.assertEqual(rows[check_id].level, \"WARN\", check_id)\n\n def test_guarded_patterns_pass_targeted_checks(self):\n source = '''\nimport numpy as np\ndef interp_monotone(target, xp, fp):\n order = np.argsort(xp)\n xp = xp[order]\n fp = fp[order]\n assert np.all(np.diff(xp) > 0)\n return np.interp(target, xp, fp)\n\nseed_scores = np.median(scores, axis=0)\nax.plot(x, seed_scores, label=\"+ Semantic guidance\")\nax.fill_between(x, seed_scores - seed_std, seed_scores + seed_std)\nax.text(0.5, 0.5, \"R²\", fontsize=7, rotation=45, rotation_mode=\"anchor\")\n'''\n rows = self.findings(source)\n for check_id in (\n \"FONT-GLYPH-FLOOR\",\n \"LEGEND-LABEL-CASE\",\n \"INTERP-MONOTONIC\",\n \"UNCERTAINTY-ENCODING\",\n \"ROTATION-ANCHOR\",\n \"ANNOTATION-WORKAROUND\",\n ):\n self.assertEqual(rows[check_id].level, \"PASS\", check_id)\n\n def test_string_split_is_not_mistaken_for_random_split(self):\n source = '''\ncolumns = [value.strip() for value in args.columns.split(\",\")]\nax.set_ylabel(\"Mean fold change\")\n'''\n rows = self.findings(source)\n self.assertEqual(rows[\"UNCERTAINTY-ENCODING\"].level, \"PASS\")\n\n def test_every_rotated_call_must_be_anchored(self):\n source = '''\nax.text(0.2, 0.3, \"A\", rotation=45, rotation_mode=\"anchor\")\nax.text(0.5, 0.6, \"B\", rotation=90)\n'''\n rows = self.findings(source)\n self.assertEqual(rows[\"ROTATION-ANCHOR\"].level, \"WARN\")\n self.assertEqual(rows[\"ROTATION-ANCHOR\"].evidence, [\"line 3: rotation=...\"])\n\n def test_python_multipanel_source_requires_alignment_gate(self):\n missing = self.findings(\"fig, axes = plt.subplots(2, 2)\")\n self.assertEqual(missing[\"PANEL-ALIGNMENT-GATE\"].level, \"FAIL\")\n\n import_only = self.findings(\n \"from audit_panel_alignment import require_matplotlib_panel_alignment\\n\"\n \"fig, axes = plt.subplots(2, 2)\"\n )\n self.assertEqual(import_only[\"PANEL-ALIGNMENT-GATE\"].level, \"FAIL\")\n\n manual_axes = self.findings(\n \"fig = plt.figure()\\n\"\n \"ax_a = fig.add_axes([0.1, 0.1, 0.3, 0.8])\\n\"\n \"ax_b = fig.add_axes([0.6, 0.1, 0.3, 0.8])\"\n )\n self.assertEqual(manual_axes[\"PANEL-ALIGNMENT-GATE\"].level, \"FAIL\")\n\n mosaic = self.findings(\"fig, axes = plt.subplot_mosaic([['a', 'b']])\")\n self.assertEqual(mosaic[\"PANEL-ALIGNMENT-GATE\"].level, \"FAIL\")\n\n wired = self.findings(\n '''\nfig, axes = plt.subplots(2, 2)\nrequire_matplotlib_panel_alignment(fig, strict=True)\n'''\n )\n self.assertEqual(wired[\"PANEL-ALIGNMENT-GATE\"].level, \"PASS\")\n\n def test_r_patchwork_source_requires_alignment_gate(self):\n missing = {\n row.check_id: row\n for row in VALIDATOR.validate_source(\n \"fig \u003c- (p_a | p_b) + plot_layout(guides = 'collect')\",\n \"r\",\n )\n }\n self.assertEqual(missing[\"PANEL-ALIGNMENT-GATE\"].level, \"FAIL\")\n\n manifest_only = {\n row.check_id: row\n for row in VALIDATOR.validate_source(\n \"fig \u003c- p_a | p_b\\nwrite_patchwork_panel_layout(fig, manifest_path='a.json')\",\n \"r\",\n )\n }\n self.assertEqual(manifest_only[\"PANEL-ALIGNMENT-GATE\"].level, \"FAIL\")\n\n wired = {\n row.check_id: row\n for row in VALIDATOR.validate_source(\n \"fig \u003c- p_a | p_b\\nrequire_patchwork_panel_alignment(fig, manifest_path='a.json')\",\n \"r\",\n )\n }\n self.assertEqual(wired[\"PANEL-ALIGNMENT-GATE\"].level, \"PASS\")\n\n def test_r_delimiter_check_ignores_comments(self):\n finding = VALIDATOR.check_syntax(\n \"# patchwork's final panel cells { are measured\\nfig \u003c- (p_a | p_b)\",\n \"r\",\n )\n self.assertEqual(finding.level, \"WARN\")\n self.assertIn(\"delimiter check passed\", finding.message)\n\n def test_r_ggsave_cairo_pdf_counts_as_editable_text(self):\n source = (\n 'svglite::svglite(\"figure.svg\", width = 3.5, height = 2.5)\\n'\n 'ggsave(\"figure.pdf\", p, width = 3.5, height = 2.5, device = cairo_pdf)\\n'\n )\n self.assertEqual(VALIDATOR.check_editable_text(source, \"r\").level, \"PASS\")\n\n def test_r_ggsave_cairo_pdf_string_form_counts_as_editable_text(self):\n source = (\n 'svglite::svglite(\"figure.svg\")\\n'\n 'ggsave(\"figure.pdf\", p, device = \"cairo_pdf\")\\n'\n )\n self.assertEqual(VALIDATOR.check_editable_text(source, \"r\").level, \"PASS\")\n\n def test_r_ggsave_without_cairo_pdf_still_fails_editable_text(self):\n source = (\n 'svglite::svglite(\"figure.svg\")\\n'\n 'ggsave(\"figure.pdf\", p, width = 3.5, height = 2.5)\\n'\n )\n self.assertEqual(VALIDATOR.check_editable_text(source, \"r\").level, \"FAIL\")\n\n\nclass NumericalSafetyTests(unittest.TestCase):\n def test_interp_monotone_handles_decreasing_grid(self):\n result = FIGURE_SAFETY.interp_monotone(7, [10, 8, 6], [5, 10, 15])\n self.assertAlmostEqual(float(result), 12.5)\n\n def test_interp_monotone_rejects_direction_changes(self):\n with self.assertRaisesRegex(ValueError, \"strictly monotone\"):\n FIGURE_SAFETY.interp_monotone(7, [10, 6, 8], [5, 10, 15])\n\n def test_label_position_clears_uncertainty(self):\n label_y = FIGURE_SAFETY.label_y_above([80, 90], spread=[2, 6])\n self.assertGreater(label_y, 96)\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8478,"content_sha256":"8d8598b643608fc643cdc2a6a2952f157831d80200a8a1676a12f7c392f3b753"},{"filename":"tests/test_schematic_url_safety.py","content":"\"\"\"URL safety tests for generate_openrouter_schematic.py (no network access).\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib.util\nimport sys\nimport tempfile\nimport unittest\nfrom argparse import Namespace\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, patch\n\nSKILL = Path(__file__).parents[1]\n\n\ndef load_module(name: str, path: Path):\n spec = importlib.util.spec_from_file_location(name, path)\n assert spec and spec.loader\n module = importlib.util.module_from_spec(spec)\n sys.modules[name] = module\n spec.loader.exec_module(module)\n return module\n\n\nSCHEMATIC = load_module(\"nature_figure_schematic_url_test\", SKILL / \"scripts\" / \"generate_openrouter_schematic.py\")\n\n\ndef _args(tmpdir: str) -> Namespace:\n return Namespace(outdir=tmpdir, basename=\"schematic\", output_format=\"png\", timeout=10, api_url=\"https://api.example/v1\")\n\n\nclass ResponseUrlSafetyTests(unittest.TestCase):\n def test_save_outputs_refuses_non_http_image_url(self):\n response = {\"data\": [{\"media_type\": \"image/png\", \"url\": \"file:///etc/hosts\"}]}\n with patch(\"urllib.request.urlopen\", side_effect=AssertionError(\"must not fetch\")) as opened:\n with self.assertRaisesRegex(SystemExit, \"non-http\"):\n SCHEMATIC.save_outputs(response, {\"prompt\": \"x\"}, _args(tempfile.mkdtemp()))\n opened.assert_not_called()\n\n def test_save_outputs_refuses_local_image_url_host(self):\n response = {\"data\": [{\"media_type\": \"image/png\", \"url\": \"http://127.0.0.1/x.png\"}]}\n with patch(\"urllib.request.urlopen\", side_effect=AssertionError(\"must not fetch\")) as opened:\n with self.assertRaisesRegex(SystemExit, \"local\"):\n SCHEMATIC.save_outputs(response, {\"prompt\": \"x\"}, _args(tempfile.mkdtemp()))\n opened.assert_not_called()\n\n def test_save_outputs_downloads_https_image_url(self):\n response = {\"data\": [{\"media_type\": \"image/png\", \"url\": \"https://cdn.example.com/x.png\"}]}\n tmpdir = tempfile.mkdtemp()\n downloaded = MagicMock()\n downloaded.__enter__.return_value.read.return_value = b\"PNGDATA\"\n with patch(\"urllib.request.urlopen\", return_value=downloaded) as opened:\n SCHEMATIC.save_outputs(response, {\"prompt\": \"x\"}, _args(tmpdir))\n opened.assert_called_once()\n self.assertEqual((Path(tmpdir) / \"schematic.png\").read_bytes(), b\"PNGDATA\")\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2439,"content_sha256":"5ed9221941095b4c80d4494a04212c3afce7f5e8f31fccf20068f77503518d45"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Nature Figure Making — Router","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Routing protocol","type":"text"}]},{"type":"paragraph","content":[{"text":"For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"0. Check for graphical-abstract and AI-schematic routes","type":"text"}]},{"type":"paragraph","content":[{"text":"For every graphical-abstract planning, generation, revision, or audit task that uses AI, read ","type":"text"},{"text":"references/ai-graphical-abstract-workflow.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ai-graphical-abstract-workflow.md","title":null}}]},{"text":" first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice, not submission clearance; verify the current official policy for the exact target journal.","type":"text"}]},{"type":"paragraph","content":[{"text":"If the request is planning or auditing only, do not ask for Python or R unless the user also asks to render or revise a data-driven figure.","type":"text"}]},{"type":"paragraph","content":[{"text":"If the user explicitly asks to generate a manuscript schematic, graphical abstract, mechanism diagram, concept illustration, or paper schematic with OpenRouter, GPT Image 2, an image-generation API, or similar wording, do ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" ask \"Python or R?\". This is a non-plotting AI-schematic route.","type":"text"}]},{"type":"paragraph","content":[{"text":"For this route:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"manifest.yaml","type":"text","marks":[{"type":"link","attrs":{"href":"manifest.yaml","title":null}}]},{"text":" and the ","type":"text"},{"text":"always_load","type":"text","marks":[{"type":"code_inline"}]},{"text":" files.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/ai-graphical-abstract-workflow.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ai-graphical-abstract-workflow.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/openrouter-image-generation.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/openrouter-image-generation.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"scripts/generate_openrouter_schematic.py","type":"text","marks":[{"type":"link","attrs":{"href":"scripts/generate_openrouter_schematic.py","title":null}}]},{"text":" when the user wants a real API call or a reproducible payload.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treat output as a draft schematic / graphical abstract, not as a quantitative data panel. Do not invent experimental values, author logos, institutional marks, or unsupported mechanisms. Keep internal usefulness separate from submission eligibility.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If the user wants a hand-built, editable vector schematic (architecture, pipeline, agent or model block diagram, or mechanism flow) rather than an AI-generated image, do ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" ask \"Python or R?\". Read ","type":"text"},{"text":"references/vector-schematic-workflow.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/vector-schematic-workflow.md","title":null}}]},{"text":" and ","type":"text"},{"text":"references/vector-schematic-style-rules.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/vector-schematic-style-rules.md","title":null}}]},{"text":", confirm the brief with the user before drawing, and run ","type":"text"},{"text":"scripts/audit_svg_schematic.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" on the SVG before delivery. This route never plots data.","type":"text"}]},{"type":"paragraph","content":[{"text":"Only continue to the Python/R backend gate for plotting, charting, data visualization, or manuscript figure assembly tasks that are not explicit OpenRouter AI image-generation requests.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Load the manifest and the core layer","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"manifest.yaml","type":"text","marks":[{"type":"link","attrs":{"href":"manifest.yaml","title":null}}]},{"text":". It declares the ","type":"text"},{"text":"backend","type":"text","marks":[{"type":"code_inline"}]},{"text":" axis, the allowed values, and the file paths each value maps to.","type":"text"}]},{"type":"paragraph","content":[{"text":"Also read every file listed under ","type":"text"},{"text":"always_load","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"static/core/contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"static/core/stance.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"). 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.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Resolve the plotting backend","type":"text"}]},{"type":"paragraph","content":[{"text":"Backend selection applies only to rendering or editing plotting code. Reuse a choice already established in the same task and its follow-ups; do not ask again merely because a new message omits the language. Read-only figure review and backend-independent data inspection may proceed without this choice. If the backend remains unresolved, retain the one-time Python/R question and pause only dependent plotting steps. Explicit approval requirements and backend exclusivity remain in force.","type":"text"}]},{"type":"paragraph","content":[{"text":"Resolve the plotting backend from the current task before consulting the saved default. Decide the ","type":"text"},{"text":"backend","type":"text","marks":[{"type":"code_inline"}]},{"text":" value in this order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the current request explicitly chooses Python or R, use that backend and save it with ","type":"text"},{"text":"scripts/nature_figure_backend.py set python","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"scripts/nature_figure_backend.py set r","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the request provides a clearly language-specific input file/workflow, use that backend and save it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Otherwise reuse a Python/R choice already established in this task. If none exists, run ","type":"text"},{"text":"scripts/nature_figure_backend.py get","type":"text","marks":[{"type":"code_inline"}]},{"text":" and use a returned ","type":"text"},{"text":"python","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"r","type":"text","marks":[{"type":"code_inline"}]},{"text":" preference.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If neither a task choice nor a saved preference exists, ask exactly one concise question — ","type":"text"},{"text":"Python or R? I will remember this as your default.","type":"text","marks":[{"type":"strong"}]},{"text":" — and pause only dependent plotting steps. After the user answers, save the answer before proceeding.","type":"text"}]}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"python","type":"text","marks":[{"type":"code_inline"}]},{"text":" — matplotlib / seaborn.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"r","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ggplot2 / patchwork / ComplexHeatmap.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Do not guess or choose a backend by aesthetics alone. Only recommend a backend when the user explicitly asks you to choose; then use ","type":"text"},{"text":"references/backend-selection.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", state the reason, save the selected backend, and proceed. Once selected, the backend is ","type":"text"},{"text":"exclusive","type":"text","marks":[{"type":"strong"}]},{"text":" for all drawing, previewing, exporting, and visual QA (see ","type":"text"},{"text":"core/contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"). This gate does not apply to the explicit OpenRouter AI-schematic route above.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Load the matching backend fragment","type":"text"}]},{"type":"paragraph","content":[{"text":"After the backend is resolved, Read the mapped fragment (","type":"text"},{"text":"static/fragments/backend/python.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"static/fragments/backend/r.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"). It carries the backend-only execution rule and the publication quick-start (rcParams/theme and export helper). Do ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" load the other backend's fragment.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Build the figure using the loaded material","type":"text"}]},{"type":"paragraph","content":[{"text":"Apply the loaded material in this order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Figure contract (","type":"text"},{"text":"core/contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":") — write the core conclusion, map the evidence chain, classify the archetype, set the journal/export contract, before any code.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-panel evidence architecture — when planning, restructuring, or auditing a labelled multi-panel figure, load ","type":"text"},{"text":"references/multipanel-evidence-architecture.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Make the figure answer one Results-level scientific question; assign panels different inferential roles, not merely different metrics. When figure order must follow the manuscript argument, also load ","type":"text"},{"text":"../nature-shared/core/nature-results-discussion.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Default stance (","type":"text"},{"text":"core/stance.md","type":"text","marks":[{"type":"code_inline"}]},{"text":") — archetype-first composition, hero panel, restrained palette, statistics/integrity as part of the figure.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Backend fragment — the exclusive Python or R quick-start and execution rule.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Template adaptation — when reusing built-in original examples, licensed external material, or user-provided plotting code, load ","type":"text"},{"text":"references/asset-adaptation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" before mapping data or changing the script.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rendered QA and delivery preflight — load ","type":"text"},{"text":"references/qa-contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", run the render-time panel-alignment gate for every multi-panel figure, ","type":"text"},{"text":"scripts/validate_figure.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" on the plotting source, ","type":"text"},{"text":"scripts/audit_pdf_text.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" on the exported PDF, and ","type":"text"},{"text":"scripts/audit_figure_collisions.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" on the same final PDF. Then inspect every panel and the complete figure at final physical size. Automated checks do not replace the panel-by-panel uncertainty, salience, spacing, and ambiguity audit.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"For every figure containing two or more comparable panels, measure the ","type":"text"},{"text":"final rendered plot-area rectangles","type":"text","marks":[{"type":"strong"}]},{"text":" before export and preserve the alignment JSON. Python figures must call ","type":"text"},{"text":"require_matplotlib_panel_alignment()","type":"text","marks":[{"type":"code_inline"}]},{"text":" from ","type":"text"},{"text":"scripts/audit_panel_alignment.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" after the final layout draw. R/patchwork figures must source ","type":"text"},{"text":"scripts/panel_alignment.R","type":"text","marks":[{"type":"code_inline"}]},{"text":", write the patchwork layout manifest at the final export dimensions, and run the same backend-neutral JSON auditor. Use a default physical tolerance of ","type":"text"},{"text":"1.5 pt","type":"text","marks":[{"type":"code_inline"}]},{"text":" for shared edges, widths, heights, panel-label anchors and repeated gutters. ","type":"text"},{"text":"FIX BEFORE DELIVERY","type":"text","marks":[{"type":"code_inline"}]},{"text":" or exit code ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":" blocks export; ","type":"text"},{"text":"NOT AUDITABLE","type":"text","marks":[{"type":"code_inline"}]},{"text":" or exit code ","type":"text"},{"text":"2","type":"text","marks":[{"type":"code_inline"}]},{"text":" blocks any claim that alignment passed. A horizontal row of three or four equal-grid-span panels must have equal final plot-area widths as well as equal heights and gutters; an intentional unequal-width design requires a recorded ","type":"text"},{"text":"panel-width","type":"text","marks":[{"type":"code_inline"}]},{"text":" exemption. Structured unequal-span grids—including two stacked panels beside one panel spanning both rows, in either column—must be inferred from shared grid start/stop boundaries and checked automatically. Nested grids, free-positioned hero panels, insets and colorbars may be excluded only through explicit comparable groups or a recorded exemption with a reason. Do not weaken the global tolerance to hide one intentional exception.","type":"text"}]},{"type":"paragraph","content":[{"text":"After every generated or revised Python/R scientific figure, export the final PDF and run the collision audit again; this is mandatory after any change to data geometry, text, fonts, legends, annotations, axes, error bars, panel size or layout, not only at final submission. Use:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python skills/nature-figure/scripts/audit_figure_collisions.py figure.pdf \\\n --json-out figure.collision-audit.json \\\n --overlay-pdf figure.collision-audit.pdf","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FIX BEFORE DELIVERY","type":"text","marks":[{"type":"code_inline"}]},{"text":" or exit code ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":": repair the figure, re-export with the selected plotting backend, and rerun all rendered QA.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REVIEW REQUIRED","type":"text","marks":[{"type":"code_inline"}]},{"text":": inspect every WARN at final physical size; record why an intentional overlay is acceptable. Use ","type":"text"},{"text":"--strict","type":"text","marks":[{"type":"code_inline"}]},{"text":" when WARN must block.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"NOT AUDITABLE","type":"text","marks":[{"type":"code_inline"}]},{"text":" or exit code ","type":"text"},{"text":"2","type":"text","marks":[{"type":"code_inline"}]},{"text":": report the dependency/PDF blocker and do not claim collision validation. Install ","type":"text"},{"text":"requirements.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":" when PyMuPDF is absent.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The collision audit reads PDF geometry for both Python and R output. It does not redraw the scientific figure or authorize cross-backend plotting. Its optional marked PDF is a QA-only diagnostic artifact and must never replace the selected backend's source or submission files.","type":"text"}]},{"type":"paragraph","content":[{"text":"When the target is the flagship journal Nature, also load ","type":"text"},{"text":"references/nature-article-requirements.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". It separates initial-review files from accepted-in-principle main and Extended Data production contracts and owns the flagship legend limit.","type":"text"}]},{"type":"paragraph","content":[{"text":"When the target is Nature Machine Intelligence, instead load ","type":"text"},{"text":"../nature-shared/journal-formats/nature-machine-intelligence.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Apply its combined six-item main display budget, ten-item Extended Data maximum, initial-versus-production boundary, 300-dpi/180-mm production checks and source- data contract. NMI's current live pages do not assign a standalone per-legend number, but its official 2018 brief guide set a historical advisory ceiling of fewer than 300 English words per complete figure legend. Count the whole legend, not each panel; aim for 150–250 words and keep it below 300 unless the live submission system or editor gives a newer instruction. Do not import flagship Nature's limit.","type":"text"}]},{"type":"paragraph","content":[{"text":"The chart serves the scientific logic; aesthetic polish is subordinate to making the core conclusion clear, defensible, and reviewable.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Reach for references only when needed","type":"text"}]},{"type":"paragraph","content":[{"text":"The files under ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" are deep references, not defaults. Open them on demand per the ","type":"text"},{"text":"references.on_demand","type":"text","marks":[{"type":"code_inline"}]},{"text":" table in the manifest — for example ","type":"text"},{"text":"references/figure-contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" to build the contract, ","type":"text"},{"text":"references/multipanel-evidence-architecture.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" to turn one Results-level question into complementary panel roles and a claim-escalating figure sequence, ","type":"text"},{"text":"references/asset-adaptation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" to reuse a plotting template safely, ","type":"text"},{"text":"references/template-catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for validated Python CSV templates, ","type":"text"},{"text":"references/api.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the Python palette and numerical/layout safety helpers, ","type":"text"},{"text":"references/r-workflow.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for R, ","type":"text"},{"text":"references/design-theory.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for color/typography/export rationale, ","type":"text"},{"text":"references/common-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"references/chart-types.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for layout/chart recipes, ","type":"text"},{"text":"references/nature-2026-observations.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for real Nature page archetypes, ","type":"text"},{"text":"references/qa-contract.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" before final delivery, ","type":"text"},{"text":"references/nature-article-requirements.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for exact flagship Nature stage and upload rules, ","type":"text"},{"text":"../nature-shared/journal-formats/nature-machine-intelligence.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for exact NMI figure rules, ","type":"text"},{"text":"references/ai-graphical-abstract-workflow.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for AI-assisted graphical-abstract planning, policy gating, human verification, and provenance, and ","type":"text"},{"text":"references/tutorials.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"references/demos.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for worked examples.","type":"text"}]},{"type":"paragraph","content":[{"text":"Do not infer flagship Nature or NMI requirements from a Nature Communications corpus or from the visual-style examples in this skill.","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"nature-figure","author":"@skillopedia","source":{"stars":51879,"repo_name":"nature-skills","origin_url":"https://github.com/Yuan1z0825/nature-skills/blob/HEAD/skills/nature-figure/SKILL.md","repo_owner":"Yuan1z0825","body_sha256":"5aefe708a2531c5ddaa8ee74d5155d747e96c0f21a33143d38316b7c5c88ffc4","cluster_key":"913470020b4118766f9d13e70a539ed34506e170e9ef5bd5502f05087f27e5ff","clean_bundle":{"format":"clean-skill-bundle-v1","source":"Yuan1z0825/nature-skills/skills/nature-figure/SKILL.md","text_bytes":584986,"source_commit":"69817e4dad8bfc52fa4e59a9d8355e4264a4d3a7","skills_sh_rank":1516,"attachment_count":71,"text_attachments":71,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/nature-figure/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"design-ux","category_label":"Design"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"design-ux","import_tag":"skills-sh-top20k-2026-11","description":"Create, revise, audit, and export manuscript scientific figures in Python or R. Use for 论文配图、科研绘图、多面板图 and submission-ready plots, or explicitly requested AI-generated graphical abstracts and mechanism schematics. Not for interactive dashboards, data cleaning, or statistics-only analysis."}},"renderedAt":1793755463125}

Nature Figure Making — Router Routing protocol For a new task, load the core and matching resources below. Reuse already loaded guidance on follow-ups; load more only when the task needs it. 0. Check for graphical-abstract and AI-schematic routes For every graphical-abstract planning, generation, revision, or audit task that uses AI, read references/ai-graphical-abstract-workflow.md first. It owns the message/audience brief, composition and palette workflow, policy gate, human scientific review, disclosure boundary, and provenance requirements. A Nature Careers article is practitioner advice,…