diff --git a/clean.xlsx b/clean.xlsx new file mode 100644 index 0000000..5fab253 Binary files /dev/null and b/clean.xlsx differ diff --git a/docs/quick_start.md b/docs/quick_start.md new file mode 100644 index 0000000..fbaa56a --- /dev/null +++ b/docs/quick_start.md @@ -0,0 +1,142 @@ +# Основные понятия + +Для того чтобы составить варианты, вам сперва нужно: 1) написать все задачи/генераторы задач, 2) составить из них пул задач и 3) описать структуру варианта. Разберём по пунктам: + +1.1 Задача - это основной строительный блок варианта. В `QuizGen` задача представляет собой класс с тремя значениями. Одно обязательные - это вопрос задачи `question`. И два опциональных -- ответ к задаче `answer` и теги `tags` (про которые позже) + +```{python} +from modules.task import QTask +from modules.tag import Tags + +task = QTask( + question="Посчитайте дисперсию для следующего ряда: 5, 6, 7, 8, 9", + answer="2", # можно опустить или не давать ответ: answer = None + tags=Tags( + ("тема", "дисперсия"), + ("сложность", "лёгкая"), + ... + ) # теги можно определять любые. Первое значение - это категория, второе - это значение в этой категории. +) +``` + +1.2 Генератор задач - это класс, с функцией generate(), которая при вызове генерирует задачу `QTask`: + +```{python} +from moduels.task.factory.default import QTaskFactoryDefault +from modules.task import QTask +from modules.tag import Tags + +#Сначала определим функцию, которая будет использоваться для генерации задач +def task_generator_function(): + # генерируем два случайных целых числа от 1 до 9 + alpha = random.randint(1, 9) + beta = random.randint(1, 9) + + # Используем сгенерированные числа, чтобы составить задачу + question = f"Чему равна дисперсия величины V({alpha} * X + {beta} * Y), если X и Y подчинены стандартному нормальному закону распределения и независимы друг от друга?" + answer = f"{alpha ** 2 + beta ** 2}" + + return QTask( + question, + answer, + ) + +# С её помощью создадим генератор задач +variance_task_factory = QTaskFactoryDefault(task_generator_function) + +# Получаем генератор, который умеет производит задачи с разными числами, каждый раз, когда мы вызываем фукнцию generate(): + +variance_task_factory.generate() # => Чему равна дисперсия величины V(3 * X + 5 * Y), если X и Y подчинены стандартному нормальному закону распределения и независимы друг от друга? Ответ: 34 + +variance_task_factory.generate() # => Чему равна дисперсия величины V(6 * X + 2 * Y), если X и Y подчинены стандартному нормальному закону распределения и независимы друг от друга? Ответ: 40 +``` + +2. Пул задач. Пул - это массив задач и генераторов задач, из которых составляются варианты. + +```{python} +from modules.variant_builder.task_pool import QTaskPool +from modules.task import QTask + +tasks = [ + QTask( + question="Текст задачи 1" + ), + QTask( + question="Текст задачи 2" + ), + QTask( + question="Текст задачи 3" + ), + variance_task_factory # каждый раз, когда в вариант отбирается генератор, он генерирует для этого варианта новую задачу с уникальными значениями +] + +# Инициируем пул задач +task_pool = QTaskPool(tasks) +``` + +3. Описание структуры варианта. Для описания структуры варианта используется специальный класс `VariantFactory`. Чтобы его создать, из скольки задач будут состоять варианты, пул задач из которых подбираются задачи и (опционально) правило отбора задач. Если последнее не указать, то будет использоваться правило, которые старается минимизировать число пересечений между любыми двумя вариантами и по возможности равномерно распределить задачи. + +Чтобы понять, как определяется структура варианта, рассмотрим простой пример. Допустим, мы хотим сделать вариант, состоящий из трёх задач и у каждой задачи есть две вариации. + +```{python} +from modules.variant_builder import VariantFactory + +# Сначала создадим пул задач +task_pool = QTaskPool([ + # Задача 1 + QTask( + question="1.1" + tags=Tag("order", "first") # Тег, чтобы отличать первую задачу от второй и третий + ), + QTask( + question="1.2" + tags=Tag("order", "first") + ), + + # Задача 2 + QTask( + question="2.1" + tags=Tag("order", "second") + ), + QTask( + question="2.2" + tags=Tag("order", "second") + ), + + # Задача 3 + QTask( + question="3.1" + tags=Tag("order", "third") + ), + QTask( + question="3.2" + tags=Tag("order", "third") + ), +]) + +# Инициируем генератор вариантов +vf = VariantFactory(number_of_tasks=3, task_pool = task_pool) + +# Теперь самое главное - укажем, что первая задача в варианте должна быть задачей один, вторая - задачей два, а третья - задачей три +vf.task[0].must.include_tag("order", "first") # первая задача должна быть задачей с пометкой order = first +vf.task[1].must.include_tag("order", "second") # вторая задача должна быть задачей с пометкой order = second +vf.task[2].must.include_tag("order", "third") # первая задача должна быть задачей с пометкой order = third + +# доступные методы include_all_tags() - должна включать все теги из списка, include_any_tag() - должна включать хотя один тег из списка, be_one_of() - должна быть одной из задач или должна быть сгенерирована определённым генератором, not(lambda b: b.must...) - логическое отрицание, or(lambda b: b.must..., lambda b: b.must...) - логическое или. + +# Сгенерируем 10 вариантов: +variants = vf.generate_variants(number_of_variants=10) # можем указать любое число +``` + +Генератор вариантов сгенерирует 10 вариантов так, чтобы они были максимально уникальны относительно друг друга. Варианты, которые составляет `VariantFactory` представлены классом `QVariant`, который есть просто собрание задач и по сути представляет собой обычный массив: + +```{python} +first_variant = variants[0] # первый вариант +first_task_of_first_variant = first_variant[0] # первая задача первого варианта + +print(first_task_of_first_variant.question) # => 1.2 +``` + + + + diff --git a/modules/tag/__init__.py b/modules/tag/__init__.py index 63e7e17..9ce1cb8 100644 --- a/modules/tag/__init__.py +++ b/modules/tag/__init__.py @@ -1,6 +1,7 @@ from collections.abc import Iterable from dataclasses import dataclass, field from enum import Enum +from itertools import chain from typing import Generic, overload, override from modules.utils.types import C, V @@ -29,12 +30,37 @@ class Tags(Generic[C, V]): _dict: dict[C, set[V]] - def __init__(self, iter: Iterable[Tag[C, V]] | None = None): + @overload + def __init__( + self, iter: Iterable[Tag[C, V] | tuple[C, V]] | None = None + ) -> None: ... + + @overload + def __init__( + self, + iter: Tag[C, V] | tuple[C, V] | None = None, + **kwargs: Tag[C, V] | tuple[C, V], + ) -> None: ... + + def __init__( + self, + iter: Iterable[Tag[C, V] | tuple[C, V]] | Tag[C, V] | tuple[C, V] | None = None, + **kwargs: Tag[C, V] | tuple[C, V], + ) -> None: self._dict = {} - if iter: + if isinstance(iter, Iterable) and not isinstance(iter, tuple): for tag in iter: - self._dict.setdefault(tag.cat, set()).add(tag.val) + if isinstance(tag, Tag): + self._dict.setdefault(tag.cat, set()).add(tag.val) + else: + self._dict.setdefault(tag[0], set()).add(tag[1]) + else: + for tag in chain([iter], kwargs.values()): + if isinstance(iter, Tag): + self._dict.setdefault(tag.cat, set()).add(tag.val) + elif isinstance(iter, tuple): + self._dict.setdefault(tag[0], set()).add(tag[1]) @overload def has_tag(self, category: C, value: V) -> bool: ... diff --git a/modules/task/factory/__init__.py b/modules/task/factory/__init__.py index edd97cf..7b1e6d2 100644 --- a/modules/task/factory/__init__.py +++ b/modules/task/factory/__init__.py @@ -1,6 +1,6 @@ import uuid from abc import ABC, abstractmethod -from typing import Generic, runtime_checkable +from typing import Callable, Generic from option import Option diff --git a/modules/task/factory/default.py b/modules/task/factory/default.py new file mode 100644 index 0000000..4fd7569 --- /dev/null +++ b/modules/task/factory/default.py @@ -0,0 +1,31 @@ +from typing import Callable, override + +from modules.tag import Tags +from modules.task import QTask +from modules.task.factory import QTaskFactory +from modules.utils.types import A, C, Q, V + + +class QTaskFactoryDefault(QTaskFactory[C, V, Q, A]): + _generator: Callable[[], QTask[C, V, Q, A]] + + def __init__(self, generator: Callable[[], QTask[C, V, Q, A]]): + self._generator = generator + + @override + def generate(self) -> QTask[C, V, Q, A]: + return self._generator() + + +def task_generator_function(): + alpha = random.randint(1, 9) + beta = random.randint(1, 9) + + question = f"Чему равна дисперсия величины V({alpha} * X + {beta} * Y), если X и Y подчинены стандартному нормальному закону распределения и независимы друг от друга?" + answer = f"{alpha ** 2 + beta ** 2}" + tags = Tags(("тема", "дисперсия"), ("сложность", "лёгкая")) + + return QTask(question, answer, tags) + + +factory = QTaskFactoryDefault(task_generator_function) diff --git a/modules/variant_builder/__init__.py b/modules/variant_builder/__init__.py index 1b3dce2..a66f18c 100644 --- a/modules/variant_builder/__init__.py +++ b/modules/variant_builder/__init__.py @@ -1,3 +1,4 @@ +from collections.abc import Iterable from typing import Generic from modules.task import QTask @@ -22,12 +23,12 @@ class VariantFactory(Generic[C, V, Q, A]): def __init__( self, number_of_tasks: int, - task_pool: QTaskPool[C, V, Q, A], + task_pool: Iterable[QTask[C, V, Q, A]], task_selector: QTaskSelector[C, V, Q, A] | None = None, ): self.task = [VariantTask() for _ in range(number_of_tasks)] self.number_of_tasks = number_of_tasks - self.task_pool = task_pool + self.task_pool = QTaskPool[C, V, Q, A](task_pool) self.task_selector = ( task_selector if task_selector is not None else LeastUsedTaskSelector() ) diff --git a/modules/variant_builder/__pycache__/__init__.cpython-313.pyc b/modules/variant_builder/__pycache__/__init__.cpython-313.pyc index 13eb2fc..a578e35 100644 Binary files a/modules/variant_builder/__pycache__/__init__.cpython-313.pyc and b/modules/variant_builder/__pycache__/__init__.cpython-313.pyc differ diff --git a/modules/variant_builder/__pycache__/default_task_selector.cpython-313.pyc b/modules/variant_builder/__pycache__/default_task_selector.cpython-313.pyc index 7867403..40aa3c1 100644 Binary files a/modules/variant_builder/__pycache__/default_task_selector.cpython-313.pyc and b/modules/variant_builder/__pycache__/default_task_selector.cpython-313.pyc differ diff --git a/modules/variant_builder/default_task_selector.py b/modules/variant_builder/default_task_selector.py index fe1a406..2e762be 100644 --- a/modules/variant_builder/default_task_selector.py +++ b/modules/variant_builder/default_task_selector.py @@ -1,13 +1,11 @@ import uuid from math import inf -from test import best_candidate, min_max_intersections from typing import override from modules.task import QTask from modules.utils.types import A, C, Q, V from modules.utils.utils import rnd from modules.variant_builder.context import DynamicCtx -from modules.variant_builder.task_pool import QTaskPool from modules.variant_builder.task_selector import QTaskSelector @@ -41,9 +39,6 @@ class LeastUsedTaskSelector(QTaskSelector[C, V, Q, A]): if min_max_intersections > max_intersections: min_max_intersections = max_intersections - if len(ctx.current_variant_tasks) == 6: - print(min_max_intersections) - best_candidates: list[QTask[C, V, Q, A]] = [] for task, score in zip(filtered_tasks, task_scores): diff --git a/parserd.xlsx b/parserd.xlsx new file mode 100644 index 0000000..96629ca Binary files /dev/null and b/parserd.xlsx differ diff --git a/poetry.lock b/poetry.lock index 17b6786..80a79a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,6 +13,18 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +description = "An implementation of lxml.xmlfile for the standard library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, + {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, +] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -41,6 +53,21 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, +] + +[package.dependencies] +et-xmlfile = "*" + [[package]] name = "option" version = "2.1.0" @@ -150,4 +177,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.8,<4" -content-hash = "aa060e205a9e141d4941339e4d9d39cde82de5f8d0900aca25f331ddf46e2a05" +content-hash = "af9b1bb0ddd8587d40decb8a6cb9a61735ebd8cd1354e18afd1bfa55aed0eb33" diff --git a/pyproject.toml b/pyproject.toml index 96f38a8..3412f72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,8 @@ readme = "README.md" requires-python = ">=3.8,<4" dependencies = [ "option (>=2.1.0,<3.0.0)", - "pytest (>=8.3.5,<9.0.0)" + "pytest (>=8.3.5,<9.0.0)", + "openpyxl (>=3.1.5,<4.0.0)" ] diff --git a/samples/clean.xlsx b/samples/clean.xlsx new file mode 100644 index 0000000..39e2d5b Binary files /dev/null and b/samples/clean.xlsx differ diff --git a/samples/Варианты ДКР2 2023 2024 2025.xlsx b/samples/Варианты ДКР2 2023 2024 2025.xlsx new file mode 100644 index 0000000..f6d3fad Binary files /dev/null and b/samples/Варианты ДКР2 2023 2024 2025.xlsx differ diff --git a/test.py b/test.py deleted file mode 100644 index 364809c..0000000 --- a/test.py +++ /dev/null @@ -1,33 +0,0 @@ -from math import inf - -tasks1 = [f"1.{i + 1}" for i in range(5)] -tasks2 = [f"2.{i + 1}" for i in range(5)] -tasks3 = [f"3.{i + 1}" for i in range(5)] - -all_variants = [] - -for i in tasks1: - for j in tasks2: - for k in tasks3: - all_variants.append([i, j, k]) - -selected_variants = [all_variants[0]] - -for _ in range(9): - best_candidate = all_variants[0] - min_max_intersections = inf - for variant in all_variants: - max_intersections = -inf - for selected_variant in selected_variants: - intersections = 0 - for j in range(3): - if selected_variant[j] == variant[j]: - intersections += 1 - if max_intersections < intersections: - max_intersections = intersections - if max_intersections < min_max_intersections: - min_max_intersections = max_intersections - best_candidate = variant - selected_variants.append(best_candidate) - -print(any([]), sep="\n") diff --git a/variants.py b/variants.py new file mode 100644 index 0000000..4d287db --- /dev/null +++ b/variants.py @@ -0,0 +1,43 @@ +from openpyxl import load_workbook + +from modules.tag import Tag +from modules.task import QTask +from modules.variant_builder import VariantFactory + +wb = load_workbook(filename="./clean.xlsx") +ws = wb["data"] + +for i in list(range(6 * 5))[::5]: + tasks: list[QTask] = [] + + name = str(ws.cell(column=1, row=(i + 1)).value) + + wb.create_sheet(name) + ws_sp = wb[name] + + for ind, col in enumerate( + ws.iter_cols(min_col=2, max_col=8, min_row=i + 1, max_row=i + 5) + ): + for cell in col: + tasks.append( + QTask[str, str, str, str | None]( + question=str(cell.value), tags=Tag("topic", str(ind)) + ) + ) + + vf = VariantFactory(7, tasks) + _ = vf.task[0].must.include_tag("topic", "0") + _ = vf.task[1].must.include_tag("topic", "1") + _ = vf.task[2].must.include_tag("topic", "2") + _ = vf.task[3].must.include_tag("topic", "3") + _ = vf.task[4].must.include_tag("topic", "4") + _ = vf.task[5].must.include_tag("topic", "5") + _ = vf.task[6].must.include_tag("topic", "6") + variants = vf.generate_variants(number_of_variants=8) + + for ind, variant in enumerate(variants): + cell = ws_sp.cell(column=1, row=ind + 1, value=f"Вариант {ind + 1}") + for task_ind, task in enumerate(variant.tasks): + cell = ws_sp.cell(column=task_ind + 2, row=ind + 1, value=task.question) + + wb.save("parserd.xlsx") diff --git a/варианты.xlsx b/варианты.xlsx new file mode 100644 index 0000000..5cb645d Binary files /dev/null and b/варианты.xlsx differ