diff --git a/.gitignore b/.gitignore index b2ea21b..444f4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ !**/ !*.gitignore -!*.py \ No newline at end of file +!*.py +!lab4/* \ No newline at end of file diff --git a/lab4/.python-version b/lab4/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/lab4/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/lab4/gp/__init__.py b/lab4/gp/__init__.py index 3bcfe12..09ceff3 100644 --- a/lab4/gp/__init__.py +++ b/lab4/gp/__init__.py @@ -1,5 +1,3 @@ from .chromosome import Chromosome -from .operation import Operation -from .terminal import Terminal -__all__ = ["Chromosome", "Operation", "Terminal"] +__all__ = ["Chromosome"] diff --git a/lab4/gp/chromosome.py b/lab4/gp/chromosome.py index 48d6e69..55c32d6 100644 --- a/lab4/gp/chromosome.py +++ b/lab4/gp/chromosome.py @@ -1,159 +1,113 @@ 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 + @classmethod + def full_init( + cls, + terminals: Sequence[Primitive], + operations: Sequence[Primitive], + max_depth: int, + ) -> Chromosome: + """Полная инициализация. - 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) + def build(level: int) -> Node: + # Если достигнута максимальная глубина — выбираем терминал + if level == max_depth: + return Node(random.choice(terminals)) - class Node: - def __init__( - self, value: Operation | Terminal, children: list["Chromosome.Node"] - ): - self.value = value - self.children = children + # Иначе выбираем операцию и создаём потомков + op = random.choice(operations) + node = Node(op) + for _ in range(op.arity): + node.add_child(build(level + 1)) + return node - def clone(self) -> "Chromosome.Node": - """Создает копию поддерева.""" - return Chromosome.Node( - self.value, [child.clone() for child in self.children] - ) + return cls(terminals, operations, build(1)) - def get_depth(self) -> int: - """Вычисляет глубину поддерева.""" - return ( - max(child.get_depth() for child in self.children) + 1 - if self.children - else 1 - ) + @classmethod + def grow_init( + cls, + terminals: Sequence[Primitive], + operations: Sequence[Primitive], + 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: - return f"({self.children[0]} {self.value.name} {self.children[1]})" + def build(level: int) -> Node: + # Если достигнута максимальная глубина, либо сыграла заданная вероятность + # — выбираем терминал + 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: - """Рекурсивно вычисляет значение поддерева. Значения терминалов должны быть - заданы предварительно.""" - 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) + return cls(terminals, operations, build(1)) diff --git a/lab4/gp/crossovers.py b/lab4/gp/crossovers.py index c58b254..f71359b 100644 --- a/lab4/gp/crossovers.py +++ b/lab4/gp/crossovers.py @@ -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 diff --git a/lab4/gp/mutations.py b/lab4/gp/mutations.py index a4383c7..3aa675b 100644 --- a/lab4/gp/mutations.py +++ b/lab4/gp/mutations.py @@ -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() diff --git a/lab4/gp/node.py b/lab4/gp/node.py new file mode 100644 index 0000000..5f5da9d --- /dev/null +++ b/lab4/gp/node.py @@ -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 diff --git a/lab4/gp/operation.py b/lab4/gp/operation.py deleted file mode 100644 index c0ba875..0000000 --- a/lab4/gp/operation.py +++ /dev/null @@ -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) diff --git a/lab4/gp/ops.py b/lab4/gp/ops.py index b63fdeb..af0faf0 100644 --- a/lab4/gp/ops.py +++ b/lab4/gp/ops.py @@ -1,6 +1,6 @@ import math -from .operation import Operation +from .primitive import Operation # Унарные операции NEG = Operation("-", 1, lambda x: -x[0]) diff --git a/lab4/gp/population.py b/lab4/gp/population.py index 8ac5c97..74492e1 100644 --- a/lab4/gp/population.py +++ b/lab4/gp/population.py @@ -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 diff --git a/lab4/gp/primitive.py b/lab4/gp/primitive.py new file mode 100644 index 0000000..f47c6f6 --- /dev/null +++ b/lab4/gp/primitive.py @@ -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) diff --git a/lab4/gp/terminal.py b/lab4/gp/terminal.py deleted file mode 100644 index 180c061..0000000 --- a/lab4/gp/terminal.py +++ /dev/null @@ -1,7 +0,0 @@ -from dataclasses import dataclass - - -@dataclass() -class Terminal: - name: str - _value: float | None = None diff --git a/lab4/gp/types.py b/lab4/gp/types.py new file mode 100644 index 0000000..39f95e4 --- /dev/null +++ b/lab4/gp/types.py @@ -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: ... diff --git a/lab4/main.py b/lab4/main.py index 89bcf50..9ee3c09 100644 --- a/lab4/main.py +++ b/lab4/main.py @@ -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] diff --git a/lab4/pyproject.toml b/lab4/pyproject.toml new file mode 100644 index 0000000..1aadee8 --- /dev/null +++ b/lab4/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "lab4" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [] + +[tool.ruff] +target-version = "py314" \ No newline at end of file diff --git a/lab4/pytest.ini b/lab4/pytest.ini new file mode 100644 index 0000000..b810370 --- /dev/null +++ b/lab4/pytest.ini @@ -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 +