| |
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import matplotlib.pyplot as plt |
| import numpy as np |
| import pandas as pd |
| from matplotlib.ticker import FuncFormatter |
|
|
|
|
| |
| |
| |
|
|
| SCRIPT_DIR = Path(__file__).resolve().parent |
| xlsx_path = SCRIPT_DIR / "Context_Consuming.xlsx" |
|
|
| |
| bio_df = pd.read_excel( |
| xlsx_path, |
| sheet_name=0, |
| usecols="A:E", |
| header=1, |
| ) |
|
|
| bio_df.columns = [ |
| "scale", |
| "task", |
| "prompt", |
| "completion", |
| "total", |
| ] |
|
|
| bio_df["scale"] = bio_df["scale"].ffill() |
| bio_df["task"] = ( |
| bio_df["task"] |
| .astype(str) |
| .str.replace("\ufeff", "", regex=False) |
| .str.strip() |
| ) |
|
|
| scale_map = { |
| 0: "No MCP", |
| 100: "Biomni-100", |
| 500: "Biomni-500", |
| 1000: "Biomni-1k", |
| 2000: "Biomni-2k", |
| } |
|
|
| bio_df["method"] = bio_df["scale"].map(scale_map) |
|
|
| |
| bio_last = bio_df.tail(10).copy() |
| bio_last["method"] = "BioManus" |
| bio_df = pd.concat([bio_df.iloc[:-10], bio_last], ignore_index=True) |
|
|
| bio_df = bio_df[bio_df["method"].notna()].copy() |
|
|
|
|
| |
| closing_df = pd.read_excel( |
| xlsx_path, |
| sheet_name=0, |
| usecols="I:L", |
| header=1, |
| ) |
|
|
| closing_df.columns = [ |
| "model", |
| "prompt", |
| "completion", |
| "total", |
| ] |
|
|
| closing_df["model"] = closing_df["model"].astype(str).str.strip() |
|
|
| closing_name_map = { |
| "Biomni": "Biomni", |
| "Biomni with 100 tool scale": "Biomni-100", |
| "Biomni with 500 tool scale": "Biomni-500", |
| "Biomni with 1000 tool scale": "Biomni-1k", |
| "Biomni with 2000 tool scale": "Biomni-2k", |
| "Biomanus": "BioManus", |
| } |
|
|
| closing_df["method"] = closing_df["model"].map(closing_name_map) |
| closing_df = closing_df[closing_df["method"].notna()].copy() |
|
|
|
|
| |
| |
| |
|
|
| method_order = [ |
| "No MCP", |
| "Biomni-100", |
| "Biomni-500", |
| "Biomni-1k", |
| "Biomni-2k", |
| "BioManus", |
| ] |
|
|
| closing_order = [ |
| "Biomni", |
| "Biomni-100", |
| "Biomni-500", |
| "Biomni-1k", |
| "Biomni-2k", |
| "BioManus", |
| ] |
|
|
| task_order = ( |
| bio_df[bio_df["method"] == "No MCP"]["task"] |
| .drop_duplicates() |
| .tolist() |
| ) |
|
|
| |
| task_score = ( |
| bio_df.groupby("task")["prompt"] |
| .mean() |
| .sort_values() |
| ) |
|
|
| task_order = [t for t in task_score.index.tolist() if t in task_order] |
|
|
|
|
| def wrap_task_label(task: str) -> str: |
| mapping = { |
| "alzheimer-mouse": "Alzheimer\nmouse", |
| "comparative-genomics": "Comparative\ngenomics", |
| "cystic-fibrosis": "Cystic\nfibrosis", |
| "deseq": "DESeq", |
| "evolution": "Evolution", |
| "giab": "GIAB", |
| "metagenomics": "Metagenomics", |
| "single-cell": "Single-cell", |
| "transcript-quant": "Transcript\nquant", |
| "viral-metagenomics": "Viral\nmetagenomics", |
| } |
| return mapping.get(task, task.replace("-", "\n")) |
|
|
|
|
| |
| |
| |
|
|
| colors = { |
| "No MCP": "#8A8F98", |
| "Biomni": "#8A8F98", |
| "Biomni-100": "#C9853B", |
| "Biomni-500": "#B8742A", |
| "Biomni-1k": "#9C5C1A", |
| "Biomni-2k": "#6F3B0D", |
| "BioManus": "#C62828", |
| } |
|
|
| fills = { |
| "No MCP": "#ECEFF3", |
| "Biomni": "#ECEFF3", |
| "Biomni-100": "#F1D9BA", |
| "Biomni-500": "#E8C390", |
| "Biomni-1k": "#D8A96E", |
| "Biomni-2k": "#C69054", |
| "BioManus": "#F1C9C9", |
| } |
|
|
| plt.rcParams.update( |
| { |
| "font.family": "DejaVu Sans", |
| "font.size": 10.3, |
| "axes.titlesize": 12.3, |
| "axes.labelsize": 10.3, |
| "xtick.labelsize": 8.8, |
| "ytick.labelsize": 9.0, |
| "legend.fontsize": 8.6, |
| "pdf.fonttype": 42, |
| "ps.fonttype": 42, |
| } |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def token_formatter(value: float, _pos=None): |
| if value >= 1_000_000: |
| return f"{value / 1_000_000:.1f}M" |
| if value >= 1_000: |
| return f"{value / 1_000:.0f}K" |
| return f"{value:.0f}" |
|
|
|
|
| def compact_token_formatter(value: float) -> str: |
| if value >= 1_000_000: |
| return f"{value / 1_000_000:.1f}M" |
| if value >= 1_000: |
| return f"{value / 1_000:.0f}K" |
| return f"{value:.0f}" |
|
|
|
|
| def percent_reduction(base: float, target: float) -> float: |
| return 100.0 * (base - target) / base |
|
|
|
|
| |
| |
| |
|
|
| pivot = bio_df.pivot_table( |
| index="method", |
| columns="task", |
| values="prompt", |
| aggfunc="mean", |
| ) |
|
|
| available_methods = [m for m in method_order if m in pivot.index] |
| task_order = [ |
| t for t in task_order |
| if t in pivot.columns and pivot.loc[available_methods, t].notna().all() |
| ] |
|
|
| pivot = pivot.loc[available_methods, task_order] |
|
|
| log_pivot = np.log10(pivot.astype(float)) |
|
|
| |
| n_tasks = len(task_order) |
| angles = np.linspace(0, 2 * np.pi, n_tasks, endpoint=False) |
| angles_closed = np.concatenate([angles, [angles[0]]]) |
|
|
|
|
| |
| |
| |
|
|
| fig = plt.figure(figsize=(12.2, 5.65)) |
|
|
| gs = fig.add_gridspec( |
| 1, |
| 2, |
| width_ratios=[1.25, 1.0], |
| wspace=0.42, |
| ) |
|
|
| ax_a = fig.add_subplot(gs[0, 0], projection="polar") |
| ax_b = fig.add_subplot(gs[0, 1]) |
|
|
|
|
| |
| |
| |
|
|
| ax_a.set_theta_offset(np.pi / 2) |
| ax_a.set_theta_direction(-1) |
|
|
| r_min = 5.0 |
| r_data_max = max(7.3, float(np.nanmax(log_pivot.values)) + 0.15) |
| r_max = r_data_max + 0.18 |
| ax_a.set_ylim(r_min, r_max) |
|
|
| ax_a.set_xticks(angles) |
| ax_a.set_xticklabels([]) |
|
|
| radial_ticks = [5, 6, 7] |
| radial_ticks = [t for t in radial_ticks if r_min <= t <= r_max] |
| ax_a.set_yticks(radial_ticks) |
| ax_a.set_yticklabels([rf"$10^{t}$" for t in radial_ticks], fontsize=8.4, color="#4B5563") |
| ax_a.set_rlabel_position(88) |
|
|
| ax_a.grid(True, color="#CBD5E1", linewidth=0.65, alpha=0.75) |
| ax_a.spines["polar"].set_color("#94A3B8") |
| ax_a.spines["polar"].set_linewidth(0.8) |
|
|
| label_radius = r_max + 0.34 |
| for angle, task in zip(angles, task_order): |
| display_angle = np.pi / 2 - angle |
| x = np.cos(display_angle) |
| y_pos = np.sin(display_angle) |
| ha = "center" |
| if x > 0.22: |
| ha = "left" |
| elif x < -0.22: |
| ha = "right" |
| va = "center" |
| if y_pos > 0.78: |
| va = "bottom" |
| elif y_pos < -0.78: |
| va = "top" |
| ax_a.text( |
| angle, |
| label_radius, |
| wrap_task_label(task), |
| ha=ha, |
| va=va, |
| fontsize=8.2, |
| color="#374151", |
| clip_on=False, |
| bbox={ |
| "boxstyle": "round,pad=0.12", |
| "facecolor": "white", |
| "edgecolor": "none", |
| "alpha": 0.82, |
| }, |
| ) |
|
|
| for method in available_methods: |
| values = log_pivot.loc[method, task_order].values.astype(float) |
| values_closed = np.concatenate([values, [values[0]]]) |
|
|
| lw = 2.5 if method == "BioManus" else 1.45 |
| alpha = 1.0 if method == "BioManus" else 0.82 |
|
|
| ax_a.plot( |
| angles_closed, |
| values_closed, |
| color=colors[method], |
| linewidth=lw, |
| alpha=alpha, |
| marker="o", |
| markersize=4.8 if method == "BioManus" else 3.2, |
| markeredgecolor="white", |
| markeredgewidth=0.55, |
| label=method, |
| ) |
|
|
| if method == "BioManus": |
| ax_a.fill( |
| angles_closed, |
| values_closed, |
| color=colors[method], |
| alpha=0.06, |
| ) |
|
|
| raw_values = pivot.loc[method, task_order].values.astype(float) |
| for angle, radius, raw_value in zip(angles, values, raw_values): |
| display_angle = np.pi / 2 - angle |
| horizontal = np.cos(display_angle) |
| vertical = np.sin(display_angle) |
| text_radius = min(radius + 0.13, r_max - 0.04) |
| ha = "left" if horizontal > 0.18 else "right" if horizontal < -0.18 else "center" |
| va = "bottom" if vertical > 0.18 else "top" if vertical < -0.18 else "center" |
| ax_a.text( |
| angle, |
| text_radius, |
| compact_token_formatter(raw_value), |
| ha=ha, |
| va=va, |
| fontsize=6.9, |
| color=colors[method], |
| fontweight="bold", |
| clip_on=False, |
| bbox={ |
| "boxstyle": "round,pad=0.10", |
| "facecolor": "white", |
| "edgecolor": "none", |
| "alpha": 0.72, |
| }, |
| ) |
|
|
| ax_a.set_title( |
| "", |
| ) |
|
|
| ax_a.legend( |
| loc="upper center", |
| bbox_to_anchor=(0.5, -0.20), |
| ncol=3, |
| frameon=False, |
| handlelength=2.2, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| closing_values = np.asarray([ |
| closing_df[closing_df["method"] == m]["prompt"].iloc[0] |
| for m in closing_order |
| ]) |
|
|
| y = np.arange(len(closing_order)) |
|
|
| for i, method in enumerate(closing_order): |
| ax_b.barh( |
| y[i], |
| closing_values[i], |
| height=0.48, |
| color=fills[method], |
| edgecolor=colors[method], |
| linewidth=1.25, |
| zorder=3, |
| ) |
|
|
| ax_b.scatter( |
| closing_values[i], |
| y[i], |
| s=32, |
| color=colors[method], |
| edgecolor="white", |
| linewidth=0.6, |
| zorder=4, |
| ) |
|
|
| ax_b.text( |
| closing_values[i] + 45_000, |
| y[i], |
| token_formatter(closing_values[i]), |
| va="center", |
| ha="left", |
| fontsize=8.8, |
| color=colors[method], |
| fontweight="bold" if method == "BioManus" else "normal", |
| ) |
|
|
| ax_b.set_yticks(y) |
| ax_b.set_yticklabels(closing_order) |
| ax_b.invert_yaxis() |
|
|
| ax_b.xaxis.set_major_formatter(FuncFormatter(token_formatter)) |
| ax_b.set_xlabel("Average prompt tokens per case") |
| ax_b.set_title( |
| "", |
| ) |
|
|
| ax_b.grid(axis="x", linestyle="-", linewidth=0.6, alpha=0.22) |
| ax_b.spines["top"].set_visible(False) |
| ax_b.spines["right"].set_visible(False) |
| ax_b.spines["left"].set_color("#CBD5E1") |
| ax_b.spines["bottom"].set_color("#CBD5E1") |
|
|
| ax_b.set_xlim(0, max(closing_values) * 1.22) |
|
|
| bio = closing_df[closing_df["method"] == "BioManus"]["prompt"].iloc[0] |
| biomni = closing_df[closing_df["method"] == "Biomni"]["prompt"].iloc[0] |
| biomni2k = closing_df[closing_df["method"] == "Biomni-2k"]["prompt"].iloc[0] |
|
|
| ax_b.text( |
| 0.03, |
| 1.04, |
| f"BioManus reduces context\n" |
| f"{percent_reduction(biomni, bio):.1f}% vs. Biomni\n" |
| f"{percent_reduction(biomni2k, bio):.1f}% vs. Biomni-2k", |
| transform=ax_b.transAxes, |
| fontsize=8.8, |
| color="#374151", |
| va="bottom", |
| bbox={ |
| "boxstyle": "round,pad=0.35", |
| "facecolor": "white", |
| "edgecolor": "#E5E7EB", |
| "alpha": 0.95, |
| }, |
| clip_on=False, |
| ) |
|
|
| fig.text( |
| 0.028, |
| 0.948, |
| "(a) BioAgentBench task-level context profile", |
| ha="left", |
| va="top", |
| fontsize=12.3, |
| fontweight="bold", |
| ) |
| fig.text( |
| 0.635, |
| 0.948, |
| "(b) Closing Scenarios aggregate context", |
| ha="left", |
| va="top", |
| fontsize=12.3, |
| fontweight="bold", |
| ) |
|
|
|
|
| |
| |
| |
| fig.subplots_adjust( |
| left=0.075, |
| right=0.985, |
| top=0.76, |
| bottom=0.24, |
| wspace=0.42, |
| ) |
| out = SCRIPT_DIR / "context_efficiency_radar_bar" |
| fig.savefig(f"{out}.pdf", bbox_inches="tight") |
| fig.savefig(f"{out}.svg", bbox_inches="tight") |
| fig.savefig(f"{out}.png", dpi=450, bbox_inches="tight") |
|
|
| print("Saved:") |
| print(f" {out}.pdf") |
| print(f" {out}.svg") |
| print(f" {out}.png") |
|
|