from sklearn.neural_network import MLPRegressor
import numpy as np
import random
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, median_absolute_error, r2_score
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import pandas as pd

def fitness(individual, X_train, X_test, y_train, y_test):
    hidden_layer_sizes = tuple(individual[:-1 - NUM_WEIGHTS])
    learning_rate_init = individual[-1 - NUM_WEIGHTS]
    weights = individual[-NUM_WEIGHTS:]
    mlp = MLPRegressor(hidden_layer_sizes=hidden_layer_sizes, learning_rate_init=learning_rate_init, random_state=0)
    # Set the weight parameters into MLPRegressor
    mlp.coefs_ = weights
    mlp.fit(X_train, y_train)
    y_pred = mlp.predict(X_test)
    mse = np.mean((y_pred - y_test) ** 2)
    return -mse

def init_population(POP_SIZE, NUM_HIDDEN_LAYERS, WEIGHT_RANGE):
    population = []
    for _ in range(POP_SIZE):
        hidden_layer_sizes = [random.randint(10, 100) for _ in range(NUM_HIDDEN_LAYERS)]
        learning_rate_init = random.uniform(0.0001, 0.1)
        weights = [random.uniform(*WEIGHT_RANGE) for _ in range(NUM_WEIGHTS)]
        individual = hidden_layer_sizes + [learning_rate_init] + weights
        population.append(individual)
    return population

def crossover(parent, pop):
    cross_points = np.random.randint(0, 2, size=len(parent)).astype(np.bool)
    parent[cross_points] = pop[np.random.randint(0, len(pop), size=1)][0][cross_points]
    return parent.tolist()

def mutate(child, MUTATION_RATE, WEIGHT_RANGE):
    for point in range(len(child) - NUM_WEIGHTS - 1, len(child) - 1):
        if np.random.rand() < MUTATION_RATE:
            child[point] = random.uniform(*WEIGHT_RANGE)  # Weight mutation
    return child

def genetic_algorithm(POP_SIZE, N_GENERATIONS, NUM_HIDDEN_LAYERS, X_train, X_test, y_train, y_test, MUTATION_RATE,
                      WEIGHT_RANGE):
    population = init_population(POP_SIZE, NUM_HIDDEN_LAYERS, WEIGHT_RANGE)
    best_losses = []
    best_individual = None

    for generation in range(N_GENERATIONS):
        print(f"Generation: {generation}")
        fitnesses = [fitness(individual, X_train, X_test, y_train, y_test) for individual in population]
        best_individual_idx = np.argmax(fitnesses)
        best_losses.append(-fitnesses[best_individual_idx])
        best_individual = population[best_individual_idx]

        parents = np.array(population)
        fitnesses = np.array(fitnesses)

        # Selection
        parents_fitness = fitnesses / fitnesses.sum()
        offspring_size = POP_SIZE - 1
        offspring_idx = np.random.choice(np.arange(len(parents)), size=offspring_size, replace=True,
                                         p=parents_fitness)
        parents = parents[offspring_idx]
        parents_fitness = parents_fitness[offspring_idx]

        # Crossover
        children = []
        for i in range(offspring_size):
            parent1 = parents[i]
            parent2 = parents[np.random.randint(0, offspring_size, size=1)][0]
            child = crossover(parent1.copy(), [parent1, parent2])
            child = mutate(child, MUTATION_RATE, WEIGHT_RANGE)
            children.append(child)

        # Elite retention
        population = [best_individual]
        population.extend(children)

        print(f"Generation: {generation}, Best Loss: {best_losses[-1]}")

    return best_individual

def median_filter(signal, kernel_size):
    assert kernel_size % 2 == 1, "Kernel size must be odd!"  # The kernel size must be an odd number
    padded_signal = np.pad(signal, (kernel_size // 2, kernel_size // 2), mode='reflect')
    filtered_signal = np.zeros_like(signal)
    for i in range(len(signal)):
        filtered_signal[i] = np.median(padded_signal[i:i + kernel_size])
    return filtered_signal

def preprocess_data():
    dataframe = pd.read_excel('data.xls', sheet_name=2, engine='xlrd')
    input_data = dataframe.iloc[:, 0:5].values
    output_data = dataframe.iloc[:, 5].values
    X = np.apply_along_axis(lambda m: median_filter(m, kernel_size=3), axis=0, arr=input_data)
    y = median_filter(output_data, kernel_size=3)
    return X, y

def train_and_predict(X_train, X_test, y_train, y_test, best_hidden_layer_sizes, best_learning_rate, max_iterations):

    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled = scaler.transform(X_test)

    mlp = MLPRegressor(hidden_layer_sizes=tuple(best_hidden_layer_sizes), learning_rate_init=best_learning_rate,
                       random_state=0, max_iter=max_iterations)

    mlp.fit(X_train_scaled, y_train)
    y_pred = mlp.predict(X_test_scaled)
    med = median_absolute_error(y_test, y_pred)
    mse = mean_squared_error(y_test, y_pred)
    r2 = r2_score(y_test, y_pred)
    print("Mean squared error:", mse)
    print("Median absolute error:", med)
    print("R2 score:", r2)
    return y_test, y_pred

def draw_chart(y_test, y_pred):
    # Plot the line graph
    plt.plot(y_test[100:150], label='True Value', color='blue', linestyle='-')
    plt.plot(y_pred[100:150], label='Predicted Value', color='red', linestyle='--')

    # Set labels and title
    plt.xlabel('Sample Size')
    plt.ylabel('Granularity Pass Rate')
    plt.title('Granularity Pass Rate Prediction Model')
    plt.rcParams['font.sans-serif'] = ['SimHei']  # Adjust font for correct display of Chinese characters
    # Show the legend
    plt.legend()

    # Display the plot
    plt.show()

if __name__ == "__main__":
    POP_SIZE = 100  # Population size
    CROSS_RATE = 0.8  # Crossover probability
    MUTATION_RATE = 0.05  # Mutation probability
    N_GENERATIONS = 500  # Number of generations
    NUM_HIDDEN_LAYERS = 4  # Number of hidden layers
    MAX_ITER = 500  # Maximum number of iterations
    NUM_WEIGHTS = 10  # Number of weights
    WEIGHT_RANGE = (-1, 1)  # Weight range
    X, y = preprocess_data()
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.04, random_state=46)
    best_params = genetic_algorithm(POP_SIZE, N_GENERATIONS, NUM_HIDDEN_LAYERS, X_train, X_test, y_train, y_test,
                                    MUTATION_RATE, WEIGHT_RANGE)
    best_hidden_layer_sizes = best_params[:-1 - NUM_WEIGHTS]
    best_hidden_layer_sizes = [int(layer) for layer in best_hidden_layer_sizes]
    best_learning_rate = best_params[-1 - NUM_WEIGHTS]
    # Display best neural network architecture parameters
    print('Optimal hidden layer sizes:', best_hidden_layer_sizes)
    print('Optimal learning rate:', best_learning_rate)
    # Train and evaluate the model
    y_test, y_pred = train_and_predict(X_train, X_test, y_train, y_test, best_hidden_layer_sizes, best_learning_rate, MAX_ITER)
    draw_chart(y_test, y_pred)