roverdevkit / scripts /make_rediscovery_distance_figure.py
jjreif's picture
Deploy roverdevkit @ 2676a67
b3d14e3
Raw
History Blame Contribute Delete
5.49 kB
"""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())