| |
| """3D lattice plots (8³ nodes): five standalone PNGs plus one 1×5 row (data_collection_strategies_3d_1x5.png). |
| |
| Blue = active / sampled, yellow = inactive. |
| |
| Logical lattice uses origin (0,0,0) for L / Ours joints; points are mapped to plot space with |
| (x', y', z') = (N-1-ix, N-1-iy, iz) |
| so the common intersection maps to corner (N-1, N-1, 0). Camera is chosen so that corner reads |
| as the lower-right of the cube on the page (tune VIEW_ELEV / VIEW_AZIM if needed). |
| """ |
| from pathlib import Path |
|
|
| import matplotlib as mpl |
| import matplotlib.font_manager as fm |
| import matplotlib.pyplot as plt |
| import numpy as np |
| from mpl_toolkits.mplot3d import Axes3D |
|
|
| N = 8 |
| |
| OURS_Y_LEN = 5 |
|
|
| ACTIVE = "#0072B2" |
| INACTIVE = "#D4B84A" |
| INACTIVE_ALPHA = 0.42 |
| FIG_DIR = Path(__file__).resolve().parents[1] / "figures" |
|
|
| |
| VIEW_ELEV = 9 |
| VIEW_AZIM = 22 |
|
|
|
|
| def _configure_font(): |
| available = {f.name for f in fm.fontManager.ttflist} |
| mpl.rcParams["font.family"] = "sans-serif" |
| preferred = ["Helvetica", "Arial", "Liberation Sans", "DejaVu Sans"] |
| chosen = [n for n in preferred if n in available] |
| mpl.rcParams["font.sans-serif"] = chosen + ["DejaVu Sans", "sans-serif"] |
|
|
|
|
| def lattice_coords(): |
| ix, iy, iz = np.meshgrid( |
| np.arange(N), np.arange(N), np.arange(N), indexing="ij" |
| ) |
| return ix.ravel(), iy.ravel(), iz.ravel() |
|
|
|
|
| def logical_to_plot(IX, IY, IZ): |
| """Map logical indices so joint (0,0,0) → plot (N-1,N-1,0).""" |
| return (N - 1 - IX, N - 1 - IY, IZ) |
|
|
|
|
| def wireframe_cube(ax, m, color="#bfbfbf", lw=0.9): |
| z0 = [(0, 0, 0), (m, 0, 0), (m, m, 0), (0, m, 0)] |
| z1 = [(0, 0, m), (m, 0, m), (m, m, m), (0, m, m)] |
| for i in range(4): |
| p0, p1 = z0[i], z0[(i + 1) % 4] |
| ax.plot([p0[0], p1[0]], [p0[1], p1[1]], [p0[2], p1[2]], c=color, lw=lw) |
| for i in range(4): |
| p0, p1 = z1[i], z1[(i + 1) % 4] |
| ax.plot([p0[0], p1[0]], [p0[1], p1[1]], [p0[2], p1[2]], c=color, lw=lw) |
| for a, b in zip(z0, z1): |
| ax.plot([a[0], b[0]], [a[1], b[1]], [a[2], b[2]], c=color, lw=lw) |
|
|
|
|
| def plot_strategy( |
| ax, |
| mask_active: np.ndarray, |
| title: str, |
| show_inactive: bool, |
| *, |
| compact: bool = False, |
| ): |
| IX, IY, IZ = lattice_coords() |
| PX, PY, PZ = logical_to_plot(IX, IY, IZ) |
| m = N - 1 |
|
|
| if compact: |
| s_in, s_act = 10, 18 |
| title_fs, label_fs = 14, 10 |
| title_pad = -22 |
| wire_lw = 0.55 |
| lw_scatter = 0.22 |
| labelpad_xy, labelpad_z = -10, -7 |
| else: |
| s_in, s_act = 24, 42 |
| title_fs, label_fs = 14, 11 |
| title_pad = 2 |
| wire_lw = 0.9 |
| lw_scatter = 0.35 |
| labelpad_xy, labelpad_z = -14, -10 |
|
|
| wireframe_cube(ax, m, lw=wire_lw) |
|
|
| if show_inactive: |
| inactive = ~mask_active |
| if np.any(inactive): |
| ax.scatter( |
| PX[inactive], |
| PY[inactive], |
| PZ[inactive], |
| c=INACTIVE, |
| s=s_in, |
| alpha=INACTIVE_ALPHA, |
| edgecolors="none", |
| depthshade=True, |
| ) |
|
|
| if np.any(mask_active): |
| ax.scatter( |
| PX[mask_active], |
| PY[mask_active], |
| PZ[mask_active], |
| c=ACTIVE, |
| s=s_act, |
| alpha=1.0, |
| edgecolors="black", |
| linewidths=lw_scatter, |
| depthshade=True, |
| ) |
|
|
| ax.set_title(title, fontsize=title_fs, fontweight="bold", pad=title_pad) |
|
|
| ticks = np.arange(N) |
| ax.set_xticks(ticks) |
| ax.set_yticks(ticks) |
| ax.set_zticks(ticks) |
| ax.set_xticklabels([]) |
| ax.set_yticklabels([]) |
| ax.set_zticklabels([]) |
|
|
| lim_lo, lim_hi = -0.4, m + 0.4 |
| ax.set_xlim(lim_lo, lim_hi) |
| ax.set_ylim(lim_lo, lim_hi) |
| ax.set_zlim(lim_lo, lim_hi) |
| ax.view_init(elev=VIEW_ELEV, azim=VIEW_AZIM) |
| ax.set_box_aspect((1, 1, 1)) |
|
|
| ax.set_xlabel("Verb", fontsize=label_fs, fontweight="bold", labelpad=labelpad_xy) |
| ax.set_ylabel("Color", fontsize=label_fs, fontweight="bold", labelpad=labelpad_xy) |
| ax.set_zlabel("Object", fontsize=label_fs, fontweight="bold", labelpad=labelpad_z) |
|
|
|
|
| def build_masks(): |
| IX, IY, IZ = lattice_coords() |
|
|
| complete = np.ones(IX.shape[0], dtype=bool) |
|
|
| rng = np.random.default_rng(0) |
| n_random = max(1, int(0.09 * N**3)) |
| choice = rng.choice(N**3, size=n_random, replace=False) |
| random_m = np.zeros(N**3, dtype=bool) |
| random_m[choice] = True |
|
|
| diagonal = (IX == IY) & (IY == IZ) |
|
|
| L_mask = ((IY == 0) & (IZ == 0)) | ((IX == 0) & (IZ == 0)) | ((IX == 0) & (IY == 0)) |
|
|
| x_arm = (IY == 0) & (IZ == 0) |
| y_arm = (IX == 0) & (IZ == 0) & (IY < OURS_Y_LEN) |
| diag_arm = (IX == IY) & (IY == IZ) |
| ours = x_arm | y_arm | diag_arm |
|
|
| return { |
| "complete": complete, |
| "random": random_m, |
| "diagonal": diagonal, |
| "L": L_mask, |
| "ours": ours, |
| } |
|
|
|
|
| def main(): |
| FIG_DIR.mkdir(parents=True, exist_ok=True) |
| _configure_font() |
| masks = build_masks() |
|
|
| specs = [ |
| ("complete", "Complete", False), |
| ("random", "Random", True), |
| ("diagonal", "Diagonal", True), |
| ("L", "L", True), |
| ("ours", "Ours", True), |
| ] |
|
|
| for key, title, show_inactive in specs: |
| fig = plt.figure(figsize=(7.2, 6.4)) |
| ax = fig.add_subplot(111, projection="3d") |
| plot_strategy(ax, masks[key], title, show_inactive=show_inactive) |
| out = FIG_DIR / f"data_collection_strategies_3d_{key}.png" |
| fig.savefig( |
| out, dpi=200, bbox_inches="tight", pad_inches=0.03, facecolor="white" |
| ) |
| plt.close(fig) |
| print(f"Saved {out}") |
|
|
| |
| fig_row = plt.figure(figsize=(19.0, 3.95)) |
| for i, (key, title, show_inactive) in enumerate(specs): |
| ax = fig_row.add_subplot(1, 5, i + 1, projection="3d") |
| plot_strategy( |
| ax, masks[key], title, show_inactive=show_inactive, compact=True |
| ) |
| fig_row.subplots_adjust(left=0.02, right=0.98, top=0.88, bottom=0.06, wspace=-0.18) |
| out_row = FIG_DIR / "data_collection_strategies_3d_1x5.png" |
| fig_row.savefig( |
| out_row, dpi=200, bbox_inches="tight", pad_inches=0.06, facecolor="white" |
| ) |
| plt.close(fig_row) |
| print(f"Saved {out_row}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|