CoolFace
Apppublic

PunitRaveendran/Air_Traffic_Control_System

sourceHugging Faceapache-2.0updated 6mo agoView on Hugging Face
1likes
inference.py446 linesDownload Raw Back to root
1"""Inference script for ATC environment using OpenAI client."""2import json3import os4import re5from typing import List, Optional, Tuple6 7from openai import OpenAI8 9from env.atc_env import ATCEnv, Action10from env.aircraft import AircraftStatus, Priority11from graders.grader1 import grade as grade112from graders.grader2 import grade as grade213from graders.grader3 import grade as grade314 15# ==========================================16# HACKATHON COMPLIANT LOGGING FUNCTIONS17# ==========================================18def log_start(task: str, env: str, model: str) -> None:19    print(f"[START] task={task} env={env} model={model}", flush=True)20 21def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:22    error_val = error if error else "null"23    done_val = str(done).lower()24    print(25        f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",26        flush=True,27    )28 29def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:30    rewards_str = ",".join(f"{r:.2f}" for r in rewards)31    clamped_score = min(max(score, 0.0), 1.0)32    print(f"[END] success={str(success).lower()} steps={steps} score={clamped_score:.3f} rewards={rewards_str}", flush=True)33# ==========================================34 35 36def will_exhaust_fuel(ac: dict) -> bool:37    """True if this aircraft will run out of fuel before it can reach the runway."""38    return ac.get("fuel_remaining_min", 999) < ac.get("eta_steps", 999)39 40 41def get_sort_key(ac: dict):42    """43    Urgency sort (ascending = handle first):44      0: fuel < ETA  → will crash without immediate action45      1: EMERGENCY priority46      2: FUEL_CRITICAL priority47      3: fuel < 15 (about to become fuel-critical)48      4: NORMAL49    Secondary: least fuel first, then earliest ETA.50    """51    fuel = ac.get("fuel_remaining_min", 999)52    eta = ac.get("eta_steps", 999)53    priority = ac.get("priority", "NORMAL")54 55    if will_exhaust_fuel(ac):56        tier = 057    elif priority == "EMERGENCY":58        tier = 159    elif priority == "FUEL_CRITICAL":60        tier = 261    elif fuel < 15:62        tier = 363    else:64        tier = 465 66    return (tier, fuel, eta)67 68 69def get_fallback_action(env: ATCEnv, task_id: int = 1, current_step: int = 0) -> List[Action]:70    """71    Deterministic greedy controller — used when LLM is bypassed or fails.72 73    Key behaviours:74    - Sorts all unassigned aircraft by urgency (crash risk > emergency > fuel_critical > normal).75    - Task 3, step >= 2: avoids RW02 (it closes at step 3; stranded assignments are now76      reset by the env on closure, but better not to assign there in the first place).77    - Assigns ALL unassigned aircraft every step to keep queue minimal.78    - Round-robin across available runways with monotonically increasing sequence positions.79    """80    state = env.state().model_dump()81    aircraft_list = state.get("aircraft", [])82 83    unassigned = [84        ac for ac in aircraft_list85        if ac.get("status") not in ("LANDED", "LANDING", "ASSIGNED")86    ]87 88    if not unassigned:89        return []90 91    ordered = sorted(unassigned, key=get_sort_key)92 93    runways = state.get("runways", [])94    available_runways = [rw for rw in runways if rw.get("status") != "CLOSED"]95 96    # Task 3: proactively avoid RW02 at step >= 2 since it closes at step 3.97    # The env will reset stuck aircraft on closure, but avoiding assignment is cleaner.98    if task_id == 3 and current_step >= 2:99        available_runways = [rw for rw in available_runways if rw["id"] != "RW02"]100 101    if not available_runways:102        return []103 104    runway_ids = [rw["id"] for rw in available_runways]105    runway_queue_pos = {rw["id"]: 1 for rw in available_runways}106 107    actions = []108    for i, ac in enumerate(ordered):109        runway_id = runway_ids[i % len(runway_ids)]110        seq_pos = runway_queue_pos[runway_id]111        runway_queue_pos[runway_id] += 1112 113        fuel = ac.get("fuel_remaining_min", 999)114        priority = ac.get("priority", "NORMAL")115        is_critical = (116            will_exhaust_fuel(ac)117            or priority in ("EMERGENCY", "FUEL_CRITICAL")118            or fuel <= 15119        )120        action_type = "expedite" if is_critical else "assign"121 122        actions.append(Action(123            aircraft_id=ac["id"],124            action_type=action_type,125            runway_id=runway_id,126            sequence_position=seq_pos,127        ))128 129    return actions130 131 132def should_skip_llm(task_id: int, current_step: int) -> bool:133    """134    Returns True to bypass the LLM and use deterministic controller instead.135 136    Task 2: Always skip — fuel-critical aircraft (AC3, AC6) must be expedited in step 1.137            LLM API failures or mis-ordering are catastrophic here.138    Task 3: Skip for steps 1-12 to reliably handle:139            - Steps 1-2: assign all 15 initial aircraft before RW02 closes140            - Step 3: RW02 closes (env auto-reassigns stuck aircraft to HOLDING)141            - Step 5: AC10 becomes EMERGENCY — must be expedited immediately142            - Steps 4-12: batch arrivals at steps 4, 8 need immediate assignment143    """144    if task_id == 2:145        return True146    if task_id == 3 and current_step <= 12:147        return True148    return False149 150 151def parse_llm_response(response_text: str) -> List[dict]:152    """Parse LLM response, stripping markdown fences before extracting JSON."""153    clean = re.sub(r'```(?:json)?', '', response_text).strip()154    clean = clean.replace('```', '')155 156    match = re.search(r'\{.*\}', clean, re.DOTALL)157    if match:158        try:159            data = json.loads(match.group(0))160            if isinstance(data, dict) and "actions" in data:161                return data["actions"]162            if isinstance(data, list):163                return data164        except json.JSONDecodeError:165            pass166 167    return []168 169 170def build_prompt(observation: dict, state: dict, task_id: int) -> str:171    """Task-specific prompts targeting each grader's exact scoring components."""172    aircraft_list = state.get("aircraft", [])173    current_step = state.get("timestep", 0)174 175    # Show only actionable aircraft, sorted by urgency176    actionable = [177        ac for ac in aircraft_list178        if ac.get("status") in ("INBOUND", "HOLDING")179    ]180    sorted_aircraft = sorted(actionable, key=get_sort_key)181 182    aircraft_lines = []183    for ac in sorted_aircraft:184        priority = ac.get("priority", "NORMAL")185        fuel = ac.get("fuel_remaining_min", 0)186        dist = ac.get("distance_nm", 0)187        eta = ac.get("eta_steps", 999)188        warning = ""189        if will_exhaust_fuel(ac):190            warning = " *** CRASH RISK: fuel < ETA — EXPEDITE NOW ***"191        elif priority in ("EMERGENCY", "FUEL_CRITICAL") or fuel < 15:192            warning = " *** CRITICAL ***"193        aircraft_lines.append(194            f"- {ac['id']}: priority={priority}, fuel={fuel:.1f}min, "195            f"dist={dist:.1f}nm, ETA={eta}steps{warning}"196        )197 198    runway_lines = []199    for rw in state.get("runways", []):200        status = rw.get("status", "UNKNOWN")201        note = " (DO NOT USE)" if status == "CLOSED" else ""202        runway_lines.append(f"- {rw['id']}: {status}{note}")203 204    if task_id == 1:205        task_focus = ("Maximize aircraft landed. Assign all INBOUND/HOLDING aircraft "206                      "to runways immediately. Spread load across runways.")207    elif task_id == 2:208        task_focus = (209            "SCORED: 40% landings + 40% fuel-critical lands + 20% no duplicate positions.\n"210            "CRITICAL: Aircraft marked CRASH RISK must be expedited first or the episode continues "211            "with a severe penalty. Always expedite FUEL_CRITICAL aircraft.\n"212            "Never give two aircraft the same sequence_position on the same runway."213        )214    else:215        extra = ""216        if current_step >= 2:217            extra += "\nWARNING: RW02 is closed or closing — assign ALL aircraft to RW01 only."218        if current_step >= 5:219            extra += "\nAC10 is EMERGENCY — expedite immediately if not yet LANDED."220        task_focus = (221            "SCORED: 25% landings + 25% emergency lands + 25% no fuel exhaustion + 25% queue<=12.\n"222            "Assign ALL unassigned aircraft every step. Expedite any CRASH RISK or EMERGENCY aircraft."223            + extra224        )225 226    prompt = f"""You are an expert ATC sequencing controller.227Timestep: {current_step}228 229Aircraft needing assignment (most urgent first):230{chr(10).join(aircraft_lines) if aircraft_lines else "  (none — all aircraft already assigned or landed)"}231 232Runways:233{chr(10).join(runway_lines)}234 235{task_focus}236 237RULES:2381. Never assign to a CLOSED runway.2392. Unique sequence_position per runway: if 3 aircraft go to RW01 use positions 1, 2, 3.2403. Use "expedite" for EMERGENCY, FUEL_CRITICAL, or CRASH RISK aircraft.2414. Use "assign" for NORMAL aircraft.2425. Only include INBOUND or HOLDING aircraft in actions.243 244Respond ONLY with this JSON:245```json246{{247  "thought_process": "brief reasoning",248  "actions": [249    {{"aircraft_id": "AC3", "action_type": "expedite", "runway_id": "RW01", "sequence_position": 1}},250    {{"aircraft_id": "AC1", "action_type": "assign", "runway_id": "RW01", "sequence_position": 2}}251  ]252}}253```"""254    return prompt255 256 257def apply_guardrails(action_dicts: List[dict], state_dump: dict) -> List[Action]:258    """259    Validate and sanitize LLM action dicts before converting to Action objects.260    Re-sorts by urgency and enforces unique sequence positions per runway.261    """262    open_runways = [263        rw["id"] for rw in state_dump.get("runways", [])264        if rw.get("status") != "CLOSED"265    ]266    aircraft_map = {a["id"]: a for a in state_dump.get("aircraft", [])}267 268    # Sort by urgency before assigning positions269    action_dicts_sorted = sorted(270        action_dicts,271        key=lambda ad: get_sort_key(aircraft_map.get(ad.get("aircraft_id", ""), {}))272    )273 274    runway_queue_pos = {}275    actions = []276 277    for action_dict in action_dicts_sorted:278        try:279            ac_id = action_dict.get("aircraft_id", "")280            action_type = action_dict.get("action_type", "hold")281            runway_id = action_dict.get("runway_id")282 283            ac_data = aircraft_map.get(ac_id)284            if ac_data:285                is_critical = (286                    will_exhaust_fuel(ac_data)287                    or ac_data.get("fuel_remaining_min", 999) <= 15288                    or ac_data.get("priority") in ("EMERGENCY", "FUEL_CRITICAL")289                )290                # Never hold a critical aircraft291                if action_type == "hold" and is_critical:292                    action_type = "expedite"293                # Critical aircraft must have an open runway294                if is_critical and (not runway_id or runway_id not in open_runways):295                    if open_runways:296                        runway_id = open_runways[0]297 298            # Never assign to closed runway299            if runway_id and runway_id not in open_runways:300                if open_runways:301                    runway_id = open_runways[0]302                else:303                    continue304 305            # Enforce unique, monotonically increasing sequence positions per runway306            if runway_id:307                if runway_id not in runway_queue_pos:308                    runway_queue_pos[runway_id] = 1309                seq_pos = runway_queue_pos[runway_id]310                runway_queue_pos[runway_id] += 1311            else:312                seq_pos = action_dict.get("sequence_position", 1)313 314            actions.append(Action(315                aircraft_id=ac_id,316                action_type=action_type,317                runway_id=runway_id,318                sequence_position=seq_pos,319            ))320 321        except Exception:322            pass323 324    return actions325 326 327def run_inference(task_id: int, model_name: str, api_base: str, api_key: str) -> Tuple[float, float, dict]:328    """Run inference on a single task."""329    client = OpenAI(base_url=api_base, api_key=api_key)330    env = ATCEnv(seed=42)331    obs = env.reset(task_id)332    episode_log = {"task_id": task_id, "steps": []}333    all_step_rewards = []334    log_start(task=f"task_{task_id}", env="atc-openenv", model=model_name)335 336    step_num = 0337    total_reward = 0.0338 339    while not obs.episode_done:340        step_num += 1341        state = env.state()342        state_dump = state.model_dump()343        current_step = state_dump.get("timestep", step_num)344        actions = []345        error_msg = None346 347        needs_action = sum(348            1 for ac in state_dump.get("aircraft", [])349            if ac.get("status") in ("INBOUND", "HOLDING")350        )351 352        if needs_action > 0:353            if should_skip_llm(task_id, current_step):354                # Use deterministic controller directly — faster and safer for critical steps355                actions = get_fallback_action(env, task_id=task_id, current_step=current_step)356            else:357                # Use LLM with deterministic fallback358                prompt = build_prompt(obs.model_dump(), state_dump, task_id)359                try:360                    response = client.chat.completions.create(361                        model=model_name,362                        messages=[363                            {364                                "role": "system",365                                "content": (366                                    "You are an expert ATC controller. "367                                    "Respond with valid JSON only. No text outside the JSON block."368                                ),369                            },370                            {"role": "user", "content": prompt},371                        ],372                        temperature=0.0,373                        max_tokens=1500,374                    )375                    response_text = response.choices[0].message.content if response.choices else ""376                    action_dicts = parse_llm_response(response_text)377                    if action_dicts:378                        actions = apply_guardrails(action_dicts, state_dump)379                except Exception as e:380                    error_msg = str(e)381 382                # Fallback if LLM returned nothing or failed383                if not actions:384                    actions = get_fallback_action(env, task_id=task_id, current_step=current_step)385 386        obs, reward, done, info = env.step(action=actions)387        total_reward += reward.value388        all_step_rewards.append(reward.value)389 390        episode_log["steps"].append({"step": step_num, "reward": reward.value})391 392        action_str = (393            json.dumps([a.model_dump() for a in actions]).replace(" ", "")394            if actions else "none"395        )396        log_step(step=step_num, action=action_str, reward=reward.value, done=done, error=error_msg)397 398    grader_inputs = env.get_grader_input()399    episode_log["grader_input"] = grader_inputs400 401    if task_id == 1:402        final_score = grade1(grader_inputs)403    elif task_id == 2:404        final_score = grade2(grader_inputs)405    else:406        final_score = grade3(grader_inputs)407 408    success = final_score >= 0.5409    log_end(success=success, steps=step_num, score=final_score, rewards=all_step_rewards)410 411    return final_score, total_reward, episode_log412 413 414def main():415    API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")416    MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")417    HF_TOKEN = os.getenv("HF_TOKEN")418 419    print(f"[DEBUG] Starting ATC inference with model: {MODEL_NAME}")420    print(f"[DEBUG] API base: {API_BASE_URL}")421 422    total_score = 0.0423    results = []424 425    for task_id in [1, 2, 3]:426        print(f"\n[DEBUG] ===== Running Task {task_id} =====")427        try:428            score, reward, log = run_inference(task_id, MODEL_NAME, API_BASE_URL, HF_TOKEN)429            results.append({"task_id": task_id, "score": score, "reward": reward})430            total_score += score431        except Exception as e:432            print(f"[DEBUG] Error running task {task_id}: {e}")433            results.append({"task_id": task_id, "score": 0.0, "reward": 0.0})434 435    avg_score = total_score / 3.0436 437    print("\n" + "=" * 50)438    print("[DEBUG] FINAL RESULTS")439    print("=" * 50)440    for r in results:441        print(f"[DEBUG] Task {r['task_id']}: score={r['score']:.4f}, reward={r['reward']:.4f}")442    print(f"[DEBUG] Average score: {avg_score:.4f}")443 444 445if __name__ == "__main__":446    main()