| |
| from __future__ import annotations |
|
|
| import re |
| from pathlib import Path |
|
|
| import matplotlib.pyplot as plt |
| import numpy as np |
| import pandas as pd |
|
|
|
|
| |
| |
| |
| XLSX_PATH = Path("Context_Consuming.xlsx") |
| OUT_PREFIX = Path("bioagentbench_context_parallel_warm") |
|
|
| METRIC = "prompt_tokens" |
|
|
| METHOD_ORDER = [ |
| "No MCP", |
| "Biomni-100", |
| "Biomni-500", |
| "Biomni-1k", |
| "Biomni-2k", |
| "BioManus", |
| ] |
|
|
| METHOD_STYLE = { |
| "No MCP": { |
| "color": "#8A8F98", |
| "linewidth": 1.7, |
| "alpha": 0.72, |
| "zorder": 2, |
| }, |
| "Biomni-100": { |
| "color": "#C77C2A", |
| "linewidth": 1.8, |
| "alpha": 0.78, |
| "zorder": 3, |
| }, |
| "Biomni-500": { |
| "color": "#B66D18", |
| "linewidth": 1.8, |
| "alpha": 0.80, |
| "zorder": 3, |
| }, |
| "Biomni-1k": { |
| "color": "#9A5200", |
| "linewidth": 1.9, |
| "alpha": 0.86, |
| "zorder": 4, |
| }, |
| "Biomni-2k": { |
| "color": "#7A3E00", |
| "linewidth": 2.1, |
| "alpha": 0.92, |
| "zorder": 5, |
| }, |
| "BioManus": { |
| "color": "#D62728", |
| "linewidth": 2.8, |
| "alpha": 1.00, |
| "zorder": 7, |
| }, |
| } |
|
|
|
|
| |
| |
| |
| def clean_text(x) -> str: |
| if pd.isna(x): |
| return "" |
| s = str(x) |
| s = s.replace("\ufeff", "").replace("\u200b", "").replace("\u200c", "") |
| s = re.sub(r"\s+", " ", s) |
| return s.strip() |
|
|
|
|
| def method_name(x) -> str | None: |
| s = clean_text(x) |
| if not s: |
| return None |
|
|
| lower = s.lower() |
| if "biomanus" in lower: |
| return "BioManus" |
|
|
| try: |
| scale = int(float(s)) |
| except ValueError: |
| return None |
|
|
| if scale == 0: |
| return "No MCP" |
| if scale == 100: |
| return "Biomni-100" |
| if scale == 500: |
| return "Biomni-500" |
| if scale == 1000: |
| return "Biomni-1k" |
| if scale == 2000: |
| return "Biomni-2k" |
|
|
| |
| return None |
|
|
|
|
| def wrap_label(task: str) -> str: |
| mapping = { |
| "alzheimer-mouse": "Alzheimer\nmouse", |
| "cystic-fibrosis": "Cystic\nfibrosis", |
| "deseq": "DESeq", |
| "transcript-quant": "Transcript\nquant", |
| "single-cell": "Single-cell", |
| "metagenomics": "Metagenomics", |
| "viral-metagenomics": "Viral\nmetagenomics", |
| "comparative-genomics": "Comparative\ngenomics", |
| "evolution": "Evolution", |
| "giab": "GIAB", |
| } |
| return mapping.get(task, task.replace("-", "\n")) |
|
|
|
|
| def geometric_mean(values: np.ndarray) -> float: |
| values = np.asarray(values, dtype=float) |
| values = values[values > 0] |
| return float(np.exp(np.mean(np.log(values)))) if len(values) else np.nan |
|
|
|
|
| def format_token(x: float) -> str: |
| if x >= 1_000_000: |
| return f"{x / 1_000_000:.1f}M" |
| if x >= 1_000: |
| return f"{x / 1_000:.0f}K" |
| return f"{x:.0f}" |
|
|
|
|
| |
| |
| |
| raw = pd.read_excel(XLSX_PATH, sheet_name=0, header=None) |
|
|
| header_candidates = raw.index[ |
| raw.iloc[:, 1].astype(str).map(clean_text).eq("Tasks") |
| ].tolist() |
|
|
| if not header_candidates: |
| raise ValueError("Cannot find header row containing 'Tasks' in column B.") |
|
|
| header_idx = header_candidates[0] |
|
|
| df = raw.iloc[header_idx + 1 :, :6].copy() |
| df.columns = [ |
| "group", |
| "task", |
| "results_match", |
| "prompt_tokens", |
| "completion_tokens", |
| "total_tokens", |
| ] |
|
|
| df["group"] = df["group"].ffill() |
| df["method"] = df["group"].map(method_name) |
| df["task"] = df["task"].map(clean_text) |
|
|
| df = df[df["task"].ne("")] |
| df = df[df["method"].notna()] |
|
|
| for col in ["prompt_tokens", "completion_tokens", "total_tokens"]: |
| df[col] = pd.to_numeric(df[col], errors="coerce") |
|
|
| df = df.dropna(subset=[METRIC]) |
| df = df[df["method"].isin(METHOD_ORDER)] |
| df["method"] = pd.Categorical(df["method"], categories=METHOD_ORDER, ordered=True) |
|
|
| pivot = df.pivot_table( |
| index="method", |
| columns="task", |
| values=METRIC, |
| aggfunc="mean", |
| ) |
|
|
| available_methods = [m for m in METHOD_ORDER if m in pivot.index] |
|
|
| common_tasks = [ |
| t for t in pivot.columns |
| if pivot.loc[available_methods, t].notna().all() |
| ] |
|
|
| pivot = pivot.loc[available_methods, common_tasks] |
|
|
| if pivot.empty: |
| raise ValueError("No valid method-task matrix found.") |
|
|
| |
| log_pivot = np.log10(pivot.astype(float)) |
| task_score = log_pivot.mean(axis=0).sort_values(ascending=True) |
| tasks = task_score.index.tolist() |
| log_pivot = log_pivot.loc[available_methods, tasks] |
|
|
|
|
| |
| |
| |
| plt.rcParams.update( |
| { |
| "font.family": "DejaVu Serif", |
| "font.size": 10.5, |
| "axes.titlesize": 14.5, |
| "axes.labelsize": 11, |
| "legend.fontsize": 9.2, |
| "xtick.labelsize": 9.0, |
| "ytick.labelsize": 9.5, |
| "pdf.fonttype": 42, |
| "ps.fonttype": 42, |
| } |
| ) |
|
|
| fig, ax = plt.subplots(figsize=(10.2, 4.8)) |
|
|
| x = np.arange(len(tasks)) |
| y_min = 5.0 |
| y_max = max(7.2, float(np.nanmax(log_pivot.values)) + 0.15) |
|
|
| |
| ax.axhspan(y_min, y_max, color="#F6EFE7", alpha=0.55, zorder=0) |
|
|
| |
| for xi in x: |
| ax.vlines( |
| xi, |
| y_min, |
| y_max, |
| color="#D99A5B", |
| linewidth=5.0, |
| alpha=0.16, |
| zorder=1, |
| ) |
|
|
| |
| for y in [5, 6, 7]: |
| if y_min <= y <= y_max: |
| ax.axhline( |
| y, |
| color="#C9C1B8", |
| linewidth=0.8, |
| linestyle=":", |
| alpha=0.75, |
| zorder=1, |
| ) |
|
|
| |
| for method in available_methods: |
| style = METHOD_STYLE[method] |
| y = log_pivot.loc[method, tasks].values.astype(float) |
|
|
| ax.plot( |
| x, |
| y, |
| color=style["color"], |
| linewidth=style["linewidth"], |
| alpha=style["alpha"], |
| marker="o", |
| markersize=4.0 if method != "BioManus" else 5.0, |
| markeredgecolor="white", |
| markeredgewidth=0.6, |
| label=method, |
| zorder=style["zorder"], |
| ) |
|
|
| |
| if "BioManus" in available_methods: |
| y_bio = log_pivot.loc["BioManus", tasks].values.astype(float) |
| ax.fill_between( |
| x, |
| y_bio, |
| y_min, |
| color=METHOD_STYLE["BioManus"]["color"], |
| alpha=0.055, |
| zorder=2, |
| ) |
|
|
| |
| ax.set_xlim(-0.25, len(tasks) - 0.75) |
| ax.set_ylim(y_min, y_max) |
|
|
| ax.set_xticks(x) |
| ax.set_xticklabels([wrap_label(t) for t in tasks], rotation=30, ha="right") |
|
|
| ax.set_yticks([5, 6, 7]) |
| ax.set_yticklabels([r"$10^5$", r"$10^6$", r"$10^7$"]) |
|
|
| ax.set_ylabel("Prompt tokens per task (log scale)") |
| ax.set_title( |
| "Context Consumption Across BioAgentBench Tasks", |
| pad=12, |
| fontweight="bold", |
| ) |
|
|
| |
| ax.spines["top"].set_visible(False) |
| ax.spines["right"].set_visible(False) |
| ax.spines["left"].set_color("#7A6A5A") |
| ax.spines["bottom"].set_color("#7A6A5A") |
|
|
| ax.tick_params(axis="x", length=0, pad=8) |
| ax.tick_params(axis="y", colors="#4B423A") |
|
|
| |
| ax.legend( |
| loc="upper center", |
| bbox_to_anchor=(0.5, -0.25), |
| ncol=6, |
| frameon=False, |
| handlelength=2.0, |
| columnspacing=1.10, |
| ) |
|
|
| fig.tight_layout(rect=[0, 0.10, 1, 1]) |
|
|
| |
| fig.savefig(f"{OUT_PREFIX}.pdf", bbox_inches="tight") |
| fig.savefig(f"{OUT_PREFIX}.png", dpi=400, bbox_inches="tight") |
| fig.savefig(f"{OUT_PREFIX}.svg", bbox_inches="tight") |
|
|
| print("Saved:") |
| print(f" {OUT_PREFIX}.pdf") |
| print(f" {OUT_PREFIX}.png") |
| print(f" {OUT_PREFIX}.svg") |
|
|
| print("\nTask order from low to high context:") |
| for i, task in enumerate(tasks, 1): |
| print(f"{i:02d}. {task}") |
|
|
| print("\nGeometric mean prompt tokens:") |
| for method in available_methods: |
| gm = geometric_mean(pivot.loc[method, tasks].values.astype(float)) |
| print(f"{method:12s}: {format_token(gm)}") |