What new ideas do you have for this GPU performance prediction app : import sys
import os
import glob
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import joblib
import json

from PyQt6.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QPushButton,
    QVBoxLayout,
    QHBoxLayout,
    QMessageBox,
    QComboBox,
    QGroupBox,
    QProgressBar,
    QButtonGroup,
    QRadioButton,
    QLineEdit,
    QSizePolicy,
)
from PyQt6.QtGui import (
    QFont,
    QPalette,
    QColor,
    QDoubleValidator,
    QIntValidator,
)
from PyQt6.QtCore import Qt, QEvent

# Define architecture mapping here for consistency
ARCHITECTURE_MAPPING = {
    "PASCAL": 0,
    "TURING": 1,
    "AMPERE": 2,
    "ADA": 3,
    "GCN5": 4,
    "RDNA": 5,
    "RDNA2": 6,
    "RDNA3": 7,
}

RESOLUTION_MAPPING = {
    "1080p": 1,
    "1440p": 2,
    "4K": 3,
}

NUMERIC_FEATURES = [
    "Num Shaders",
    "Clock Speed (GHz)",
    "ROPs",
    "TMUs",
    "L2 Cache (MB)",
    "VRAM (GB)",
    "Bandwidth (GB/s)",
    "TFlops",
]

DEFAULT_MODEL_PATH = "models/Foundation_Model.pth"
DEFAULT_SCALER_PATH = "models/Foundation_Model_scaler.pkl"
GAME_MODEL_PATH = "models/Game_Model.pth"
GAME_SCALER_PATH = "models/Foundation_Model_scaler.pkl"
GPU_DATA_PATH = "gpu_data.csv"

# --- Model Architecture ---
class GPUPerformanceModel(nn.Module):
    def __init__(self, input_size, num_architectures, num_resolutions=0, num_games=0, mode='foundation'):
        super(GPUPerformanceModel, self).__init__()
        embedding_dim_arch = 4

        if mode == 'foundation':
            total_embedding_dim = embedding_dim_arch
            self.arch_embedding = nn.Embedding(num_architectures, embedding_dim=embedding_dim_arch)
        elif mode == 'game':
            embedding_dim_res = 2
            embedding_dim_game = 4
            self.arch_embedding = nn.Embedding(num_architectures, embedding_dim=embedding_dim_arch)
            self.res_embedding = nn.Embedding(num_resolutions, embedding_dim=embedding_dim_res)
            self.game_embedding = nn.Embedding(num_games, embedding_dim=embedding_dim_game)
            total_embedding_dim = embedding_dim_arch + embedding_dim_res + embedding_dim_game

        self.mode = mode

        self.model = nn.Sequential(
            nn.Linear(input_size + total_embedding_dim, 128),
            nn.ReLU(),
            nn.Dropout(0.125),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Dropout(0.125),
            nn.Linear(64, 1)
        )

    def forward(self, x_num, x_arch, x_res=None, x_game=None):
        x_arch = self.arch_embedding(x_arch)
        if self.mode == 'foundation':
            x = torch.cat([x_num, x_arch], dim=1)
        elif self.mode == 'game':
            x_res = self.res_embedding(x_res)
            x_game = self.game_embedding(x_game)
            x_cat = torch.cat([x_arch, x_res, x_game], dim=1)
            x = torch.cat([x_num, x_cat], dim=1)
        return self.model(x)

class PredictionApp(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("GPU Performance Predictor")
        self.setMinimumSize(800, 600)

        self.foundation_model = None
        self.game_model = None
        self.scaler = None
        self.game_model_config = None
        self.foundation_model_config = None

        # --- Input Fields with Validation ---
        self.create_input_fields()

        # --- Architecture Dropdown ---
        self.architecture_label = QLabel("Architecture:")
        self.architecture_combo = QComboBox()
        self.architecture_combo.addItems(list(ARCHITECTURE_MAPPING.keys()))
        self.architecture_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

        # --- Resolution Dropdown ---
        self.resolution_label = QLabel("Resolution:")
        self.resolution_combo = QComboBox()
        self.resolution_combo.addItems(['1080p', '1440p', '4K'])
        self.resolution_combo.setVisible(False)
        self.resolution_label.setVisible(False)

        # --- Buttons ---
        self.predict_button = QPushButton("Predict Performance")
        self.predict_button.clicked.connect(self.predict_performance)
        self.predict_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

        # --- Mode Selection Radio Buttons ---
        self.mode_group = QButtonGroup(self)
        self.foundation_mode_radio = QRadioButton("Foundation Model", self)
        self.game_mode_radio = QRadioButton("Game Model", self)
        self.mode_group.addButton(self.foundation_mode_radio)
        self.mode_group.addButton(self.game_mode_radio)
        self.foundation_mode_radio.setChecked(True)
        self.foundation_mode_radio.toggled.connect(self.update_mode)
        self.game_mode_radio.toggled.connect(self.update_mode)

        # --- Game Selection ---
        self.game_label = QLabel("Select Game:")
        self.game_combo = QComboBox()
        self.game_combo.addItem("Select Game")
        self.game_label.setVisible(False)
        self.game_combo.setVisible(False)
        self.game_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

        # Load game mappings
        self.load_game_mappings()

        # --- Layout ---
        main_layout = QHBoxLayout()

        # --- Left Column Layout ---
        left_column_layout = QVBoxLayout()

        # Prediction Mode
        mode_group = QGroupBox("Prediction Mode")
        mode_layout = QVBoxLayout()
        mode_layout.addWidget(self.foundation_mode_radio)
        mode_layout.addWidget(self.game_mode_radio)
        mode_layout.addWidget(self.game_label)
        mode_layout.addWidget(self.game_combo)
        mode_group.setLayout(mode_layout)
        left_column_layout.addWidget(mode_group)

        left_column_layout.addStretch(1)

        # --- Right Column Layout ---
        right_column_layout = QVBoxLayout()

        # Input Fields Group
        input_group = QGroupBox("GPU Specifications")
        input_layout = QVBoxLayout()

        # Performance Characteristics Group
        perf_group = QGroupBox("Performance Characteristics")
        perf_layout = QVBoxLayout()
        perf_layout.addWidget(self.num_shaders_edit)
        perf_layout.addWidget(self.clock_speed_edit)
        perf_layout.addWidget(self.rops_edit)
        perf_layout.addWidget(self.tmus_edit)
        perf_layout.addWidget(self.tflops_edit)
        perf_group.setLayout(perf_layout)
        input_layout.addWidget(perf_group)

        # Memory Characteristics Group
        memory_group = QGroupBox("Memory Characteristics")
        memory_layout = QVBoxLayout()
        memory_layout.addWidget(self.l2_cache_edit)
        memory_layout.addWidget(self.vram_edit)
        memory_layout.addWidget(self.bandwidth_edit)
        memory_group.setLayout(memory_layout)
        input_layout.addWidget(memory_group)

        input_layout.addWidget(self.architecture_label)
        input_layout.addWidget(self.architecture_combo)
        input_layout.addWidget(self.resolution_label)
        input_layout.addWidget(self.resolution_combo)
        input_group.setLayout(input_layout)
        right_column_layout.addWidget(input_group)

        right_column_layout.addWidget(self.predict_button)

        # --- Output ---
        self.output_label = QLabel("Predicted Performance:")
        self.output_label.setFont(QFont("Arial", 14))
        self.output_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        right_column_layout.addWidget(self.output_label)

        self.performance_bar = QProgressBar(self)
        self.performance_bar.setTextVisible(False)
        right_column_layout.addWidget(self.performance_bar)

        right_column_layout.addStretch(1)

        # --- Combine Columns ---
        main_layout.addLayout(left_column_layout)
        main_layout.addLayout(right_column_layout)
        main_layout.setStretch(0, 1)
        main_layout.setStretch(1, 2)

        self.setLayout(main_layout)
        self.apply_dark_mode()

        # --- Load Models ---
        self.load_models()

        # --- Initially set the mode ---
        self.update_mode()

    def create_input_fields(self):
        self.num_shaders_edit = self.create_validated_field(
            "Num Shaders:", validator=QIntValidator(0, 100000)
        )
        self.clock_speed_edit = self.create_validated_field(
            "Clock Speed (GHz):", validator=QDoubleValidator(0.0, 5.0, 4)
        )
        self.rops_edit = self.create_validated_field(
            "ROPs:", validator=QIntValidator(0, 256)
        )
        self.tmus_edit = self.create_validated_field(
            "TMUs:", validator=QIntValidator(0, 640)
        )
        self.l2_cache_edit = self.create_validated_field(
            "L2 Cache (MB):", validator=QDoubleValidator(0.0, 128.0, 3)
        )
        self.vram_edit = self.create_validated_field(
            "VRAM (GB):", validator=QDoubleValidator(0.0, 128.0, 2)
        )
        self.bandwidth_edit = self.create_validated_field(
            "Bandwidth (GB/s):", validator=QDoubleValidator(0.0, 2000.0, 2)
        )
        self.tflops_edit = self.create_validated_field(
            "TFlops:", validator=QDoubleValidator(0.0, 100.0, 4)
        )

    def create_validated_field(self, label_text, validator):
        layout = QVBoxLayout()

        label = QLabel(label_text)
        label.setToolTip(self.get_tooltip(label_text))
        layout.addWidget(label)

        edit = QLineEdit()
        edit.setValidator(validator)
        edit.textChanged.connect(lambda: self.validate_input(edit))
        edit.installEventFilter(self)
        edit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
        layout.addWidget(edit)

        widget = QWidget()
        widget.setLayout(layout)
        widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
        return widget

    def get_tooltip(self, field_name):
        tooltips = {
            "Num Shaders:": "The number of shader cores in the GPU.",
            "Clock Speed (GHz):": "The clock speed of the GPU's core, in GHz.",
            "ROPs:": "Render Output Units: Responsible for writing pixels to memory.",
            "TMUs:": "Texture Mapping Units: Handle texture operations.",
            "TFlops:": "Teraflops: A measure of theoretical computing performance.",
            "L2 Cache (MB):": "The size of the GPU's L2 cache, in MB.",
            "VRAM (GB):": "The amount of video memory on the GPU, in GB.",
            "Bandwidth (GB/s):": "The speed of data transfer between the GPU and memory, in GB/s."
        }
        return tooltips.get(field_name, "")

    def eventFilter(self, source, event):
        if event.type() == QEvent.Type.KeyPress:
            if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
                self.focusNextChild()
                return True
        return super().eventFilter(source, event)

    def apply_dark_mode(self):
        palette = QPalette()
        palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
        palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white)
        palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
        palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
        palette.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white)
        palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.white)
        palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white)
        palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
        palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
        palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
        palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
        palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
        palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
        self.setPalette(palette)

    def load_models(self):
        # Load Foundation Model
        if os.path.exists(DEFAULT_MODEL_PATH) and os.path.exists(DEFAULT_SCALER_PATH):
            self.foundation_model = self.load_model_from_path(DEFAULT_MODEL_PATH, mode='foundation')
            self.scaler = joblib.load(DEFAULT_SCALER_PATH)
        else:
            QMessageBox.warning(self, "Warning", "Foundation model files not found.")

        # Load Game Model
        if os.path.exists(GAME_MODEL_PATH) and os.path.exists(GAME_SCALER_PATH):
            self.game_model = self.load_model_from_path(GAME_MODEL_PATH, mode='game')
            # Use the same scaler as foundation model
        else:
            QMessageBox.warning(self, "Warning", "Game model files not found.")

    def load_model_from_path(self, model_path, mode):
        try:
            config_path = model_path.replace(".pth", ".json")
            with open(config_path, "r") as f:
                if mode == 'foundation':
                    self.foundation_model_config = json.load(f)
                    config = self.foundation_model_config
                elif mode == 'game':
                    self.game_model_config = json.load(f)
                    config = self.game_model_config
                    self.GAME_MAPPING = config['GAME_MAPPING']
                    self.NORMALIZATION_PARAMS = config['normalization_params']
                    self.GAME_MAPPING_REV = {v: k for k, v in self.GAME_MAPPING.items()}
            input_size = config["model_params"]["input_size"]
            num_architectures = config["model_params"]["num_architectures"]

            if mode == 'foundation':
                model = GPUPerformanceModel(input_size, num_architectures, mode='foundation')
                model.load_state_dict(torch.load(model_path))
                model.eval()
                return model
            elif mode == 'game':
                num_resolutions = config["model_params"]["num_resolutions"]
                num_games = config["model_params"]["num_games"]
                model = GPUPerformanceModel(input_size, num_architectures, num_resolutions, num_games, mode='game')
                model.load_state_dict(torch.load(model_path))
                model.eval()
                return model
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Error loading model or config: {str(e)}")
            return None

    def load_game_mappings(self):
        # Load game mappings from the game model config
        if os.path.exists('models/Game_Model.json'):
            with open('models/Game_Model.json', 'r') as f:
                self.game_model_config = json.load(f)
                self.GAME_MAPPING = self.game_model_config['GAME_MAPPING']
                self.NORMALIZATION_PARAMS = self.game_model_config['normalization_params']
                self.GAME_MAPPING_REV = {v: k for k, v in self.GAME_MAPPING.items()}
                self.game_combo.addItems([self.GAME_MAPPING_REV[i] for i in range(len(self.GAME_MAPPING))])
        else:
            QMessageBox.warning(self, "Warning", "Game model configuration not found.")

    def update_mode(self):
        if self.foundation_mode_radio.isChecked():
            self.set_foundation_mode()
        else:
            self.set_game_specific_mode()

    def set_foundation_mode(self):
        # Enable input fields
        self.enable_input_fields(True)
        self.architecture_combo.setEnabled(True)
        self.architecture_label.setVisible(True)
        self.architecture_combo.setVisible(True)
        self.resolution_label.setVisible(False)
        self.resolution_combo.setVisible(False)
        self.game_label.setVisible(False)
        self.game_combo.setVisible(False)
        self.predict_button.setEnabled(True)

    def set_game_specific_mode(self):
        # Enable input fields
        self.enable_input_fields(True)
        self.architecture_combo.setEnabled(True)
        self.architecture_label.setVisible(True)
        self.architecture_combo.setVisible(True)
        self.resolution_label.setVisible(True)
        self.resolution_combo.setVisible(True)
        self.game_label.setVisible(True)
        self.game_combo.setVisible(True)
        self.predict_button.setEnabled(True)

    def enable_input_fields(self, enable):
        fields = [
            self.num_shaders_edit,
            self.clock_speed_edit,
            self.rops_edit,
            self.tmus_edit,
            self.tflops_edit,
            self.l2_cache_edit,
            self.vram_edit,
            self.bandwidth_edit,
        ]
        for field in fields:
            field.layout().itemAt(1).widget().setEnabled(enable)
            field.setEnabled(enable)
        self.architecture_combo.setEnabled(enable)

    def validate_input(self, edit):
        if not edit.hasAcceptableInput():
            edit.setStyleSheet("background-color: pink;")
        else:
            edit.setStyleSheet("")

    def predict_performance(self):
        try:
            num_shaders = int(self.num_shaders_edit.layout().itemAt(1).widget().text())
            clock_speed = float(self.clock_speed_edit.layout().itemAt(1).widget().text())
            rops = int(self.rops_edit.layout().itemAt(1).widget().text())
            tmus = int(self.tmus_edit.layout().itemAt(1).widget().text())
            l2_cache = float(self.l2_cache_edit.layout().itemAt(1).widget().text())
            vram = float(self.vram_edit.layout().itemAt(1).widget().text())
            bandwidth = float(self.bandwidth_edit.layout().itemAt(1).widget().text())
            tflops = float(self.tflops_edit.layout().itemAt(1).widget().text())
            architecture = ARCHITECTURE_MAPPING[self.architecture_combo.currentText()]
            architecture_tensor = torch.tensor([architecture], dtype=torch.long)

            input_data_num = np.array([[num_shaders, clock_speed, rops, tmus, l2_cache, vram, bandwidth, tflops]])
            input_data_num_scaled = self.scaler.transform(input_data_num)
            input_data_num_tensor = torch.tensor(input_data_num_scaled, dtype=torch.float32)

            if self.foundation_mode_radio.isChecked():
                # Foundation Model Prediction
                if self.foundation_model is None or self.scaler is None:
                    QMessageBox.warning(self, "Warning", "Foundation model or scaler not loaded.")
                    return

                # Make prediction
                with torch.no_grad():
                    prediction = self.foundation_model(input_data_num_tensor, architecture_tensor).item()

                # Display the prediction
                self.output_label.setText(
                    f"Predicted Relative Performance: {prediction:.2f}"
                )
                # Update the progress bar
                performance_percentage = int(min(max(prediction / 7.0 * 100, 0), 100))
                self.performance_bar.setValue(performance_percentage)

            else:
                # Game Model Prediction
                if self.game_model is None or self.scaler is None:
                    QMessageBox.warning(self, "Warning", "Game model or scaler not loaded.")
                    return

                resolution_text = self.resolution_combo.currentText()
                resolution = RESOLUTION_MAPPING.get(resolution_text)
                if resolution is None:
                    QMessageBox.warning(self, "Warning", f"Invalid resolution selected: {resolution_text}")
                    return
                resolution_tensor = torch.tensor([resolution], dtype=torch.long)

                game_name = self.game_combo.currentText()
                if game_name == "Select Game":
                    QMessageBox.warning(self, "Warning", "Please select a game.")
                    return
                game_id = self.GAME_MAPPING.get(game_name)
                if game_id is None:
                    QMessageBox.warning(self, "Warning", f"Game {game_name} not recognized.")
                    return
                game_tensor = torch.tensor([game_id], dtype=torch.long)

                # Make prediction
                with torch.no_grad():
                    prediction_normalized = self.game_model(
                        input_data_num_tensor, architecture_tensor, resolution_tensor, game_tensor
                    ).item()

                # Rescale prediction back to original FPS
                fps_min = self.NORMALIZATION_PARAMS['fps_min']
                fps_max = self.NORMALIZATION_PARAMS['fps_max']

                if fps_max == fps_min:
                    prediction = fps_min  # All FPS values are the same
                else:
                    prediction = (prediction_normalized - 0.3) / (7 - 0.3)
                    prediction = prediction * (fps_max - fps_min) + fps_min

                # Display the prediction
                self.output_label.setText(
                    f"Predicted FPS: {prediction:.2f} fps at {resolution_text}"
                )
                # Update the progress bar
                performance_percentage = int(min(max(prediction / 240.0 * 100, 0), 100))
                self.performance_bar.setValue(performance_percentage)

        except ValueError as e:
            QMessageBox.critical(self, "Error", f"Invalid input: {str(e)}")
        except Exception as e:
            QMessageBox.critical(self, "Error", f"An error occurred during prediction: {str(e)}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = PredictionApp()
    window.show()
    sys.exit(app.exec())