import numpy as np


## INPUTS

var = 3 # D : Cantidad de variables de decision o dimensiones del problema
individuos = 4 # m : Cantidad de individuos
max_iter = 100 # T : maximo de iteraciones
dominio = [-100, 100] # Restriccion de Dominio

#Función Objetivo
def objective(x):
    return np.sum(x**2)

## PASO 1
# Definir aleatoriamente la poblacion inicial, obteniendo m soluciones iniciales factibles 

poblacion = np.random.uniform(dominio[0],dominio[1], size=(individuos, var)) # Crea valores aleatoriamente dentro de un rango segun una distribucion uniforme. 1er argumento es el limite menor del rango, 2do argumento es el limite mayor y 3er argumento dice como queremos la salida del método. En este caso crea matriz de 4 filas (individuos) y 3 columnas (variables), que es la poblacion inicial


## PASO 2
# Evaluar la función objetivo para cada individuo
fitness = np.array([objective(ind) for ind in poblacion]) #vector que guarda el valor de funcion objetivo para cada individuo (lista con 4 valores)

## PASO INTERMEDIO
# Crear listas que sirvan para guardar el historial de cada individuo, es decir, cada actualizacion de sus variables. Util para graficar y hacer reportes.

historial_x1 = [[] for _ in range(individuos)]
historial_x2 = [[] for _ in range(individuos)]
historial_x3 = [[] for _ in range(individuos)]
historial_best = [[] for _ in range(max_iter+1)] # va a guardar la mejor solución para cada iteracion


## PASO 3
# Comparar entre soluciones y definir Best como la mejor de ellas
best_index = np.argmin(fitness) #guarda el indice del individuo que optimice mejor (minimice fitness)
best = poblacion[best_index] #con el indice anterior, guarda el individuo que optimice mejor
historial_best[0].append(int(best_index+1)) #historia de best (para informacion solamente)



## PASO 4 : Ciclos for del Pendulum Search Algorithm (PSA)

for t in range(max_iter):   #for de iteracion
    
    for i in range(individuos): #for de soluciones / individuos
        for j in range(var): #for de variables / dimensiones
            rand = np.random.rand() #genera numero aleatorio entre 0 y 1
            pend = 2 * np.exp(-t / max_iter) * np.cos(2 * np.pi * rand) #calculo de pend
            x_calculado = poblacion[i, j] + pend * (best[j] - poblacion[i, j]) #actualizacion de variable
            
            #FACTIBILIDAD
            x_calculado = np.clip(x_calculado, dominio[0], dominio[1]) #si es necesario se repara la solucion por restriccion de dominio
            poblacion[i, j] = x_calculado #se actualiza

        #Evaluar funcion objetivo para la solucion actual
        fitness[i] = objective(poblacion[i]) # guarda el valor de la funcion objetivo para la solucion actual
        historial_x1[i].append(float(poblacion[i, 0])) #guarda el valor de la variable x1 para el individuo i
        historial_x2[i].append(float(poblacion[i, 1])) #guarda el valor de la variable x2 para el individuo i
        historial_x3[i].append(float(poblacion[i, 2])) #guarda el valor de la variable x3 para el individuo i

    #Se actualiza la mejor solucion Best
    best_index = np.argmin(fitness) #guarda el indice del individuo que optimice mejor
    best = poblacion[best_index] #con el indice anterior, guarda el individuo que optimice mejor
    historial_best[t+1].append(int(best_index+1)) #historia de best (para informacion solamente)



# Mostrar resultados finales
print("\n--------------------------------\nResultados finales:\n\nFAMILIA FINAL")
for i in range(individuos):
    print(f"Individuo {i+1}: x = {poblacion[i]}, fitness = {fitness[i]:.4f}")

# Mostrar mejor individuo
best_idx = np.argmin(fitness)
print(f"\nBEST:\nIndividuo {best_idx +1}: x = {poblacion[best_idx]}, fitness = {fitness[best_idx]:.4f}")



