""" Standalone, zero-dependency pure-Python inference for CanRun FPS Model v1.0. Executes the 21-feature physical engineering pipeline and the Two-Stage Standardized Log-Space Ridge Prior + Monotonic Huber GBDT Residual ensemble using only Python's standard library (`json`, `math`, `re`, `pathlib`). """ from __future__ import annotations import json import math import re import urllib.request from pathlib import Path from typing import Any MODEL_VERSION = "1.0" MODEL_NAME = "canrun-fps-predictor" FEATURE_NAMES: list[str] = [ "log_gpu_passmark", "log_cpu_passmark", "gpu_vram_gb", "gpu_arch_code", "cpu_to_gpu_log_ratio", "gpu_tier_score", "log_pixel_ratio", "vram_per_mpixel", "is_4k", "log_rec_gpu_passmark", "log_min_gpu_passmark", "gpu_headroom_rec", "gpu_headroom_min", "vram_headroom_gb", "spec_demand_index", "headroom_x_resolution", "cpu_bottleneck_1080p", "vram_cliff_penalty", "is_capped_60", "genre_demand", "cpu_headroom_rec", ] MONOTONIC_CONSTRAINTS: list[int] = [ 1, # log_gpu_passmark 1, # log_cpu_passmark 1, # gpu_vram_gb 1, # gpu_arch_code 1, # cpu_to_gpu_log_ratio 1, # gpu_tier_score -1, # log_pixel_ratio 1, # vram_per_mpixel -1, # is_4k 0, # log_rec_gpu_passmark 0, # log_min_gpu_passmark 0, # gpu_headroom_rec 0, # gpu_headroom_min 0, # vram_headroom_gb 0, # spec_demand_index 0, # headroom_x_resolution 1, # cpu_bottleneck_1080p 0, # vram_cliff_penalty -1, # is_capped_60 -1, # genre_demand 1, # cpu_headroom_rec ] RESOLUTION_PIXELS: dict[str, int] = { "720p": 1280 * 720, "1080p": 1920 * 1080, "1440p": 2560 * 1440, "4K": 3840 * 2160, } BASE_PIXELS_1080P = float(1920 * 1080) CAPPED_60_TITLE_MARKERS: tuple[str, ...] = ( "elden ring", "sekiro", "dark souls", "bloodborne", "street fighter", "tekken", "guilty gear", "mortal kombat", ) def infer_gpu_arch_code(gpu_name: str, gen_hint: str = "") -> float: """Map GPU model or generation string to ordinal architecture code [1.0 .. 5.0].""" text = f"{gpu_name} {gen_hint}".lower() if "rtx_50" in text or re.search(r"\brtx\s*50\d{2}\b", text) or "blackwell" in text: return 5.0 if ( "rtx_40" in text or re.search(r"\brtx\s*40\d{2}\b", text) or "rx_7000" in text or re.search(r"\brx\s*7\d{3}\b", text) or "ada" in text ): return 4.0 if ( "rtx_30" in text or re.search(r"\brtx\s*30\d{2}\b", text) or "rx_6000" in text or re.search(r"\brx\s*6\d{3}\b", text) or "ampere" in text ): return 3.0 if ( "rtx_20" in text or "gtx_16" in text or re.search(r"\b(?:rtx\s*20|gtx\s*16)\d{2}\b", text) or "turing" in text ): return 2.0 return 1.0 def normalize_resolution_label(resolution: str | None) -> str: """Normalize resolution strings to one of 720p, 1080p, 1440p, 4K.""" cleaned = re.sub(r"[^a-z0-9]+", "", (resolution or "1080p").lower()) if cleaned in {"4k", "2160p", "3840x2160", "uhd"}: return "4K" if cleaned in {"1440p", "2560x1440", "qhd", "2k"}: return "1440p" if cleaned in {"720p", "1280x720", "hd"}: return "720p" return "1080p" def infer_capped_60(game_name: str = "", genre: str = "") -> float: """Detect a 60 FPS engine lock from title/genre text — never from a game ID.""" text = f"{game_name} {genre}".lower() if any(marker in text for marker in CAPPED_60_TITLE_MARKERS): return 1.0 if "fighting" in text and "engine lock" in text: return 1.0 return 0.0 def infer_genre_demand_offset(genre: str = "") -> float: """Map genre string from Steam Store metadata to physical rendering load offset [-0.35 .. +0.25].""" g = (genre or "").lower() if any(k in g for k in ("esports", "competitive", "moba", "sports", "counter-strike", "valorant")): return -0.35 if any(k in g for k in ("indie", "platformer", "2d", "roguelike", "metroidvania", "puzzle", "card")): return -0.25 if any(k in g for k in ("aaa", "horror", "simulation", "flight", "cyberpunk", "open world")): return 0.25 if "rpg" in g: return 0.15 return 0.0 def extract_feature_vector( *, gpu_passmark: float, cpu_passmark: float = 28000.0, gpu_vram_gb: float = 8.0, gpu_name: str = "", gpu_gen: str = "", resolution: str = "1080p", rec_gpu_passmark: float = 0.0, min_gpu_passmark: float = 0.0, rec_cpu_passmark: float = 13000.0, rec_vram_gb: float = 6.0, rec_ram_gb: float = 16.0, release_year: int = 2023, genre: str = "", is_capped_60: float = 0.0, game_name: str = "", ) -> list[float]: """Compute the 21 leak-free domain features in deterministic FEATURE_NAMES order.""" safe_gpu_pm = max(400.0, float(gpu_passmark or 400.0)) safe_cpu_pm = max(1500.0, float(cpu_passmark or 28000.0)) safe_vram = max(1.0, float(gpu_vram_gb or 8.0)) if rec_gpu_passmark <= 0 and min_gpu_passmark > 0: safe_min_gpu_pm = max(400.0, float(min_gpu_passmark)) safe_rec_gpu_pm = safe_min_gpu_pm * 1.65 elif rec_gpu_passmark > 0 and min_gpu_passmark <= 0: safe_rec_gpu_pm = max(400.0, float(rec_gpu_passmark)) safe_min_gpu_pm = max(400.0, safe_rec_gpu_pm / 1.65) elif rec_gpu_passmark <= 0 and min_gpu_passmark <= 0: safe_rec_gpu_pm = 6385.0 safe_min_gpu_pm = 3800.0 else: safe_rec_gpu_pm = max(400.0, float(rec_gpu_passmark)) safe_min_gpu_pm = max(400.0, min(safe_rec_gpu_pm, float(min_gpu_passmark))) safe_rec_cpu_pm = max(1500.0, float(rec_cpu_passmark or 13000.0)) safe_rec_vram = max(1.0, float(rec_vram_gb or 6.0)) safe_rec_ram = max(4.0, float(rec_ram_gb or 16.0)) safe_year = max(2012, min(2027, int(release_year or 2023))) genre_offset = infer_genre_demand_offset(genre) res_key = normalize_resolution_label(resolution) pixels = float(RESOLUTION_PIXELS.get(res_key, RESOLUTION_PIXELS["1080p"])) mpixels = pixels / 1_000_000.0 log_gpu_passmark = math.log(safe_gpu_pm) log_cpu_passmark = math.log(safe_cpu_pm) raw_arch = infer_gpu_arch_code(gpu_name, gpu_gen) gpu_arch_code = 1.0 + 3.8 * (safe_gpu_pm / 40000.0) + 0.01 * raw_arch effective_vram = 4.0 + 20.0 * (safe_gpu_pm / 40000.0) + 0.008 * min(24.0, safe_vram) cpu_to_gpu_log_ratio = 0.75 * log_gpu_passmark + 0.25 * log_cpu_passmark gpu_tier_score = safe_gpu_pm / 40000.0 log_pixel_ratio = math.log(pixels / BASE_PIXELS_1080P) vram_per_mpixel = effective_vram / mpixels is_4k = 1.0 if res_key == "4K" else 0.0 log_rec_gpu_passmark = math.log(safe_rec_gpu_pm) log_min_gpu_passmark = math.log(safe_min_gpu_pm) gpu_headroom_rec = log_gpu_passmark - log_rec_gpu_passmark gpu_headroom_min = log_gpu_passmark - log_min_gpu_passmark vram_headroom_gb = effective_vram - safe_rec_vram spec_demand_index = ( 0.50 * ((log_rec_gpu_passmark - math.log(4000.0)) / (math.log(32000.0) - math.log(4000.0))) + 0.18 * ((math.log(safe_rec_cpu_pm) - math.log(4000.0)) / (math.log(25000.0) - math.log(4000.0))) + 0.12 * (min(32.0, safe_rec_ram) / 32.0) + 0.10 * ((safe_year - 2015.0) / 11.0) + genre_offset ) headroom_x_resolution = gpu_headroom_rec * (1.0 + 0.25 * max(0.0, log_pixel_ratio)) cpu_bottleneck_1080p = log_cpu_passmark / (1.0 + 0.35 * max(0.0, log_pixel_ratio)) vram_cliff_penalty = max(0.0, safe_rec_vram * (1.0 + 0.35 * is_4k) - effective_vram) capped_60 = 1.0 if float(is_capped_60 or 0.0) >= 0.5 else infer_capped_60(game_name, genre) cpu_headroom_rec = log_cpu_passmark - math.log(safe_rec_cpu_pm) return [ log_gpu_passmark, log_cpu_passmark, effective_vram, gpu_arch_code, cpu_to_gpu_log_ratio, gpu_tier_score, log_pixel_ratio, vram_per_mpixel, is_4k, log_rec_gpu_passmark, log_min_gpu_passmark, gpu_headroom_rec, gpu_headroom_min, vram_headroom_gb, spec_demand_index, headroom_x_resolution, cpu_bottleneck_1080p, vram_cliff_penalty, capped_60, genre_offset, cpu_headroom_rec, ] class CanRunFPSModel: """ Zero-dependency Pure-Python inference engine for CanRun FPS Model v1.0. """ def __init__(self, weights: dict[str, Any]) -> None: self.weights = weights self.version = str(weights.get("version", MODEL_VERSION)) self.feature_names: list[str] = list(weights["feature_names"]) self.scaler_mean: list[float] = [float(v) for v in weights["scaler_mean"]] self.scaler_scale: list[float] = [float(v) for v in weights["scaler_scale"]] self.ridge_weights: list[float] = [float(v) for v in weights["ridge_weights"]] self.ridge_intercept: float = float(weights["ridge_intercept"]) self.gbdt_baseline: float = float(weights["gbdt_baseline"]) self.gbdt_trees: list[list[dict[str, Any]]] = weights["gbdt_trees"] self.conformal_quantiles: dict[str, dict[str, float]] = weights.get("conformal_quantiles", {}) @classmethod def from_pretrained( cls, model_id_or_path: str | Path = "grasant/canrun-fps-predictor", *, filename: str = "model.json", ) -> CanRunFPSModel: """ Load `model.json` from a local directory/file path or download from Hugging Face Hub. """ candidate = Path(model_id_or_path) if candidate.is_file(): data = json.loads(candidate.read_text(encoding="utf-8")) return cls(data) if candidate.is_dir() and (candidate / filename).is_file(): data = json.loads((candidate / filename).read_text(encoding="utf-8")) return cls(data) local_default = Path(__file__).resolve().parent / filename if str(model_id_or_path) in {".", "grasant/canrun-fps-predictor", "grasant/canrun"} and local_default.is_file(): data = json.loads(local_default.read_text(encoding="utf-8")) return cls(data) from huggingface_hub import hf_hub_download downloaded = Path(hf_hub_download(repo_id=str(model_id_or_path), filename=filename)) data = json.loads(downloaded.read_text(encoding="utf-8")) return cls(data) def predict_from_features(self, x_vec: list[float]) -> float: """Execute Stage-0 Log-Ridge Prior + Stage-1 Monotonic GBDT in pure Python.""" log_prior = self.ridge_intercept for x_val, mu, sc, w in zip(x_vec, self.scaler_mean, self.scaler_scale, self.ridge_weights): z = (x_val - mu) / sc if sc != 0.0 else 0.0 log_prior += z * w residual = self.gbdt_baseline for tree_nodes in self.gbdt_trees: idx = 0 while not tree_nodes[idx]["leaf"]: node = tree_nodes[idx] f_val = x_vec[node["f"]] if f_val <= node["t"]: idx = node["l"] else: idx = node["r"] residual += float(tree_nodes[idx]["v"]) log_fps = max(math.log(5.0), min(math.log(500.0), log_prior + residual)) return math.exp(log_fps) def predict_fps( self, *, gpu_passmark: float, cpu_passmark: float = 28000.0, gpu_vram_gb: float = 8.0, gpu_name: str = "", gpu_gen: str = "", resolution: str = "1080p", rec_gpu_passmark: float = 0.0, min_gpu_passmark: float = 0.0, rec_cpu_passmark: float = 13000.0, rec_vram_gb: float = 6.0, rec_ram_gb: float = 16.0, release_year: int = 2023, genre: str = "", is_capped_60: float = 0.0, game_name: str = "", ) -> float: """Predict rasterized FPS for the given hardware and Steam game specification.""" x_vec = extract_feature_vector( gpu_passmark=gpu_passmark, cpu_passmark=cpu_passmark, gpu_vram_gb=gpu_vram_gb, gpu_name=gpu_name, gpu_gen=gpu_gen, resolution=resolution, rec_gpu_passmark=rec_gpu_passmark, min_gpu_passmark=min_gpu_passmark, rec_cpu_passmark=rec_cpu_passmark, rec_vram_gb=rec_vram_gb, rec_ram_gb=rec_ram_gb, release_year=release_year, genre=genre, is_capped_60=is_capped_60, game_name=game_name, ) fps = self.predict_from_features(x_vec) if x_vec[18] >= 0.5: fps = min(60.0, fps) return round(fps, 2) def predict_with_interval( self, *, regime: str = "unseen_game_spec", **kwargs: Any, ) -> dict[str, float | str]: """Return point FPS prediction and 80% split-conformal log interval.""" res_key = normalize_resolution_label(str(kwargs.get("resolution", "1080p"))) fps = self.predict_fps(**kwargs) q_map = self.conformal_quantiles.get(regime, {}) q = float(q_map.get(res_key, 0.0535)) fps_low = round(fps * math.exp(-q), 1) fps_high = round(fps * math.exp(q), 1) if float(kwargs.get("is_capped_60", 0.0) or 0.0) >= 0.5 or infer_capped_60( str(kwargs.get("game_name", "")), str(kwargs.get("genre", "")) ): fps_low = min(60.0, fps_low) fps_high = min(60.0, fps_high) return { "expected_fps": fps, "conformal_80_low": fps_low, "conformal_80_high": fps_high, "resolution": res_key, "model_version": self.version, } def fetch_steam_store_metadata(app_id: int | str, timeout: float = 10.0) -> dict[str, Any]: """ Fetch public game requirements and metadata directly from Valve's unauthenticated Steam Store API (`store.steampowered.com/api/appdetails`) at runtime without redistributing Steam's store database. """ clean_id = str(app_id).strip() if not clean_id.isdigit(): raise ValueError(f"Steam app_id must be numeric, got {app_id!r}") url = f"https://store.steampowered.com/api/appdetails?appids={clean_id}&l=english" req = urllib.request.Request(url, headers={"User-Agent": "CanRunFPSPredictor/1.0"}) with urllib.request.urlopen(req, timeout=timeout) as resp: payload = json.loads(resp.read().decode("utf-8")) entry = payload.get(clean_id, {}) if not entry.get("success") or not isinstance(entry.get("data"), dict): raise RuntimeError(f"Steam Store API returned no data for app_id={clean_id}") data = entry["data"] pc_req = data.get("pc_requirements") or {} genres = ", ".join(g.get("description", "") for g in (data.get("genres") or []) if isinstance(g, dict)) release_str = str((data.get("release_date") or {}).get("date", "")) year_match = re.search(r"\b(19\d{2}|20\d{2})\b", release_str) release_year = int(year_match.group(1)) if year_match else 2023 return { "steam_app_id": int(clean_id), "game_name": str(data.get("name", "")), "release_year": release_year, "genre": genres, "minimum_html": str(pc_req.get("minimum", "")) if isinstance(pc_req, dict) else "", "recommended_html": str(pc_req.get("recommended", "")) if isinstance(pc_req, dict) else "", }