CoolFace
Apppublic

manzz05/Kitchenflow

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
kitchenflow_env_environment.py478 linesDownload Raw Back to root
1"""2KitchenFlow-v1 — Ghost Kitchen Dispatcher Environment3 4Three tasks of increasing difficulty:5  T1  single_order_dispatch    Easy   — 1 order, stable traffic, 30-min window6  T2  multi_order_coordination Medium — 3 orders, varying preps & distances7  T3  peak_hour_rush           Hard   — 5 orders, traffic spikes, dynamic conditions8 9Simulation mechanics (each step = 1 minute):10  • Food prep advances linearly to 1.0 over its prep_time_min11  • Once food is ready (progress=1.0), it starts cooling at COOLING_RATE °C/min12  • When driver is summoned, ETA = dist_km / (BASE_SPEED_KM_MIN / traffic_index)13  • Driver distance shrinks each minute at their current travel speed14  • Delivery happens when: food_ready AND driver_arrived15  • Episode ends when all orders resolved OR time_limit reached16 17Reward function (per order):18  +10  if |driver_arrival_min - food_ready_min| ≤ 2   (perfect timing)19  +5   if |driver_arrival_min - food_ready_min| ≤ 5   (good timing)20  -1   per °C below PERFECT_SERVING_TEMP at delivery21  -20  if driver waits > 15 min (risk of cancellation)22  -5   if food waits > 10 min  (cold food + customer dissatisfaction)23  -10  if order not resolved within time limit (failed delivery)24"""25 26import math27import random28from copy import deepcopy29from typing import Any, Dict, List, Optional, Tuple30from uuid import uuid431 32from openenv.core.env_server.interfaces import Environment33from openenv.core.env_server.types import State34 35try:36    from ..models import KitchenAction, KitchenObservation37except ImportError:38    from models import KitchenAction, KitchenObservation39 40# ─────────────────────────────────────────────41# Physical constants42# ─────────────────────────────────────────────43BASE_SPEED_KM_MIN   = 0.50   # 30 km/h city average44INITIAL_FOOD_TEMP   = 85.0   # °C when food is freshly prepared45PERFECT_TEMP        = 75.0   # °C ideal serving temperature46COOLING_RATE        = 1.8    # °C per minute food sits waiting47DELIVERY_COOL_RATE  = 0.4    # °C per minute during transit (insulated bag)48MAX_DRIVER_WAIT_MIN = 15     # minutes — driver may cancel after this49MAX_FOOD_WAIT_MIN   = 10     # minutes — food quality degrades badly50 51# ─────────────────────────────────────────────52# Traffic patterns  (Uber Movement-style)53# ─────────────────────────────────────────────54def _traffic_at(minute: int, scenario: str) -> float:55    """Return traffic index for a given simulation minute and scenario."""56    if scenario == "stable":57        return 1.258 59    elif scenario == "moderate":60        # Gentle wave: light → moderate → light61        base = 1.362        wave = 0.3 * math.sin(math.pi * minute / 20)63        return round(base + wave, 2)64 65    elif scenario == "peak_hour":66        # Spike between minutes 10–20 (lunch rush), calmer outside67        if minute < 8:68            return 1.169        elif minute < 12:70            return 1.1 + (minute - 8) * 0.25     # ramp up71        elif minute < 20:72            return 2.1 + 0.1 * math.sin(minute)  # peak ~2.0–2.273        else:74            return 2.1 - (minute - 20) * 0.08    # ramp down75    return 1.076 77 78# ─────────────────────────────────────────────79# Order template definitions80# ─────────────────────────────────────────────81ORDER_TEMPLATES = {82    # id, item_name, prep_time_min, initial_driver_dist_km83    "fast":    {"item_name": "Fries & Shake",      "prep_time_min": 10, "driver_dist_km": 1.2},84    "medium":  {"item_name": "Chicken Burger",     "prep_time_min": 15, "driver_dist_km": 2.0},85    "slow":    {"item_name": "Loaded Nachos",      "prep_time_min": 20, "driver_dist_km": 3.5},86    "complex": {"item_name": "Full Grill Platter", "prep_time_min": 25, "driver_dist_km": 4.0},87    "express": {"item_name": "Hot Dog Combo",      "prep_time_min":  8, "driver_dist_km": 0.8},88}89 90 91def _make_order(order_id: str, template_key: str) -> Dict[str, Any]:92    t = ORDER_TEMPLATES[template_key]93    return {94        "order_id":              order_id,95        "item_name":             t["item_name"],96        "template":              template_key,97        "prep_time_min":         t["prep_time_min"],98        "food_prep_progress":    0.0,99        "driver_dist_km":        t["driver_dist_km"],100        "_initial_dist_km":      t["driver_dist_km"],101        "food_temp_c":           INITIAL_FOOD_TEMP,102        "driver_summoned":       False,103        "driver_summon_min":     None,104        "driver_eta_min":        None,    # estimated minutes remaining until arrival105        "driver_arrived":        False,106        "driver_arrived_min":    None,107        "food_ready":            False,108        "food_ready_min":        None,109        "delivered":             False,110        "failed":                False,111        "minutes_food_waited":   0,112        "minutes_driver_waited": 0,113        "status":                "preparing",114        "score":                 None,115    }116 117 118# ─────────────────────────────────────────────119# Scenario definitions120# ─────────────────────────────────────────────121SCENARIOS = {122    "T1_single_order_dispatch": {123        "description": (124            "One order is being prepared. Your job is to summon the driver "125            "at exactly the right moment so they arrive just as the food is bagged. "126            "Watch food_prep_progress (0→1) and driver_dist_km. "127            "Traffic is stable. Submit dispatch_decisions: {ORD001: 1} to call the driver."128        ),129        "difficulty":    "easy",130        "max_time_min":  30,131        "traffic_mode":  "stable",132        "orders":        [("ORD001", "medium")],133    },134    "T2_multi_order_coordination": {135        "description": (136            "Three orders are in progress — a quick snack, a burger, and a loaded platter. "137            "Each has a different prep time and driver distance. "138            "Coordinate dispatch so all three drivers arrive on time. "139            "Traffic fluctuates — watch the traffic_index each minute."140        ),141        "difficulty":    "medium",142        "max_time_min":  35,143        "traffic_mode":  "moderate",144        "orders":        [("ORD001", "fast"), ("ORD002", "medium"), ("ORD003", "slow")],145    },146    "T3_peak_hour_rush": {147        "description": (148            "Five orders land during the lunch rush. "149            "Traffic spikes between minutes 10–20 (index up to 2.2), "150            "slowing all drivers. One driver may be far from the hub. "151            "Maximise food quality and minimise driver idle time across all 5 orders."152        ),153        "difficulty":    "hard",154        "max_time_min":  45,155        "traffic_mode":  "peak_hour",156        "orders":        [157            ("ORD001", "express"),158            ("ORD002", "fast"),159            ("ORD003", "medium"),160            ("ORD004", "slow"),161            ("ORD005", "complex"),162        ],163    },164}165 166 167# ─────────────────────────────────────────────168# Per-order grader169# ─────────────────────────────────────────────170def _score_order(order: Dict[str, Any]) -> Tuple[float, str]:171    """Grade a single completed or failed order. Returns (score 0–1, feedback)."""172 173    if order["failed"]:174        return 0.0, f"{order['order_id']} FAILED (timeout) → score=0.0"175 176    food_ready_min    = order["food_ready_min"]    or 0177    driver_arrived_min = order["driver_arrived_min"] or 0178    food_waited       = order["minutes_food_waited"]179    driver_waited     = order["minutes_driver_waited"]180    temp              = order["food_temp_c"]181 182    # Compute raw score183    raw = 0.0184    timing_gap = abs(driver_arrived_min - food_ready_min)185    if timing_gap <= 2:186        raw += 10    # perfect187    elif timing_gap <= 5:188        raw += 5     # good189 190    temp_penalty   = max(0.0, PERFECT_TEMP - temp)191    raw -= temp_penalty192 193    if driver_waited > MAX_DRIVER_WAIT_MIN:194        raw -= 20195    if food_waited > MAX_FOOD_WAIT_MIN:196        raw -= 5197 198    # Normalize: best possible = +10, worst practical = -35199    # Range span = 45200    score = max(0.0, min(1.0, (raw + 35) / 45.0))201 202    fb = (203        f"{order['order_id']} ({order['item_name']}): "204        f"food_ready=min{food_ready_min} driver=min{driver_arrived_min} "205        f"gap={timing_gap}min temp={temp:.1f}°C "206        f"raw={raw:.1f} score={score:.2f}"207    )208    return round(score, 4), fb209 210 211# ─────────────────────────────────────────────212# Environment213# ─────────────────────────────────────────────214class KitchenflowEnvironment(Environment):215    SUPPORTS_CONCURRENT_SESSIONS: bool = True216 217    def __init__(self):218        self._state    = State(episode_id=str(uuid4()), step_count=0)219        self._task_id  = ""220        self._task_idx = 0221        self._scenario: Dict[str, Any] = {}222        self._orders:   List[Dict[str, Any]] = []223        self._time_min  = 0224        self._max_time  = 30225        self._traffic_mode = "stable"226        self._done     = False227        self._episode_score = 0.0228        self._order_scores: Dict[str, float] = {}229 230    # ── helpers ─────────────────────────────────────────────────────────────231 232    def _traffic(self) -> float:233        return _traffic_at(self._time_min, self._traffic_mode)234 235    def _order_snapshot(self, o: Dict) -> Dict[str, Any]:236        return {237            "order_id":              o["order_id"],238            "item_name":             o["item_name"],239            "food_prep_progress":    round(o["food_prep_progress"], 3),240            "driver_dist_km":        round(o["driver_dist_km"], 2),241            "food_temp_c":           round(o["food_temp_c"], 1),242            "driver_summoned":       o["driver_summoned"],243            "driver_eta_min":        o["driver_eta_min"],244            "food_ready":            o["food_ready"],245            "driver_arrived":        o["driver_arrived"],246            "delivered":             o["delivered"],247            "failed":                o["failed"],248            "minutes_food_waited":   o["minutes_food_waited"],249            "minutes_driver_waited": o["minutes_driver_waited"],250            "status":                o["status"],251        }252 253    def _build_obs(self, feedback: str) -> KitchenObservation:254        delivered = sum(1 for o in self._orders if o["delivered"])255        failed    = sum(1 for o in self._orders if o["failed"])256        t_penalty = sum(257            max(0, PERFECT_TEMP - o["food_temp_c"])258            for o in self._orders if o["delivered"]259        )260        w_penalty = sum(261            20 for o in self._orders262            if o["delivered"] and o["minutes_driver_waited"] > MAX_DRIVER_WAIT_MIN263        )264        running_score = (265            sum(self._order_scores.values()) / len(self._orders)266            if self._order_scores else 0.0267        )268        return KitchenObservation(269            task_id=self._task_id,270            task_description=self._scenario.get("description", ""),271            difficulty=self._scenario.get("difficulty", ""),272            time_min=self._time_min,273            max_time_min=self._max_time,274            traffic_index=round(self._traffic(), 2),275            orders=[self._order_snapshot(o) for o in self._orders],276            orders_delivered=delivered,277            orders_failed=failed,278            total_temp_penalty=round(t_penalty, 1),279            total_waste_penalty=round(w_penalty, 1),280            last_action_feedback=feedback,281            score=round(running_score, 4),282            attempts=self._time_min,283            max_attempts=self._max_time,284            done=self._done,285            reward=0.0,286        )287 288    # ── reset ────────────────────────────────────────────────────────────────289 290    def reset(self, task_id: Optional[str] = None) -> KitchenObservation:291        task_ids = list(SCENARIOS.keys())292        if task_id and task_id in SCENARIOS:293            self._task_id = task_id294        else:295            self._task_id = task_ids[self._task_idx % len(task_ids)]296            self._task_idx += 1297 298        self._scenario     = SCENARIOS[self._task_id]299        self._time_min     = 0300        self._max_time     = self._scenario["max_time_min"]301        self._traffic_mode = self._scenario["traffic_mode"]302        self._done         = False303        self._episode_score = 0.0304        self._order_scores  = {}305        self._state         = State(episode_id=str(uuid4()), step_count=0)306 307        self._orders = [308            _make_order(oid, tkey)309            for oid, tkey in self._scenario["orders"]310        ]311 312        return self._build_obs(313            "Kitchen is open. Watch food_prep_progress and traffic_index carefully. "314            "Summon the driver at the right moment!"315        )316 317    # ── step ─────────────────────────────────────────────────────────────────318 319    def step(self, action: KitchenAction) -> KitchenObservation:320        if not self._orders:321            self.reset()322 323        self._state.step_count += 1324        self._time_min += 1325        traffic = self._traffic()326        speed   = BASE_SPEED_KM_MIN / traffic     # km per minute327 328        events: List[str] = [f"Min {self._time_min} | traffic={traffic:.2f}"]329 330        # Process each order331        for o in self._orders:332            if o["delivered"] or o["failed"]:333                continue334 335            oid = o["order_id"]336 337            # 1. Advance food preparation338            if not o["food_ready"]:339                o["food_prep_progress"] = min(340                    1.0,341                    o["food_prep_progress"] + 1.0 / o["prep_time_min"]342                )343                if o["food_prep_progress"] >= 1.0:344                    o["food_ready"]     = True345                    o["food_ready_min"] = self._time_min346                    o["status"]         = "food_ready"347                    o["food_temp_c"]    = INITIAL_FOOD_TEMP348                    events.append(f"  🍔 {oid} food READY (min {self._time_min})")349 350            # 2. Handle dispatch action351            decision = action.dispatch_decisions.get(oid, 0)352            if decision == 1 and not o["driver_summoned"] and not o["delivered"]:353                o["driver_summoned"]  = True354                o["driver_summon_min"] = self._time_min355                eta = o["driver_dist_km"] / speed356                o["driver_eta_min"] = round(eta, 1)357                o["status"] = "driver_en_route"358                events.append(359                    f"  🛵 {oid} driver SUMMONED — dist={o['driver_dist_km']:.1f}km "360                    f"ETA≈{eta:.1f}min"361                )362 363            # 3. Move driver closer364            if o["driver_summoned"] and not o["driver_arrived"]:365                moved = speed366                o["driver_dist_km"] = max(0.0, o["driver_dist_km"] - moved)367                # Update ETA368                if speed > 0:369                    o["driver_eta_min"] = round(o["driver_dist_km"] / speed, 1)370 371                if o["driver_dist_km"] <= 0.0:372                    o["driver_arrived"]     = True373                    o["driver_arrived_min"] = self._time_min374                    o["driver_dist_km"]     = 0.0375                    o["driver_eta_min"]     = 0376                    events.append(f"  🏍️  {oid} driver ARRIVED (min {self._time_min})")377 378            # 4. Cool food if it's ready and waiting379            if o["food_ready"] and not o["delivered"]:380                if not o["driver_arrived"]:381                    # Food sitting under heat lamp, slowly cooling382                    o["food_temp_c"] = max(383                        40.0, o["food_temp_c"] - COOLING_RATE384                    )385                    o["minutes_food_waited"] += 1386                else:387                    # Both ready → deliver388                    # Cool slightly during final hand-off389                    o["food_temp_c"] = max(40.0, o["food_temp_c"] - 0.5)390 391            # 5. Driver idle at hub waiting for food392            if o["driver_arrived"] and not o["food_ready"]:393                o["minutes_driver_waited"] += 1394 395            # 6. Deliver if both ready396            if o["food_ready"] and o["driver_arrived"] and not o["delivered"]:397                o["delivered"] = True398                o["status"]    = "delivered"399                s, fb = _score_order(o)400                o["score"]     = s401                self._order_scores[oid] = s402                events.append(f"  ✅ {oid} DELIVERED  {fb}")403 404            # 7. Timeout405            if self._time_min >= self._max_time and not o["delivered"]:406                o["failed"] = True407                o["status"] = "failed"408                self._order_scores[oid] = 0.0409                events.append(f"  ❌ {oid} FAILED (timeout)")410 411        # Check if episode is done412        all_resolved = all(o["delivered"] or o["failed"] for o in self._orders)413        self._done   = all_resolved or self._time_min >= self._max_time414 415        # Final episode score416        if self._done and self._order_scores:417            self._episode_score = round(418                sum(self._order_scores.values()) / len(self._orders), 4419            )420 421        # Per-step shaped reward422        step_reward = self._shaped_reward()423 424        feedback = " | ".join(events)425        obs = self._build_obs(feedback)426        obs.score  = self._episode_score if self._done else round(427            sum(self._order_scores.values()) / len(self._orders), 4428        ) if self._order_scores else 0.0429        obs.reward = round(step_reward, 4)430        return obs431 432    def _shaped_reward(self) -> float:433        """434        Per-step shaped reward to guide the agent before episode end.435        Rewards progress: food staying warm, efficient driver dispatch.436        """437        reward = 0.0438        for o in self._orders:439            if o["delivered"]:440                continue441            if o["failed"]:442                reward -= 0.5443                continue444 445            # Small bonus while food is staying warm (not yet ready or just ready)446            if o["food_ready"] and o["food_temp_c"] >= PERFECT_TEMP:447                reward += 0.05448 449            # Penalty each minute food sits waiting without a driver450            if o["food_ready"] and not o["driver_summoned"]:451                reward -= 0.10452 453            # Penalty each minute driver sits idle at hub454            if o["driver_arrived"] and not o["food_ready"]:455                reward -= 0.15456 457        return reward458 459    @property460    def state(self) -> State:461        return self._state462 463    def close(self):464        pass465 466 467# Expose task list for the /tasks endpoint468TASKS = [469    {470        "task_id":     tid,471        "description": sc["description"],472        "difficulty":  sc["difficulty"],473        "max_time_min": sc["max_time_min"],474        "n_orders":    len(sc["orders"]),475    }476    for tid, sc in SCENARIOS.items()477]478