Files
genetic-algorithms/lab4/gp/crossovers.py
2025-11-04 15:02:02 +03:00

193 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import random
from typing import Optional
from .chromosome import Chromosome
from .operation import Operation
from .terminal import Terminal
def crossover_subtree(
p1: Chromosome, p2: Chromosome, max_depth: int | None = None
) -> tuple[Chromosome, Chromosome]:
"""
Кроссовер поддеревьев: выбираются случайные узлы в каждом родителе,
затем соответствующие поддеревья обмениваются местами. Возвращаются два новых потомка.
Аргументы:
p1 : первый родитель (не изменяется).
p2 : второй родитель (не изменяется).
max_depth : максимальная допустимая глубина потомков. Если None, ограничение не применяется.
Примечания:
- Для «совместимости» узлов сперва пытаемся подобрать пары одного класса
(оба Terminal или оба Operation). Если подходящей пары не нашлось за
разумное число попыток — допускаем любой обмен.
- Если задан max_depth, проверяется глубина результирующих потомков,
и при превышении лимита выбор узлов повторяется.
- Обмен выполняется на КЛОНАХ родителей, чтобы не портить входные деревья.
"""
# -------- Вспомогательные функции --------
def enumerate_nodes_with_meta(
root: Chromosome.Node,
) -> list[
tuple[Chromosome.Node, Optional[Chromosome.Node], Optional[int], list[int], int]
]:
"""
Возвращает список кортежей: (node, parent, index_in_parent, path_from_root, depth_from_root)
path_from_root — список индексов детей от корня до узла.
depth_from_root: 1 для корня, 2 для детей корня и т.д.
"""
out: list[
tuple[
Chromosome.Node,
Optional[Chromosome.Node],
Optional[int],
list[int],
int,
]
] = []
def dfs(
node: Chromosome.Node,
parent: Optional[Chromosome.Node],
idx: Optional[int],
path: list[int],
depth: int,
) -> None:
out.append((node, parent, idx, path, depth))
for i, ch in enumerate(node.children):
dfs(ch, node, i, path + [i], depth + 1)
dfs(root, None, None, [], 1)
return out
def get_parent_and_index_by_path(
root: Chromosome.Node, path: list[int]
) -> tuple[Optional[Chromosome.Node], Optional[int], Chromosome.Node]:
"""
По пути возвращает (parent, index_in_parent, node).
Для корня parent/index равны None.
"""
if not path:
return None, None, root
parent = root
for i in path[:-1]:
parent = parent.children[i]
idx = path[-1]
return parent, idx, parent.children[idx]
def is_op(node: Chromosome.Node) -> bool:
return isinstance(node.value, Operation)
def is_term(node: Chromosome.Node) -> bool:
return isinstance(node.value, Terminal)
def check_depth_after_swap(
node_depth: int, new_subtree: Chromosome.Node, max_d: int
) -> bool:
"""
Проверяет, не превысит ли глубина дерева max_d после замены узла на глубине node_depth
на поддерево new_subtree.
node_depth: глубина узла, который заменяем (1 для корня)
new_subtree: поддерево, которое вставляем
max_d: максимальная допустимая глубина
Возвращает True, если глубина будет в пределах нормы.
"""
# Глубина нового поддерева
subtree_depth = new_subtree.get_depth()
# Итоговая глубина = глубина узла + глубина поддерева - 1
# (т.к. node_depth уже включает этот узел)
resulting_depth = node_depth + subtree_depth - 1
return resulting_depth <= max_d
# -------- Выбор доминантного/рецессивного родителя (просто случайно) --------
dom, rec = (p1, p2) if random.random() < 0.5 else (p2, p1)
# Собираем все узлы с метаданными
dom_nodes = enumerate_nodes_with_meta(dom.root)
rec_nodes = enumerate_nodes_with_meta(rec.root)
# Пытаемся выбрать совместимые узлы: оба термины ИЛИ оба операции.
# Дадим несколько попыток, затем, если не повезло — возьмём любые.
MAX_TRIES = 64
chosen_dom = None
chosen_rec = None
for _ in range(MAX_TRIES):
nd = random.choice(dom_nodes)
# Предпочтём узел того же «класса»
if is_term(nd[0]):
same_type_pool = [nr for nr in rec_nodes if is_term(nr[0])]
elif is_op(nd[0]):
same_type_pool = [nr for nr in rec_nodes if is_op(nr[0])]
else:
same_type_pool = rec_nodes # на всякий
if same_type_pool:
nr = random.choice(same_type_pool)
# Если задан max_depth, проверяем, что обмен не приведёт к превышению глубины
if max_depth is not None:
nd_node, _, _, nd_path, nd_depth = nd
nr_node, _, _, nr_path, nr_depth = nr
# Проверяем обе возможные замены
dom_ok = check_depth_after_swap(nd_depth, nr_node, max_depth)
rec_ok = check_depth_after_swap(nr_depth, nd_node, max_depth)
if dom_ok and rec_ok:
chosen_dom, chosen_rec = nd, nr
break
# Иначе пробуем другую пару
else:
# Если ограничения нет, принимаем первую подходящую пару
chosen_dom, chosen_rec = nd, nr
break
# Если подобрать подходящую пару не удалось
if chosen_dom is None or chosen_rec is None:
# Возвращаем клоны родителей без изменений
return (p1.clone(), p2.clone())
_, _, _, dom_path, _ = chosen_dom
_, _, _, rec_path, _ = chosen_rec
# -------- Создаём клоны родителей --------
c_dom = dom.clone()
c_rec = rec.clone()
# Выцепляем соответствующие позиции на клонах по тем же путям
c_dom_parent, c_dom_idx, c_dom_node = get_parent_and_index_by_path(
c_dom.root, dom_path
)
c_rec_parent, c_rec_idx, c_rec_node = get_parent_and_index_by_path(
c_rec.root, rec_path
)
# Клонируем поддеревья, чтобы не смешивать ссылки между хромосомами
subtree_dom = c_dom_node.clone()
subtree_rec = c_rec_node.clone()
# Меняем местами
if c_dom_parent is None:
# Меняем корень
c_dom.root = subtree_rec
else:
c_dom_parent.children[c_dom_idx] = subtree_rec # type: ignore[index]
if c_rec_parent is None:
c_rec.root = subtree_dom
else:
c_rec_parent.children[c_rec_idx] = subtree_dom # type: ignore[index]
# Возвращаем потомков в том же порядке, что и вход (p1 -> first, p2 -> second)
if dom is p1:
return (c_dom, c_rec)
else:
return (c_rec, c_dom)