CoolFace
Apppublic

aparekh02/overflow-openenv

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
app.py425 linesDownload Raw Back to root
1"""2OpenENV RL Demo — Gradio UI entrypoint for HuggingFace Spaces.3 4Runs inside the overflow_env package root. All imports use absolute paths5so they work both as a package (installed) and as a Space (flat root).6"""7 8import sys, os9# When running as HF Space, make server/ importable with absolute paths10sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))11 12import math, time, threading13import numpy as np14import torch15import torch.optim as optim16import matplotlib17matplotlib.use("Agg")18import matplotlib.pyplot as plt19import matplotlib.patches as patches20import gradio as gr21 22from server.overflow_environment import OverflowEnvironment23from models import OverflowAction24from policies.flat_mlp_policy import FlatMLPPolicy25from policies.policy_spec import build_obs, build_ticket_vector, OBS_DIM26 27 28STEPS_PER_EPISODE = 2029NUM_LANES = 330ROAD_LENGTH = 20031 32 33# ── Observation adapter ───────────────────────────────────────────────────────34 35def obs_to_vec(overflow_obs) -> np.ndarray:36    cars = overflow_obs.cars37    if not cars:38        return np.zeros(OBS_DIM, dtype=np.float32)39    ego = next((c for c in cars if c.carId == 0), cars[0])40    ego_spd = ego.speed / 4.541    ego_x   = ego.position.x42    ego_y   = (ego.lane - 2) * 3.743    tickets = []44    for car in cars:45        if car.carId == 0:46            continue47        rx = car.position.x - ego.position.x48        ry = (car.lane - ego.lane) * 3.749        cs = car.speed / 4.550        d  = math.sqrt(rx**2 + ry**2)51        if d > 80:52            continue53        cl = max(ego_spd - cs * math.copysign(1, max(rx, 0.01)), 0.1)54        tickets.append(build_ticket_vector(55            severity_weight=1.0 if d < 8 else 0.75 if d < 15 else 0.5,56            ttl=5.0, pos_x=rx, pos_y=ry, pos_z=0.0,57            vel_x=cs, vel_y=0.0, vel_z=0.0, heading=0.0,58            size_length=4.0, size_width=2.0, size_height=1.5,59            distance=d, time_to_collision=min(d / cl, 30.0),60            bearing=math.atan2(ry, max(rx, 0.01)),61            ticket_type="collision_risk", entity_type="vehicle", confidence=1.0,62        ))63    tv = np.array(tickets, dtype=np.float32) if tickets else None64    return build_obs(ego_x=ego_x, ego_y=ego_y, ego_z=0.0,65                     ego_vx=ego_spd, ego_vy=0.0,66                     heading=0.0, speed=ego_spd,67                     steer=0.0, throttle=0.5, brake=0.0,68                     ticket_vectors=tv)69 70 71def action_to_decision(a: np.ndarray) -> str:72    s, t, b = float(a[0]), float(a[1]), float(a[2])73    if abs(s) > 0.35: return "lane_change_left" if s < 0 else "lane_change_right"74    if b > 0.25:      return "brake"75    if t > 0.20:      return "accelerate"76    return "maintain"77 78 79# ── Global training state ─────────────────────────────────────────────────────80 81policy    = FlatMLPPolicy(obs_dim=OBS_DIM)82optimizer = optim.Adam(policy.parameters(), lr=3e-4, eps=1e-5)83 84_buf_obs   = []85_buf_acts  = []86_buf_rews  = []87_buf_logps = []88_buf_vals  = []89_buf_dones = []90 91episode_history = []92step_log        = []93_running        = False94_lock           = threading.Lock()95 96 97def _ppo_mini_update():98    if len(_buf_obs) < 2:99        return100    obs_t  = torch.tensor(np.array(_buf_obs),  dtype=torch.float32)101    acts_t = torch.tensor(np.array(_buf_acts), dtype=torch.float32)102    rews_t = torch.tensor(_buf_rews,            dtype=torch.float32)103    logp_t = torch.tensor(_buf_logps,           dtype=torch.float32)104    vals_t = torch.tensor(_buf_vals,            dtype=torch.float32)105    done_t = torch.tensor(_buf_dones,           dtype=torch.float32)106 107    gamma, lam = 0.99, 0.95108    adv = torch.zeros_like(rews_t)109    gae = 0.0110    for t in reversed(range(len(rews_t))):111        nv  = 0.0 if t == len(rews_t) - 1 else float(vals_t[t + 1])112        d   = rews_t[t] + gamma * nv * (1 - done_t[t]) - vals_t[t]113        gae = d + gamma * lam * (1 - done_t[t]) * gae114        adv[t] = gae115    ret = adv + vals_t116    adv = (adv - adv.mean()) / (adv.std() + 1e-8)117 118    policy.train()119    act_mean, val = policy(obs_t)120    val = val.squeeze(-1)121    dist    = torch.distributions.Normal(act_mean, torch.ones_like(act_mean) * 0.3)122    logp    = dist.log_prob(acts_t).sum(dim=-1)123    entropy = dist.entropy().sum(dim=-1).mean()124    ratio   = torch.exp(logp - logp_t)125    pg      = torch.max(-adv * ratio, -adv * ratio.clamp(0.8, 1.2)).mean()126    vf      = 0.5 * ((val - ret) ** 2).mean()127    loss    = pg + 0.5 * vf - 0.02 * entropy128    optimizer.zero_grad()129    loss.backward()130    torch.nn.utils.clip_grad_norm_(policy.parameters(), 0.5)131    optimizer.step()132 133 134def run_episodes_loop():135    global _running136    ep_num = 0137    env    = OverflowEnvironment()138 139    while _running:140        ep_num += 1141        obs    = env.reset()142        ep_rew = 0.0143        outcome = "timeout"144 145        _buf_obs.clear(); _buf_acts.clear(); _buf_rews.clear()146        _buf_logps.clear(); _buf_vals.clear(); _buf_dones.clear()147 148        for step in range(1, STEPS_PER_EPISODE + 1):149            if not _running:150                break151 152            obs_vec = obs_to_vec(obs)153            policy.eval()154            with torch.no_grad():155                obs_t = torch.tensor(obs_vec, dtype=torch.float32).unsqueeze(0)156                act_mean, val = policy(obs_t)157                dist   = torch.distributions.Normal(act_mean.squeeze(0),158                                                    torch.ones(3) * 0.3)159                action = dist.sample().clamp(-1, 1)160                logp   = dist.log_prob(action).sum()161 162            decision = action_to_decision(action.numpy())163            obs      = env.step(OverflowAction(decision=decision, reasoning=""))164            reward   = float(obs.reward or 0.0)165            done     = obs.done166            ep_rew  += reward167 168            _buf_obs.append(obs_vec)169            _buf_acts.append(action.numpy())170            _buf_rews.append(reward)171            _buf_logps.append(float(logp))172            _buf_vals.append(float(val.squeeze()))173            _buf_dones.append(float(done))174 175            with _lock:176                step_log.append({177                    "ep":        ep_num,178                    "step":      step,179                    "decision":  decision,180                    "reward":    round(reward, 2),181                    "ep_reward": round(ep_rew, 2),182                    "incident":  obs.incident_report or "",183                    "cars":      [(c.carId, c.lane, c.position.x, c.speed)184                                  for c in obs.cars],185                })186 187            if done:188                outcome = "CRASH" if "CRASH" in (obs.incident_report or "") else "GOAL"189                break190 191            time.sleep(0.6)192 193        _ppo_mini_update()194 195        with _lock:196            episode_history.append({197                "ep":      ep_num,198                "steps":   step,199                "reward":  round(ep_rew, 2),200                "outcome": outcome,201            })202 203 204# ── Plot helpers ──────────────────────────────────────────────────────────────205 206DECISION_COLORS = {207    "accelerate":        "#22c55e",208    "brake":             "#ef4444",209    "lane_change_left":  "#f59e0b",210    "lane_change_right": "#f59e0b",211    "maintain":          "#60a5fa",212}213 214 215def render_road(cars_snapshot, last_decision, last_incident):216    fig, ax = plt.subplots(figsize=(10, 2.8))217    fig.patch.set_facecolor("#0f172a")218    ax.set_facecolor("#1e293b")219 220    ax.set_xlim(0, ROAD_LENGTH)221    ax.set_ylim(0, NUM_LANES + 1)222    ax.set_yticks([])223    ax.set_xlabel("Position", color="#94a3b8", fontsize=9)224    ax.tick_params(colors="#94a3b8")225    for spine in ax.spines.values():226        spine.set_edgecolor("#334155")227 228    for lane in range(1, NUM_LANES):229        ax.axhline(y=lane + 0.5, color="#334155", linewidth=1, linestyle="--", alpha=0.6)230 231    for lane in range(1, NUM_LANES + 1):232        ax.text(2, lane, f"L{lane}", color="#475569", fontsize=8, va="center")233 234    ax.axvspan(160, ROAD_LENGTH, alpha=0.12, color="#22c55e")235    ax.text(162, NUM_LANES + 0.6, "GOAL ZONE", color="#22c55e", fontsize=7, alpha=0.8)236 237    car_w, car_h = 8, 0.55238    for car_id, lane, pos_x, speed in cars_snapshot:239        is_ego  = car_id == 0240        color   = "#3b82f6" if is_ego else "#94a3b8"241        outline = "#60a5fa" if is_ego else "#475569"242        lw      = 2.0 if is_ego else 1.0243        rect = patches.FancyBboxPatch(244            (pos_x - car_w / 2, lane - car_h / 2),245            car_w, car_h,246            boxstyle="round,pad=0.05",247            facecolor=color, edgecolor=outline, linewidth=lw, alpha=0.92,248        )249        ax.add_patch(rect)250        label = f"{'EGO' if is_ego else f'C{car_id}'}\n{speed:.0f}"251        ax.text(pos_x, lane, label, ha="center", va="center",252                fontsize=6.5, color="white", fontweight="bold" if is_ego else "normal")253 254    dec_color = DECISION_COLORS.get(last_decision, "#60a5fa")255    ax.text(ROAD_LENGTH - 2, NUM_LANES + 0.65,256            f"Action: {last_decision.replace('_', ' ').upper()}",257            color=dec_color, fontsize=8, fontweight="bold", ha="right")258 259    if "CRASH" in last_incident:260        ax.text(ROAD_LENGTH / 2, NUM_LANES + 0.65, "CRASH",261                color="#ef4444", fontsize=10, fontweight="bold", ha="center")262    elif "NEAR MISS" in last_incident:263        ax.text(ROAD_LENGTH / 2, NUM_LANES + 0.65, "NEAR MISS",264                color="#f59e0b", fontsize=9, fontweight="bold", ha="center")265    elif "GOAL" in last_incident:266        ax.text(ROAD_LENGTH / 2, NUM_LANES + 0.65, "GOAL REACHED",267                color="#22c55e", fontsize=10, fontweight="bold", ha="center")268 269    plt.tight_layout(pad=0.3)270    return fig271 272 273def render_reward_curve(eps):274    fig, ax = plt.subplots(figsize=(10, 2.8))275    fig.patch.set_facecolor("#0f172a")276    ax.set_facecolor("#1e293b")277    for spine in ax.spines.values():278        spine.set_edgecolor("#334155")279    ax.tick_params(colors="#94a3b8")280    ax.set_xlabel("Episode", color="#94a3b8", fontsize=9)281    ax.set_ylabel("Total Reward", color="#94a3b8", fontsize=9)282 283    if not eps:284        ax.text(0.5, 0.5, "Waiting for episodes...", transform=ax.transAxes,285                ha="center", va="center", color="#475569", fontsize=11)286        plt.tight_layout(pad=0.3)287        return fig288 289    xs = [e["ep"] for e in eps]290    ys = [e["reward"] for e in eps]291    outcome_colors = {"CRASH": "#ef4444", "GOAL": "#22c55e", "timeout": "#60a5fa"}292    for x, y, e in zip(xs, ys, eps):293        ax.bar(x, y, color=outcome_colors.get(e["outcome"], "#60a5fa"), alpha=0.6, width=0.7)294 295    if len(ys) >= 3:296        w = min(5, len(ys))297        smoothed = np.convolve(ys, np.ones(w) / w, mode="valid")298        ax.plot(xs[w - 1:], smoothed, color="#f8fafc", linewidth=2)299 300    ax.axhline(0, color="#334155", linewidth=0.8)301 302    from matplotlib.patches import Patch303    legend_els = [Patch(facecolor="#ef4444", label="crash"),304                  Patch(facecolor="#22c55e", label="goal"),305                  Patch(facecolor="#60a5fa", label="timeout")]306    ax.legend(handles=legend_els, facecolor="#1e293b", labelcolor="#94a3b8",307              fontsize=8, framealpha=0.6, edgecolor="#334155", loc="upper left")308 309    plt.tight_layout(pad=0.3)310    return fig311 312 313# ── Gradio UI ─────────────────────────────────────────────────────────────────314 315def start_training():316    global _running317    if not _running:318        _running = True319        step_log.clear()320        episode_history.clear()321        threading.Thread(target=run_episodes_loop, daemon=True).start()322    return gr.update(value="Running...", interactive=False), gr.update(interactive=True)323 324 325def stop_training():326    global _running327    _running = False328    return gr.update(value="Start", interactive=True), gr.update(interactive=False)329 330 331def get_updates():332    with _lock:333        logs = list(step_log[-20:])334        eps  = list(episode_history[-50:])335        last = step_log[-1] if step_log else None336 337    road_fig   = render_road(last["cars"], last["decision"], last["incident"]) if last \338                 else render_road([], "maintain", "")339    reward_fig = render_reward_curve(eps)340 341    lines = []342    for e in reversed(logs):343        flag = ""344        if "CRASH" in e["incident"]:      flag = " 💥"345        elif "GOAL" in e["incident"]:     flag = " ✓"346        elif "NEAR MISS" in e["incident"]: flag = " ⚠"347        lines.append(348            f"ep {e['ep']:>3d} | step {e['step']:>2d} | "349            f"{e['decision']:<20} | r={e['reward']:>+6.2f} | "350            f"ep_total={e['ep_reward']:>7.2f}{flag}"351        )352    step_text = "\n".join(lines) if lines else "Waiting for first episode..."353 354    ep_lines = ["Episode | Steps | Total Reward | Outcome", "-" * 44]355    for e in reversed(eps[-15:]):356        ep_lines.append(357            f"  {e['ep']:>4d}  |  {e['steps']:>3d}  | "358            f"  {e['reward']:>+8.2f}   | {e['outcome']}"359        )360    ep_text = "\n".join(ep_lines) if eps else "No episodes completed yet."361 362    if len(eps) >= 2:363        rewards = [e["reward"] for e in eps]364        n    = len(rewards)365        half = max(n // 2, 1)366        early = sum(rewards[:half]) / half367        late  = sum(rewards[half:]) / max(n - half, 1)368        arrow = "↑ improving" if late > early else "↓ declining"369        trend_text = f"Early {half} eps: {early:+.2f}  →  Last {n-half} eps: {late:+.2f}   {arrow}"370    else:371        trend_text = "Collecting data..."372 373    status = "● RUNNING" if _running else "■ STOPPED"374    return road_fig, reward_fig, step_text, ep_text, trend_text, status375 376 377_EMPTY_ROAD   = render_road([], "maintain", "")378_EMPTY_REWARD = render_reward_curve([])379 380with gr.Blocks(title="OpenENV RL Demo", theme=gr.themes.Base()) as demo:381    gr.Markdown(382        "# OpenENV RL — Live Policy Training\n"383        "**FlatMLPPolicy** drives Car 0 on a 3-lane road for 20 steps per episode. "384        "PPO mini-update after each episode — watch rewards trend upward over time."385    )386 387    with gr.Row():388        start_btn  = gr.Button("Start", variant="primary", scale=1)389        stop_btn   = gr.Button("Stop",  variant="stop", interactive=False, scale=1)390        status_box = gr.Textbox(value="■ STOPPED", label="Status",391                                interactive=False, scale=0, min_width=130)392 393    gr.Markdown("### Road View")394    road_plot = gr.Plot(value=_EMPTY_ROAD, show_label=False)395 396    gr.Markdown("### Episode Reward Curve")397    reward_plot = gr.Plot(value=_EMPTY_REWARD, show_label=False)398 399    gr.Markdown("### Live Step Feed (last 20 steps)")400    step_display = gr.Textbox(401        value="Press Start to begin...",402        lines=14, max_lines=14, interactive=False,403    )404 405    with gr.Row():406        with gr.Column():407            gr.Markdown("### Episode History")408            ep_display = gr.Textbox(lines=10, interactive=False)409        with gr.Column():410            gr.Markdown("### Reward Trend")411            trend_display = gr.Textbox(lines=3, interactive=False)412 413    timer = gr.Timer(value=1.0)414    timer.tick(415        fn=get_updates,416        outputs=[road_plot, reward_plot, step_display, ep_display, trend_display, status_box],417    )418 419    start_btn.click(fn=start_training, outputs=[start_btn, stop_btn])420    stop_btn.click(fn=stop_training,   outputs=[start_btn, stop_btn])421 422 423if __name__ == "__main__":424    demo.launch()425