This commit is contained in:
2025-11-04 15:02:02 +03:00
parent 83be98e923
commit 8e8e0abd0d
6 changed files with 657 additions and 1 deletions

143
lab4/gp/mutations.py Normal file
View File

@@ -0,0 +1,143 @@
import random
from typing import Optional
from .chromosome import Chromosome
from .operation import Operation
from .terminal import Terminal
def mutate_grow(parent: Chromosome, max_depth: int, max_tries: int = 64) -> Chromosome:
"""
Растущая мутация (subtree-growing mutation).
Аргументы:
parent : исходная хромосома (не изменяется).
max_depth : верхняя граница глубины мутанта.
max_tries : ограничение попыток подбора узла/поддерева.
Возвращает:
Новый экземпляр Chromosome (мутант).
"""
# ---------- Вспомогательные ----------
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)
depth_from_root: 1 для корня, 2 для детей корня и т.д.
"""
out: list[
tuple[
Chromosome.Node,
Optional[Chromosome.Node],
Optional[int],
list[int],
int,
]
] = []
def dfs(
n: Chromosome.Node,
p: Optional[Chromosome.Node],
idx: Optional[int],
path: list[int],
depth: int,
) -> None:
out.append((n, p, idx, path, depth))
for i, ch in enumerate(n.children):
dfs(ch, n, 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]:
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 build_depth_limited_subtree(
chromo: Chromosome, max_depth_limit: int, current_depth: int = 1
) -> Chromosome.Node:
"""
Строит случайное поддерево с ограничением по глубине.
current_depth: текущая глубина узла (1 для корня поддерева).
max_depth_limit: максимальная допустимая глубина поддерева.
"""
# Если достигли максимальной глубины — обязательно терминал
if current_depth >= max_depth_limit:
term = random.choice(chromo.terminals)
return Chromosome.Node(term, [])
# Иначе случайно выбираем между операцией и терминалом
# С большей вероятностью выбираем операцию, если глубина позволяет
if random.random() < 0.7: # 70% вероятность операции
op = random.choice(chromo.operations)
children = [
build_depth_limited_subtree(chromo, max_depth_limit, current_depth + 1)
for _ in range(op.arity)
]
return Chromosome.Node(op, children)
else:
term = random.choice(chromo.terminals)
return Chromosome.Node(term, [])
# ---------- Подготовка ----------
# Если в дереве только терминал — мутация невозможна (нужен нетерминал)
if isinstance(parent.root.value, Terminal):
return parent.clone()
# Работаем на клоне
child = parent.clone()
# Список нетерминальных узлов с путями и глубинами
nodes = enumerate_nodes_with_meta(child.root)
internal = [
(n, p, i, path, depth)
for (n, p, i, path, depth) in nodes
if isinstance(n.value, Operation)
]
if not internal:
# На всякий случай: если всё терминалы
return child
# ---------- Основной цикл подбора позиции ----------
for _ in range(max_tries):
node, _, _, path, node_depth = random.choice(internal)
# Вычисляем максимальную допустимую глубину для нового поддерева
# max_depth - node_depth + 1 (так как node_depth начинается с 1)
allowed_subtree_depth = max_depth - node_depth + 1
if allowed_subtree_depth < 1:
# Этот узел слишком глубоко — попробуем другой
continue
# Строим новое поддерево с ограничением по глубине
new_sub = build_depth_limited_subtree(child, allowed_subtree_depth)
# Вставляем его на место узла мутации
parent_node, idx, _ = get_parent_and_index_by_path(child.root, path)
if parent_node is None:
child.root = new_sub
else:
parent_node.children[idx] = new_sub # type: ignore[index]
# Проверяем, что не превысили максимальную глубину
if child.get_depth() <= max_depth:
return child
else:
# Откат: пересоздаём клон
child = parent.clone()
# Если не удалось подобрать подходящее место/поддерево — вернём немутированного клона
return parent.clone()