added quick_start.md

This commit is contained in:
2025-04-28 22:02:51 +03:00
parent f9eb2f35a8
commit b74238f14e
17 changed files with 279 additions and 46 deletions

BIN
clean.xlsx Normal file

Binary file not shown.

142
docs/quick_start.md Normal file
View File

@ -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
```

View File

@ -1,6 +1,7 @@
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from itertools import chain
from typing import Generic, overload, override from typing import Generic, overload, override
from modules.utils.types import C, V from modules.utils.types import C, V
@ -29,12 +30,37 @@ class Tags(Generic[C, V]):
_dict: dict[C, set[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 = {} self._dict = {}
if iter: if isinstance(iter, Iterable) and not isinstance(iter, tuple):
for tag in iter: for tag in iter:
if isinstance(tag, Tag):
self._dict.setdefault(tag.cat, set()).add(tag.val) 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 @overload
def has_tag(self, category: C, value: V) -> bool: ... def has_tag(self, category: C, value: V) -> bool: ...

View File

@ -1,6 +1,6 @@
import uuid import uuid
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Generic, runtime_checkable from typing import Callable, Generic
from option import Option from option import Option

View File

@ -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)

View File

@ -1,3 +1,4 @@
from collections.abc import Iterable
from typing import Generic from typing import Generic
from modules.task import QTask from modules.task import QTask
@ -22,12 +23,12 @@ class VariantFactory(Generic[C, V, Q, A]):
def __init__( def __init__(
self, self,
number_of_tasks: int, 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, task_selector: QTaskSelector[C, V, Q, A] | None = None,
): ):
self.task = [VariantTask() for _ in range(number_of_tasks)] self.task = [VariantTask() for _ in range(number_of_tasks)]
self.number_of_tasks = 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 = ( self.task_selector = (
task_selector if task_selector is not None else LeastUsedTaskSelector() task_selector if task_selector is not None else LeastUsedTaskSelector()
) )

View File

@ -1,13 +1,11 @@
import uuid import uuid
from math import inf from math import inf
from test import best_candidate, min_max_intersections
from typing import override from typing import override
from modules.task import QTask from modules.task import QTask
from modules.utils.types import A, C, Q, V from modules.utils.types import A, C, Q, V
from modules.utils.utils import rnd from modules.utils.utils import rnd
from modules.variant_builder.context import DynamicCtx from modules.variant_builder.context import DynamicCtx
from modules.variant_builder.task_pool import QTaskPool
from modules.variant_builder.task_selector import QTaskSelector 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: if min_max_intersections > max_intersections:
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]] = [] best_candidates: list[QTask[C, V, Q, A]] = []
for task, score in zip(filtered_tasks, task_scores): for task, score in zip(filtered_tasks, task_scores):

BIN
parserd.xlsx Normal file

Binary file not shown.

29
poetry.lock generated
View File

@ -13,6 +13,18 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {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]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.2" version = "1.2.2"
@ -41,6 +53,21 @@ files = [
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, {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]] [[package]]
name = "option" name = "option"
version = "2.1.0" version = "2.1.0"
@ -150,4 +177,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.8,<4" python-versions = ">=3.8,<4"
content-hash = "aa060e205a9e141d4941339e4d9d39cde82de5f8d0900aca25f331ddf46e2a05" content-hash = "af9b1bb0ddd8587d40decb8a6cb9a61735ebd8cd1354e18afd1bfa55aed0eb33"

View File

@ -9,7 +9,8 @@ readme = "README.md"
requires-python = ">=3.8,<4" requires-python = ">=3.8,<4"
dependencies = [ dependencies = [
"option (>=2.1.0,<3.0.0)", "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)"
] ]

BIN
samples/clean.xlsx Normal file

Binary file not shown.

Binary file not shown.

33
test.py
View File

@ -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")

43
variants.py Normal file
View File

@ -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")

BIN
варианты.xlsx Normal file

Binary file not shown.