1
Fork 0
This repository has been archived on 2026-08-15. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
2026SEL3-project-Brittle_St.../scripts/plots/analyze_comparisons.py
2026-05-19 10:28:26 +02:00

418 lines
14 KiB
Python

"""
Poster Comparison Visualizations
This script generates a Forward Velocity plot and three secondary plots (Accumulated Reward, Success
Rate, Distance Remaining).
"""
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from plot_config import (
BEST_PERFORMER_COLOR,
BEST_PERFORMER_TEXT,
COLORS,
LEGEND_KWARGS,
apply_style,
create_common_parser,
)
def load_and_preprocess_data(filepath):
"""Loads CSV and prepares the metrics for plotting."""
df = pd.read_csv(filepath)
# Ensure success rate can be averaged numerically
if "reached_target" in df.columns:
df["reached_target"] = df["reached_target"].astype(int)
return df
def _add_square_placeholders(ax, x_positions, labels):
"""Adds square placeholders for images below the x-axis."""
for x, label in zip(x_positions, labels):
# Create a roughly square rectangle in a mix of data/axes coords
# Shifted down to avoid overlapping with x-tick labels
rect = plt.Rectangle(
(x - 0.25, -0.40),
0.5,
0.18,
transform=ax.get_xaxis_transform(),
facecolor="#F0F0F0",
edgecolor="#A9A9A9",
linestyle="--",
zorder=1,
clip_on=False,
)
ax.add_patch(rect)
ax.text(
x,
-0.31,
f"[ Insert {label}\nImage ]",
transform=ax.get_xaxis_transform(),
ha="center",
va="center",
fontsize=10,
color="#888888",
zorder=2,
)
def plot_grouped_bar(
df,
metric_col,
ylabel,
title,
output_filename,
output_dir,
higher_is_better=True,
show_titles=False,
figsize=(12, 8),
):
"""Generates and saves a highly customized grouped bar chart (grouped by Morphology)."""
grouped = (
df.groupby(["num_active_arms", "architecture"])[metric_col]
.agg(["mean", "std"])
.reset_index()
)
morphologies = sorted(grouped["num_active_arms"].unique(), reverse=True)
architectures = grouped["architecture"].unique()
fig, ax = plt.subplots(figsize=figsize)
bar_width = 0.35
group_spacing = 1.3
x_indices = np.arange(len(morphologies)) * group_spacing
all_bars = {}
all_means = []
for i, arch in enumerate(architectures):
arch_data = grouped[grouped["architecture"] == arch]
means = [
arch_data[arch_data["num_active_arms"] == m]["mean"].values[0]
if not arch_data[arch_data["num_active_arms"] == m].empty
else 0
for m in morphologies
]
stds = [
arch_data[arch_data["num_active_arms"] == m]["std"].values[0]
if not arch_data[arch_data["num_active_arms"] == m].empty
else 0
for m in morphologies
]
all_means.extend(means)
x_pos = x_indices + (i * bar_width) - (bar_width / 2 if len(architectures) == 2 else 0)
color = COLORS.get(arch, "#888888")
clean_label = arch.replace("_", " ").title()
bars = ax.bar(
x_pos,
means,
bar_width,
yerr=stds,
label=clean_label,
color=color,
capsize=8,
error_kw={"elinewidth": 2, "alpha": 0.7},
)
all_bars[arch] = (x_pos, means, stds, bars)
for m_idx, _ in enumerate(morphologies):
m_means = {arch: all_bars[arch][1][m_idx] for arch in architectures}
best_arch = (
max(m_means, key=m_means.get) if higher_is_better else min(m_means, key=m_means.get)
)
best_x = all_bars[best_arch][0][m_idx]
best_y = all_bars[best_arch][1][m_idx]
best_std = all_bars[best_arch][2][m_idx]
offset = best_std + (abs(max(m_means.values())) * 0.05) if m_means.values() else 0
ax.text(
best_x,
best_y + offset,
BEST_PERFORMER_TEXT,
ha="center",
va="bottom",
fontsize=28,
color=BEST_PERFORMER_COLOR,
)
# Aesthetics
ax.set_ylabel(ylabel, labelpad=15)
if show_titles:
ax.set_title(title, pad=25, fontweight="bold")
x_ticks_pos = (
x_indices
+ bar_width # center the label in the 3 bars
+ (bar_width / 2 if len(architectures) % 2 == 0 else 0)
- (bar_width / 2 if len(architectures) == 2 else 0)
)
ax.set_xticks(x_ticks_pos)
ax.set_xticklabels([f"{m} Arms" for m in morphologies])
ax.tick_params(axis="x") # More padding for the squares
# X-axis at zero
ax.axhline(0, color="black", linewidth=1.5)
ax.spines["bottom"].set_visible(False)
# Y-axis limits explicitly including 0
if all_means:
min_val = min([*all_means, 0])
max_val = max([*all_means, 0])
margin = (max_val - min_val) * 0.15 if max_val != min_val else 0.1
ax.set_ylim(min_val - margin, max_val + margin * 1.5) # Extra top margin for stars
# Format y-ticks to not have excessive decimals, include 0
ticks = (
[min_val, max_val]
if min_val == 0 and max_val == 0
else sorted(list(set([min_val, 0, max_val])))
)
ax.set_yticks(ticks)
ax.yaxis.set_major_formatter(
plt.FuncFormatter(lambda x, _: f"{x:.2f}" if abs(x) < 10 else f"{x:.0f}")
)
ax.legend(**LEGEND_KWARGS, ncol=len(architectures))
ax.set_facecolor("white")
fig.patch.set_facecolor("white")
os.makedirs(output_dir, exist_ok=True)
base_path = os.path.join(output_dir, os.path.splitext(output_filename)[0])
plt.savefig(f"{base_path}.png", dpi=300, bbox_inches="tight")
plt.savefig(f"{base_path}.svg", format="svg", bbox_inches="tight")
plt.close()
def plot_grouped_bar_alt(
df,
metric_col,
ylabel,
title,
output_filename,
output_dir,
higher_is_better=True,
show_titles=False,
figsize=(12, 8),
):
"""Generates and saves a highly customized grouped bar chart (grouped by Architecture)."""
grouped = (
df.groupby(["architecture", "num_active_arms"])[metric_col]
.agg(["mean", "std"])
.reset_index()
)
architectures = sorted(grouped["architecture"].unique())
morphologies = sorted(grouped["num_active_arms"].unique(), reverse=True)
fig, ax = plt.subplots(figsize=figsize)
bar_width = 0.8 / len(morphologies)
x_indices = np.arange(len(architectures))
all_bars = {}
all_means = []
for i, m in enumerate(morphologies):
m_data = grouped[grouped["num_active_arms"] == m]
means = [
m_data[m_data["architecture"] == arch]["mean"].values[0]
if not m_data[m_data["architecture"] == arch].empty
else 0
for arch in architectures
]
stds = [
m_data[m_data["architecture"] == arch]["std"].values[0]
if not m_data[m_data["architecture"] == arch].empty
else 0
for arch in architectures
]
all_means.extend(means)
# Offset bars based on morphology index
offset = (i - len(morphologies) / 2 + 0.5) * bar_width
x_pos = x_indices + offset
# We can use a color gradient or different colors for morphologies
# For simplicity, using a colormap
color = plt.cm.viridis(i / max(1, len(morphologies) - 1))
bars = ax.bar(
x_pos,
means,
bar_width,
yerr=stds,
label=f"{m} Arms",
color=color,
capsize=4,
error_kw={"elinewidth": 1.5, "alpha": 0.7},
)
all_bars[m] = (x_pos, means, stds, bars)
for a_idx, arch in enumerate(architectures):
a_means = {m: all_bars[m][1][a_idx] for m in morphologies}
best_m = (
max(a_means, key=a_means.get) if higher_is_better else min(a_means, key=a_means.get)
)
best_x = all_bars[best_m][0][a_idx]
best_y = all_bars[best_m][1][a_idx]
best_std = all_bars[best_m][2][a_idx]
offset = best_std + (abs(max(a_means.values())) * 0.05) if a_means.values() else 0
ax.text(
best_x,
best_y + offset,
BEST_PERFORMER_TEXT,
ha="center",
va="bottom",
fontsize=20,
color=BEST_PERFORMER_COLOR,
)
# Aesthetics
ax.set_ylabel(ylabel, labelpad=15)
if show_titles:
ax.set_title(title + " (Alt)", pad=25, fontweight="bold")
ax.set_xticks(x_indices)
ax.set_xticklabels([arch.replace("_", " ").title() for arch in architectures])
ax.tick_params(axis="x", pad=25)
# X-axis at zero
ax.axhline(0, color="black", linewidth=1.5)
ax.spines["bottom"].set_visible(False)
if all_means:
min_val = min([*all_means, 0])
max_val = max([*all_means, 0])
margin = (max_val - min_val) * 0.15 if max_val != min_val else 0.1
ax.set_ylim(min_val - margin, max_val + margin * 1.5)
ticks = (
[min_val, max_val]
if min_val == 0 and max_val == 0
else sorted(list(set([min_val, 0, max_val])))
)
ax.set_yticks(ticks)
ax.yaxis.set_major_formatter(
plt.FuncFormatter(lambda x, _: f"{x:.2f}" if abs(x) < 10 else f"{x:.0f}")
)
ax.legend(**LEGEND_KWARGS, ncol=len(morphologies))
ax.set_facecolor("white")
fig.patch.set_facecolor("white")
os.makedirs(output_dir, exist_ok=True)
base_path = os.path.join(output_dir, os.path.splitext(output_filename)[0])
plt.savefig(f"{base_path}.png", dpi=300, bbox_inches="tight")
plt.savefig(f"{base_path}.svg", format="svg", bbox_inches="tight")
plt.close()
if __name__ == "__main__":
parser = create_common_parser(description="Generate comparison poster plots.")
parser.add_argument(
"input_csv", help="Path to the input CSV file containing evaluation results."
)
args = parser.parse_args()
INPUT_CSV = args.input_csv
OUTPUT_DIR = args.output_dir
if not os.path.exists(INPUT_CSV):
print(f"Error: Could not find {INPUT_CSV}. Please ensure the file exists.")
else:
df = load_and_preprocess_data(INPUT_CSV)
print("Data loaded successfully. Generating poster plots...")
apply_style(font_size=args.font_size)
kwargs = {"show_titles": args.show_titles, "figsize": (args.fig_width, args.fig_height)}
# Velocity Conversion: m/s to cm/s
if "approx_max_velocity" in df.columns:
df["approx_max_velocity"] = df["approx_max_velocity"] * 100
# 1. Primary Plot: Forward Velocity
plot_grouped_bar(
df=df,
metric_col="approx_max_velocity",
ylabel="",
title="Maximal forward velocity (in cm/s)",
output_filename="poster_plot_velocity.png",
output_dir=OUTPUT_DIR,
higher_is_better=True,
**kwargs,
)
plot_grouped_bar_alt(
df=df,
metric_col="approx_max_velocity",
ylabel="Max Forward Velocity (cm/s)",
title="Graceful Degradation: Velocity Across Morphologies",
output_filename="poster_plot_velocity_alt.png",
output_dir=OUTPUT_DIR,
higher_is_better=True,
**kwargs,
)
# 2. Secondary Plot: Accumulated Reward
plot_grouped_bar(
df=df,
metric_col="eval_return",
ylabel="Mean Cumulative Reward",
title="Overall Efficiency Across Morphologies",
output_filename="poster_plot_reward.png",
output_dir=OUTPUT_DIR,
higher_is_better=True,
**kwargs,
)
plot_grouped_bar_alt(
df=df,
metric_col="eval_return",
ylabel="Mean Cumulative Reward",
title="Overall Efficiency Across Morphologies",
output_filename="poster_plot_reward_alt.png",
output_dir=OUTPUT_DIR,
higher_is_better=True,
**kwargs,
)
# 3. Secondary Plot: Success Rate
plot_grouped_bar(
df=df,
metric_col="reached_target",
ylabel="Success Rate (%)",
title="Target Acquisition Consistency",
output_filename="poster_plot_success_rate.png",
output_dir=OUTPUT_DIR,
higher_is_better=True,
**kwargs,
)
plot_grouped_bar_alt(
df=df,
metric_col="reached_target",
ylabel="Success Rate (%)",
title="Target Acquisition Consistency",
output_filename="poster_plot_success_rate_alt.png",
output_dir=OUTPUT_DIR,
higher_is_better=True,
**kwargs,
)
# 4. Secondary Plot: Final Distance Remaining
plot_grouped_bar(
df=df,
metric_col="final_xy_dist",
ylabel="Distance to Target Remaining",
title="Navigational Accuracy (Lower is Better)",
output_filename="poster_plot_distance.png",
output_dir=OUTPUT_DIR,
higher_is_better=False, # For distance, a lower score is better
**kwargs,
)
plot_grouped_bar_alt(
df=df,
metric_col="final_xy_dist",
ylabel="Distance to Target Remaining",
title="Navigational Accuracy (Lower is Better)",
output_filename="poster_plot_distance_alt.png",
output_dir=OUTPUT_DIR,
higher_is_better=False,
**kwargs,
)
print(f"All plots generated in the '{OUTPUT_DIR}/' directory.")