Deployed 6a66208 with MkDocs version: 1.6.1
This commit is contained in:
parent
3cd3e9ea81
commit
635f59cb32
20 changed files with 917 additions and 89 deletions
|
|
@ -6,18 +6,17 @@ Rate, Distance Remaining).
|
|||
"""
|
||||
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from plot_config import (
|
||||
COLORS,
|
||||
apply_style,
|
||||
BEST_PERFORMER_MARKER,
|
||||
BEST_PERFORMER_TEXT,
|
||||
BEST_PERFORMER_COLOR,
|
||||
create_common_parser,
|
||||
BEST_PERFORMER_TEXT,
|
||||
COLORS,
|
||||
LEGEND_KWARGS,
|
||||
apply_style,
|
||||
create_common_parser,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -84,7 +83,8 @@ def plot_grouped_bar(
|
|||
|
||||
fig, ax = plt.subplots(figsize=figsize)
|
||||
bar_width = 0.35
|
||||
x_indices = np.arange(len(morphologies))
|
||||
group_spacing = 1.3
|
||||
x_indices = np.arange(len(morphologies)) * group_spacing
|
||||
all_bars = {}
|
||||
all_means = []
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ def plot_grouped_bar(
|
|||
)
|
||||
all_bars[arch] = (x_pos, means, stds, bars)
|
||||
|
||||
for m_idx, m in enumerate(morphologies):
|
||||
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)
|
||||
|
|
@ -144,12 +144,13 @@ def plot_grouped_bar(
|
|||
|
||||
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", pad=25) # More padding for the squares
|
||||
ax.tick_params(axis="x") # More padding for the squares
|
||||
|
||||
# X-axis at zero
|
||||
ax.axhline(0, color="black", linewidth=1.5)
|
||||
|
|
@ -172,20 +173,7 @@ def plot_grouped_bar(
|
|||
plt.FuncFormatter(lambda x, _: f"{x:.2f}" if abs(x) < 10 else f"{x:.0f}")
|
||||
)
|
||||
|
||||
_add_square_placeholders(ax, x_ticks_pos, [f"{m} Arms" for m in morphologies])
|
||||
|
||||
# Add custom legend entry for best performer
|
||||
ax.plot(
|
||||
[],
|
||||
[],
|
||||
marker=BEST_PERFORMER_MARKER,
|
||||
color="w",
|
||||
markerfacecolor=BEST_PERFORMER_COLOR,
|
||||
markersize=15,
|
||||
label="Best Performance",
|
||||
ls="",
|
||||
)
|
||||
ax.legend(**LEGEND_KWARGS, ncol=len(architectures) + 1)
|
||||
ax.legend(**LEGEND_KWARGS, ncol=len(architectures))
|
||||
ax.set_facecolor("white")
|
||||
fig.patch.set_facecolor("white")
|
||||
|
||||
|
|
@ -305,22 +293,7 @@ def plot_grouped_bar_alt(
|
|||
plt.FuncFormatter(lambda x, _: f"{x:.2f}" if abs(x) < 10 else f"{x:.0f}")
|
||||
)
|
||||
|
||||
# In this alt plot, placeholders might be per architecture
|
||||
_add_square_placeholders(
|
||||
ax, x_indices, [arch.replace("_", "\n").title() for arch in architectures]
|
||||
)
|
||||
|
||||
ax.plot(
|
||||
[],
|
||||
[],
|
||||
marker=BEST_PERFORMER_MARKER,
|
||||
color="w",
|
||||
markerfacecolor=BEST_PERFORMER_COLOR,
|
||||
markersize=15,
|
||||
label="Best Performance",
|
||||
ls="",
|
||||
)
|
||||
ax.legend(**LEGEND_KWARGS, ncol=len(morphologies) + 1)
|
||||
ax.legend(**LEGEND_KWARGS, ncol=len(morphologies))
|
||||
ax.set_facecolor("white")
|
||||
fig.patch.set_facecolor("white")
|
||||
|
||||
|
|
@ -358,8 +331,8 @@ if __name__ == "__main__":
|
|||
plot_grouped_bar(
|
||||
df=df,
|
||||
metric_col="approx_max_velocity",
|
||||
ylabel="Max Forward Velocity (cm/s)",
|
||||
title="Graceful Degradation: Velocity Across Morphologies",
|
||||
ylabel="",
|
||||
title="Maximal forward velocity (in cm/s)",
|
||||
output_filename="poster_plot_velocity.png",
|
||||
output_dir=OUTPUT_DIR,
|
||||
higher_is_better=True,
|
||||
|
|
|
|||
|
|
@ -44,20 +44,24 @@ class Columns(str, Enum):
|
|||
# ... (rest of the file remains same, just need to update plotting functions and obtain_data)
|
||||
"""Column names expected in every evaluation CSV."""
|
||||
|
||||
CHECKPOINT = "checkpoint"
|
||||
ARCH = "architecture"
|
||||
TIMESTEPS = "total_trained_timesteps"
|
||||
REWARD = "accumulated_reward"
|
||||
TIMESTEPS = "trained_timesteps"
|
||||
REWARD = "eval_return"
|
||||
VELOCITY = "velocity"
|
||||
EVAL_STEPS = "eval_steps"
|
||||
FINAL_XY_DIST = "final_xy_dist"
|
||||
INITIAL_XY_DIST = "initial_xy_dist"
|
||||
REACHED_TARGET = "reached_target"
|
||||
|
||||
|
||||
# Maps architecture display names to the path of their evaluation CSV.
|
||||
# Update these paths once real evaluation data is available.
|
||||
FILE_MAPPING: dict[str, str] = {
|
||||
"centralized 2 arms": "runs/dummy/dummy_centralized_2_arms.csv",
|
||||
"centralized 5 arms": "runs/dummy/dummy_centralized_5_arms.csv",
|
||||
"decentralized fully connected": "runs/dummy/dummy_decentralized_fully_connected.csv",
|
||||
"decentralized ring-level": "runs/dummy/dummy_decentralized_ring-level.csv",
|
||||
"decentralized segment-level": "runs/dummy/dummy_decentralized_segment-level.csv",
|
||||
# "centralized 2 arms": "runs/dummy/dummy_centralized_2_arms.csv",
|
||||
"centralized 5 arms": "runs/final-v2-centralized/checkpoint_evaluation.csv",
|
||||
"decentralized fully connected": "runs/final-v2-fully-conn/checkpoint_evaluation.csv",
|
||||
"decentralized ring-level": "runs/final-v2-ring/checkpoint_evaluation.csv",
|
||||
}
|
||||
|
||||
# Architecture profiles for dummy data generation: (max_reward, max_velocity, sigmoid_speed)
|
||||
|
|
@ -108,7 +112,14 @@ def load_metrics(file_mapping: dict[str, str]) -> pd.DataFrame:
|
|||
Loads one CSV per architecture, injects the architecture name as a column,
|
||||
and returns the combined DataFrame with only the required columns.
|
||||
"""
|
||||
required = [Columns.TIMESTEPS, Columns.REWARD, Columns.VELOCITY]
|
||||
required = [
|
||||
Columns.CHECKPOINT,
|
||||
Columns.TIMESTEPS,
|
||||
Columns.REWARD,
|
||||
Columns.INITIAL_XY_DIST,
|
||||
Columns.FINAL_XY_DIST,
|
||||
Columns.EVAL_STEPS,
|
||||
]
|
||||
dfs = []
|
||||
|
||||
for arch_name, filepath in file_mapping.items():
|
||||
|
|
@ -124,17 +135,30 @@ def load_metrics(file_mapping: dict[str, str]) -> pd.DataFrame:
|
|||
continue
|
||||
|
||||
df = df[required].copy()
|
||||
df[Columns.VELOCITY] = (df[Columns.INITIAL_XY_DIST] - df[Columns.FINAL_XY_DIST]) / df[
|
||||
Columns.EVAL_STEPS
|
||||
]
|
||||
df[Columns.ARCH] = arch_name
|
||||
df[Columns.VELOCITY] = (df[Columns.INITIAL_XY_DIST] - df[Columns.FINAL_XY_DIST]) / df[
|
||||
Columns.EVAL_STEPS
|
||||
]
|
||||
|
||||
dfs.append(df)
|
||||
|
||||
return pd.concat(dfs, ignore_index=True) if dfs else pd.DataFrame()
|
||||
|
||||
|
||||
def _convergence_timestep(series: pd.Series, timesteps: pd.Series) -> float:
|
||||
def _convergence_timestep(
|
||||
series: pd.Series, timesteps: pd.Series, checkpoints: pd.Series
|
||||
) -> tuple[float, int, int]:
|
||||
"""Returns the first timestep where the smoothed series reaches 95% of its peak."""
|
||||
smoothed = series.rolling(window=SMOOTHING_WINDOW, min_periods=1).mean()
|
||||
threshold = smoothed.max() * CONVERGENCE_THRESHOLD
|
||||
return timesteps[smoothed >= threshold].iloc[0]
|
||||
|
||||
mask = smoothed >= threshold
|
||||
first_idx = mask.idxmax()
|
||||
|
||||
return timesteps.loc[first_idx], first_idx, checkpoints.loc[first_idx]
|
||||
|
||||
|
||||
def analyze_convergence(df: pd.DataFrame) -> pd.DataFrame:
|
||||
|
|
@ -144,21 +168,40 @@ def analyze_convergence(df: pd.DataFrame) -> pd.DataFrame:
|
|||
"""
|
||||
results = []
|
||||
|
||||
centralized_base = 0
|
||||
|
||||
for arch in df[Columns.ARCH].unique():
|
||||
arch_data = df[df[Columns.ARCH] == arch].sort_values(Columns.TIMESTEPS)
|
||||
|
||||
reward_timestep, reward_checkpoint_idx, reward_checkpoint = _convergence_timestep(
|
||||
arch_data[Columns.REWARD],
|
||||
arch_data[Columns.TIMESTEPS],
|
||||
arch_data[Columns.CHECKPOINT],
|
||||
)
|
||||
|
||||
velocity_timestep, velocity_checkpoint_idx, velocity_checkpoint = _convergence_timestep(
|
||||
arch_data[Columns.VELOCITY],
|
||||
arch_data[Columns.TIMESTEPS],
|
||||
arch_data[Columns.CHECKPOINT],
|
||||
)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"Architecture": arch,
|
||||
"Reward_Convergence_Timestep": _convergence_timestep(
|
||||
arch_data[Columns.REWARD], arch_data[Columns.TIMESTEPS]
|
||||
),
|
||||
"Velocity_Convergence_Timestep": _convergence_timestep(
|
||||
arch_data[Columns.VELOCITY], arch_data[Columns.TIMESTEPS]
|
||||
),
|
||||
"Reward_Convergence_Timestep": reward_timestep,
|
||||
"Reward_Convergence_Checkpoint_Idx": reward_checkpoint_idx,
|
||||
"Reward_Convergence_Checkpoint": reward_checkpoint,
|
||||
"Velocity_Convergence_Timestep": velocity_timestep,
|
||||
"Velocity_Convergence_Checkpoint_Idx": velocity_checkpoint_idx,
|
||||
"Velocity_Convergence_Checkpoint": velocity_checkpoint,
|
||||
}
|
||||
)
|
||||
|
||||
if arch == "centralized 5 arms":
|
||||
centralized_base = reward_checkpoint
|
||||
else:
|
||||
print(arch, "speedup:", 1 - reward_checkpoint / centralized_base)
|
||||
|
||||
return pd.DataFrame(results)
|
||||
|
||||
|
||||
|
|
@ -303,6 +346,7 @@ def plot_results(df: pd.DataFrame, results: pd.DataFrame, output_dir: str, **kwa
|
|||
def obtain_data() -> pd.DataFrame:
|
||||
"""Resolves the file mapping, falling back to generated dummy CSVs if needed."""
|
||||
global USING_DUMMY_DATA
|
||||
|
||||
if not any(os.path.exists(p) for p in FILE_MAPPING.values()):
|
||||
logger.info("No real evaluation files found. Generating dummy CSVs at expected locations.")
|
||||
generate_dummy_csvs(FILE_MAPPING)
|
||||
|
|
@ -319,6 +363,12 @@ def run_analysis(output_dir: str, **kwargs):
|
|||
return
|
||||
|
||||
results = analyze_convergence(df)
|
||||
print(
|
||||
results[
|
||||
["Architecture", "Reward_Convergence_Checkpoint_Idx", "Reward_Convergence_Checkpoint"]
|
||||
]
|
||||
)
|
||||
|
||||
plot_results(df, results, output_dir, **kwargs)
|
||||
logger.info("Analysis complete. Plots saved to disk.")
|
||||
|
||||
|
|
|
|||
|
|
@ -4,26 +4,24 @@ import matplotlib.pyplot as plt
|
|||
# Shared Color Palette (Colorblind friendly, high contrast)
|
||||
# Matches poster design
|
||||
COLORS = {
|
||||
"CENTRALIZED": "#2B4162", # Deep Slate Blue
|
||||
"FULLY_CONNECTED": "#FA9F42", # Vibrant Orange
|
||||
"RING_LEVEL": "#4E937A", # Muted Teal
|
||||
"SEGMENT_LEVEL": "#B4436C", # Soft Red
|
||||
"DECENTRALIZED": "#4E937A", # Default decentralized fallback
|
||||
"CENTRALIZED": "#0D567C", # Blue
|
||||
"FULLY_CONNECTED": "#8C0E0F", # Reddish
|
||||
"RING": "#FCB305", # Pale Yellow
|
||||
}
|
||||
|
||||
|
||||
def apply_style(font_size=28):
|
||||
def apply_style(font_size=36):
|
||||
"""
|
||||
Applies the shared typography and aesthetic settings to Matplotlib.
|
||||
"""
|
||||
plt.rcParams.update(
|
||||
{
|
||||
"font.size": font_size,
|
||||
"axes.labelsize": font_size + 4,
|
||||
"axes.titlesize": font_size + 8,
|
||||
"xtick.labelsize": font_size - 4,
|
||||
"ytick.labelsize": font_size - 4,
|
||||
"legend.fontsize": font_size - 6,
|
||||
"axes.labelsize": font_size,
|
||||
"axes.titlesize": font_size,
|
||||
"xtick.labelsize": font_size,
|
||||
"ytick.labelsize": font_size,
|
||||
"legend.fontsize": font_size,
|
||||
"axes.linewidth": 2,
|
||||
"axes.spines.top": False,
|
||||
"axes.spines.right": False,
|
||||
|
|
@ -44,7 +42,7 @@ BEST_PERFORMER_COLOR = "#D4AF37" # Gold
|
|||
# Centralized Legend Configuration
|
||||
LEGEND_KWARGS = {
|
||||
"loc": "upper center",
|
||||
"bbox_to_anchor": (0.5, -0.5),
|
||||
"bbox_to_anchor": (0.5, -0.12),
|
||||
"frameon": False,
|
||||
}
|
||||
|
||||
|
|
|
|||
Reference in a new issue