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 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: ...

View File

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

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

View File

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

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"},
]
[[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"

View File

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

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.