aqgo 0.1.12
Es el optimizador cuántico de AletheIA
Requires Python: >=3.10

# AletheIA Quantum Genetic Optimizers (AQGO)

## Equipo de desarrollo

| Nombre                  | Rol                         | Información de contacto  | Perfil de LinkedIn                                          |
|-------------------------|-----------------------------|--------------------------|-------------------------------------------------------------|
| Daniel Sarabia Torres   | Full Stack AI & Q Developer | dsarabiatorres@gmail.com | https://es.linkedin.com/in/danielsarabiatorres              |
| Luciano Ezequiel Bizin  | Full Stack AI & Q Developer | lucianobizin@gmail.com   | https://www.linkedin.com/in/luciano-ezequiel-bizin-81b85497 |

## Instalación

```pip install aqgo```

## Introducción

Este proyecto implementa un algoritmo genético cuántico diseñado para abordar problemas de optimización combinatoria mediante el uso de tecnologías cuánticas (AerSimulator y ordenadores cuánticos de IBM). 

La arquitectura del algoritmo fusiona principios clásicos de evolución genética con procesamiento de datos de manera cuántica, buscando así, ganar capacidad exploratoria de manera más eficiente en grandes espacios de soluciones.

El objetivo principal de la librería `AletheIA Quantum Genetic Optimizers (AQGO)` es resolver dos clases generales de problemas:

- *`bounds_restricted`*: Problemas con límites o restricciones estructurales parciales, en los que se permite cierta flexibilidad en la búsqueda de los mejores números dentro de márgenes definidos (por ejemplo, la búsqueda de hiperparámetros para un modelo de ML o DL).

- *`totally_restricted`*: Problemas con restricciones estrictas en los valores permitidos y en las configuraciones válidas, como el Traveling Salesman Problem (TSP) -único problema de este tipo que, al momento, se puede resolver con el optimizador-, donde solo se aceptan permutaciones válidas sin repeticiones.

## Uso de la computación cuántica

La computación cuántica se integra en la librería de tres maneras:

1. *Generación de aleatoriedad cuántica*: Para crear poblaciones iniciales y aplicar mutaciones. Se aprovecha la aleatoriedad genuina ofrecida por simuladores cuánticos o por hardware cuántico real, logrando una diversificación más natural de las soluciones.

2. *Reproducción de hijos mediante VQC*: Para generar los hijos en los problemas de tipo `bounds_restricted` se utiliza un `Variational Quantum Circuit`, el cual reune los circuitos cuánticos de dos padres ganadores a partir de los cuales se reproducirá un hijo.

3. *Solución de subproblemas mediante QAOA*: Para generar los hijos en los problemas de tipo `totally_restricted`, se emplea la clase `QAOA` de Qiskit para resolver sub-rutas óptimas, que luego se concatenan en una única ruta. Es decir, con el QAOA se resuelven distintos subproblemas de `optimización cuadrática binaria (QUBO)`, potenciando la búsqueda de micro-soluciones que constituyen la macro-solución del problema.

--------------------------------------------

## Explicación detallada de casos de uso 

Se explica de manera detallada ambos casos de uso: `bounds_restricted` (búsqueda de hiperparámetros para un modelo de ML/DL) y `totally_restricted` (TSP)

## Caso de uso 1: 

En este apartado se describe el caso de uso de encontrar los mejores hiperparámetros para un modelo de IA, en el ejemplo en concreto, para un modelo básico de ML.

### Entendimiento del problema: El problema de encontrar los mejores hiperparámetros para un modelo de IA

El problema de encontrar los mejores hiperparámetros para un modelo de IA, en general, excepto en casos con muchos hiperparámetros, puede ser abarcado sin ningún inconveniente con los simuladores u ordenadores cuánticos de hoy en día. 

Imaginar que queremos entrenar una red neuronal o un modelo de ML, y para tal situación, necesitamos encontrar los mejores hiperparámetros para nuestro modelo.

El desafío radica en que, a medida que aumenta el número de hiperparámetros a optimizar (aunque también puede ser la estructura de la red neuronal o la cantidad de neuronas si se ha categorizado estos valores), podría resultar en un proceso muy costoso (computacional y económicamente) incluso para ordenadores cuánticos.

### Ejemplo de código

```

def example_1_bounds_no_predefinidos():

    # -- Definimos la función objetivo
    def objective_function(individual, dataset_loader: Literal["fetch_california_housing", "glass", "breast_cancer"] = "breast_cancer"):
    
        # Cargamos el dataset según el tipo de problema a resolver (claramente se puede utilizar un dataset propio)
        match dataset_loader:
            case "fetch_california_housing":
                data = fetch_california_housing()
                problem_type = 'regression'
                test_size = 0.2
            case "breast_cancer":
                data = load_breast_cancer()
                problem_type = 'binary'
                test_size = 0.7
            case "glass":
                data = fetch_openml(name="glass", version=1, as_frame=True)
                problem_type = 'multiclass'
                test_size = 0.2
            case _:
                data = fetch_california_housing()
                problem_type = 'regression'
                test_size = 0.2

        # -- Obtenemos el individuo a evaluar
        individual_dict = individual.get_individual_values()
        
        # -- Dividimos los datos y la target
        X, y = data.data, data.target

        # -- Dividimos los datos en train y test, y los normalizamos
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=42)
        
        scaler = StandardScaler()
        X_train = pd.DataFrame(scaler.fit_transform(X_train), columns=data.feature_names)
        X_test = pd.DataFrame(scaler.transform(X_test), columns=data.feature_names)

        # -- Entrenamos y predecimos
        if problem_type == 'regression':
            model = lgb.LGBMRegressor(
                n_estimators=int(individual_dict["n_estimators"]),
                max_depth=individual_dict["max_depth"],
                verbose=-1,
                random_state=42
            )
            model.fit(X_train, y_train)
            y_pred = model.predict(X_test)
            score = r2_score(y_test, y_pred)
            mae = mean_absolute_error(y_test, y_pred)

        else:
            model = lgb.LGBMClassifier(
                n_estimators=int(individual_dict["n_estimators"]),
                max_depth=individual_dict["max_depth"],
                verbose=-1,
                random_state=42
            )
            model.fit(X_train, y_train)
            y_pred = model.predict(X_test)
            score = accuracy_score(y_test, y_pred)

        return score

    # -- Creamos el diccionario de bounds
    bounds = BoundCreator()
    bounds.add_interval_bound("n_estimators", 50, 1000, 10, 1500, "int")
    bounds.add_predefined_bound("max_depth", (1, 2, 3, 4, 5, 6, 7, 8, 9), "int")

    # -- Instanciamos el optimizador
    return GenethicOptimizer(bounds_dict=bounds.get_bound(),
                             num_generations=50,
                             num_individuals=100,
                             max_qubits=20,
                             objective_function=objective_function,
                             metric_to_optimize="accuracy",
                             problem_restrictions="bound_restricted",
                             return_to_origin=None,
                             problem_type="maximize",
                             tournament_method="ea_simple",
                             podium_size=3,
                             mutate_probability=0.25,
                             mutate_gen_probability=0.25,
                             mutation_policy="normal",
                             verbose=True,
                             early_stopping_generations="gradient",
                             variability_explossion_mode="crazy",
                             variability_round_decimals=3,
                             randomness_quantum_technology="simulator",
                             randomness_service="aer",
                             optimization_quantum_technology="simulator",
                             optimization_service="aer",
                             qm_api_key="IBM_QUANTUM_API",
                             qm_connection_service="ibm_quantum",
                             quantum_machine="least_busy"
                             )
                             
# -- Instanciamos la función que ejecuta el AletheIA Quantum Genetic Optimizers
genetic_optimizer_object: GenethicOptimizer = example_1_bounds_no_predefinidos()

# -- Instanciamos el ploteo de resultados (opcional)
genetic_optimizer_object.plot_generation_stats()
genetic_optimizer_object.plot_evolution()

# -- Obtenemos los valores del mejor individuo
best_individual_value = genetic_optimizer_object.get_best_individual_values()

# -- Pintamos los valores del mejor individuo en consola
print(best_individual_value)

```

#### El enfoque genético cuántico

El código proporcionado implementa un algoritmo genético cuántico para abordar la búsqueda de los mejores hiperparámetros de un modelo de IA. 

A continuación, se explica cómo funciona este algoritmo y sus potenciales ventajas:

1. *Introducción programática:* 

* El `individual`, que recibe la función `objective_function` como parámetro, representa un conjunto de hiperparámetros (ej: {max_depth: 2, n_estimators: 325}).
* El aspecto cuántico de este algoritmo genético ocurre al momento de generarse la primera generación (se utiliza un circuito cuántico para generar valores binarios aleatorios, que luego se reescalan entre -pi y pi, y sirven como ángulos de rotación de otro circuito cuántico que termina generando el individuo), y al momento de la reproducción de nuevos hijos a partir de la primera generación, combinando y ejecutando los circuitos de los mejores padres con algunas modificaciones cuánticas intermedias.
* ¿Cómo sucede este proceso de reproducción? Nos quedamos con los mejores padres de la generación previa (en este caso se utiliza el torneo EaSimple), se seleccionan aleatoriamente dos de los mejores padres, se aplica un circuito que combina mediante entrelazamiento y rotaciones ambos circuitos, se lo mide, y dicho número binario que se obtiene se reescala entre -pi y pi (ángulos) y, a su vez, este ángulo se reescala para el bound en cuestión que se quiere construir. Cada bound tiene su combinación y ejecución de circuitos cuánticos. Si tenemos 2 hiperparámetros se ejecutan 2 circuitos iguales, pero con mediciones distintas. 

2. *Función objetivo (`objective_function`):*

Esta función calcula el accuracy o el MAE dependiendo del problema, o cualquier otra métrica de interés.

* Toma un `individual`, es decir, un objeto cuyos valores representan los hiperparámetros de entrada del modelo de IA.
* Utiliza esos hiperparámetros para calcular el resultado de la función objetivo.
* Se devuelve el `score`, que el algoritmo genético busca minimizar/maximizar según el tipo de problema.

3. *Algoritmo genético (`GenethicOptimizer`):* Esta clase implementa la lógica central del algoritmo genético cuántico.

    * *Inicialización:* Comienza creando una población de soluciones potenciales (hiperparámetros) mediante un proceso de aleatoriedad cuántica.
    * *Evaluación:* La función `objective_function` se utiliza para evaluar la "aptitud" de cada individuo en la población.
    * *Selección:* Los individuos con mejor aptitud tienen más probabilidades de ser seleccionados como "mejores padres" para crear la siguiente generación.
    * *Cruce por circuitos cuánticos (rotaciones y entrelazamientos):* El material genético (hiperparámetros) de dos individuos padres se combinan para crear nuevos individuos descendientes. 
    * *Mutación:* Se introducen pequeños cambios aleatorios en los valores de los hiperparámetros de los descendientes para mantener la diversidad en la población y evitar que quedarnos atrapados en óptimos locales.
    * *Operaciones cuánticas:* Los parámetros `max_qubits`, `randomness_quantum_technology`, `optimization_quantum_technology` y los detalles de conexión (`qm_api_key`, `qm_connection_service`, `quantum_machine`, `optimization_service`) permiten ejecutar el algoritmo, o bien con el AerSimulator o bien en ordenadores cuánticos de IBM.
    * *Terminación:* El algoritmo continúa durante un número específico de generaciones (`num_generations`) o hasta que se cumple un criterio de detención (`early_stopping_generations`).

4. *Límites (`BoundCreator`):* Los límites/bounds definen los posibles valores para cada "gen" del individuo (en este caso, el valor de un hiperparámetro).

### Posibles usos reales

Si bien el desarrollo de hardware cuántico todavía posee un largo camino por recorrer, eso no significa que hasta que se alcance un nivel más sofisticado de desarrollo de hardware cuántico, no se pueda hacer nada con ellos.

El `Aletheia Quantum Genetic Optimizers` está diseñado para resolver varios tipos de problemas de optimización. 

Uno de los escenarios para los que se ha programado, es para encontrar los mejores hiperparámetros de modelos de IA, o resolver problemas similares. 

A nivel real -al momento- puede ser utilizado para resolver cuantiosos problemas de este tipo:

| Industria                | Tipo de datos                                      | Ejemplos de modelos a usar         | Beneficio del enfoque cuántico-genético                                         |
|--------------------------|----------------------------------------------------|------------------------------------|---------------------------------------------------------------------------------|
| Finanzas                 | Transacciones bancarias                            | LGBMClassifier, XGBoost            | Detección más precisa de fraude, menos falsos positivos, búsqueda más rápida    |
| Energía                  | Variables climáticas, consumo histórico            | LGBMRegressor, Redes neuronales    | Mejor predicción de demanda, optimización eficiente de recursos energéticos     |
| Salud                    | Datos clínicos, imágenes médicas                   | RandomForest, CNNs, LGBMClassifier | Diagnósticos automáticos más precisos y rápidos                                 |
| Marketing y Ventas       | Historial de usuarios, CRM                         | XGBoost, CatBoost, Árboles         | Mejor segmentación y predicción de abandono (churn)                             |
| Ciberseguridad           | Logs de red, sesiones de conexión                  | RandomForest, SVM, LSTM            | Mejora en la detección de tráfico malicioso, reducción de falsos negativos      |
| Agricultura              | Condiciones ambientales, suelo, clima              | LGBMRegressor, Redes neuronales    | Predicción precisa del rendimiento, decisiones agronómicas más acertadas        |
| Industria 4.0            | Datos sensoriales de maquinaria                    | LSTM, RandomForest, SVM            | Mantenimiento predictivo, detección temprana de fallas                          |
| Educación                | Comportamiento de estudiantes, resultados          | Sistemas de recomendación, Árboles | Personalización del aprendizaje, mayor engagement y retención de alumnos        |
| Transporte y Logística   | Rutas, tiempos de entrega, demanda                 | Regresión, KNN, RandomForest       | Optimización del reparto, predicción de demanda, mejora en eficiencia logística |
| Recursos Humanos         | CVs, entrevistas, datos de rendimiento laboral     | NLP + árboles, XGBoost             | Mejora del proceso de selección, predicción de rendimiento de empleados         |
| Medioambiente            | Datos satelitales, sensores ambientales            | LGBM, CNN, RandomForest            | Monitoreo ambiental en tiempo real, predicción de fenómenos climáticos          |
| Telecomunicaciones       | Datos de llamadas, uso de apps, geolocalización    | CatBoost, Redes neuronales         | Predicción de churn, optimización de red, segmentación de usuarios              |
| eCommerce                | Historial de compras, comportamiento de navegación | XGBoost, Recommender Systems       | Mejora en sistemas de recomendación y personalización                           |
| Sector Legal             | Documentos legales, jurisprudencia, contratos      | NLP + Transformers, Decision Trees | Análisis automatizado de textos legales, detección de cláusulas de riesgo       |
| Juegos y Entretenimiento | Comportamiento de jugadores, patrones de uso       | RandomForest, Deep Q-Learning      | Optimización de diseño de niveles, detección de comportamiento irregular        |


### Ventajas de usar el AletheIA Quantum Genetic Optimizers para este tipo de problemas

* *Exploración mejorada del espacio de soluciones:* Los fenómenos cuánticos como la superposición y el entrelazamiento hacen que este algoritmo, tenga al menos en teoría, la capacidad de explorar una mayor cantidad de combinaciones de hiperparámetros de manera distinta a como lo hace un optimizador clásico.
* *Operadores genéticos más eficientes:* Los operadores de cruce/reproducción y mutación, basados en procesos cuánticos, podrían ser más efectivos para combinar hiperparámetros al introducir una mayor diversidad en el proceso de búsqueda de soluciones.
* *Menor tendencia a quedar atascados en óptimos locales:* La naturaleza probabilística de las mediciones cuánticas podría ayudar al algoritmo a escapar de soluciones subóptimas.

#### Conclusiones luego de ver los resultados

Para este tipo de problemas, el `AletheIA Quantum Genetic Optimizers` es muy prometedor para resolver este tipo de problemas combinatorios.

En general, en las pruebas realizadas, con el fin de contrastar su potencialidad con respecto a otros algoritmos genético clásicos, ha superado al `AletheIA Genetic Optimizers`, que de por sí es muy bueno, en cantidad de generaciones requeridas para alcanzar la mejor métrica posible para el modelo y problema evaluado, tanto para problemas de clasificación como de regresión. Sin embargo, se debe tener en cuenta, que no converge como lo hace un optimizador clásico, donde toda la población va acompañando la suba o baja de la métrica evaluada. En el caso del `AletheIA Quantum Genetic Optimizers` se aprecia un procedimiento más elitista, es decir, encuentra por capacidad explotaria máximos o mínimos locales/globales muy buenos, pero en cada generación lo hacen pocos individuos de la población.

--------------------------------------------

## Caso de uso 2: 

En este apartado se describe el caso de uso para resolver el Problema del Viajero (TSP, por sus siglas en inglés) en sus variantes de ciclo cerrado y abierto.

### Entendimiento del problema: El problema del viajante de comercio (TSP)

El problema del viajante de comercio es un problema clásico de optimización combinatoria. 

Imaginar un viajante que necesita visitar un conjunto de ciudades y cuyo objetivo es encontrar la ruta más corta posible que visite cada ciudad exactamente una vez:

* *Ciclo cerrado (con retorno al origen):* El viajante debe regresar a la ciudad de inicio después de visitar todas las demás ciudades formando un tour o ciclo completo.
* *Ciclo abierto (sin retorno al origen):* El viajante visita cada ciudad exactamente una vez, pero no necesita regresar a la ciudad de inicio.

El desafío radica en que, a medida que aumenta el número de ciudades, la cantidad de posibles rutas crece factorialmente, lo que hace que encontrar la solución óptima sea computacionalmente muy costoso.

*NOTA: Actualmente como existen limitaciones de hardware (memoria RAM) o de costos (los precios para las iteraciones necesarias son elevados) se ideó dividir el problema de las rutas en distintos subproblemas. Por ejemplo, un problema de 20 ciudades se subdivide en 4 grupos de 5 ciudades, se aplica el QAOA a cada subgrupo para intentar encontrar la mejor combinación posible, para finalmente concatenar cada subruta en una sola.*

### Ejemplo de código

```
def example_tsp():

    # Coordenadas de ciudades (ejemplo con 20 ciudades)
    original_cities = {
        0: (4.178515558765522, 3.8658505110962347),
        1: (9.404615166221248, 8.398020682045034),
        2: (0.3782334121284714, 8.295288013706802),
        3: (9.669161753695562, 5.593501025856912),
        4: (9.870966532678576, 4.756484445482374),
        5: (3.5045826424785007, 1.1043994011149494),
        6: (5.548867108083866, 5.842473649079045),
        7: (1.11377627026643, 1.304647970128091),
        8: (5.133591646349645, 3.8238217557909038),
        9: (7.074655346940579, 3.6554091142752734),
        10: (9.640123872995837, 1.3285594561699254),
        11: (0.021205320973052277, 7.018385604153457),
        12: (2.048903069073358, 2.562383464533476),
        13: (2.289964825687684, 4.325937821712228),
        14: (6.315627335092245, 3.7506598107821656),
        15: (1.0589427543395036, 6.2520630725232),
        16: (9.218474645470067, 4.106769373018785),
        17: (4.62163288328154, 9.583091224200263),
        18: (7.477615269848112, 7.597659062497909),
        19: (0.25092704950321565, 6.699275814039302),
    }

    # -- Creación de función objetivo que debe resolver el AletheIA Quantum Genetic Optimizers
    def objective_function(individual):
    
        # -- Definimos si es un problema de retorno al origen o no
        return_to_origin: bool = True

        # -- Definimos las ciudades (Dict --> {id_ciudad: (latitud y longitud)}
        original_cities = {
            0: (4.178515558765522, 3.8658505110962347),
            1: (9.404615166221248, 8.398020682045034),
            2: (0.3782334121284714, 8.295288013706802),
            3: (9.669161753695562, 5.593501025856912),
            4: (9.870966532678576, 4.756484445482374),
            5: (3.5045826424785007, 1.1043994011149494),
            6: (5.548867108083866, 5.842473649079045),
            7: (1.11377627026643, 1.304647970128091),
            8: (5.133591646349645, 3.8238217557909038),
            9: (7.074655346940579, 3.6554091142752734),
            10: (9.640123872995837, 1.3285594561699254),
            11: (0.021205320973052277, 7.018385604153457),
            12: (2.048903069073358, 2.562383464533476),
            13: (2.289964825687684, 4.325937821712228),
            14: (6.315627335092245, 3.7506598107821656),
            15: (1.0589427543395036, 6.2520630725232),
            16: (9.218474645470067, 4.106769373018785),
            17: (4.62163288328154, 9.583091224200263),
            18: (7.477615269848112, 7.597659062497909),
            19: (0.25092704950321565, 6.699275814039302),
        }

        # -- Obtenemos los valores únicos de las rutas y los ordenamos
        route = list(individual.get_individual_values().values())
        cities = original_cities.copy()

        # -- Si el problema es de retorno al origen (ciclo cerrado) agregamos la ruta de la última ciudad a la primera
        if return_to_origin:
            route.append(route[0])
            cities[len([z for z in cities.keys()])] = cities[route[-1]]

        # -- Creamos la matriz de distancias
        num_cities = len(cities)
        distance_matrix = np.zeros((num_cities, num_cities))
        for i in range(num_cities):
            for j in range(num_cities):
                if i != j:
                    x1, y1 = cities[i]
                    x2, y2 = cities[j]
                    distance_matrix[i][j] = np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)

        # -- Calculamos la distancia total
        total_distance = sum(distance_matrix[route[i]][route[(i + 1) % len(route)]] for i in range(len(route)))

        # -- Retornamos la distancia social para la ruta evaluada
        return total_distance

    # -- Creamos el diccionario de bounds
    range_list: tuple = tuple([z for z in range(0, 20)])
    
    bounds = BoundCreator()
    bounds.add_predefined_bound("city_zero", range_list, "int")
    bounds.add_predefined_bound("city_first", range_list, "int")
    bounds.add_predefined_bound("city_second", range_list, "int")
    bounds.add_predefined_bound("city_third", range_list, "int")
    bounds.add_predefined_bound("city_fourth", range_list, "int")
    bounds.add_predefined_bound("city_fifth", range_list, "int")
    bounds.add_predefined_bound("city_sixth", range_list, "int")
    bounds.add_predefined_bound("city_seventh", range_list, "int")
    bounds.add_predefined_bound("city_eighth", range_list, "int")
    bounds.add_predefined_bound("city_ninth", range_list, "int")
    bounds.add_predefined_bound("city_tenth", range_list, "int")
    bounds.add_predefined_bound("city_eleventh", range_list, "int")
    bounds.add_predefined_bound("city_twelfth", range_list, "int")
    bounds.add_predefined_bound("city_thirteenth", range_list, "int")
    bounds.add_predefined_bound("city_fourteenth", range_list, "int")
    bounds.add_predefined_bound("city_fifteenth", range_list, "int")
    bounds.add_predefined_bound("city_sixteenth", range_list, "int")
    bounds.add_predefined_bound("city_seventeenth", range_list, "int")
    bounds.add_predefined_bound("city_eighteenth", range_list, "int")
    bounds.add_predefined_bound("city_nineteenth", range_list, "int")

    # -- Instanciamos el optimizador
    return GenethicOptimizer(bounds_dict=bounds.get_bound(),
                             num_generations=500,
                             num_individuals=5,
                             max_qubits=20,
                             objective_function=objective_function,
                             metric_to_optimize="other",
                             problem_restrictions="totally_restricted",
                             return_to_origin="return_to_origin",
                             problem_type="minimize",
                             tournament_method="ea_simple",
                             podium_size=3,
                             mutate_probability=0.25,
                             mutate_gen_probability=0.10,
                             mutation_policy="normal",
                             verbose=True,
                             early_stopping_generations="gradient",
                             variability_explossion_mode="crazy",
                             variability_round_decimals=3,
                             randomness_quantum_technology="simulator",
                             randomness_service="aer",
                             optimization_quantum_technology="simulator",
                             optimization_service="aer",
                             qm_api_key="IBMQUANTUM_IP",
                             qm_connection_service="ibm_quantum",
                             quantum_machine="least_busy",
                             element_matrix=original_cities
                             )
                             
# -- Instanciamos la función que ejecuta el AletheIA Quantum Genetic Optimizers
genetic_optimizer_object: GenethicOptimizer = example_2_tsp()

# -- Instanciamos el ploteo de resultados (opcional)
genetic_optimizer_object.plot_generation_stats()
genetic_optimizer_object.plot_evolution()

# -- Obtenemos los valores del mejor individuo
best_individual_value = genetic_optimizer_object.get_best_individual_values()

# -- Pintamos los valores del mejor individuo en consola
print(best_individual_value)

```

#### El enfoque genético cuántico

El código proporcionado implementa un algoritmo genético cuántico para abordar el problema del Traveling Salesman Problem (TSP). 

A continuación, se explica cómo funciona este algoritmo y sus potenciales ventajas:

1. *Introducción programática:* 

* El `individual`, que recibe la función `objective_function` como parámetros, representa una ruta potencial (una permutación específica de las ciudades -por ejemplo: [2, 9, 4, 5, 7, ... 1]).
* El aspecto cuántico de este algoritmo genético se ejecuta al momento de generarse la primera generación (se utiliza un circuito para generar una semilla del método random que genera la lista de ciudades), y al momento de la reproducción de nuevos hijos luego de la primera generación.
* ¿Cómo sucede este proceso de reproducción? Nos quedamos con los mejores padres de la generación previa (en este caso se utiliza el torneo EaSimple), se seleccionan aleatoriamente dos de los mejores padres, se aplica un cruce de tipo OX1, se divide en clústers o sublistas las rutas, se aplica un QAOA a cada ruta, y se unifican las subrutas en una ruta final. 

2. *Función objetivo (`objective_function`):*

Esta función calcula la distancia total de una ruta dada.

* Toma un `individual` (un objeto que posee como valores la solución potencial de ciudades) como entrada.
* Utiliza las coordenadas de `original_cities` para calcular las distancias entre ciudades consecutivas en la ruta.
* El parámetro `return_to_origin` controla si se incluye la distancia desde la última ciudad de vuelta a la primera (para un ciclo cerrado).
* Devuelve la `total_distance`, que el algoritmo genético busca minimizar.

3. *Algoritmo genético (`GenethicOptimizer`):* Esta clase implementa la lógica central del algoritmo genético cuántico.

    * *Inicialización:* Comienza creando una población de soluciones potenciales (rutas). La naturaleza cuántica se utiliza para generar esta población inicial. Se utiliza la superposición cuántica (puertas H) para generar semillas de un generador random de individuos (rutas).
    * *Evaluación:* La función `objective_function` se utiliza para evaluar la "aptitud" de cada individuo en la población (menor distancia = mayor aptitud, ya que se está minimizando la distancia entre ciudades).
    * *Selección:* Los individuos con mejor aptitud tienen más probabilidades de ser seleccionados como "mejores padres" para crear la siguiente generación.
    * *Cruce OX1 (recombinación):* El material genético (partes de las rutas) de dos individuos padres se combina para crear nuevos individuos. Una vez ejecutada la recombinación, se subdivide la ruta final en grupos de, mínimo 2 y máximo 5 elementos, se aplica un QAOA para optimizar cada sublista, y estas se vuelven a unificar en una sola ruta. 
    * *Mutación:* Se introducen pequeños cambios aleatorios en las rutas de los hijos para mantener la diversidad en la población y evitar quedar atrapado en óptimos locales.
    * *Operaciones cuánticas:* Los parámetros `max_qubits`, `randomness_quantum_technology`, `optimization_quantum_technology` y los detalles de conexión (`qm_api_key`, `qm_connection_service`, `quantum_machine`, `optimization_service`) permiten que el algoritmo se pueda ejecutar o bien en simulador o bien en ordenadores cuánticos de IBM.
    * *Terminación:* El algoritmo continúa durante un número específico de generaciones (`num_generations`) o hasta que se cumple un criterio de detención (`early_stopping_generations`).

4. *Restricciones del problema (`problem_restrictions="totally_restricted"`):* Este parámetro indica que el algoritmo debe encontrar una ruta válida, es decir, una ruta en la que cada ciudad se visite exactamente una vez (y regrese al origen si `return_to_origin` es `True`). En el problema del TSP solo se revisa que estén todas las ciudades.

5. *Límites (`BoundCreator`):* Los límites definen los posibles valores para cada "gen"/propiedad del individuo (en este caso, el índice de una ciudad).

### Posibles usos reales

Si bien el desarrollo de hardware cuántico todavía posee un largo camino por recorrer, eso no significa que hasta que alcancen un nivel de mayor sofisticación, no se pueda hacer nada con ellos.

El `Aletheia Quantum Genetic Optimizers` está diseñado para resolver varios tipos de problemas de optimización. Uno de los escenarios para los que se ha programado, es para encontrar una ruta óptima para un problema de tipo TSP. 

A nivel real -al momento- puede ser utilizado para resolver problemas TSP del ámbito de:

* *Logística y servicios de entrega:*
    * *Optimización de rutas (en este caso solo euclidianas, por el momento):* Encontrar las rutas de entrega más eficientes para mensajeros, servicios postales o minoristas, minimizando el tiempo de viaje, el consumo de combustible y los costos. Esto podría ser un ciclo cerrado si el conductor necesita regresar a un depósito o, un ciclo abierto si termina en un destino que no es el inicial.

* *Fabricación y robótica:*
    * *Perforación de placas de circuito impreso:* Optimizar la trayectoria de una máquina perforadora para crear agujeros en una placa de circuito, minimizando el tiempo necesario para moverse entre los puntos de perforación.
    * *Soldadura/pintura robótica:* Encontrar la secuencia más eficiente de puntos para que un brazo robótico suelde o pinte componentes.
    * *Corte láser o fresado CNC:* Determinar la mejor secuencia de cortes en una pieza de material para reducir los movimientos innecesarios de la herramienta.

* *Transporte y viajes:*
    * *Planificación de tours:* Crear el itinerario más corto o eficiente para que un turista visite múltiples atracciones en el menor tiempo posible.
    * *Mantenimiento de infraestructuras:* Definir la ruta óptima para un equipo de mantenimiento que debe inspeccionar una serie de ubicaciones (por ejemplo, torres de telecomunicaciones, estaciones eléctricas o pozos petroleros).

* *Ciencias biológicas y medicina:* 
  * Reconstruir el orden de los fragmentos de una secuencia de ADN, encontrando el camino más corto para visitar cada fragmento identificado, basándose en la información de superposición.
  * Automatización en laboratorios: Optimizar el recorrido de brazos robóticos que deben dispensar líquidos o recolectar muestras en distintos puntos.

* *Agricultura de precisión*:
  * Monitoreo de cultivos: Determinar la mejor ruta para drones o tractores autónomos que deben visitar múltiples puntos de monitoreo o tratamiento dentro de un campo.

* *Astronomía:* 
  * Optimizar la secuencia de observaciones para telescopios a fin de minimizar el tiempo dedicado a moverse entre objetos celestes.
  
* Mantenimiento de flotas aéreas o ferroviarias:*
  * Inspección de unidades: Encontrar la secuencia más eficiente de revisiones técnicas o inspecciones a realizar en una base o red distribuida de vehículos.

### Ventajas de usar el AletheIA Quantum Genetic Optimizers para este tipo de problemas

* *Exploración mejorada del espacio de soluciones:* Los fenómenos cuánticos como la superposición y el entrelazamiento hacen que este algoritmo, tenga al menos en teoría, la capacidad de explorar una mayor cantidad de rutas de manera distinta a un optimizador clásico. Su potencialidad máxima, la encontrará cuando no se deba dividir el problema en subgrupos y se pueda evaluar la ruta completa con el QAOA.
* *Operadores genéticos más eficientes:* Los operadores de cruce/reproducción y mutación basados en procesos cuánticos utilizados, podrían ser más efectivos para combinar partes prometedoras de soluciones e introducir una diversidad beneficiosa al problema.
* *Menor tendencia a quedar atascados en óptimos locales:* La naturaleza probabilística de las mediciones cuánticas podría ayudar al algoritmo a escapar de soluciones subóptimas.

#### Ciclo cerrado vs. ciclo abierto

La variable `return_to_origin: bool = True` dentro de su función `objective_function` es clave para manejar ambas variantes del TSP:

* *Ciclo cerrado:* Cuando `return_to_origin` es `True` (como en su `objective_function`), la distancia desde la última ciudad de la ruta a la primera ciudad, se suma a la distancia total, cerrando así el ciclo/recorrido.
* *Ciclo abierto:* Para resolver un TSP de ciclo abierto, simplemente se necesita establecer `return_to_origin` en `False`. En este caso, el algoritmo intentará encontrar el camino más corto que visita todas las ciudades, partiendo del primer elemento (ciudad 0) sin retornar al punto de partida (puede terminar en cualquier punto excepto en el primero).

--------------------------------------------

## 1. Clase principal: GeneticOptimizer

La clase `GenethicOptimizer` implementa un algoritmo genético cuántico para resolver problemas de optimización combinatoria con restricciones específicas. 

Esta clase utiliza tanto computación cuántica o simuladores cuánticos para generar individuos, y optimizar el espacio de soluciones mediante la evolución reproductiva de una población.

### 1.1. Atributos

- **bounds_dict**: Diccionario de parámetros a optimizar y sus valores. Ejemplo: `{'learning_rate': (0.0001, 0.1)}`
- **num_generations**: Número de generaciones a ejecutar.
- **num_individuals**: Número de individuos a generar.
- **max_qubits**: Número máximo de qubits a emplear para reproducir individuos.
- **objective_function**: Función objetivo que se utiliza para puntuar a cada individuo. Debe retornar un valor numérico (float).
- **metric_to_optimize**: Métrica a optimizar (e.g., 'accuracy', 'recall', 'f1', 'mae').
- **problem_restrictions**: Restricciones aplicadas al problema ('bound_restricted' o 'totally_restricted').
- **return_to_origin**: Indica si el problema debe terminar en el origen (solo para problemas 'totally_restricted').
- **problem_type**: Tipo de optimización ('minimize' o 'maximize').
- **tournament_method**: Método de selección de individuos para reproducción (e.g., 'ea_simple').
- **podium_size**: Tamaño del grupo de individuos que competirá en el torneo de selección de los mejores padres.
- **mutate_probability**: Probabilidad de mutación del individuo.
- **mutate_gen_probability**: Probabilidad de que mute un gen del individuo.
- **mutation_policy**: Política de mutación ('soft', 'normal', 'hard').
- **verbose**: Indica si se deben mostrar mensajes detallados sobre la evolución.
- **early_stopping_generations**: Número de generaciones para considerar el "early stopping".
- **variability_explossion_mode**: Modo de explosión de variabilidad ('crazy').
- **variability_round_decimals**: Número floatantes para redondear estadísticas de variabilidad.
- **randomness_quantum_technology**: Tecnología cuántica utilizada para los procesos random de generación de la población.
- **randomness_service**: Servicio de computación cuántica utilizado para los procesos random de generación de individuos.
- **optimization_quantum_technology**: Tecnología cuántica utilizada para la optimización de la población.
- **optimization_service**: Servicio de computación cuántica utilizado para la optimización.
- **qm_api_key**: Clave API para acceder a los ordenadores cuánticos.
- **qm_connection_service**: Servicio de conexión cuántica utilizado (e.g., 'ibm_quantum').
- **quantum_machine**: Nombre de la máquina cuántica a utilizar (e.g., 'ibm_brisbane').
- **element_matrix**: Matriz de distancias para problemas combinatorios (e.g., TSP).

### 1.2. Métodos

#### 1.2.2. `__init__()`
Este es el constructor de la clase `GenethicOptimizer`. 

Se encarga de inicializar todos los parámetros del algoritmo, como el tipo