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

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@
!**/ !**/
!*.gitignore !*.gitignore
!*.py !*.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 .chromosome import Chromosome
from .operation import Operation
from .terminal import Terminal
__all__ = ["Chromosome", "Operation", "Terminal"] __all__ = ["Chromosome"]

View File

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

View File

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

View File

@@ -1,143 +1,3 @@
import random import random
from typing import Optional
from .chromosome import Chromosome 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 import math
from .operation import Operation from .primitive import Operation
# Унарные операции # Унарные операции
NEG = Operation("-", 1, lambda x: -x[0]) NEG = Operation("-", 1, lambda x: -x[0])

View File

@@ -1,42 +1,30 @@
import random from typing import Sequence
from typing import Callable
from .chromosome import Chromosome, InitFunc, init_full, init_grow from .chromosome import Chromosome
from .primitive import Primitive
type Population = list[Chromosome] type Population = list[Chromosome]
def ramped_initialization( def ramped_initialization(
population_size: int, chromosomes_per_variation: int,
depths: list[int], depths: list[int],
make_chromosome: Callable[[InitFunc], Chromosome], terminals: Sequence[Primitive],
operations: Sequence[Primitive],
) -> Population: ) -> Population:
"""Комбинация методов grow и full инициализации хромосом для инициализации начальной """Комбинация методов grow и full инициализации хромосом для инициализации начальной
популяции. популяции.
Начальная популяция генерируется так, чтобы в нее входили деревья с разной
максимальной длиной примерно поровну. Для каждой глубины первая половина деревьев
генерируется полным методом, а вторая растущей инициализацией.
""" """
population: Population = [] population: Population = []
per_depth = population_size / len(depths)
for depth in depths: for depth in depths:
n_full = int(per_depth / 2)
n_grow = int(per_depth / 2)
population.extend( 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( 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 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 import Chromosome, ops
from gp.chromosome import init_grow
from gp.population import ramped_initialization from gp.population import ramped_initialization
from gp.primitive import Var
operations = ops.ALL 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.full_init(terminals, operations, max_depth=3)
print("Depth:", chrom.root.get_subtree_depth())
chrom = Chromosome(operations, terminals, init_func=lambda c: init_grow(c, 8))
print("Depth:", chrom.get_depth())
print("Formula:", chrom) 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] 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( # population = ramped_initialization(
100, # 5,
[3, 4, 5, 6, 7, 8], # [3, 4, 5, 6, 7, 8],
lambda init_func: Chromosome(operations, terminals, init_func), # terminals,
) # operations,
print("Population size:", len(population)) # )
print("Population:") # print("Population size:", len(population))
[print(str(chrom)) for chrom in 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