Spaces:
Running
Running
| """Render the §5.4 rediscovery distance bar chart (Fig. 4). | |
| Reproduces ``paper/figures/fig_rediscovery_distance.png`` from the two | |
| committed CSV artifacts so the paper figure is regenerable rather than a | |
| hand-made PNG: | |
| - ``reports/rediscovery_loo_evaluator/summary.csv`` — per-rover nearest- | |
| Pareto design-space distance (the bars); | |
| - ``reports/rediscovery_baseline/feasible_baseline.csv`` — the | |
| feasible-design null and each rover's distance to the feasible-region | |
| centroid (the overlays added for the §5.4 pre-submission item). | |
| The chart shows, per rover: | |
| - a horizontal bar = rediscovery distance (blue in-scope < 50 kg, grey | |
| out-of-scope Yutu-2, kept as a reference point); | |
| - a marker = the rover's distance to the feasible-region centroid (the | |
| "typical feasible design"); | |
| - two vertical reference lines = the unit-cube random-pair null | |
| ($\approx$1.20) and the feasibility-restricted null ($\approx$1.17). | |
| Usage | |
| ----- | |
| :: | |
| python scripts/make_rediscovery_distance_figure.py | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| from pathlib import Path | |
| from statistics import median | |
| import pandas as pd | |
| from roverdevkit.tradespace.visualize import set_paper_rcparams | |
| # Scope ceiling: the tool and the rediscovery check target sub-50 kg | |
| # micro-rovers. Yutu-2 (~96 kg modelled / ~135 kg published) sits above | |
| # it and is drawn greyed as an out-of-scope reference point. | |
| _SCOPE_CEILING_KG: float = 50.0 | |
| _IN_SCOPE_COLOR = "#1f77b4" | |
| _OUT_SCOPE_COLOR = "#b0b0b0" | |
| _CENTROID_COLOR = "#d62728" | |
| def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: | |
| p = argparse.ArgumentParser( | |
| description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter | |
| ) | |
| p.add_argument( | |
| "--rediscovery-summary", | |
| type=Path, | |
| default=Path("reports/rediscovery_loo_evaluator/summary.csv"), | |
| ) | |
| p.add_argument( | |
| "--feasible-baseline", | |
| type=Path, | |
| default=Path("reports/rediscovery_baseline/feasible_baseline.csv"), | |
| ) | |
| p.add_argument( | |
| "--out", | |
| type=Path, | |
| default=Path("paper/figures/fig_rediscovery_distance.png"), | |
| ) | |
| return p.parse_args(argv) | |
| def main(argv: list[str] | None = None) -> int: | |
| args = _parse_args(argv) | |
| redis = pd.read_csv(args.rediscovery_summary) | |
| feas = pd.read_csv(args.feasible_baseline) | |
| df = redis.merge( | |
| feas[ | |
| [ | |
| "rover_name", | |
| "rover_to_centroid_distance", | |
| "feasible_random_pair_mean", | |
| "unit_cube_random_pair", | |
| ] | |
| ], | |
| on="rover_name", | |
| how="left", | |
| ) | |
| df["out_of_scope"] = df["mass_modelled_kg"] > _SCOPE_CEILING_KG | |
| # Smallest distance at the bottom, largest (Yutu-2) at the top. | |
| df = df.sort_values("design_space_distance", ascending=True).reset_index(drop=True) | |
| in_scope = df[~df["out_of_scope"]] | |
| in_scope_median = float(median(in_scope["design_space_distance"])) | |
| unit_cube_null = float(df["unit_cube_random_pair"].iloc[0]) | |
| feasible_null = float(df["feasible_random_pair_mean"].median()) | |
| set_paper_rcparams() | |
| import matplotlib.pyplot as plt | |
| from matplotlib.lines import Line2D | |
| from matplotlib.patches import Patch | |
| fig, ax = plt.subplots(figsize=(7.2, 4.0)) | |
| y = range(len(df)) | |
| colors = [ | |
| _OUT_SCOPE_COLOR if oos else _IN_SCOPE_COLOR for oos in df["out_of_scope"] | |
| ] | |
| ax.barh(list(y), df["design_space_distance"], color=colors, zorder=2) | |
| # Feasible-region centroid markers (the "typical feasible design"). | |
| ax.scatter( | |
| df["rover_to_centroid_distance"], | |
| list(y), | |
| marker="D", | |
| s=34, | |
| facecolor="none", | |
| edgecolor=_CENTROID_COLOR, | |
| linewidths=1.4, | |
| zorder=4, | |
| ) | |
| # Reference nulls. | |
| ax.axvline(unit_cube_null, ls="--", color="black", lw=1.2, zorder=3) | |
| ax.axvline(feasible_null, ls=":", color="#555555", lw=1.4, zorder=3) | |
| ax.set_yticks(list(y)) | |
| ax.set_yticklabels(df["rover_name"]) | |
| ax.set_xlabel("design-space distance (normalised L2, 9-D)") | |
| ax.set_title(f"Rediscovery distance per rover (in-scope median = {in_scope_median:.2f})") | |
| ax.set_xlim(0, max(unit_cube_null, float(df["rover_to_centroid_distance"].max())) + 0.22) | |
| legend_handles = [ | |
| Patch(facecolor=_IN_SCOPE_COLOR, label="rediscovery distance, in scope (< 50 kg)"), | |
| Patch(facecolor=_OUT_SCOPE_COLOR, label="rediscovery distance, out of scope (> 50 kg)"), | |
| Line2D( | |
| [0], [0], marker="D", linestyle="none", markerfacecolor="none", | |
| markeredgecolor=_CENTROID_COLOR, markersize=7, | |
| label="feasible-region centroid", | |
| ), | |
| Line2D([0], [0], color="black", ls="--", lw=1.2, | |
| label=f"unit-cube null ({unit_cube_null:.2f})"), | |
| Line2D([0], [0], color="#555555", ls=":", lw=1.4, | |
| label=f"feasible-design null ({feasible_null:.2f})"), | |
| ] | |
| ax.legend( | |
| handles=legend_handles, | |
| loc="upper center", | |
| bbox_to_anchor=(0.5, -0.16), | |
| ncol=2, | |
| fontsize=8, | |
| frameon=False, | |
| ) | |
| args.out.parent.mkdir(parents=True, exist_ok=True) | |
| fig.savefig(args.out) | |
| plt.close(fig) | |
| print(f"Wrote {args.out}") | |
| print( | |
| f" in-scope median={in_scope_median:.3f}, " | |
| f"unit-cube null={unit_cube_null:.3f}, feasible null={feasible_null:.3f}" | |
| ) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |