""" simulate_grid.py — Sweep a 10×10 grid across the workspace and analyze tension differential, belt lengths, and worst-case positions. Outputs: - workspace_heatmap.png (matplotlib heatmap of tension differential) - workspace_heatmap.csv (fallback if no matplotlib, also written as data log) """ from __future__ import annotations import csv import math import os import sys import numpy as np from kinematics import ( belt_lengths, solve_forward, BELT_NAMES, TEST_GRID, ) from tension_analysis import analyze_tension, resting_lengths # Output directory OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__)) # Workspace bounds X_MIN, X_MAX = -0.6, 0.6 Y_MIN, Y_MAX = -1.2, 1.2 def sweep_grid(nx: int = 10, ny: int = 10, z: float = 0.0) -> tuple[np.ndarray, np.ndarray, np.ndarray, tuple]: """Sweep an nx × ny grid across the workspace at height z. Returns: xs, ys: 1D arrays of X and Y grid lines diff_map: (ny, nx) array of max-min tension differential at each point worst: ((x, y), max_diff) — the point with greatest tension differential """ xs = np.linspace(X_MIN, X_MAX, nx) ys = np.linspace(Y_MIN, Y_MAX, ny) diff_map = np.zeros((ny, nx)) worst_diff = 0.0 worst_xy = (0.0, 0.0) rest = resting_lengths(0.0, 0.0, 0.0) for i, x in enumerate(xs): for j, y in enumerate(ys): result = analyze_tension(x, y, z, rest_lengths=rest) tmin = np.min(result.tension_multipliers) tmax = np.max(result.tension_multipliers) diff = tmax - tmin diff_map[j, i] = diff if diff > worst_diff: worst_diff = diff worst_xy = (x, y) return xs, ys, diff_map, (worst_xy, worst_diff) def write_csv(xs: np.ndarray, ys: np.ndarray, diff_map: np.ndarray, path: str) -> None: """Write the grid data as a CSV file.""" with open(path, "w", newline="") as f: writer = csv.writer(f) # Header: first cell empty, then X coordinates header = [""] + [f"{x:.6f}" for x in xs] writer.writerow(header) for j, y in enumerate(ys): row = [f"{y:.6f}"] + [f"{diff_map[j, i]:.6f}" for i in range(len(xs))] writer.writerow(row) print(f" Wrote CSV: {path}") def plot_heatmap(xs: np.ndarray, ys: np.ndarray, diff_map: np.ndarray, worst_xy: tuple, worst_diff: float, path: str) -> bool: """Generate and save a heatmap using matplotlib. Returns True on success, False if matplotlib is unavailable. """ try: import matplotlib.pyplot as plt except ImportError: return False fig, ax = plt.subplots(figsize=(10, 8)) X, Y = np.meshgrid(xs, ys) levels = 50 cf = ax.contourf(X, Y, diff_map, levels=levels, cmap="plasma") cbar = fig.colorbar(cf, ax=ax, label="Tension Differential (multiplier range)") # Mark worst point wx, wy = worst_xy ax.plot(wx, wy, marker="*", color="white", markersize=14, markeredgecolor="black", markeredgewidth=1.0) ax.annotate(f"Worst: ({wx:.3f}, {wy:.3f})\nDiff = {worst_diff:.3f}", xy=(wx, wy), xytext=(wx + 0.12, wy + 0.08), color="white", fontsize=9, arrowprops=dict(arrowstyle="->", color="white", lw=1.2), bbox=dict(boxstyle="round,pad=0.3", facecolor="black", edgecolor="white", alpha=0.7)) # Mark the 9 test points for name, (tx, ty, tz) in TEST_GRID: ax.plot(tx, ty, marker="o", color="cyan", markersize=4, alpha=0.8) ax.set_xlabel("X (m)") ax.set_ylabel("Y (m)") ax.set_title("Gordix 8-Belt — Tension Differential Across Workspace\n" "(10×10 grid, Z=0)") ax.set_aspect("equal") ax.grid(True, alpha=0.3) fig.tight_layout() fig.savefig(path, dpi=150) plt.close(fig) print(f" Saved heatmap: {path}") return True def _run_sweep(): print("=" * 70) print(" Grid Sweep — Gordix 8-Belt Workspace Analysis") print("=" * 70) xs, ys, diff_map, (worst_xy, worst_diff) = sweep_grid(10, 10, 0.0) print(f"\n Grid: 10 × 10 = 100 points") print(f" Workspace: X=[{X_MIN:.2f}, {X_MAX:.2f}] Y=[{Y_MIN:.2f}, {Y_MAX:.2f}]") print(f"\n Worst-case tension differential:") print(f" Point: ({worst_xy[0]:.4f}, {worst_xy[1]:.4f}) m") print(f" Differential: {worst_diff:.4f} (tension multiplier range)") # Also report raw belt length range rest = resting_lengths(0.0, 0.0, 0.0) bl = belt_lengths(worst_xy[0], worst_xy[1], 0.0) vals = list(bl.values()) print(f" Belt lengths: {min(vals):.6f} – {max(vals):.6f} m") print(f" ΔL from rest:") for name in BELT_NAMES: delta = (bl[name] - rest[name]) * 1000 print(f" {name:<12}: {delta:+8.4f} mm") # Save CSV always csv_path = os.path.join(OUTPUT_DIR, "workspace_heatmap.csv") write_csv(xs, ys, diff_map, csv_path) # Save PNG if possible png_path = os.path.join(OUTPUT_DIR, "workspace_heatmap.png") ok = plot_heatmap(xs, ys, diff_map, worst_xy, worst_diff, png_path) if not ok: print(" [matplotlib not available — skipped PNG, CSV saved]") print() return worst_xy, worst_diff if __name__ == "__main__": _run_sweep()