save
This commit is contained in:
@@ -22,11 +22,20 @@ class Chromosome:
|
|||||||
"""Вычисляет глубину дерева. Дерево из одного только корня имеет глубину 1."""
|
"""Вычисляет глубину дерева. Дерево из одного только корня имеет глубину 1."""
|
||||||
return self.root.get_depth() if self.root is not None else 0
|
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:
|
def eval(self, values: list[float]) -> float:
|
||||||
"""Вычисляет значение хромосомы для заданных значений терминалов."""
|
"""Вычисляет значение хромосомы для заданных значений терминалов."""
|
||||||
if self.root is None:
|
if self.root is None:
|
||||||
raise ValueError("Chromosome is not initialized")
|
raise ValueError("Chromosome is not initialized")
|
||||||
|
|
||||||
|
# Мне это не нравится, но, возможно, это будет работать
|
||||||
for terminal, value in zip(self.terminals, values):
|
for terminal, value in zip(self.terminals, values):
|
||||||
terminal._value = value
|
terminal._value = value
|
||||||
|
|
||||||
@@ -65,6 +74,12 @@ class Chromosome:
|
|||||||
self.value = value
|
self.value = value
|
||||||
self.children = children
|
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:
|
def get_depth(self) -> int:
|
||||||
"""Вычисляет глубину поддерева."""
|
"""Вычисляет глубину поддерева."""
|
||||||
return (
|
return (
|
||||||
|
|||||||
192
lab4/gp/crossovers.py
Normal file
192
lab4/gp/crossovers.py
Normal 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
143
lab4/gp/mutations.py
Normal 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()
|
||||||
@@ -41,7 +41,7 @@ def safe_pow(a, b):
|
|||||||
return float("inf")
|
return float("inf")
|
||||||
try:
|
try:
|
||||||
return a**b
|
return a**b
|
||||||
except OverflowError, ValueError:
|
except (OverflowError, ValueError):
|
||||||
return float("inf")
|
return float("inf")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
lab4/tests/__init__.py
Normal file
2
lab4/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Tests for gp library
|
||||||
|
|
||||||
304
lab4/tests/test_chromosome.py
Normal file
304
lab4/tests/test_chromosome.py
Normal 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])
|
||||||
Reference in New Issue
Block a user