| |
| """ |
| inference.py β Space Traffic Control |
| ===================================== |
| OpenENV-compatible inference script for the Meta AI Hackathon. |
| |
| Wiring: |
| - SpaceTrafficEnv = the openenv environment |
| - HF Inference Router (Qwen2.5-72B) = the LLM agent brain |
| - RuleBasedAgent = fallback + action executor |
| - LLM reads state β decides direction β RL executes |
| |
| STDOUT FORMAT (mandatory): |
| [START] task=<task> env=<benchmark> model=<model> |
| [STEP] step=<n> action=<str> reward=<0.00> done=<true|false> error=<null|msg> |
| [END] success=<true|false> steps=<n> score=<0.000> rewards=<r1,r2,...> |
| |
| ENVIRONMENT VARIABLES: |
| API_BASE_URL β HF Inference Router endpoint |
| MODEL_NAME β LLM model identifier |
| HF_TOKEN β Hugging Face token |
| SPACE_MODE β "2D" or "3D" (default: "2D") |
| """ |
|
|
| import asyncio |
| import os |
| import textwrap |
| from typing import List, Optional |
|
|
| import numpy as np |
| from openai import OpenAI |
|
|
| |
| from space_traffic_control import ( |
| SpaceTrafficEnv, |
| RuleBasedAgent, |
| distances, |
| risk_level, |
| DANGER_RADIUS, |
| COLLISION_RADIUS, |
| MAX_STEPS, |
| ) |
|
|
| |
| |
| |
| API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") |
| API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1") |
| MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct") |
| SPACE_MODE = os.getenv("SPACE_MODE", "2D") |
| TASK_NAME = "satellite-collision-avoidance" |
| BENCHMARK = "space-traffic-control" |
|
|
| EPISODE_STEPS = 50 |
| TEMPERATURE = 0.3 |
| MAX_TOKENS = 120 |
| SUCCESS_SCORE_THRESHOLD = 0.4 |
|
|
| |
| |
| |
| ACTION_MAP_2D = { |
| "maintain": 0, |
| "turn left": 1, |
| "turn right": 2, |
| "accelerate": 3, |
| "decelerate": 4, |
| } |
|
|
| ACTION_MAP_3D = { |
| "maintain": 0, |
| "yaw left": 1, |
| "yaw right": 2, |
| "pitch up": 3, |
| "pitch down": 4, |
| "accelerate": 5, |
| "decelerate": 6, |
| } |
|
|
| |
| |
| |
| def log_start(task: str, env: str, model: str) -> None: |
| print(f"[START] task={task} env={env} model={model}", flush=True) |
|
|
|
|
| def log_step(step: int, action: str, reward: float, |
| done: bool, error: Optional[str]) -> None: |
| error_val = error if error else "null" |
| done_val = str(done).lower() |
| print( |
| f"[STEP] step={step} action={action} reward={reward:.2f} " |
| f"done={done_val} error={error_val}", |
| flush=True, |
| ) |
|
|
|
|
| def log_end(success: bool, steps: int, |
| score: float, rewards: List[float]) -> None: |
| rewards_str = ",".join(f"{r:.2f}" for r in rewards) |
| print( |
| f"[END] success={str(success).lower()} steps={steps} " |
| f"score={score:.3f} rewards={rewards_str}", |
| flush=True, |
| ) |
|
|
|
|
| |
| |
| |
| def build_system_prompt(mode: str) -> str: |
| if mode == "2D": |
| actions = ( |
| "maintain, turn left, turn right, accelerate, decelerate" |
| ) |
| else: |
| actions = ( |
| "maintain, yaw left, yaw right, " |
| "pitch up, pitch down, accelerate, decelerate" |
| ) |
|
|
| return textwrap.dedent(f""" |
| You are the navigation AI for a satellite in {mode} space. |
| Your job is to avoid space debris at all costs. |
| |
| Think of it like driving a car: |
| - Satellite = your vehicle |
| - Debris = obstacles on the road |
| - Collision = game over (-10 reward) |
| - Safe step = +1 reward |
| |
| STRICT RULES β follow these exactly: |
| |
| 1. EMERGENCY (distance < 5, risk > 0.8): |
| β Immediately turn left or turn right |
| β NEVER accelerate when debris is close |
| β This is your top priority |
| |
| 2. DANGER ZONE (distance 5-8, risk 0.5-0.8): |
| β Decelerate first, then turn away |
| β Do NOT maintain β you must act |
| |
| 3. CAUTION (distance 8-15, risk 0.2-0.5): |
| β Gentle turn left or turn right |
| β You can maintain speed here |
| |
| 4. SAFE (distance > 15, risk < 0.2): |
| β maintain or accelerate carefully |
| β Never accelerate above safe speed |
| |
| IMPORTANT: |
| - Collision = instant failure, avoid at ALL costs |
| - Decelerating when close to debris saves lives |
| - Accelerating into debris = collision |
| - Reply with ONLY the action name, nothing else |
| - One word or two words maximum |
| |
| Available actions: {actions} |
| """).strip() |
| |
| |
| |
| def build_user_prompt(env: SpaceTrafficEnv, step: int, |
| last_reward: float, history: List[str]) -> str: |
| sat = env.satellite |
| dlist = env.debris_list |
| dists = distances(sat.pos, dlist) |
| risk = risk_level(sat.pos, dlist) |
| min_d = float(dists.min()) |
| top3 = np.argsort(dists)[:3] |
|
|
| |
| debris_lines = [] |
| for i, idx in enumerate(top3): |
| d = dlist[idx] |
| if env.mode == "2D": |
| debris_lines.append( |
| f" Debris {i+1}: pos=({d.pos[0]:.1f},{d.pos[1]:.1f}) " |
| f"dist={dists[idx]:.1f}" |
| ) |
| else: |
| debris_lines.append( |
| f" Debris {i+1}: pos=({d.pos[0]:.1f},{d.pos[1]:.1f}," |
| f"{d.pos[2]:.1f}) dist={dists[idx]:.1f}" |
| ) |
| debris_str = "\n".join(debris_lines) |
|
|
| |
| if min_d <= COLLISION_RADIUS: |
| danger = "[CRITICAL] COLLISION IMMINENT - turn left or right NOW, do NOT accelerate" |
| elif min_d <= DANGER_RADIUS: |
| danger = "[WARNING] DANGER ZONE - decelerate then turn away immediately" |
| elif min_d <= DANGER_RADIUS * 1.5: |
| danger = "[CAUTION] debris nearby, turn gently, do not accelerate" |
| else: |
| danger = "[CLEAR] path is safe, maintain or gentle accelerate" |
| |
| history_block = "\n".join(history[-3:]) if history else "None" |
|
|
| if env.mode == "2D": |
| pos_str = f"({sat.pos[0]:.1f}, {sat.pos[1]:.1f})" |
| vel_str = f"({sat.vel[0]:.2f}, {sat.vel[1]:.2f})" |
| else: |
| pos_str = f"({sat.pos[0]:.1f}, {sat.pos[1]:.1f}, {sat.pos[2]:.1f})" |
| vel_str = f"({sat.vel[0]:.2f}, {sat.vel[1]:.2f}, {sat.vel[2]:.2f})" |
|
|
| return textwrap.dedent(f""" |
| Step: {step} |
| Satellite position : {pos_str} |
| Satellite velocity : {vel_str} |
| Speed : {sat.speed:.2f} |
| Closest debris dist: {min_d:.2f} |
| Risk level : {risk:.2f} (0=safe, 1=collision) |
| Status : {danger} |
| |
| 3 Nearest debris: |
| {debris_str} |
| |
| Last reward : {last_reward:.2f} |
| Recent actions: |
| {history_block} |
| |
| What is your next action? |
| """).strip() |
| |
| |
| |
| def parse_llm_action(text: str, mode: str) -> int: |
| """ |
| Convert LLM text response to numeric action. |
| Falls back to 0 (maintain) if unrecognised. |
| """ |
| text = text.lower().strip() |
| action_map = ACTION_MAP_2D if mode == "2D" else ACTION_MAP_3D |
|
|
| |
| if text in action_map: |
| return action_map[text] |
|
|
| |
| for key, val in action_map.items(): |
| if key in text: |
| return val |
|
|
| |
| return 0 |
|
|
|
|
| |
| |
| |
| def get_llm_action(client: OpenAI, env: SpaceTrafficEnv, |
| step: int, last_reward: float, |
| history: List[str]) -> tuple[str, int]: |
| """ |
| Ask the LLM what action to take. |
| Returns (action_text, action_int). |
| Falls back to rule-based agent on failure. |
| """ |
| system = build_system_prompt(env.mode) |
| user = build_user_prompt(env, step, last_reward, history) |
|
|
| try: |
| completion = client.chat.completions.create( |
| model=MODEL_NAME, |
| messages=[ |
| {"role": "system", "content": system}, |
| {"role": "user", "content": user}, |
| ], |
| temperature=TEMPERATURE, |
| max_tokens=MAX_TOKENS, |
| stream=False, |
| ) |
| text = (completion.choices[0].message.content or "").strip() |
| if not text: |
| raise ValueError("Empty response from LLM") |
| action_int = parse_llm_action(text, env.mode) |
| return text, action_int |
|
|
| except Exception as exc: |
| |
| print(f"[DEBUG] LLM call failed: {exc} β using rule-based fallback", |
| flush=True) |
| fallback = RuleBasedAgent(env) |
| obs = env._get_obs() |
| action_int = fallback.act(obs) |
| action_map = ACTION_MAP_2D if env.mode == "2D" else ACTION_MAP_3D |
| text = {v: k for k, v in action_map.items()}.get(action_int, "maintain") |
| return text, action_int |
|
|
|
|
| |
| |
| |
| def compute_score(rewards: List[float], survived: bool, |
| steps: int) -> float: |
| """ |
| Normalised score in [0, 1]: |
| - Base: total positive reward / max possible reward |
| - Bonus: +0.2 if survived without collision |
| - Penalty: -0.1 per collision |
| """ |
| if not rewards: |
| return 0.0 |
| max_possible = EPISODE_STEPS * (1.0 + 2.0 + 5.0) |
| base = sum(r for r in rewards if r > 0) / max_possible |
| bonus = 0.2 if survived else -0.1 |
| score = base + bonus |
| return float(min(max(score, 0.0), 1.0)) |
|
|
|
|
| |
| |
| |
| async def main() -> None: |
| |
| client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY) |
| env = SpaceTrafficEnv(mode=SPACE_MODE, render_mode="none") |
|
|
| history: List[str] = [] |
| rewards: List[float] = [] |
| steps_taken: int = 0 |
| score: float = 0.0 |
| success: bool = False |
| survived: bool = True |
| last_error: Optional[str] = None |
|
|
| log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME) |
|
|
| try: |
| |
| env.reset() |
| last_reward = 0.0 |
|
|
| for step in range(1, EPISODE_STEPS + 1): |
|
|
| |
| action_text, action_int = get_llm_action( |
| client, env, step, last_reward, history |
| ) |
|
|
| |
| try: |
| obs, reward, done, info = env.step(action_int) |
| last_error = None |
| except Exception as e: |
| last_error = str(e) |
| reward = 0.0 |
| done = True |
|
|
| rewards.append(reward) |
| steps_taken = step |
| last_reward = reward |
|
|
| |
| if info.get("min_dist", 99) <= COLLISION_RADIUS: |
| survived = False |
| last_error = "collision" |
|
|
| log_step( |
| step=step, |
| action=action_text, |
| reward=reward, |
| done=done, |
| error=last_error, |
| ) |
|
|
| history.append( |
| f"Step {step}: {action_text!r} β reward {reward:+.2f} " |
| f"dist={info.get('min_dist', 0):.1f} " |
| f"risk={info.get('risk', 0):.2f}" |
| ) |
|
|
| if done: |
| break |
|
|
| |
| score = compute_score(rewards, survived, steps_taken) |
| success = score >= SUCCESS_SCORE_THRESHOLD |
|
|
| except Exception as e: |
| last_error = str(e) |
| print(f"[DEBUG] Episode error: {e}", flush=True) |
|
|
| finally: |
| try: |
| env.close() |
| except Exception as e: |
| print(f"[DEBUG] env.close() error: {e}", flush=True) |
|
|
| log_end( |
| success=success, |
| steps=steps_taken, |
| score=score, |
| rewards=rewards, |
| ) |
|
|
|
|
| |
| |
| |
| if __name__ == "__main__": |
| asyncio.run(main()) |