193 lines
8.2 KiB
Python
193 lines
8.2 KiB
Python
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)
|