Spaces:
Runtime error
Runtime error
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse | |
| import asyncio | |
| import threading | |
| import json | |
| import uvicorn | |
| from pydantic import BaseModel | |
| from engine import start_simulation, run_batch_simulation | |
| app = FastAPI(title="Catsy Simulation") | |
| # Mount static files for HTML/CSS/JS | |
| app.mount("/static", StaticFiles(directory="static"), name="static") | |
| async def get_index(): | |
| return FileResponse("static/index.html") | |
| class SimConfig(BaseModel): | |
| cashiers: int | |
| baristas: int | |
| tables: int | |
| resArrivalMin: float | |
| resArrivalMax: float | |
| arrival: float | |
| decideMin: float | |
| decideMax: float | |
| payMin: float | |
| payMax: float | |
| prepMin: float | |
| prepMax: float | |
| dwellMin: float | |
| dwellMax: float | |
| balkProb: float | |
| balkThreshold: int | |
| renegeProb: float | |
| maxStrikes: int | |
| takeoutProb: float | |
| resProb: float | |
| warmupTime: float | |
| replications: int | |
| duration: int = 7200 | |
| async def analyze_simulation(cfg: SimConfig): | |
| import asyncio | |
| config = { | |
| "cashier_count": cfg.cashiers, | |
| "barista_count": cfg.baristas, | |
| "table_count": cfg.tables, | |
| "res_arrival_min": cfg.resArrivalMin, | |
| "res_arrival_max": cfg.resArrivalMax, | |
| "avg_arrival_time": cfg.arrival, | |
| "decide_min": cfg.decideMin, | |
| "decide_max": cfg.decideMax, | |
| "pay_min": cfg.payMin, | |
| "pay_max": cfg.payMax, | |
| "prep_min": cfg.prepMin, | |
| "prep_max": cfg.prepMax, | |
| "dwell_min": cfg.dwellMin, | |
| "dwell_max": cfg.dwellMax, | |
| "balk_prob": cfg.balkProb, | |
| "balk_threshold": cfg.balkThreshold, | |
| "renege_prob": cfg.renegeProb, | |
| "max_strikes": cfg.maxStrikes, | |
| "takeout_prob": cfg.takeoutProb, | |
| "res_prob": cfg.resProb, | |
| "warmup_time": cfg.warmupTime, | |
| } | |
| rep_count = cfg.replications if hasattr(cfg, 'replications') else 10 | |
| duration_s = cfg.duration if hasattr(cfg, 'duration') and cfg.duration else 7200 | |
| # Run replications in a threadpool to prevent freezing the server | |
| results = await asyncio.to_thread(run_batch_simulation, config, rep_count, duration_s) | |
| return results | |
| SIM_STATE = {"paused": False} | |
| async def pause_simulation(): | |
| SIM_STATE["paused"] = True | |
| return {"status": "paused"} | |
| async def resume_simulation(): | |
| SIM_STATE["paused"] = False | |
| return {"status": "resumed"} | |
| # Background task queue for this specific connection | |
| # (To support multiple connections properly, we'd need a ConnectionManager, but for a single UI this is fine) | |
| async def websocket_endpoint(websocket: WebSocket): | |
| await websocket.accept() | |
| # Parse dynamic configuration | |
| cashiers = int(websocket.query_params.get("cashiers", 1)) | |
| baristas = int(websocket.query_params.get("baristas", 2)) | |
| tables = int(websocket.query_params.get("tables", 5)) | |
| res_arrival_min = float(websocket.query_params.get("resArrivalMin", 30.0)) | |
| res_arrival_max = float(websocket.query_params.get("resArrivalMax", 180.0)) | |
| arrival = float(websocket.query_params.get("arrival", 3.0)) | |
| decide_min = float(websocket.query_params.get("decideMin", 10.0)) | |
| decide_max = float(websocket.query_params.get("decideMax", 60.0)) | |
| pay_min = float(websocket.query_params.get("payMin", 2.0)) | |
| pay_max = float(websocket.query_params.get("payMax", 10.0)) | |
| prep_min = float(websocket.query_params.get("prepMin", 120.0)) | |
| prep_max = float(websocket.query_params.get("prepMax", 300.0)) | |
| dwell_min = float(websocket.query_params.get("dwellMin", 900.0)) | |
| dwell_max = float(websocket.query_params.get("dwellMax", 3600.0)) | |
| duration_str = websocket.query_params.get("duration", "0") | |
| balk_prob = float(websocket.query_params.get("balkProb", 0.5)) | |
| balk_threshold = int(websocket.query_params.get("balkThreshold", 8)) | |
| renege_prob = float(websocket.query_params.get("renegeProb", 0.3)) | |
| max_strikes = int(websocket.query_params.get("maxStrikes", 3)) | |
| takeout_prob = float(websocket.query_params.get("takeoutProb", 0.5)) | |
| res_prob = float(websocket.query_params.get("resProb", 0.2)) | |
| warmup_time = float(websocket.query_params.get("warmupTime", 0.0)) | |
| config = { | |
| "cashier_count": cashiers, | |
| "barista_count": baristas, | |
| "table_count": tables, | |
| "res_arrival_min": res_arrival_min, | |
| "res_arrival_max": res_arrival_max, | |
| "avg_arrival_time": arrival, | |
| "decide_min": decide_min, | |
| "decide_max": decide_max, | |
| "pay_min": pay_min, | |
| "pay_max": pay_max, | |
| "prep_min": prep_min, | |
| "prep_max": prep_max, | |
| "dwell_min": dwell_min, | |
| "dwell_max": dwell_max, | |
| "duration": float(duration_str) if duration_str else 0, | |
| "balk_prob": balk_prob, | |
| "balk_threshold": balk_threshold, | |
| "renege_prob": renege_prob, | |
| "max_strikes": max_strikes, | |
| "takeout_prob": takeout_prob, | |
| "res_prob": res_prob, | |
| "warmup_time": warmup_time, | |
| } | |
| import queue | |
| thread_queue = queue.Queue() | |
| def run_sim(): | |
| # Pass config to simulation | |
| start_simulation(thread_queue, config, speed_factor=0.5, sim_state=SIM_STATE) | |
| sim_thread = threading.Thread(target=run_sim, daemon=True) | |
| sim_thread.start() | |
| try: | |
| while True: | |
| # Check for events from the simulation thread | |
| try: | |
| event = thread_queue.get_nowait() | |
| await websocket.send_text(json.dumps(event)) | |
| except queue.Empty: | |
| await asyncio.sleep(0.01) # Yield to event loop | |
| except Exception as e: | |
| print(f"Client disconnected or connection error: {e}") | |
| if __name__ == "__main__": | |
| uvicorn.run("main:app", host="127.0.0.1", port=8000) | |