Please add the following features to this GPU performance prediction app : A new widget to the left side under the widget containing the model switching and buttons to load model and scaler that will display the geomean FPS for all 3 resolutions 1080p, 1440p and 4K on all the available games directly after clicking the predict performance button, the user can then select or unselect the games he doesn't want to be included in the final geomean, the other feature should be inside a new tab separate from the main tab, this new tab should display 4 different charts that can be swapped by the user, the user can select which one of them is shown, these charts are the following: relative performance chart, 1080p FPS chart, 1440p FPS chart, 4K FPS chart, the first chart "relative performance chart" should display the current user inputted GPU after clicking the predict performance button in the main tab, this GPU should be compared to the closest 3 higher scoring GPUs and the 3 lower scoring GPUs, the bars of the chart are horizontal, the color of the bar of the user inputted GPU should have a different color from that of the other existent GPUs, the same is set for all the left charts, the charts should follow the game selection as well as the inputted configuration that the user has chosen in the main tab and they should be updated each time a new change has been done, the user can switch between the 4 charts using a drop down menu at the top, you should implement both of these features from start to finish and give us the full new GUI code from start to finish "FULL CODE", here is the gui.py code: 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())