diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2f1f4a31..95acb0a1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.5.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - id: requirements-txt-fixer - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 24.1.1 hooks: - id: black diff --git a/README.md b/README.md index ffc866079..a6a885ef3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,20 @@ - Сторонние пакеты из `requirements.txt` файла - Английский язык для документации или самодокументирующийся код +## Содержание + +* [Из чего складывается оценка за курс](#из-чего-складывается-оценка-за-курс) + * [Летучки](#летучки) + * [Домашние практические работы](#домашние-практические-работы) +* [Работа с проектом](#работа-с-проектом) +* [Домашние практические работы](#домашние-практические-работы-1) +* [Код](#код) +* [Тесты](#тесты) +* [Эксперименты](#эксперименты) +* [Структура репозитория](#структура-репозитория) +* [Контакты](#контакты) +* [Вместо введения](#вместо-введения) + ## Из чего складывается оценка за курс Оценка за курс складывается из баллов, полученных за работу в семестре. Баллы начисляются за следующее. @@ -66,7 +80,7 @@ ### Домашние практические работы Работы бывают двух типов: -- С полностью автоматической проверкой. Подразумевается, что к этим задачам известен набор тестов и если он проходит, то задача засчитывается. Количество баллов за такие задачи не менее 60. То есть написав все летучки и сдав все такие задачи можно гарантированно получить 3 (E-D) за курс. +- С полностью автоматической проверкой. Подразумевается, что к этим задачам известны название и сигнатуры функций, а также набор тестов; если тесты проходят, то задача засчитывается. Количество баллов за такие задачи не менее 60. То есть написав все летучки и сдав все такие задачи можно гарантированно получить 3 (E-D) за курс. - Требующие проверки преподавателем или ассистентом. Как правило, это задачи на постановку экспериментов или разработку относительно нетривиальных решений. Они основаны на задачах предыдущего типа, потому решать их в изоляции затруднительно. У всех задач есть дедлайн (как правило --- неделя с момента, когда она была задана) после которого максимальный балл за задачу падает в два раза. @@ -123,6 +137,22 @@ ## Тесты +Тесты бывают двух видов: заготовленные преподавателем и ваши собственные. + +Заготовленные тесты существуют в папке `tests/autotests` и используются для проверки задач с полностью автоматической проверкой. +При работе с ними следует соблюдать следующие правила: +- В данных тестах обычно можно изменять только одно --- блок + ```python + try: + from project.task2 import regex_to_dfa, graph_to_nfa + except ImportError: + pytestmark = pytest.mark.skip("Task 2 is not ready to test!") + ``` + В нём необходимо указать из какого(их) модуля(ей) импортировать требуемые функции, в ином случае тесты пропускаются. +- В случае, если вы нашли ошибку **И** готовы её исправить, файл можно изменять, а затем отправлять изменение с помощью Pull Request в основной репозиторий. +- Если же вы нашли ошибку и не готовы заниматься её исправлением, то об этом нужно срочно сообщить преподавателю и не предпринимать других действий! + +К вашим собственным тестам применяются следующие правила: - Тесты для домашних заданий размещайте в папке `tests`. - Формат именования файлов с тестами `test_[какой модуль\класс\функцию тестирует].py`. - Для работы с тестами рекомендуется использовать [`pytest`](https://docs.pytest.org/en/6.2.x/). @@ -149,6 +179,7 @@ ├── scripts - вспомогательные скрипты для автоматизации разработки ├── tasks - файлы с описанием домашних заданий ├── tests - директория для unit-тестов домашних работ +│ └── autotests - директория с автотестами для домашних работ ├── README.md - основная информация о проекте └── requirements.txt - зависимости для настройки репозитория ``` diff --git a/tasks/task2.md b/tasks/task2.md index a2da608c0..d55f6b0bb 100644 --- a/tasks/task2.md +++ b/tasks/task2.md @@ -1,11 +1,22 @@ # Задача 2. Построение детерминированного конечного автомата по регулярному выражению и недетерминированного конечного автомата по графу -* **Мягкий дедлайн**: 18.09.2023, 23:59 -* **Жёсткий дедлайн**: 21.09.2023, 23:59 +* **Жёсткий дедлайн**: 21.02.2024, 23:59 * Полный балл: 5 ## Задача -- [ ] Используя возможности [pyformlang](https://pyformlang.readthedocs.io/en/latest/) реализовать **функцию** построения минимального ДКА по заданному регулярному выражению. [Формат регулярного выражения.](https://pyformlang.readthedocs.io/en/latest/usage.html#regular-expression). +- [ ] Используя возможности [pyformlang](https://pyformlang.readthedocs.io/en/latest/) реализовать **функцию** построения минимального ДКА по заданному регулярному выражению. [Формат регулярного выражения](https://pyformlang.readthedocs.io/en/latest/usage.html#regular-expression). + - Требуемая функция: + ```python + def regex_to_dfa(regex: str) -> DeterministicFiniteAutomaton: + pass + ``` - [ ] Используя возможности [pyformlang](https://pyformlang.readthedocs.io/en/latest/) реализовать **функцию** построения недетерминированного конечного автомата по [графу](https://networkx.org/documentation/stable/reference/classes/multidigraph.html), в том числе по любому из графов, которые можно получить, пользуясь функциональностью, реализованной в [Задаче 1](https://github.com/FormalLanguageConstrainedPathQuerying/formal-lang-course/blob/main/tasks/task1.md) (загруженный из набора данных по имени граф, сгенерированный синтетический граф). Предусмотреть возможность указывать стартовые и финальные вершины. Если они не указаны, то считать все вершины стартовыми и финальными. -- [ ] Добавить необходимые тесты. + - Требуемая функция: + ```python + def graph_to_nfa( + graph: MultiDiGraph, start_states: Set[int], final_states: Set[int] + ) -> NondeterministicFiniteAutomaton: + pass + ``` +- [ ] Добавить собственные тесты при необходимости. diff --git a/tasks/task5.md b/tasks/task5.md index 6319c6a43..bc128cb72 100644 --- a/tasks/task5.md +++ b/tasks/task5.md @@ -37,7 +37,7 @@ - [ ] Создать Python notebook, подключить необходимые зависимости. - [ ] Подключить решения из предыдущих работ. - [ ] Сформировать набор данных. - - [ ] Выбрать некоторые графы из [набора](https://jetbrains-research.github.io/CFPQ_Data/dataset/index.html). Не забудьте обосновать, почему выбрали именно эти графы. + - [ ] Выбрать некоторые графы из [набора](https://formallanguageconstrainedpathquerying.github.io/CFPQ_Data/graphs/index.html). Не забудьте обосновать, почему выбрали именно эти графы. - [ ] Используя функцию из первой домашней работы узнать метки рёбер графов и на основе этой информации сформулировать не менее четырёх различных запросов к каждому графу. Лучше использовать наиболее часто встречающиеся метки. Требования к запросам: - Запросы ко всем графам должны следовать некоторому общему шаблону. Например, если есть графы ```g1``` и ```g2``` с различными наборами меток, то ожидается, что запросы к ним будут выглядеть, например, так: - ```g1```: @@ -49,7 +49,7 @@ - ```(m1 | m3)+ m2*``` - ```m1 m2 m3 (m3|m1)*``` - В запросах должны использоваться все общепринятые конструкции регулярных выражений (замыкание, конкатенация, альтернатива). То есть хотя бы в одном запросе к каждому графу должна быть каждая из этих конструкций. - - [ ] Для генерации множеств стартовых вершин воспользоваться [этой функцией](https://jetbrains-research.github.io/CFPQ_Data/reference/graphs/generated/cfpq_data.graphs.utils.multiple_source_utils.html#cfpq_data.graphs.utils.multiple_source_utils.generate_multiple_source). Не забывайте, что от того, как именно устроено стартовое множество, сильно зависит время вычисления запроса. + - [ ] Для генерации множеств стартовых вершин воспользоваться [этой функцией](https://formallanguageconstrainedpathquerying.github.io/CFPQ_Data/reference/graphs/generated/cfpq_data.graphs.utils.multiple_source_utils.html#cfpq_data.graphs.utils.multiple_source_utils.generate_multiple_source). Не забывайте, что от того, как именно устроено стартовое множество, сильно зависит время вычисления запроса. - [ ] Сформулировать этапы эксперимента. Что нужно сделать, чтобы ответить на поставленные вопросы? Почему? - [ ] Провести необходимые эксперименты, замеры - [ ] Оформить результаты экспериментов diff --git a/tests/autotests/test_task2.py b/tests/autotests/test_task2.py new file mode 100644 index 000000000..285a01950 --- /dev/null +++ b/tests/autotests/test_task2.py @@ -0,0 +1,160 @@ +# This file contains test cases that you need to pass to get a grade +# You MUST NOT touch anything here except ONE block below +# You CAN modify this file IF AND ONLY IF you have found a bug and are willing to fix it +# Otherwise, please report it +import pyformlang.finite_automaton +from networkx import MultiDiGraph +from pyformlang.regular_expression import Regex +import pytest +import random +import itertools +import networkx as nx + +# Fix import statements in try block to run tests +try: + from project.task2 import regex_to_dfa, graph_to_nfa +except ImportError: + pytestmark = pytest.mark.skip("Task 2 is not ready to test!") + +REGEX_TO_TEST = [ + "(aa)*", + "a | a", + "a* | a", + "(ab) | (ac)", + "(ab) | (abc)", + "(abd) | (abc)", + "(abd*) | (abc*)", + "(abd)* | (abc)*", + "((abd) | (abc))*", + "a*a*", + "a*a*b", + "a* | (a | b)*", + "a*(a | b)*", + "(a | c)*(a | b)*", +] + + +class TestRegexToDfa: + @pytest.mark.parametrize("regex_str", REGEX_TO_TEST, ids=lambda regex: regex) + def test(self, regex_str: str) -> None: + regex = Regex(regex_str) + regex_cfg = regex.to_cfg() + regex_words = regex_cfg.get_words() + + if regex_cfg.is_finite(): + all_word_parts = list(regex_words) + word_parts = random.choice(all_word_parts) + else: + index = random.randint(0, 2**9) + word_parts = next(itertools.islice(regex_words, index, None)) + + word = map(lambda x: x.value, word_parts) + + dfa = regex_to_dfa(regex_str) + + minimized_dfa = dfa.minimize() + assert dfa.is_deterministic() + assert dfa.is_equivalent_to(minimized_dfa) + assert dfa.accepts(word) + + +LABELS = ["a", "b", "c", "x", "y", "z", "alpha", "beta", "gamma", "ɛ"] + + +class GraphWordsHelper: + graph = None + all_paths = None + + def __init__(self, graph: MultiDiGraph): + self.graph = graph + self.all_paths = nx.shortest_path(graph) + + def is_reachable(self, source, target): + if source not in self.all_paths.keys(): + return False + return target in self.all_paths[source].keys() + + def _take_a_step(self, node): + for node_to, edge_dict in dict(self.graph[node]).items(): + for edge_data in edge_dict.values(): + yield {"node_to": node_to, "label": edge_data["label"]} + + def _is_final_node(self, node): + return self.graph.nodes(data=True)[node]["is_final"] + + def generate_words_by_node(self, node, word=None): + if word is None: + word = list() + for trans in self._take_a_step(node): + tmp = word.copy() + label = trans["label"] + if label != "ɛ": + tmp.append(label) + if self._is_final_node(trans["node_to"]): + yield tmp.copy() + yield from self.generate_words_by_node(trans["node_to"], tmp.copy()) + + def take_words_by_node(self, node, n): + final_nodes = list(map(lambda x: x[0], self.graph.nodes(data="is_final"))) + if any( + map(lambda final_node: self.is_reachable(node, final_node), final_nodes) + ): + return itertools.islice(self.generate_words_by_node(node), 0, n) + return [] + + def get_all_words_less_then_n(self, n: int) -> list[str]: + start_nodes = list(map(lambda x: x[0], self.graph.nodes(data="is_start"))) + result = list() + for start in start_nodes: + result.extend(self.take_words_by_node(start, n)) + return result + + +@pytest.fixture(scope="class", params=range(5)) +def graph(request) -> MultiDiGraph: + n_of_nodes = random.randint(1, 20) + graph = nx.scale_free_graph(n_of_nodes) + + for _, _, data in graph.edges(data=True): + data["label"] = random.choice(LABELS) + + return graph + + +class TestGraphToNfa: + def test_not_specified(self, graph: MultiDiGraph) -> None: + nfa: pyformlang.finite_automaton.NondeterministicFiniteAutomaton = graph_to_nfa( + graph, set(), set() + ) + words_helper = GraphWordsHelper(graph) + words = words_helper.get_all_words_less_then_n(random.randint(10, 100)) + if len(words) == 0: + assert nfa.is_empty() + else: + word = random.choice(words) + assert nfa.accepts(word) + + def test_random( + self, + graph: MultiDiGraph, + ) -> None: + start_nodes = set( + random.choices( + list(graph.nodes().keys()), k=random.randint(1, len(graph.nodes)) + ) + ) + final_nodes = set( + random.choices( + list(graph.nodes().keys()), k=random.randint(1, len(graph.nodes)) + ) + ) + nfa: pyformlang.finite_automaton.NondeterministicFiniteAutomaton = graph_to_nfa( + graph, start_nodes, final_nodes + ) + words_helper = GraphWordsHelper(graph) + words = words_helper.get_all_words_less_then_n(random.randint(10, 100)) + if len(words) == 0: + assert nfa.is_empty() + else: + word = random.choice(words) + assert nfa.accepts(word)