| """ |
| Visualisation utilities for Trigger-Off experiment results. |
| |
| Requires: matplotlib |
| """ |
|
|
| from __future__ import annotations |
|
|
| from typing import Any, Dict, List, Optional |
|
|
|
|
| |
| |
| |
|
|
| def _ensure_matplotlib() -> None: |
| try: |
| import matplotlib |
| except ImportError as exc: |
| raise ImportError( |
| "matplotlib is required for visualisation. " |
| "Install with: pip install matplotlib" |
| ) from exc |
|
|
|
|
| |
| |
| |
|
|
| def plot_main_results( |
| results_dict: Dict[str, Dict[str, float]], |
| metric: str = "step_acc", |
| title: str = "Trigger-Off: Main Results", |
| save_path: Optional[str] = None, |
| ) -> None: |
| """ |
| Plot a bar chart comparing Step-Acc (or another metric) across baselines. |
| |
| Args: |
| results_dict: {baseline_name: {metric_name: value, ...}} |
| e.g. {"SeeClick-Clean": {"step_acc": 0.72, ...}, |
| "SeeClick+TriggerOff": {"step_acc": 0.68, ...}} |
| metric: Which metric to plot (default: "step_acc"). |
| title: Chart title. |
| save_path: If provided, save the figure to this path. |
| """ |
| _ensure_matplotlib() |
| import matplotlib.pyplot as plt |
| import numpy as np |
|
|
| labels = list(results_dict.keys()) |
| values = [results_dict[k].get(metric, 0.0) for k in labels] |
|
|
| x = np.arange(len(labels)) |
| width = 0.55 |
|
|
| fig, ax = plt.subplots(figsize=(max(6, len(labels) * 1.4), 5)) |
| bars = ax.bar(x, values, width, color="steelblue", edgecolor="white", alpha=0.85) |
|
|
| ax.set_ylabel(metric.replace("_", " ").title()) |
| ax.set_title(title) |
| ax.set_xticks(x) |
| ax.set_xticklabels(labels, rotation=20, ha="right", fontsize=9) |
| ax.set_ylim(0, min(1.05, max(values) * 1.2 + 0.05)) |
|
|
| for bar, val in zip(bars, values): |
| ax.text( |
| bar.get_x() + bar.get_width() / 2.0, |
| bar.get_height() + 0.01, |
| f"{val:.3f}", |
| ha="center", |
| va="bottom", |
| fontsize=8, |
| ) |
|
|
| fig.tight_layout() |
|
|
| if save_path: |
| fig.savefig(save_path, dpi=150, bbox_inches="tight") |
| else: |
| plt.show() |
|
|
| plt.close(fig) |
|
|
|
|
| |
| |
| |
|
|
| def plot_ablation( |
| ablation_results: Dict[str, List[float]], |
| variable_name: str, |
| x_values: List[Any], |
| metric: str = "step_acc", |
| title: Optional[str] = None, |
| save_path: Optional[str] = None, |
| ) -> None: |
| """ |
| Plot a line chart for ablation experiments. |
| |
| Args: |
| ablation_results: {series_name: [value_at_x1, value_at_x2, ...]} |
| e.g. {"clean_acc": [0.72, 0.71, 0.70], |
| "attack_acc": [0.40, 0.32, 0.28], |
| "immunity_acc": [0.70, 0.69, 0.68]} |
| variable_name: X-axis label, e.g. "poison_ratio". |
| x_values: Values on the X-axis, e.g. [0.10, 0.15, 0.20, 0.25]. |
| metric: Metric label for Y axis (default: "step_acc"). |
| title: Chart title. |
| save_path: If provided, save the figure to this path. |
| """ |
| _ensure_matplotlib() |
| import matplotlib.pyplot as plt |
|
|
| fig, ax = plt.subplots(figsize=(7, 4)) |
|
|
| markers = ["o", "s", "^", "D", "v", "P"] |
| for i, (series_name, y_values) in enumerate(ablation_results.items()): |
| marker = markers[i % len(markers)] |
| ax.plot(x_values, y_values, marker=marker, label=series_name, linewidth=2) |
|
|
| ax.set_xlabel(variable_name.replace("_", " ").title()) |
| ax.set_ylabel(metric.replace("_", " ").title()) |
| ax.set_title(title or f"Ablation: {variable_name}") |
| ax.legend(loc="best", fontsize=9) |
| ax.set_ylim(0, 1.05) |
| ax.grid(True, linestyle="--", alpha=0.5) |
|
|
| fig.tight_layout() |
|
|
| if save_path: |
| fig.savefig(save_path, dpi=150, bbox_inches="tight") |
| else: |
| plt.show() |
|
|
| plt.close(fig) |
|
|
|
|
| |
| |
| |
|
|
| def print_results_table(results: Dict[str, Any]) -> None: |
| """ |
| Print a nicely formatted results table to stdout. |
| |
| Args: |
| results: Dict of {scenario_name: EvalResult_or_dict}. |
| """ |
| |
| rows: Dict[str, Dict[str, Any]] = {} |
| for name, r in results.items(): |
| if hasattr(r, "to_dict"): |
| rows[name] = r.to_dict() |
| elif isinstance(r, dict): |
| rows[name] = r |
| else: |
| rows[name] = {"value": r} |
|
|
| if not rows: |
| print("(No results to display.)") |
| return |
|
|
| |
| columns = ["step_acc", "task_sr", "asr", "immunity_rate"] |
| col_labels = { |
| "step_acc": "Step-Acc", |
| "task_sr": "Task-SR", |
| "asr": "ASR", |
| "immunity_rate": "Imm-Rate", |
| } |
|
|
| |
| name_width = max(len(n) for n in rows) + 2 |
| col_width = 10 |
|
|
| header = f"{'Scenario':<{name_width}}" + "".join( |
| f"{col_labels.get(c, c):>{col_width}}" for c in columns |
| ) |
| separator = "-" * len(header) |
|
|
| print("\n" + separator) |
| print(header) |
| print(separator) |
|
|
| for name, row in rows.items(): |
| line = f"{name:<{name_width}}" |
| for col in columns: |
| val = row.get(col, None) |
| if val is None: |
| cell = "N/A" |
| else: |
| cell = f"{val:.4f}" |
| line += f"{cell:>{col_width}}" |
| print(line) |
|
|
| print(separator + "\n") |
|
|