import numpy as np
import pandas as pd

# Заданные параметры для функции
a, b, c, d = 14, 2, -26, 1

# Целевая функция
def target_function(x):
    return a + b * x + c * x**2 + d * x**3

# Диапазон x
x_min, x_max = -10, 53

# Длина битовой строки (хромосомы)
chromosome_length = 6
population_size = 6  # количество особей
num_generations = 50  # количество поколений
mutation_rate = 0.05  # вероятность мутации для каждого бита

# Методы отбора
SELECTION_ROULETTE = 'рулетка'
SELECTION_TOURNAMENT = 'турнир'
SELECTION_RANK = 'ранговый'

def generate_random_x_values(size, x_min=-10, x_max=53):
    # Генерируем случайные числа в диапазоне [0, 63]
    random_values = np.random.randint(0, 2**chromosome_length, size=size)
    # Масштабируем их в нужный диапазон
    scaled_values = x_min + (random_values * (x_max - x_min) / (2**chromosome_length - 1))
    return np.round(scaled_values).astype(int)

def generate_random_selection_numbers(size):
    return np.random.randint(0, 101, size=size)

def generate_balanced_groups(size):
    # Создаем список групп, где каждая группа встречается ровно два раза
    groups = []
    num_pairs = size // 2
    for i in range(1, num_pairs + 1):
        groups.extend([i, i])
    # Если размер популяции нечетный, добавляем группу
    if size % 2 != 0:
        groups.append(num_pairs + 1)
    np.random.shuffle(groups)
    return groups

# Методы отбора
def roulette_wheel_selection(population, fitness):
    total_fitness = fitness.sum()
    probabilities = fitness / total_fitness
    selected_indices = np.random.choice(len(population), size=len(population), p=probabilities)
    return selected_indices

def tournament_selection(population, fitness, tournament_size=3):
    selected = []
    for _ in range(len(population)):
        participants = np.random.choice(len(population), tournament_size, replace=False)
        winner = participants[np.argmax(fitness[participants])]
        selected.append(winner)
    return selected

def rank_selection(population, fitness):
    sorted_indices = np.argsort(-fitness)  # Сортируем по убыванию
    ranks = np.empty_like(sorted_indices)
    ranks[sorted_indices] = np.arange(1, len(population)+1)
    probabilities = ranks / ranks.sum()
    selected_indices = np.random.choice(len(population), size=len(population), p=probabilities)
    return selected_indices

def elitism(population, fitness, elite_size):
    elite_indices = np.argsort(-fitness)[:elite_size]
    return elite_indices

def mutate(chromosome, mutation_rate=0.05):
    chromosome_bin = list(f"{chromosome:06b}")
    for i in range(len(chromosome_bin)):
        if np.random.rand() < mutation_rate:
            chromosome_bin[i] = '1' if chromosome_bin[i] == '0' else '0'
    mutated = int("".join(chromosome_bin), 2)
    # Масштабируем обратно в диапазон x
    x = x_min + (mutated * (x_max - x_min) / (2**chromosome_length - 1))
    return np.round(x).astype(int)

# Функция скрещивания
def crossover(parent1, parent2):
    parent1_bin = f"{parent1:06b}"
    parent2_bin = f"{parent2:06b}"

    # Скрещивание по описанному правилу (точечный кроссовер в середине)
    crossover_point = chromosome_length // 2
    offspring1_bin = parent1_bin[:crossover_point] + parent2_bin[crossover_point:]
    offspring2_bin = parent2_bin[:crossover_point] + parent1_bin[crossover_point:]

    # Преобразуем обратно в десятичное представление
    offspring1 = int(offspring1_bin, 2)
    offspring2 = int(offspring2_bin, 2)

    # Масштабируем обратно в диапазон x
    offspring1 = x_min + (offspring1 * (x_max - x_min) / (2**chromosome_length - 1))
    offspring2 = x_min + (offspring2 * (x_max - x_min) / (2**chromosome_length - 1))

    # Округляем до ближайшего целого
    offspring1 = np.round(offspring1).astype(int)
    offspring2 = np.round(offspring2).astype(int)

    return offspring1, offspring2

# Пользовательский выбор метода отбора
def choose_selection_method():
    print("Выберите метод отбора:")
    print("1. Рулеточный отбор (Roulette Wheel Selection)")
    print("2. Турнирный отбор (Tournament Selection)")
    print("3. Ранговый отбор (Rank Selection)")
    choice = input("Введите номер метода отбора (1/2/3): ")
    if choice == '1':
        return SELECTION_ROULETTE
    elif choice == '2':
        return SELECTION_TOURNAMENT
    elif choice == '3':
        return SELECTION_RANK
    else:
        print("Неверный выбор. По умолчанию выбран рулеточный отбор.")
        return SELECTION_ROULETTE

# Настройки отбора
selection_method = choose_selection_method()
elite_size = 1  # количество элитных особей

# Инициализация популяции
USE_PREDETERMINED_VALUES = True

if USE_PREDETERMINED_VALUES:
    x_values = [36, 35, 29, 40, 30, 51]  # Предопределенные значения X
else:
    x_values = generate_random_x_values(population_size)

# Основной цикл генетического алгоритма
for generation in range(num_generations):
    # Создаем DataFrame для хранения данных текущей популяции
    table = pd.DataFrame({
        'Особь': x_values,
    })

    # Вычисление целевой функции для каждого x
    table['f(x)'] = table['Особь'].apply(target_function)

    # Обработка отрицательных значений f(x) путем сдвига
    min_fx = table['f(x)'].min()
    if min_fx < 0:
        table['f(x)'] = table['f(x)'] - min_fx + 1  # Сдвиг всех значений на положительное число

    # Вычисляем сумму значений функции для нормализации
    total_fx = table['f(x)'].sum()
    table['P_i'] = table['f(x)'] / total_fx  # Вероятность отбора
    table['S_i'] = table['P_i'] * 100  # В процентах

    # Вычисляем границы для рулеточного отбора
    boundaries = [0]
    for si in table['S_i']:
        boundaries.append(boundaries[-1] + si)
    table['Границы'] = [(boundaries[i], boundaries[i + 1]) for i in range(len(boundaries) - 1)]

    # Генерация случайных чисел для отбора
    random_numbers = generate_random_selection_numbers(population_size)
    table['Случайные числа'] = random_numbers

    # Определяем, какая особь соответствует каждой границе (только для рулетки)
    def get_selected_individual(rnd_number):
        for idx, (low, high) in enumerate(table['Границы']):
            if low <= rnd_number < high:
                return idx
        return len(table) - 1  # В случае погрешностей

    if selection_method == SELECTION_ROULETTE:
        selected_indices = table['Случайные числа'].apply(get_selected_individual).tolist()
    elif selection_method == SELECTION_TOURNAMENT:
        selected_indices = tournament_selection(x_values, table['f(x)'].values)
    elif selection_method == SELECTION_RANK:
        selected_indices = rank_selection(x_values, table['f(x)'].values)
    else:
        selected_indices = table['Случайные числа'].apply(get_selected_individual).tolist()

    # Применение элитизма: сохраняем лучших особей
    if elite_size > 0:
        elite_indices = elitism(population_size, table['f(x)'].values, elite_size)
        selected_indices_elite = table.iloc[elite_indices].index.tolist()
    else:
        elite_indices = []
        selected_indices_elite = []

    # Генерация новых групп для скрещивания
    groups = generate_balanced_groups(len(selected_indices))
    
    # Создаем DataFrame для отбора
    selected_table = pd.DataFrame({
        'Пул': selected_indices,
        'Группы': groups
    })

    # Определяем пары для скрещивания на основе групп
    pairing_indices = []
    for group, members in selected_table.groupby('Группы'):
        selected_inds = members['Пул'].tolist()
        # Пары обрабатываются попарно
        for i in range(0, len(selected_inds) - 1, 2):
            pairing_indices.append((selected_inds[i], selected_inds[i + 1]))
    
    # Добавляем элитных особей к потоку наследования
    offspring_list = []
    if elite_size > 0:
        offspring_list.extend([x_values[idx] for idx in elite_indices])

    # Создаем потомков через кроссовер
    for parent1_idx, parent2_idx in pairing_indices:
        parent1 = table.at[parent1_idx, 'Особь']
        parent2 = table.at[parent2_idx, 'Особь']
        off1, off2 = crossover(parent1, parent2)
        # Применяем мутацию
        off1 = mutate(off1, mutation_rate)
        off2 = mutate(off2, mutation_rate)
        offspring_list.extend([off1, off2])

    # Формируем новую популяцию
    # Если популяция превышает заданный размер, обрезаем
    x_values = offspring_list[:population_size - elite_size] + x_values[:elite_size]

    # Вывод информации о текущем поколении
    best_fx = table['f(x)'].max()
    best_x = table.loc[table['f(x)'].idxmax(), 'Особь']
    print(f"\nПоколение {generation + 1}:")
    print(table)
    print(f"Лучшее f(x): {best_fx} для x = {best_x}")

# Итоговая таблица после всех поколений
print("\nИтоговая таблица после всех поколений:")
print(table)

# Вычисляем максимальные и минимальные значения с помощью классической оптимизации
import sympy as sp

# Определение функции
x = sp.symbols('x')
func = a + b * x + c * x**2 + d * x**3

# Находим критические точки
f_prime = sp.diff(func, x)
critical_points = sp.solve(f_prime, x)
critical_points = [cp.evalf() for cp in critical_points if cp.is_real and x_min <= cp.evalf() <= x_max]

# Вычисляем значения функции в критических и граничных точках
points = critical_points + [x_min, x_max]
f_values = [target_function(float(pt)) for pt in points]
max_f = max(f_values)
min_f = min(f_values)
optimal_x_max = points[f_values.index(max_f)]
optimal_x_min = points[f_values.index(min_f)]

print("\nКлассические методы оптимизации:")
print(f"Максимальное значение f(x) = {max_f} при x = {optimal_x_max}")
print(f"Минимальное значение f(x) = {min_f} при x = {optimal_x_min}")

# Итоговые лучшие и худшие значения из генетического алгоритма
best_individual = table.loc[table['f(x)'].idxmax()]
worst_individual = table.loc[table['f(x)'].idxmin()]

print("\nРезультаты генетического алгоритма:")
print(f"Максимальное значение f(x) = {best_individual['f(x)']} для x = {best_individual['Особь']}")
print(f"Минимальное значение f(x) = {worst_individual['f(x)']} для x = {worst_individual['Особь']}")


Лабораторная работа №2
                         Оптимизация процессов с  использованием генетических алгоритмов 
1) Разработать программу, реализующую генетический алгоритм поиска максимального и минимального значений целевой функции f(x) = a + bx + cx2 + dx3 в интервале x = [-10, 53].
2) Варианты заданий на выполнение лабораторной работы.
№ варианта	     a     	     b     	     c     	     d     
9	14	2	-26	1
3) В отчете должен содержаться следующий материал:
- номер варианта;
- краткое описание задания;
- расчет максимальных и минимальных значений целевой функции (включая граничные точки), выполненный с помощью классической теории оптимизации;
- результаты расчета максимальных и минимальных значений целевой функции с помощью генетического алгоритма;
- текст программы;
- выводы.
 
4) Рекомендации по разработке программы:
- длина битовой строки (хромосомы, особи) - 6 бит;
- при определении значения целевой функции f(x) необходимо битовое представление особи преобразовать в десятичное и отнять от него 10 (например, особь - "011100", десятичное представление особи - "28", x = 28 - 10 = 18). В этом случае для любой особи из 6 бит, значение x будет лежать в интервале [-10, 53].


сделай так чтобы показывалось каждый поколение как рассчитываются потомки и почему сделай чтобы они рассчитывались правильно для каждого метода  а также сделай количество поколений 5 то есть 5 прогонов чтобы было 

вот код старый
import numpy as np
import pandas as pd

# Заданные параметры для функции
a, b, c, d = 14, 2, -26, 1

# Целевая функция
def target_function(x):
    return a + b * x + c * x**2 + d * x**3

# Диапазон x
x_min, x_max = -10, 53

# Длина битовой строки (хромосомы)
chromosome_length = 6
population_size = 6  # количество особей

# Функция для генерации случайных значений X в заданном диапазоне
def generate_random_x_values(size, x_min=-10, x_max=53):
    # Генерируем случайные числа в диапазоне [0, 63]
    random_values = np.random.randint(0, 2**chromosome_length, size=size)
    # Масштабируем их в нужный диапазон
    scaled_values = x_min + (random_values * (x_max - x_min) / (2**chromosome_length - 1))
    return np.round(scaled_values).astype(int)

# Функция для генерации случайных чисел для отбора
def generate_random_selection_numbers(size):
    return np.random.randint(0, 101, size=size)

# Функция для генерации сбалансированных групп
def generate_balanced_groups(size):
    # Создаем список групп, где каждая группа встречается ровно два раза
    groups = []
    for i in range(1, (size // 2) + 1):
        groups.extend([i, i])
    np.random.shuffle(groups)
    return groups

# Используем предопределенные значения (можно закомментировать для случайной генерации)
USE_PREDETERMINED_VALUES = False

if USE_PREDETERMINED_VALUES:
    x_values = [36, 35, 29, 40, 30, 51]  # Предопределенные значения X
    random_numbers = [91, 21, 12, 7, 24, 15]  # Предопределенные случайные числа
    group_ids = [1, 3, 2, 1, 2, 3]  # Предопределенные группы
else:
    np.random.seed()  # Инициализация генератора случайных чисел
    x_values = generate_random_x_values(population_size)
    random_numbers = generate_random_selection_numbers(population_size)
    group_ids = generate_balanced_groups(population_size)

# Создаем DataFrame для хранения данных
table = pd.DataFrame({
    'Особь': x_values,
})

# Вычисление целевой функции для каждого x
table['f(x)'] = table['Особь'].apply(target_function)

# Вычисляем сумму значений функции для нормализации
total_fx = table['f(x)'].sum()
table['P_i'] = table['f(x)'] / total_fx  # Вероятность отбора
table['S_i'] = table['P_i'] * 100  # В процентах

# Вычисляем границы для отбора
boundaries = [0]
for si in table['S_i']:
    boundaries.append(boundaries[-1] + si)
table['Границы'] = [(boundaries[i], boundaries[i + 1]) for i in range(len(boundaries) - 1)]

# Добавляем сгенерированные случайные числа
table['Случайные числа'] = random_numbers

# Определяем, какая особь соответствует каждой границе
def get_selected_individual(rnd_number):
    for idx, (low, high) in enumerate(table['Границы']):
        if low <= rnd_number < high:
            return idx
    return None

table['Пул'] = table['Случайные числа'].apply(get_selected_individual)

# Добавляем сгенерированные группы
table['Группы'] = group_ids

# Определяем пары для скрещивания на основе выбранных особей и групп
pairing_indices = []
for group, members in table.groupby('Группы'):
    selected_inds = members['Пул'].tolist()
    # Если группа имеет более одного члена, создаем пары
    for i in range(0, len(selected_inds) - 1, 2):
        pairing_indices.append((selected_inds[i], selected_inds[i + 1]))

# Вывод итоговой таблицы
print("Итоговая таблица:")
print(table)

# Функция скрещивания с выводом информации о процессе
def crossover(parent1, parent2):
    # Преобразование в бинарный формат
    parent1_bin = f"{parent1:06b}"
    parent2_bin = f"{parent2:06b}"

    # Скрещивание по описанному правилу
    offspring1_bin = parent1_bin[:3] + parent2_bin[3:]
    offspring2_bin = parent2_bin[:3] + parent1_bin[3:]

    # Преобразуем обратно в десятичное представление
    offspring1 = int(offspring1_bin, 2)
    offspring2 = int(offspring2_bin, 2)

    # Вывод информации о процессе скрещивания
    print(f"\nРодитель 1 (десятичный): {parent1}, (бинарный): {parent1_bin}")
    print(f"Родитель 2 (десятичный): {parent2}, (бинарный): {parent2_bin}")
    print(f"Потомок 1 (десятичный): {offspring1}, (бинарный): {offspring1_bin}")
    print(f"Потомок 2 (десятичный): {offspring2}, (бинарный): {offspring2_bin}")

    return offspring1, offspring2

# Создаем потомков и добавляем их в таблицу
offspring_list = []
for parent1_idx, parent2_idx in pairing_indices:
    parent1 = table.at[parent1_idx, 'Особь']
    parent2 = table.at[parent2_idx, 'Особь']
    off1, off2 = crossover(parent1, parent2)
    offspring_list.extend([off1, off2])

# Заполняем колонку потомков
table['Потомки'] = pd.Series(offspring_list)