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

View File

@@ -22,11 +22,20 @@ class Chromosome:
"""Вычисляет глубину дерева. Дерево из одного только корня имеет глубину 1."""
return self.root.get_depth() if self.root is not None else 0
def clone(self) -> "Chromosome":
"""Создает копию хромосомы."""
return Chromosome(
self.operations,
self.terminals,
lambda _: self.root.clone(),
)
def eval(self, values: list[float]) -> float:
"""Вычисляет значение хромосомы для заданных значений терминалов."""
if self.root is None:
raise ValueError("Chromosome is not initialized")
# Мне это не нравится, но, возможно, это будет работать
for terminal, value in zip(self.terminals, values):
terminal._value = value
@@ -65,6 +74,12 @@ class Chromosome:
self.value = value
self.children = children
def clone(self) -> "Chromosome.Node":
"""Создает копию поддерева."""
return Chromosome.Node(
self.value, [child.clone() for child in self.children]
)
def get_depth(self) -> int:
"""Вычисляет глубину поддерева."""
return (

192
lab4/gp/crossovers.py Normal file
View File

@@ -0,0 +1,192 @@
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)

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()

View File

@@ -41,7 +41,7 @@ def safe_pow(a, b):
return float("inf")
try:
return a**b
except OverflowError, ValueError:
except (OverflowError, ValueError):
return float("inf")

2
lab4/tests/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# Tests for gp library

View File

@@ -0,0 +1,304 @@
import math
import sys
from pathlib import Path
import pytest
# Добавляем путь к родительской директории для корректных импортов
sys.path.insert(0, str(Path(__file__).parent.parent))
from gp.chromosome import Chromosome
from gp.operation import Operation
from gp.ops import ADD, COS, DIV, EXP, MUL, NEG, SIN, SUB
from gp.terminal import Terminal
class TestChromosomeEval:
"""Тесты для метода eval класса Chromosome."""
def test_eval_single_terminal(self):
"""Тест вычисления хромосомы с одним терминалом."""
x = Terminal("x")
def init_single_terminal(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(x, [])
chromosome = Chromosome([ADD], [x], init_single_terminal)
result = chromosome.eval([5.0])
assert result == 5.0
result = chromosome.eval([-3.5])
assert result == -3.5
def test_eval_addition(self):
"""Тест вычисления простого сложения: x + y."""
x = Terminal("x")
y = Terminal("y")
def init_addition(chr: Chromosome) -> Chromosome.Node:
# Создаём дерево: x + y
return Chromosome.Node(
ADD,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
)
chromosome = Chromosome([ADD], [x, y], init_addition)
result = chromosome.eval([3.0, 4.0])
assert result == 7.0
result = chromosome.eval([10.0, -5.0])
assert result == 5.0
def test_eval_subtraction(self):
"""Тест вычисления вычитания: x - y."""
x = Terminal("x")
y = Terminal("y")
def init_subtraction(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(
SUB,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
)
chromosome = Chromosome([SUB], [x, y], init_subtraction)
result = chromosome.eval([10.0, 3.0])
assert result == 7.0
result = chromosome.eval([5.0, 8.0])
assert result == -3.0
def test_eval_multiplication(self):
"""Тест вычисления умножения: x * y."""
x = Terminal("x")
y = Terminal("y")
def init_multiplication(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(
MUL,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
)
chromosome = Chromosome([MUL], [x, y], init_multiplication)
result = chromosome.eval([3.0, 4.0])
assert result == 12.0
result = chromosome.eval([-2.0, 5.0])
assert result == -10.0
def test_eval_division(self):
"""Тест вычисления деления: x / y."""
x = Terminal("x")
y = Terminal("y")
def init_division(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(
DIV,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
)
chromosome = Chromosome([DIV], [x, y], init_division)
result = chromosome.eval([10.0, 2.0])
assert result == 5.0
result = chromosome.eval([7.0, 2.0])
assert result == 3.5
def test_eval_division_by_zero(self):
"""Тест деления на ноль (должно вернуть inf)."""
x = Terminal("x")
y = Terminal("y")
def init_division(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(
DIV,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
)
chromosome = Chromosome([DIV], [x, y], init_division)
result = chromosome.eval([10.0, 0.0])
assert result == float("inf")
def test_eval_unary_negation(self):
"""Тест вычисления унарного минуса: -x."""
x = Terminal("x")
def init_negation(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(NEG, [Chromosome.Node(x, [])])
chromosome = Chromosome([NEG], [x], init_negation)
result = chromosome.eval([5.0])
assert result == -5.0
result = chromosome.eval([-3.0])
assert result == 3.0
def test_eval_sin(self):
"""Тест вычисления синуса: sin(x)."""
x = Terminal("x")
def init_sin(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(SIN, [Chromosome.Node(x, [])])
chromosome = Chromosome([SIN], [x], init_sin)
result = chromosome.eval([0.0])
assert result == pytest.approx(0.0)
result = chromosome.eval([math.pi / 2])
assert result == pytest.approx(1.0)
result = chromosome.eval([math.pi])
assert result == pytest.approx(0.0, abs=1e-10)
def test_eval_cos(self):
"""Тест вычисления косинуса: cos(x)."""
x = Terminal("x")
def init_cos(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(COS, [Chromosome.Node(x, [])])
chromosome = Chromosome([COS], [x], init_cos)
result = chromosome.eval([0.0])
assert result == pytest.approx(1.0)
result = chromosome.eval([math.pi / 2])
assert result == pytest.approx(0.0, abs=1e-10)
result = chromosome.eval([math.pi])
assert result == pytest.approx(-1.0)
def test_eval_complex_expression(self):
"""Тест вычисления сложного выражения: (x + y) * (x - y)."""
x = Terminal("x")
y = Terminal("y")
def init_complex(chr: Chromosome) -> Chromosome.Node:
# (x + y) * (x - y)
return Chromosome.Node(
MUL,
[
Chromosome.Node(
ADD,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
),
Chromosome.Node(
SUB,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
),
],
)
chromosome = Chromosome([ADD, SUB, MUL], [x, y], init_complex)
# (5 + 3) * (5 - 3) = 8 * 2 = 16
result = chromosome.eval([5.0, 3.0])
assert result == 16.0
# (10 + 2) * (10 - 2) = 12 * 8 = 96
result = chromosome.eval([10.0, 2.0])
assert result == 96.0
def test_eval_nested_expression(self):
"""Тест вычисления вложенного выражения: sin(x + y)."""
x = Terminal("x")
y = Terminal("y")
def init_nested(chr: Chromosome) -> Chromosome.Node:
# sin(x + y)
return Chromosome.Node(
SIN,
[
Chromosome.Node(
ADD,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
)
],
)
chromosome = Chromosome([ADD, SIN], [x, y], init_nested)
result = chromosome.eval([math.pi / 4, math.pi / 4])
# sin(π/4 + π/4) = sin(π/2) = 1
assert result == pytest.approx(1.0)
def test_eval_exp(self):
"""Тест вычисления экспоненты: exp(x)."""
x = Terminal("x")
def init_exp(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(EXP, [Chromosome.Node(x, [])])
chromosome = Chromosome([EXP], [x], init_exp)
result = chromosome.eval([0.0])
assert result == pytest.approx(1.0)
result = chromosome.eval([1.0])
assert result == pytest.approx(math.e)
result = chromosome.eval([2.0])
assert result == pytest.approx(math.e**2)
def test_eval_multiple_calls(self):
"""Тест многократного вызова eval с разными значениями."""
x = Terminal("x")
y = Terminal("y")
def init_mul(chr: Chromosome) -> Chromosome.Node:
return Chromosome.Node(
MUL,
[
Chromosome.Node(x, []),
Chromosome.Node(y, []),
],
)
chromosome = Chromosome([MUL], [x, y], init_mul)
# Проверяем, что терминалы правильно обновляются
assert chromosome.eval([2.0, 3.0]) == 6.0
assert chromosome.eval([4.0, 5.0]) == 20.0
assert chromosome.eval([10.0, 0.5]) == 5.0
def test_eval_without_root_raises_error(self):
"""Тест, что eval вызывает ошибку, если root = None."""
def init_none(chr: Chromosome) -> Chromosome.Node:
return None # type: ignore
chromosome = Chromosome([ADD], [Terminal("x")], init_none)
with pytest.raises(ValueError, match="Chromosome is not initialized"):
chromosome.eval([1.0])