import pandas as pd
import time
import numpy as classical_np  # Renamed to avoid conflict
import matplotlib.pyplot as plt
import os
import torch
from torch import nn
from torch.cuda import seed_all
from torch.utils.data import Dataset, DataLoader
from collections import Counter
import torch.nn.functional
import pennylane as qml
from pennylane import numpy as pnp
from datasets import load_dataset
from tqdm import tqdm  # Added progress bar
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
import re
from sklearn.model_selection import train_test_split
from qiskit import QuantumCircuit
from qiskit.visualization import circuit_drawer
from sklearn.metrics import f1_score, recall_score, accuracy_score
import random

# Set random seeds for reproducibility
seed = 42
random.seed(seed)
classical_np.random.seed(seed)
torch.manual_seed(seed)

os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"

# Download NLTK resources
nltk.download('punkt')
nltk.download('stopwords')

# Hyperparameters
epoch = 40
min_freq = 1
sequence_length = 5
batch_size = 32
hidden_size = 64
embedding_dim = 100  # Embedding dimension
dropout_rate = 0.5
lr = 0.001
n_qlayers = 1

# Load dataset
ds = load_dataset("rahulsikder223/SentEval-MPQA")
combined_df = pd.concat([pd.DataFrame(ds['train']), pd.DataFrame(ds['test'])], ignore_index=True)

print(ds)

# Text cleaning function
def clean_text(text):
    text = text.lower()
    text = re.sub(r'<.*?>', '', text)
    text = re.sub(r'http\S+|www\S+|https\S+', '', text, flags=re.MULTILINE)
    text = re.sub(r'\d+', '', text)
    text = re.sub(r'[^\w\s]', '', text)
    words = word_tokenize(text)
    stop_words = set(stopwords.words('english')) - {'not', 'no', 'nor', 'but'}
    words = [word for word in words if word not in stop_words]
    return ' '.join(words)

# Clean all sentences
combined_df['sentence'] = combined_df['sentence'].apply(clean_text)

# Split the data
train_data, temp_data = train_test_split(
    combined_df, test_size=0.2, stratify=combined_df['label'], random_state=seed
)
val_data, test_data = train_test_split(
    temp_data, test_size=0.5, stratify=temp_data['label'], random_state=seed
)

# Tokenize and encode
def tokenize_and_encode(sentences, vocab, seq_length):
    token_ids = [
        [vocab.get(token, vocab['<UNK>']) for token in sentence.split()]
        for sentence in sentences
    ]
    token_ids = [
        t_ids[:seq_length] if len(t_ids) > seq_length else t_ids + [vocab['<PAD>']] * (seq_length - len(t_ids))
        for t_ids in token_ids
    ]
    return token_ids

# Build vocabulary
all_train_tokens = [token for sentence in train_data['sentence'] for token in sentence.split()]
word_counts = Counter(all_train_tokens)
filtered_words = [word for word, count in word_counts.items() if count >= min_freq]
vocabulary = {word: i + 2 for i, word in enumerate(filtered_words)}
vocabulary['<UNK>'] = 1
vocabulary['<PAD>'] = 0

print(f"Vocabulary size: {len(vocabulary)}")

# Encode datasets
train_token_ids = tokenize_and_encode(train_data['sentence'], vocabulary, sequence_length)
val_token_ids = tokenize_and_encode(val_data['sentence'], vocabulary, sequence_length)
test_token_ids = tokenize_and_encode(test_data['sentence'], vocabulary, sequence_length)

# Custom Dataset
class SentimentDataset(Dataset):
    def __init__(self, token_ids, labels):
        self.token_ids = token_ids
        self.labels = labels

    def __len__(self):
        return len(self.token_ids)

    def __getitem__(self, idx):
        return (
            torch.tensor(self.token_ids[idx], dtype=torch.long),
            torch.tensor(self.labels[idx], dtype=torch.long),
        )

# Create Datasets
train_dataset = SentimentDataset(train_token_ids, train_data['label'].tolist())
val_dataset = SentimentDataset(val_token_ids, val_data['label'].tolist())
test_dataset = SentimentDataset(test_token_ids, test_data['label'].tolist())

# Create DataLoaders
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

# Label statistics
print(f"Train Positive count: {train_data[train_data['label'] == 1].shape[0]}")
print(f"Train Negative count: {train_data[train_data['label'] == 0].shape[0]}")
print(f"Validation Positive count: {val_data[val_data['label'] == 1].shape[0]}")
print(f"Validation Negative count: {val_data[val_data['label'] == 0].shape[0]}")
print(f"Test Positive count: {test_data[test_data['label'] == 1].shape[0]}")
print(f"Test Negative count: {test_data[test_data['label'] == 0].shape[0]}")

# Quantum Self-Attention with Separate Devices
class QuantumAttention(nn.Module):
    def __init__(self, embed_size, n_qubits=4, n_qlayers=1, backend='default.qubit'):
        super(QuantumAttention, self).__init__()
        self.embed_size = embed_size
        self.n_qubits = n_qubits
        self.n_qlayers = n_qlayers
        self.scaling = classical_np.sqrt(n_qubits)

        # Define separate quantum devices for query, key, and value
        self.query_device = qml.device(backend, wires=n_qubits)
        self.key_device = qml.device(backend, wires=n_qubits)
        self.value_device = qml.device(backend, wires=n_qubits)

        # Define QNodes for query, key, and value using separate devices
        def quantum_circuit(inputs, weights):
            for i in range(len(inputs)):
                qml.RY(inputs[i], wires=i)
            for layer in range(n_qlayers):
                for wire in range(n_qubits):
                    qml.RX(weights[layer, wire, 0], wires=wire)
                    qml.RY(weights[layer, wire, 1], wires=wire)
                for i in range(n_qubits - 1):
                    qml.CNOT(wires=[i, i + 1])
            return [qml.expval(qml.PauliZ(wire)) for wire in range(n_qubits)]

        weight_shapes = {"weights": (n_qlayers, n_qubits, 2)}

        # Create separate QNodes for query, key, and value
        self.query_qnode = qml.QNode(quantum_circuit, self.query_device, interface='torch')
        self.key_qnode = qml.QNode(quantum_circuit, self.key_device, interface='torch')
        self.value_qnode = qml.QNode(quantum_circuit, self.value_device, interface='torch')

        # Create TorchLayer for each QNode
        self.query_layer = qml.qnn.TorchLayer(self.query_qnode, weight_shapes)
        self.key_layer = qml.qnn.TorchLayer(self.key_qnode, weight_shapes)
        self.value_layer = qml.qnn.TorchLayer(self.value_qnode, weight_shapes)

    def forward(self, query, key, value):
        # Quantum processing
        q_query = self.query_layer(query)  # [batch_size * sequence_length, n_qubits]
        q_key = self.key_layer(key)        # [batch_size * sequence_length, n_qubits]
        q_value = self.value_layer(value)  # [batch_size * sequence_length, n_qubits]

        # Reshape to [batch_size, sequence_length, n_qubits]
        batch_size, seq_length, _ = query.shape
        q_query = q_query.view(batch_size, seq_length, self.n_qubits)
        q_key = q_key.view(batch_size, seq_length, self.n_qubits)
        q_value = q_value.view(batch_size, seq_length, self.n_qubits)

        # Compute attention scores (dot product)
        scores = torch.matmul(q_query, q_key.transpose(-2, -1)) / self.scaling  # [batch_size, sequence_length, sequence_length]
        attn_weights = torch.softmax(scores, dim=-1)  # [batch_size, sequence_length, sequence_length]
        out = torch.matmul(attn_weights, q_value)  # [batch_size, sequence_length, n_qubits]
        return out

# Quantum LSTM
class QLSTM(nn.Module):
    def __init__(self,
                 input_size,
                 hidden_size,
                 n_qubits=4,
                 n_qlayers=1,
                 n_vrotations=3,
                 batch_first=True,
                 return_sequences=False,
                 return_state=False,
                 backend="default.qubit",
                 dropout_rate=0.5):
        super(QLSTM, self).__init__()
        self.n_inputs = input_size
        self.hidden_size = hidden_size
        self.num_layers = n_qlayers
        self.concat_size = self.n_inputs + self.hidden_size
        self.n_qubits = n_qubits
        self.n_qlayers = n_qlayers
        self.n_vrotations = n_vrotations
        self.backend = backend

        self.batch_first = batch_first
        self.return_sequences = return_sequences
        self.return_state = return_state

        self.wires_forget = [f"wire_forget_{i}" for i in range(self.n_qubits)]
        self.wires_input = [f"wire_input_{i}" for i in range(self.n_qubits)]
        self.wires_update = [f"wire_update_{i}" for i in range(self.n_qubits)]
        self.wires_output = [f"wire_output_{i}" for i in range(self.n_qubits)]
        self.wires_output_h = [f"wire_output_h_{i}" for i in range(self.n_qubits)]

        # Define separate quantum devices for each gate
        self.dev_forget = qml.device(self.backend, wires=self.wires_forget)
        self.dev_input = qml.device(self.backend, wires=self.wires_input)
        self.dev_update = qml.device(self.backend, wires=self.wires_update)
        self.dev_output = qml.device(self.backend, wires=self.wires_output)
        self.dev_output_h = qml.device(self.backend, wires=self.wires_output_h)

        # Initialize dropout layer
        self.dropout = nn.Dropout(dropout_rate)

        # Define QNodes for each gate using separate devices
        def circuit(inputs, weights):
            for i in range(len(inputs)):
                qml.RY(inputs[i], wires=self.wires_forget[i % self.n_qubits])
            for layer in range(self.n_qlayers):
                for wire in range(self.n_qubits):
                    qml.RX(weights[layer, wire, 0], wires=self.wires_forget[wire])
                    qml.RY(weights[layer, wire, 1], wires=self.wires_forget[wire])
                for i in range(self.n_qubits - 1):
                    qml.CNOT(wires=[self.wires_forget[i], self.wires_forget[i + 1]])
            return [qml.expval(qml.PauliZ(wire)) for wire in self.wires_forget]

        weight_shapes = {"weights": (self.n_qlayers, self.n_qubits, 2)}

        # Create separate QNodes for each gate
        self.qlayer_forget = qml.QNode(circuit, self.dev_forget, interface="torch")
        self.qlayer_input = qml.QNode(circuit, self.dev_input, interface="torch")
        self.qlayer_update = qml.QNode(circuit, self.dev_update, interface="torch")
        self.qlayer_output = qml.QNode(circuit, self.dev_output, interface="torch")
        self.qlayer_output_h = qml.QNode(circuit, self.dev_output_h, interface="torch")

        # Create TorchLayers for each QNode
        self.TorchLayers = {
            'forget': qml.qnn.TorchLayer(self.qlayer_forget, weight_shapes),
            'input': qml.qnn.TorchLayer(self.qlayer_input, weight_shapes),
            'update': qml.qnn.TorchLayer(self.qlayer_update, weight_shapes),
            'output': qml.qnn.TorchLayer(self.qlayer_output, weight_shapes),
            'output_h': qml.qnn.TorchLayer(self.qlayer_output_h, weight_shapes)
        }

        # Define linear layers
        self.clayer_in = nn.Linear(self.concat_size, self.n_qubits)
        self.clayer_out_f = nn.Linear(self.n_qubits, self.hidden_size)
        self.clayer_out_i = nn.Linear(self.n_qubits, self.hidden_size)
        self.clayer_out_g = nn.Linear(self.n_qubits, self.hidden_size)
        self.clayer_out_o = nn.Linear(self.n_qubits, self.hidden_size)

    def vqc(self, features, weights, wires_type):
        if features.dim() == 1:
            features = features.unsqueeze(0)
        if not isinstance(features, torch.Tensor):
            features = torch.tensor(features, dtype=torch.float32)

        features = features[:, :self.n_qubits]
        ry_params = torch.atan2(features, torch.ones_like(features))
        rz_params = torch.atan2(features ** 2, torch.ones_like(features))

        for i in range(self.n_qubits):
            qml.Hadamard(wires=wires_type[i])

        for i in range(self.n_qubits):
            qml.RY(ry_params[:, i], wires=wires_type[i])
            qml.RZ(rz_params[:, i], wires=wires_type[i])

        for i in range(1, 3):
            for j in range(self.n_qubits):
                if j + i < self.n_qubits:
                    qml.CNOT(wires=[wires_type[j], wires_type[j + i]])
                else:
                    qml.CNOT(wires=[wires_type[j], wires_type[j + i - self.n_qubits]])

        for layer in range(self.n_qlayers):
            for i in range(self.n_qubits):
                qml.RX(weights[layer, 0, i], wires=wires_type[i])
                qml.RY(weights[layer, 1, i], wires=wires_type[i])
                qml.RZ(weights[layer, 2, i], wires=wires_type[i])

    def _circuit_forget(self, inputs, weights):
        self.vqc(inputs, weights, self.wires_forget)
        return [qml.expval(qml.PauliZ(wires=i)) for i in self.wires_forget]

    def _circuit_input(self, inputs, weights):
        self.vqc(inputs, weights, self.wires_input)
        return [qml.expval(qml.PauliZ(wires=i)) for i in self.wires_input]

    def _circuit_update(self, inputs, weights):
        self.vqc(inputs, weights, self.wires_update)
        return [qml.expval(qml.PauliZ(wires=i)) for i in self.wires_update]

    def _circuit_output(self, inputs, weights):
        self.vqc(inputs, weights, self.wires_output)
        return [qml.expval(qml.PauliZ(wires=i)) for i in self.wires_output]

    def _circuit_output_h(self, inputs, weights):
        self.vqc(inputs, weights, self.wires_output_h)
        return [qml.expval(qml.PauliZ(wires=i)) for i in self.wires_output_h]

    def to_qiskit_circuit(self, inputs, weights, wires_type):
        circuit = QuantumCircuit(len(wires_type), len(wires_type))
        features = torch.tensor(inputs[:, :self.n_qubits], dtype=torch.float32)
        ry_params = torch.atan2(features, torch.ones_like(features))
        rz_params = torch.atan2(features ** 2, torch.ones_like(features))

        for i in range(len(wires_type)):
            circuit.h(i)

        for i in range(len(wires_type)):
            circuit.ry(ry_params[:, i].item(), i)
            circuit.rz(rz_params[:, i].item(), i)

        for i in range(1, 3):
            for j in range(len(wires_type)):
                if j + i < len(wires_type):
                    circuit.cx(j, j + i)
                else:
                    circuit.cx(j, (j + i) % len(wires_type))

        for layer in range(self.n_qlayers):
            for i in range(len(wires_type)):
                circuit.rx(weights[layer, 0, i].item(), i)
                circuit.ry(weights[layer, 1, i].item(), i)
                circuit.rz(weights[layer, 2, i].item(), i)

        for i in range(len(wires_type)):
            circuit.measure(i, i)

        return circuit

    def visualize_qiskit_circuit(self):
        example_inputs = classical_np.random.random((1, self.n_qubits))
        example_weights = classical_np.random.random((self.n_qlayers, 3, self.n_qubits))  # Adjusted for vrotations=3
        qiskit_circuit = self.to_qiskit_circuit(example_inputs, example_weights, self.wires_forget)
        circuit_drawer(qiskit_circuit, output='mpl').show()

    def forward(self, x, init_states=None):
        batch_size, seq_length, _ = x.size()

        if init_states is None:
            h_t = torch.zeros(
                self.num_layers, batch_size, self.hidden_size, dtype=torch.float32, device=x.device
            )
            c_t = torch.zeros(
                self.num_layers, batch_size, self.hidden_size, dtype=torch.float32, device=x.device
            )
        else:
            h_t, c_t = init_states

        for t in range(seq_length):
            x_t = x[:, t, :]  # [batch_size, input_size]
            h_t_last_layer = h_t[-1, :, :]  # [batch_size, hidden_size]

            combined = torch.cat((h_t_last_layer, x_t), dim=1)  # [batch_size, hidden_size + input_size]
            combined = self.clayer_in(combined)  # [batch_size, n_qubits]

            # Use Quantum Layers
            f_t_intermediate = self.TorchLayers['forget'](combined)
            i_t_intermediate = self.TorchLayers['input'](combined)
            g_t_intermediate = self.TorchLayers['update'](combined)
            o_t_intermediate = self.TorchLayers['output'](combined)

            # Convert intermediate quantum outputs to hidden size
            f_t = torch.sigmoid(self.clayer_out_f(f_t_intermediate))  # [batch_size, hidden_size]
            i_t = torch.sigmoid(self.clayer_out_i(i_t_intermediate))
            g_t = torch.tanh(self.clayer_out_g(g_t_intermediate))
            o_t = torch.sigmoid(self.clayer_out_o(o_t_intermediate))

            # Update cell state and hidden state
            c_t_new = f_t * c_t[-1, :, :] + i_t * g_t  # [batch_size, hidden_size]
            h_t_new = o_t * torch.tanh(c_t_new)  # [batch_size, hidden_size]
            h_t_new = self.dropout(h_t_new)  # [batch_size, hidden_size]

            # Update hidden and cell states
            h_t[-1, :, :] = h_t_new
            c_t[-1, :, :] = c_t_new  # Fixed: was c_t instead of c_t_new

        return h_t_last_layer  # [batch_size, hidden_size]

# Classification Model with Quantum Attention and Quantum LSTM
class QShallowClassificationLSTM(nn.Module):
    def __init__(
        self,
        vocab_size,
        embedding_dim,
        hidden_size,
        n_qubits=4,
        n_qlayers=1,
        num_classes=2,
        dropout_rate=0.5,
        backend='default.qubit'
    ):
        super(QShallowClassificationLSTM, self).__init__()
        self.vocab_size = vocab_size
        self.hidden_size = hidden_size
        self.num_layers = n_qlayers
        self.num_classes = num_classes

        # Classical Embedding Layer
        self.embedding = nn.Embedding(vocab_size, embedding_dim)

        # Dropout Layer
        self.dropout = nn.Dropout(dropout_rate)

        # Quantum Self-Attention Layer
        self.quantum_attention = QuantumAttention(
            embed_size=embedding_dim,
            n_qubits=n_qubits,
            n_qlayers=n_qlayers,
            backend=backend
        )

        # Linear Layers for Query, Key, Value
        self.query_linear = nn.Linear(embedding_dim, embedding_dim)
        self.key_linear = nn.Linear(embedding_dim, embedding_dim)
        self.value_linear = nn.Linear(embedding_dim, embedding_dim)

        # Quantum LSTM Layer (input size adjusted to attention output)
        self.lstm = QLSTM(
            input_size=n_qubits,  # Output size from QuantumAttention
            hidden_size=hidden_size,
            n_qubits=n_qubits,
            n_qlayers=n_qlayers,
            backend=backend
        )

        # Classification Layer
        self.linear = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        batch_size = x.shape[0]

        # Embedding
        x = self.embedding(x)  # [batch_size, sequence_length, embedding_dim]
        x = self.dropout(x)

        # Generate Query, Key, Value
        query = self.query_linear(x)  # [batch_size, sequence_length, embedding_dim]
        key = self.key_linear(x)      # [batch_size, sequence_length, embedding_dim]
        value = self.value_linear(x)  # [batch_size, sequence_length, embedding_dim]

        # Quantum Self-Attention
        x = self.quantum_attention(query, key, value)  # [batch_size, sequence_length, n_qubits]

        # LSTM
        outputs = self.lstm(x)  # [batch_size, hidden_size]

        # Classification
        out = self.linear(outputs)  # [batch_size, num_classes]

        return out

# Visualization of Quantum Circuit
qlstm_instance = QLSTM(
    input_size=10, hidden_size=64, n_qubits=4, n_qlayers=n_qlayers, n_vrotations=3, backend="default.qubit"
)
qlstm_instance.visualize_qiskit_circuit()

# Function to calculate accuracy, recall, and F1 score
def calculate_accuracy_and_scores(data_loader, Qmodel):
    all_labels = []
    all_predictions = []

    Qmodel.eval()
    with torch.no_grad():
        for X, y in data_loader:
            X = X.to(Qmodel.embedding.weight.device)
            y = y.to(Qmodel.embedding.weight.device)
            outputs = Qmodel(X)
            _, predicted = torch.max(outputs.data, 1)
            all_labels.extend(y.cpu().numpy())
            all_predictions.extend(predicted.cpu().numpy())

    accuracy = 100 * accuracy_score(all_labels, all_predictions)
    recall = recall_score(all_labels, all_predictions, pos_label=1)
    f1 = f1_score(all_labels, all_predictions, pos_label=1)

    return accuracy, recall, f1

# Vocabulary size
vocab_size = len(vocabulary)

# Instantiate the classification model
device = 'cuda' if torch.cuda.is_available() else 'cpu'
Qmodel = QShallowClassificationLSTM(
    vocab_size=vocab_size,
    embedding_dim=embedding_dim,
    hidden_size=hidden_size,
    n_qubits=4,
    n_qlayers=n_qlayers,
    num_classes=2,
    backend='default.qubit'
).to(device)  # Move model to GPU if available

# Define loss function and optimizer
loss_function = nn.CrossEntropyLoss()
Qoptimizer = torch.optim.Adam(Qmodel.parameters(), lr=lr)

# Training function
def train_model(data_loader, Qmodel, loss_function, optimizer, progress_bar):
    total_loss = 0
    Qmodel.train()

    for X, y in data_loader:
        X = X.to(Qmodel.embedding.weight.device)
        y = y.to(Qmodel.embedding.weight.device)
        output = Qmodel(X)
        loss = loss_function(output, y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        progress_bar.update(1)

    avg_loss = total_loss / len(data_loader)
    return avg_loss

# Testing function
def test_model(data_loader, Qmodel, loss_function, progress_bar):
    total_loss = 0
    Qmodel.eval()
    with torch.no_grad():
        for X, y in data_loader:
            X = X.to(Qmodel.embedding.weight.device)
            y = y.to(Qmodel.embedding.weight.device)
            output = Qmodel(X)
            total_loss += loss_function(output, y).item()
            progress_bar.update(1)

    avg_loss = total_loss / len(data_loader)
    return avg_loss

# Initialize lists to store metrics
quantum_loss_train = []
quantum_loss_val = []
quantum_accuracy_val = []

train_accuracy_list = []
train_recall_list = []
train_f1_list = []

val_accuracy_list = []
val_recall_list = []
val_f1_list = []

# Verify one batch
for X, y in train_loader:
    print(f"Input Shape: {X.shape}")   # [batch_size, sequence_length]
    print(f"Target Shape: {y.shape}")  # [batch_size]
    break

# Training Loop
for ix_epoch in range(epoch):
    total_steps = len(train_loader) + len(val_loader)
    progress_bar = tqdm(total=total_steps, desc=f"Epoch {ix_epoch + 1}")

    # Training loss
    train_loss = train_model(train_loader, Qmodel, loss_function, Qoptimizer, progress_bar)

    # Validation loss
    val_loss = test_model(val_loader, Qmodel, loss_function, progress_bar)

    # Calculate metrics
    train_accuracy, train_recall, train_f1 = calculate_accuracy_and_scores(train_loader, Qmodel)
    val_accuracy, val_recall, val_f1 = calculate_accuracy_and_scores(val_loader, Qmodel)

    progress_bar.close()

    # Append metrics
    train_accuracy_list.append(train_accuracy)
    train_recall_list.append(train_recall)
    train_f1_list.append(train_f1)

    val_accuracy_list.append(val_accuracy)
    val_recall_list.append(val_recall)
    val_f1_list.append(val_f1)

    # Append losses
    quantum_loss_train.append(train_loss)
    quantum_loss_val.append(val_loss)
    quantum_accuracy_val.append(val_accuracy)

    # Print metrics
    print(f"\nTrain Loss: {train_loss:.4f}")
    print(f"Validation Loss: {val_loss:.4f}")

    print(f"Train Accuracy: {train_accuracy:.4f}%")
    print(f"Validation Accuracy: {val_accuracy:.4f}%\n")

    print(f"Train Recall: {train_recall:.4f}")
    print(f"Train F1 Score: {train_f1:.4f}")

    print(f"Validation Recall: {val_recall:.4f}")
    print(f"Validation F1 Score: {val_f1:.4f}\n")

# Evaluate on Test Set
test_accuracy, test_recall, test_f1 = calculate_accuracy_and_scores(test_loader, Qmodel)

print(f'Test Set Evaluation:')
print(f"Test Accuracy: {test_accuracy:.4f}%")
print(f"Test Recall: {test_recall:.4f}")
print(f"Test F1 Score: {test_f1:.4f}")

# Plot Loss
plt.figure(figsize=(12, 6))
plt.plot(quantum_loss_train, label='Quantum Train Loss')
plt.plot(quantum_loss_val, label='Validation Loss')
plt.title('Quantum LSTM Model Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()

# Plot Metrics
plt.figure(figsize=(12, 6))
plt.plot(train_accuracy_list, label='Train Accuracy')
plt.plot(val_accuracy_list, label='Validation Accuracy')
plt.plot(train_recall_list, label='Train Recall')
plt.plot(val_recall_list, label='Validation Recall')
plt.plot(train_f1_list, label='Train F1 Score')
plt.plot(val_f1_list, label='Validation F1 Score')
plt.title('Model Evaluation Metrics Across Epochs')
plt.xlabel('Epoch')
plt.ylabel('Metrics')
plt.legend()
plt.show()我的模型报错Traceback (most recent call last):
  File "C:\pycharm\QLSTM分类\AAA实验\MPQA量子注意力.py", line 584, in <module>
    train_loss = train_model(train_loader, Qmodel, loss_function, Qoptimizer, progress_bar)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\pycharm\QLSTM分类\AAA实验\MPQA量子注意力.py", line 531, in train_model
    output = Qmodel(X)
             ^^^^^^^^^
  File "C:\python\Lib\site-packages\torch\nn\modules\module.py", line 1532, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\torch\nn\modules\module.py", line 1541, in _call_impl
    return forward_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\pycharm\QLSTM分类\AAA实验\MPQA量子注意力.py", line 467, in forward
    x = self.quantum_attention(query, key, value)  # [batch_size, sequence_length, n_qubits]
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\torch\nn\modules\module.py", line 1532, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\torch\nn\modules\module.py", line 1541, in _call_impl
    return forward_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\pycharm\QLSTM分类\AAA实验\MPQA量子注意力.py", line 178, in forward
    q_query = self.query_layer(query)  # [batch_size * sequence_length, n_qubits]
              ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\torch\nn\modules\module.py", line 1532, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\torch\nn\modules\module.py", line 1541, in _call_impl
    return forward_call(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\pennylane\qnn\torch.py", line 402, in forward
    results = self._evaluate_qnode(inputs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\pennylane\qnn\torch.py", line 423, in _evaluate_qnode
    res = self.qnode(**kwargs)
          ^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\pennylane\workflow\qnode.py", line 1048, in __call__
    res = qml.execute(
          ^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\pennylane\workflow\execution.py", line 668, in execute
    tapes, post_processing = transform_program(tapes)
                             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\pennylane\transforms\core\transform_program.py", line 477, in __call__
    new_tapes, fn = transform(tape, *targs, **tkwargs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\python\Lib\site-packages\pennylane\devices\preprocess.py", line 132, in validate_device_wires
    raise WireError(
pennylane.wires.WireError: Cannot run circuit(s) on default.qubit as they contain wires not found on the device: {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159}
Epoch 1:   0%|          | 0/300 [00:08<?, ?it/s]
请你仔细分析出错原因，并且给出修改后的完整代码