another save

This commit is contained in:
2025-11-05 20:07:35 +03:00
parent 8e8e0abd0d
commit 26bd6da1b4
16 changed files with 328 additions and 512 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
!**/
!*.gitignore
!*.py
!lab4/*

1
lab4/.python-version Normal file
View File

@@ -0,0 +1 @@
3.14

View File

@@ -1,5 +1,3 @@
from .chromosome import Chromosome
from .operation import Operation
from .terminal import Terminal
__all__ = ["Chromosome", "Operation", "Terminal"]
__all__ = ["Chromosome"]

View File

@@ -1,119 +1,64 @@
import random
from typing import Callable, Sequence
from typing import Sequence
from .operation import Operation
from .terminal import Terminal
type InitFunc = Callable[["Chromosome"], "Chromosome.Node"]
from .node import Node
from .primitive import Primitive
class Chromosome:
def __init__(
self,
operations: Sequence[Operation],
terminals: Sequence[Terminal],
init_func: InitFunc,
terminals: Sequence[Primitive],
operations: Sequence[Primitive],
root: Node,
):
self.operations = operations
self.terminals = terminals
self.root = init_func(self)
self.operations = operations
self.root = root
def get_depth(self) -> int:
"""Вычисляет глубину дерева. Дерево из одного только корня имеет глубину 1."""
return self.root.get_depth() if self.root is not None else 0
def copy(self) -> Chromosome:
return Chromosome(self.terminals, self.operations, self.root.copy_subtree())
def clone(self) -> "Chromosome":
"""Создает копию хромосомы."""
return Chromosome(
self.operations,
self.terminals,
lambda _: self.root.clone(),
)
def prune(self, max_depth: int) -> None:
self.root.prune(self.terminals, max_depth)
def eval(self, values: list[float]) -> float:
"""Вычисляет значение хромосомы для заданных значений терминалов."""
if self.root is None:
raise ValueError("Chromosome is not initialized")
def shrink_mutation(self) -> None:
"""Усекающая мутация. Заменяет случайно выбранную операцию на случайный терминал."""
operation_nodes = [n for n in self.root.list_nodes() if n.value.arity > 0]
# Мне это не нравится, но, возможно, это будет работать
for terminal, value in zip(self.terminals, values):
terminal._value = value
if not operation_nodes:
return
return self.root._eval()
target_node = random.choice(operation_nodes)
target_node.prune(self.terminals, max_depth=1)
def grow_mutation(self, max_depth: int) -> None:
"""Растущая мутация. Заменяет случайно выбранный узел на случайное поддерево."""
target_node = random.choice(self.root.list_nodes())
max_subtree_depth = max_depth - target_node.get_level() + 1
subtree = Chromosome.grow_init(
self.terminals, self.operations, max_subtree_depth
).root
if target_node.parent:
target_node.parent.replace_child(target_node, subtree)
else:
self.root = subtree
def __str__(self) -> str:
"""Строковое представление хромосомы в виде формулы в инфиксной форме."""
return str(self.root)
def _tree_lines(
self, node: "Chromosome.Node", prefix: str = "", is_last: bool = True
) -> list[str]:
connector = "└── " if is_last else "├── "
lines = [prefix + connector + node.value.name]
child_prefix = prefix + (" " if is_last else "")
for i, child in enumerate(node.children):
last = i == len(node.children) - 1
lines.extend(self._tree_lines(child, child_prefix, last))
return lines
def str_tree(self) -> str:
"""Строковое представление древовидной структуры формулы."""
if self.root is None:
return ""
lines = [self.root.value.name]
for i, child in enumerate(self.root.children):
last = i == len(self.root.children) - 1
lines.extend(self._tree_lines(child, "", last))
return "\n".join(lines)
class Node:
def __init__(
self, value: Operation | Terminal, children: list["Chromosome.Node"]
):
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 (
max(child.get_depth() for child in self.children) + 1
if self.children
else 1
)
def __str__(self) -> str:
"""Рекурсивный перевод древовидного вида формулы в строку в инфиксной форме."""
if isinstance(self.value, Terminal):
return self.value.name
if self.value.arity == 2:
return f"({self.children[0]} {self.value.name} {self.children[1]})"
return (
f"{self.value.name}({', '.join(str(child) for child in self.children)})"
)
def _eval(self) -> float:
"""Рекурсивно вычисляет значение поддерева. Значения терминалов должны быть
заданы предварительно."""
if isinstance(self.value, Terminal):
return self.value._value # type: ignore
return self.value._eval([child._eval() for child in self.children])
def _random_terminal(terminals: Sequence[Terminal]) -> Terminal:
return random.choice(terminals)
def init_full(chromosome: Chromosome, max_depth: int) -> Chromosome.Node:
@classmethod
def full_init(
cls,
terminals: Sequence[Primitive],
operations: Sequence[Primitive],
max_depth: int,
) -> Chromosome:
"""Полная инициализация.
В полном методе при генерации дерева, пока не достигнута максимальная глубина,
@@ -121,22 +66,29 @@ def init_full(chromosome: Chromosome, max_depth: int) -> Chromosome.Node:
(максимальной глубины) выбираются только терминальные символы.
"""
def build(level: int) -> Chromosome.Node:
def build(level: int) -> Node:
# Если достигнута максимальная глубина — выбираем терминал
if level == max_depth:
return Chromosome.Node(_random_terminal(chromosome.terminals), [])
return Node(random.choice(terminals))
# Иначе выбираем операцию и создаём потомков
op = random.choice(chromosome.operations)
node = Chromosome.Node(op, [build(level + 1) for _ in range(op.arity)])
op = random.choice(operations)
node = Node(op)
for _ in range(op.arity):
node.add_child(build(level + 1))
return node
return build(1)
return cls(terminals, operations, build(1))
def init_grow(
chromosome: Chromosome, max_depth: int, terminal_probability: float = 0.5
) -> Chromosome.Node:
@classmethod
def grow_init(
cls,
terminals: Sequence[Primitive],
operations: Sequence[Primitive],
max_depth: int,
# min_depth: int, # ???
terminal_probability: float = 0.5,
) -> Chromosome:
"""Растущая инициализация.
В растущей инициализации генерируются нерегулярные деревья с различной глубиной
@@ -145,15 +97,17 @@ def init_grow(
прекращается по текущей ветви и поэтому дерево имеет нерегулярную структуру.
"""
def build(level: int) -> Chromosome.Node:
def build(level: int) -> Node:
# Если достигнута максимальная глубина, либо сыграла заданная вероятность
# — выбираем терминал
if level == max_depth or random.random() < terminal_probability:
return Chromosome.Node(_random_terminal(chromosome.terminals), [])
return Node(random.choice(terminals))
# Иначе выбираем случайную операцию и создаём потомков
op = random.choice(chromosome.operations)
children = [build(level + 1) for _ in range(op.arity)]
return Chromosome.Node(op, children)
op = random.choice(operations)
node = Node(op)
for _ in range(op.arity):
node.add_child(build(level + 1))
return node
return build(1)
return cls(terminals, operations, build(1))

View File

@@ -1,192 +1,28 @@
import random
from typing import Optional
from .chromosome import Chromosome
from .operation import Operation
from .terminal import Terminal
from .node import swap_subtrees
def crossover_subtree(
p1: Chromosome, p2: Chromosome, max_depth: int | None = None
parent1: Chromosome, parent2: Chromosome, max_depth: int
) -> tuple[Chromosome, Chromosome]:
"""Кроссовер поддеревьев.
Выбираются случайные узлы в каждом родителе, затем соответствующие им поддеревья
меняются местами. Если глубина результирующих хромосом превышает max_depth,
то их деревья обрезаются до max_depth.
"""
Кроссовер поддеревьев: выбираются случайные узлы в каждом родителе,
затем соответствующие поддеревья обмениваются местами. Возвращаются два новых потомка.
child1 = parent1.copy()
child2 = parent2.copy()
Аргументы:
p1 : первый родитель (не изменяется).
p2 : второй родитель (не изменяется).
max_depth : максимальная допустимая глубина потомков. Если None, ограничение не применяется.
# Выбираем случайные узлы, не включая корень
cut1 = random.choice(child1.root.list_nodes()[1:])
cut2 = random.choice(child2.root.list_nodes()[1:])
Примечания:
- Для «совместимости» узлов сперва пытаемся подобрать пары одного класса
(оба Terminal или оба Operation). Если подходящей пары не нашлось за
разумное число попыток — допускаем любой обмен.
- Если задан max_depth, проверяется глубина результирующих потомков,
и при превышении лимита выбор узлов повторяется.
- Обмен выполняется на КЛОНАХ родителей, чтобы не портить входные деревья.
"""
swap_subtrees(cut1, cut2)
# -------- Вспомогательные функции --------
child1.prune(max_depth)
child2.prune(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)
return child1, child2

View File

@@ -1,143 +1,3 @@
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()

113
lab4/gp/node.py Normal file
View File

@@ -0,0 +1,113 @@
import random
from typing import Sequence
from .primitive import Primitive
from .types import Context, Value
class Node:
def __init__(self, value: Primitive):
self.value = value
self.parent: Node | None = None
self.children: list[Node] = []
def add_child(self, child: Node) -> None:
self.children.append(child)
child.parent = self
def remove_child(self, child: Node) -> None:
self.children.remove(child)
child.parent = None
def replace_child(self, old_child: Node, new_child: Node) -> None:
self.children[self.children.index(old_child)] = new_child
old_child.parent = None
new_child.parent = self
def remove_children(self) -> None:
for child in self.children:
child.parent = None
self.children = []
def copy_subtree(self) -> Node:
node = Node(self.value)
for child in self.children:
node.add_child(child.copy_subtree())
return node
def list_nodes(self) -> list[Node]:
nodes: list[Node] = [self]
for child in self.children:
nodes.extend(child.list_nodes())
return nodes
def prune(self, terminals: Sequence[Primitive], max_depth: int) -> None:
"""Усечение поддерева до заданной глубины.
Заменяет операции на глубине max_depth на случайные терминалы.
"""
def prune_recursive(node: Node, current_depth: int) -> None:
if node.value.arity == 0: # Терминалы остаются без изменений
return
if current_depth >= max_depth:
node.remove_children()
node.value = random.choice(terminals)
return
for child in node.children:
prune_recursive(child, current_depth + 1)
prune_recursive(self, 1)
def get_subtree_depth(self) -> int:
"""Вычисляет глубину поддерева, начиная с текущего узла."""
return (
max(child.get_subtree_depth() for child in self.children) + 1
if self.children
else 1
)
def get_level(self) -> int:
"""Вычисляет уровень узла в дереве (расстояние от корня). Корень имеет уровень 1."""
return self.parent.get_level() + 1 if self.parent else 1
def eval(self, context: Context) -> Value:
return self.value.eval(
[child.eval(context) for child in self.children], context
)
def __str__(self) -> str:
"""Рекурсивный перевод древовидного вида формулы в строку в инфиксной форме."""
if self.value.arity == 0:
return self.value.name
if self.value.arity == 2:
return f"({self.children[0]} {self.value.name} {self.children[1]})"
return f"{self.value.name}({', '.join(str(child) for child in self.children)})"
def to_str_tree(self, prefix="", is_last: bool = True) -> str:
"""Строковое представление древовидной структуры."""
lines = prefix + ("└── " if is_last else "├── ") + self.value.name + "\n"
child_prefix = prefix + (" " if is_last else "")
for i, child in enumerate(self.children):
is_child_last = i == len(self.children) - 1
lines += child.to_str_tree(child_prefix, is_child_last)
return lines
def swap_subtrees(a: Node, b: Node) -> None:
if a.parent is None or b.parent is None:
raise ValueError("Нельзя обменять корни деревьев")
# Сохраняем ссылки на родителей
a_parent = a.parent
b_parent = b.parent
i = a_parent.children.index(a)
j = b_parent.children.index(b)
a_parent.children[i], b_parent.children[j] = b, a
a.parent, b.parent = b_parent, a_parent

View File

@@ -1,11 +0,0 @@
from typing import Callable
class Operation:
def __init__(self, name: str, arity: int, eval_fn: Callable[[list[float]], float]):
self.name = name
self.arity = arity
self.eval_fn = eval_fn
def _eval(self, args: list[float]) -> float:
return self.eval_fn(args)

View File

@@ -1,6 +1,6 @@
import math
from .operation import Operation
from .primitive import Operation
# Унарные операции
NEG = Operation("-", 1, lambda x: -x[0])

View File

@@ -1,42 +1,30 @@
import random
from typing import Callable
from typing import Sequence
from .chromosome import Chromosome, InitFunc, init_full, init_grow
from .chromosome import Chromosome
from .primitive import Primitive
type Population = list[Chromosome]
def ramped_initialization(
population_size: int,
chromosomes_per_variation: int,
depths: list[int],
make_chromosome: Callable[[InitFunc], Chromosome],
terminals: Sequence[Primitive],
operations: Sequence[Primitive],
) -> Population:
"""Комбинация методов grow и full инициализации хромосом для инициализации начальной
популяции.
Начальная популяция генерируется так, чтобы в нее входили деревья с разной
максимальной длиной примерно поровну. Для каждой глубины первая половина деревьев
генерируется полным методом, а вторая растущей инициализацией.
"""
population: Population = []
per_depth = population_size / len(depths)
for depth in depths:
n_full = int(per_depth / 2)
n_grow = int(per_depth / 2)
population.extend(
make_chromosome(lambda c: init_full(c, depth)) for _ in range(n_full)
Chromosome.full_init(terminals, operations, depth)
for _ in range(chromosomes_per_variation)
)
population.extend(
make_chromosome(lambda c: init_grow(c, depth)) for _ in range(n_grow)
Chromosome.grow_init(terminals, operations, depth)
for _ in range(chromosomes_per_variation)
)
# Из-за округления хромосом может оказаться меньше заданного количества,
# поэтому дозаполняем остаток популяции случайными хромосомами
while len(population) < population_size:
depth = random.choice(depths)
init_func = init_full if random.random() < 0.5 else init_grow
population.append(make_chromosome(lambda c: init_func(c, depth)))
return population

35
lab4/gp/primitive.py Normal file
View File

@@ -0,0 +1,35 @@
from dataclasses import dataclass
from typing import Callable, Sequence
from .types import Context, Value
type OperationFn = Callable[[Sequence[Value]], Value]
@dataclass(frozen=True)
class Primitive:
name: str
arity: int
operation_fn: OperationFn | None
def eval(self, args: Sequence[Value], context: Context) -> Value:
if self.operation_fn is None:
return context[self]
return self.operation_fn(args)
def __post_init__(self) -> None:
if self.arity != 0 and self.operation_fn is None:
raise ValueError("Operation is required for primitive with non-zero arity")
def Var(name: str) -> Primitive:
return Primitive(name=name, arity=0, operation_fn=None)
def Const(name: str, val: Value) -> Primitive:
return Primitive(name=name, arity=0, operation_fn=lambda _args: val)
def Operation(name: str, arity: int, operation_fn: OperationFn) -> Primitive:
return Primitive(name=name, arity=arity, operation_fn=operation_fn)

View File

@@ -1,7 +0,0 @@
from dataclasses import dataclass
@dataclass()
class Terminal:
name: str
_value: float | None = None

13
lab4/gp/types.py Normal file
View File

@@ -0,0 +1,13 @@
from typing import TYPE_CHECKING, Callable, Protocol
if TYPE_CHECKING:
from .chromosome import Chromosome
from .node import Node
from .primitive import Primitive
type InitFunc = Callable[["Chromosome"], "Node"]
type Value = float
class Context(Protocol):
def __getitem__(self, key: "Primitive", /) -> Value: ...

View File

@@ -1,24 +1,25 @@
from gp import Chromosome, Terminal, ops
from gp.chromosome import init_grow
from gp import Chromosome, ops
from gp.population import ramped_initialization
from gp.primitive import Var
operations = ops.ALL
terminals = [Var(f"x{i}") for i in range(1, 9)]
terminals = [Terminal(f"x{i}") for i in range(1, 9)]
chrom = Chromosome(operations, terminals, init_func=lambda c: init_grow(c, 8))
print("Depth:", chrom.get_depth())
chrom = Chromosome.full_init(terminals, operations, max_depth=3)
print("Depth:", chrom.root.get_subtree_depth())
print("Formula:", chrom)
print("Tree:\n", chrom.str_tree())
print("Tree:\n", chrom.root.to_str_tree())
values = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
print("Value for ", values, ":", chrom.eval(values))
context = {var: value for var, value in zip(terminals, values)}
print("Value for ", values, ":", chrom.root.eval(context))
population = ramped_initialization(
100,
[3, 4, 5, 6, 7, 8],
lambda init_func: Chromosome(operations, terminals, init_func),
)
print("Population size:", len(population))
print("Population:")
[print(str(chrom)) for chrom in population]
# population = ramped_initialization(
# 5,
# [3, 4, 5, 6, 7, 8],
# terminals,
# operations,
# )
# print("Population size:", len(population))
# print("Population:")
# [print(str(chrom)) for chrom in population]

8
lab4/pyproject.toml Normal file
View File

@@ -0,0 +1,8 @@
[project]
name = "lab4"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = []
[tool.ruff]
target-version = "py314"

26
lab4/pytest.ini Normal file
View File

@@ -0,0 +1,26 @@
[tool:pytest]
# Пути для поиска тестов
testpaths = tests
# Паттерны для имён файлов с тестами
python_files = test_*.py
# Паттерны для имён классов с тестами
python_classes = Test*
# Паттерны для имён функций-тестов
python_functions = test_*
# Опции для более подробного вывода
addopts =
-v
--strict-markers
--tb=short
--disable-warnings
# Маркеры для категоризации тестов
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
unit: unit tests
integration: integration tests