From 7334565d69d3b0ea8314ef524013055135ac5c37 Mon Sep 17 00:00:00 2001 From: Robin Meersman Date: Fri, 15 May 2026 20:52:06 +0200 Subject: [PATCH 1/6] feat(convergence analysis): updated convergence analysis script to also print out index of found item --- .gitignore | 3 ++ configs/centralized-final.yaml | 6 ++-- configs/evaluation/poster.yaml | 6 ++-- configs/fully-connected-final.yaml | 6 ++-- configs/ring-final.yaml | 6 ++-- scripts/plots/analyze_convergence.py | 52 +++++++++++++++++++++++----- 6 files changed, 60 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index dd76b50..b40f5c8 100644 --- a/.gitignore +++ b/.gitignore @@ -523,3 +523,6 @@ Network Trash Folder Temporary Items .apdisk *.pdf + +# plot directory +poster_plots/ \ No newline at end of file diff --git a/configs/centralized-final.yaml b/configs/centralized-final.yaml index f40570d..9c50437 100644 --- a/configs/centralized-final.yaml +++ b/configs/centralized-final.yaml @@ -23,7 +23,7 @@ morphology: morph_mode: CENTRALIZED experiment: - exp_name: "final-models/centralized/" + exp_name: "final-models-v2/centralized/" seed: 42 torch_deterministic: true cuda: true @@ -34,8 +34,8 @@ logging: save_checkpoints: true upload_final_model: true upload_checkpoints: true - checkpoint_frequency: 20 - wandb_project_name: "final-models" + checkpoint_frequency: 10 + wandb_project_name: "final-models-v2" evaluation: evaluate_checkpoints: true diff --git a/configs/evaluation/poster.yaml b/configs/evaluation/poster.yaml index 0777cd1..948e4c7 100644 --- a/configs/evaluation/poster.yaml +++ b/configs/evaluation/poster.yaml @@ -9,12 +9,14 @@ eval_seed: 0 # Cross-model comparison settings # We use 10 episodes to get a more robust average for the final poster results. comparison_base_seed: 0 -comparison_num_episodes: 2 +comparison_num_episodes: 10 comparison_output_csv: "runs/evaluation/comparison.csv" # Paths to the .cleanrl_model files to be compared (relative to workspace root). comparison_models: - - "runs/input-space-2-arms/2026-05-02/08-14-58/final_model.flax" + - "runs/final-v2-centralized/artifacts/12-19-01_checkpoint_v22/checkpoint_step_230.flax" + - "runs/final-v2-fully-conn/artifacts/14-02-00_checkpoint_v17/checkpoint_step_180.flax" + - "runs/final-v2-ring/artifacts/15-27-03_checkpoint_v21/checkpoint_step_220.flax" # Path to the morphologies to evaluate against. comparison_morphologies: diff --git a/configs/fully-connected-final.yaml b/configs/fully-connected-final.yaml index 8d60451..29983e3 100644 --- a/configs/fully-connected-final.yaml +++ b/configs/fully-connected-final.yaml @@ -26,7 +26,7 @@ morphology: morph_mode: FULLY_CONNECTED experiment: - exp_name: "final-models/fully-connected/" + exp_name: "final-models-v2/fully-connected/" seed: 42 torch_deterministic: true cuda: true @@ -37,8 +37,8 @@ logging: save_checkpoints: true upload_final_model: true upload_checkpoints: true - checkpoint_frequency: 20 - wandb_project_name: "final-models" + checkpoint_frequency: 10 + wandb_project_name: "final-models-v2" evaluation: evaluate_checkpoints: true diff --git a/configs/ring-final.yaml b/configs/ring-final.yaml index ffba64f..a0d852a 100644 --- a/configs/ring-final.yaml +++ b/configs/ring-final.yaml @@ -26,7 +26,7 @@ morphology: morph_mode: RING experiment: - exp_name: "final-models/ring/" + exp_name: "final-models-v2/ring/" seed: 42 torch_deterministic: true cuda: true @@ -37,8 +37,8 @@ logging: save_checkpoints: true upload_final_model: true upload_checkpoints: true - checkpoint_frequency: 20 - wandb_project_name: "final-models" + checkpoint_frequency: 10 + wandb_project_name: "final-models-v2" evaluation: evaluate_checkpoints: true diff --git a/scripts/plots/analyze_convergence.py b/scripts/plots/analyze_convergence.py index 2612bb9..ec21a9a 100644 --- a/scripts/plots/analyze_convergence.py +++ b/scripts/plots/analyze_convergence.py @@ -44,6 +44,7 @@ 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" @@ -108,7 +109,18 @@ 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. """ +<<<<<<< Updated upstream 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, + ] +>>>>>>> Stashed changes dfs = [] for arch_name, filepath in file_mapping.items(): @@ -130,11 +142,17 @@ def load_metrics(file_mapping: dict[str, str]) -> pd.DataFrame: 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: @@ -147,15 +165,27 @@ def analyze_convergence(df: pd.DataFrame) -> pd.DataFrame: 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, } ) @@ -319,6 +349,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.") From 31a70480fb17b07f80d874e12814b42bf4c4808f Mon Sep 17 00:00:00 2001 From: Robin Meersman Date: Sat, 16 May 2026 12:35:07 +0200 Subject: [PATCH 2/6] other: prep for merge conflicts with other branch --- scripts/plots/analyze_comparisons.py | 2 +- scripts/plots/analyze_convergence.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/scripts/plots/analyze_comparisons.py b/scripts/plots/analyze_comparisons.py index 3ac6e8a..fa5c05c 100644 --- a/scripts/plots/analyze_comparisons.py +++ b/scripts/plots/analyze_comparisons.py @@ -172,7 +172,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_square_placeholders(ax, x_ticks_pos, [f"{m} Arms" for m in morphologies]) # Add custom legend entry for best performer ax.plot( diff --git a/scripts/plots/analyze_convergence.py b/scripts/plots/analyze_convergence.py index ec21a9a..cf2e2ce 100644 --- a/scripts/plots/analyze_convergence.py +++ b/scripts/plots/analyze_convergence.py @@ -44,11 +44,14 @@ 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. @@ -109,9 +112,6 @@ 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. """ -<<<<<<< Updated upstream - required = [Columns.TIMESTEPS, Columns.REWARD, Columns.VELOCITY] -======= required = [ Columns.CHECKPOINT, Columns.TIMESTEPS, @@ -120,7 +120,6 @@ def load_metrics(file_mapping: dict[str, str]) -> pd.DataFrame: Columns.FINAL_XY_DIST, Columns.EVAL_STEPS, ] ->>>>>>> Stashed changes dfs = [] for arch_name, filepath in file_mapping.items(): From fe7aecb62a3fa2293fd4acecc0ebf8d3a6c75f38 Mon Sep 17 00:00:00 2001 From: Robin Meersman Date: Sat, 16 May 2026 12:36:58 +0200 Subject: [PATCH 3/6] feat(plot colors): copied over the config from the other (chaotic) branch where config was wrongfully pushed to --- scripts/plots/plot_config.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/scripts/plots/plot_config.py b/scripts/plots/plot_config.py index 5fd6ffd..15f0e5c 100644 --- a/scripts/plots/plot_config.py +++ b/scripts/plots/plot_config.py @@ -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_LEVEL": "#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, From 708b06157729b3f383d967805f34c35575de7f16 Mon Sep 17 00:00:00 2001 From: Robin Meersman Date: Sat, 16 May 2026 15:07:15 +0200 Subject: [PATCH 4/6] feat(plotting): updated plots to not contain placeholder squares + updated colors --- scripts/plots/analyze_comparisons.py | 60 +++++++++++++++------------- scripts/plots/analyze_convergence.py | 1 + scripts/plots/plot_config.py | 4 +- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/scripts/plots/analyze_comparisons.py b/scripts/plots/analyze_comparisons.py index fa5c05c..24e28fc 100644 --- a/scripts/plots/analyze_comparisons.py +++ b/scripts/plots/analyze_comparisons.py @@ -84,7 +84,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 +119,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 +145,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) @@ -175,17 +177,18 @@ def plot_grouped_bar( # _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.plot( + # [], + # [], + # marker=BEST_PERFORMER_MARKER, + # color="w", + # markerfacecolor=BEST_PERFORMER_COLOR, + # markersize=15, + # label="Best Performance", + # ls="", + # ) + + ax.legend(**LEGEND_KWARGS, ncol=len(architectures)) ax.set_facecolor("white") fig.patch.set_facecolor("white") @@ -306,21 +309,22 @@ def plot_grouped_bar_alt( ) # In this alt plot, placeholders might be per architecture - _add_square_placeholders( - ax, x_indices, [arch.replace("_", "\n").title() for arch in architectures] - ) + # _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.plot( + # [], + # [], + # marker=BEST_PERFORMER_MARKER, + # color="w", + # markerfacecolor=BEST_PERFORMER_COLOR, + # markersize=15, + # label="Best Performance", + # ls="", + # ) + + ax.legend(**LEGEND_KWARGS, ncol=len(morphologies)) ax.set_facecolor("white") fig.patch.set_facecolor("white") diff --git a/scripts/plots/analyze_convergence.py b/scripts/plots/analyze_convergence.py index cf2e2ce..df9826c 100644 --- a/scripts/plots/analyze_convergence.py +++ b/scripts/plots/analyze_convergence.py @@ -44,6 +44,7 @@ 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 = "trained_timesteps" REWARD = "eval_return" diff --git a/scripts/plots/plot_config.py b/scripts/plots/plot_config.py index 15f0e5c..48f9106 100644 --- a/scripts/plots/plot_config.py +++ b/scripts/plots/plot_config.py @@ -6,7 +6,7 @@ import matplotlib.pyplot as plt COLORS = { "CENTRALIZED": "#0D567C", # Blue "FULLY_CONNECTED": "#8C0E0F", # Reddish - "RING_LEVEL": "#FCB305", # Pale Yellow + "RING": "#FCB305", # Pale Yellow } @@ -42,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, } From 2493c8d2b306fa55bd1dee966706b4e2bfaf4283 Mon Sep 17 00:00:00 2001 From: Robin Meersman Date: Tue, 19 May 2026 10:16:18 +0200 Subject: [PATCH 5/6] feat(plotting): removed best marker legend entry --- scripts/plots/analyze_comparisons.py | 19 +++++++++---------- scripts/plots/analyze_convergence.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/scripts/plots/analyze_comparisons.py b/scripts/plots/analyze_comparisons.py index 24e28fc..d001b03 100644 --- a/scripts/plots/analyze_comparisons.py +++ b/scripts/plots/analyze_comparisons.py @@ -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, ) @@ -362,8 +361,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, diff --git a/scripts/plots/analyze_convergence.py b/scripts/plots/analyze_convergence.py index 3bb078d..bc12c79 100644 --- a/scripts/plots/analyze_convergence.py +++ b/scripts/plots/analyze_convergence.py @@ -135,6 +135,9 @@ 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 @@ -165,6 +168,8 @@ 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) @@ -192,6 +197,11 @@ def analyze_convergence(df: pd.DataFrame) -> pd.DataFrame: } ) + if arch == "centralized 5 arms": + centralized_base = reward_checkpoint + else: + print(arch, "speedup:", 1 - reward_checkpoint / centralized_base) + return pd.DataFrame(results) From 1ddd065ef5b77a08a18577345faf718770f2eb2c Mon Sep 17 00:00:00 2001 From: Robin Meersman Date: Tue, 19 May 2026 10:28:26 +0200 Subject: [PATCH 6/6] cleanup(plotting): removed commented code --- scripts/plots/analyze_comparisons.py | 30 ---------------------------- 1 file changed, 30 deletions(-) diff --git a/scripts/plots/analyze_comparisons.py b/scripts/plots/analyze_comparisons.py index d001b03..8a66b4c 100644 --- a/scripts/plots/analyze_comparisons.py +++ b/scripts/plots/analyze_comparisons.py @@ -173,20 +173,6 @@ 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)) ax.set_facecolor("white") fig.patch.set_facecolor("white") @@ -307,22 +293,6 @@ 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)) ax.set_facecolor("white") fig.patch.set_facecolor("white")