Download mathly2/exam.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 4.85 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/exam.py
- Command line
-
hf download hf://spaces/wzender/mathly/mathly2/exam.py
-
curl -L -o exam.py https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/exam.py
4.85 kB
| """Drawing an exercise test or a simulation for a real test (PRD EXT-4, EXT-5, SIM-2): mixed questions from all covered lessons, | |
| weighted toward the student's weak skills, never from a skill the student has not started.""" | |
| from __future__ import annotations | |
| import random | |
| EXERCISE_SHARE = {1: 0.6, 2: 0.3} # the share of questions that come from weak skills in exercise test 1 and 2; the simulation has none | |
| FOCUS = 3 # the exercise test on the weak topics: only the skills that lost points or are still being learned | |
| FOCUS_SIZE = 15 | |
| BALANCED, MANUAL = 1, 4 # practice tests by mode (the number in the attempt's scope "<test>:<n>"); 2 is an older balanced one | |
| MODE_OF = {1: "balanced", 2: "balanced", FOCUS: "focus", MANUAL: "manual"} | |
| NUMBER_OF = {"balanced": BALANCED, "focus": FOCUS, "manual": MANUAL} | |
| TITLES = {"balanced": "诪讘讞谉 转专讙讜诇 诪讗讜讝谉", "focus": "诪讘讞谉 转专讙讜诇 诪诪讜拽讚", "manual": "诪讘讞谉 转专讙讜诇 讘讘讞讬专讛"} | |
| def mode_of_scope(scope: str) -> str: | |
| _, _, number = scope.partition(":") | |
| return MODE_OF.get(int(number), "balanced") if number.isdigit() else "balanced" | |
| def _round(x: float) -> int: | |
| return int(x + 0.5) | |
| def draw(bank, lessons: list[str], states: dict[str, str], n: int, *, share: float = 0.0, weak_before: set[str] | None = None, | |
| used: dict[str, int] | None = None, rng: random.Random | None = None, include_unstarted: bool = False) -> dict: | |
| """{"questions": [{skill_id, key}], "weak": [skill ids], "practice_first": [skill ids]}. | |
| Covered skills are those of `lessons` that have questions. A skill in the not-started state is never drawn: it is listed in | |
| `practice_first`. A weak skill is a started covered skill in the learning state, or one in `weak_before` (answered wrong in an | |
| earlier exercise test of this real test). With `include_unstarted`, not-started skills are drawn too (and still listed in `practice_first`). Weak skills give one question each, up to `round(n * share)`; the rest comes from the | |
| other skills one per skill round after round (least-used questions first, ties random), then from the weak ones again.""" | |
| used, rng, weak_before = used or {}, rng or random.Random(), weak_before or set() | |
| covered = [s["id"] for s in bank.skills if s["unit"] in lessons and bank.question_keys(s["id"])] | |
| started = [i for i in covered if states.get(i, "not_started") != "not_started"] | |
| practice_first = [i for i in covered if i not in started] | |
| if include_unstarted: | |
| started = covered | |
| weak = [i for i in started if states.get(i) == "learning" or i in weak_before] | |
| other = [i for i in started if i not in weak] | |
| pools = {i: sorted(bank.question_keys(i), key=lambda key: (used.get(key, 0), rng.random())) for i in started} | |
| taken: list[dict] = [] | |
| seen: set[str] = set() | |
| def take(skill_id: str) -> bool: | |
| for key in pools[skill_id]: | |
| if key not in seen: | |
| seen.add(key) | |
| taken.append({"skill_id": skill_id, "key": key}) | |
| return True | |
| return False | |
| wanted = _round(n * share) | |
| shuffled_weak = rng.sample(weak, len(weak)) | |
| for skill_id in shuffled_weak[:wanted]: | |
| take(skill_id) | |
| order = rng.sample(other, len(other)) if share else rng.sample(started, len(started)) # no weak share: an even spread over all | |
| for group in ([order, shuffled_weak] if share else [order]): # the weak skills are used again only when the others run out | |
| progress = True | |
| while len(taken) < n and progress: | |
| progress = False | |
| for skill_id in group: | |
| if len(taken) >= n: | |
| break | |
| if take(skill_id): | |
| progress = True | |
| rng.shuffle(taken) | |
| return {"questions": taken[:n], "weak": weak, "practice_first": practice_first} | |
| def draw_focus(bank, weak: list[str], n: int = FOCUS_SIZE, used: dict[str, int] | None = None, rng: random.Random | None = None) -> list[dict]: | |
| """`n` questions from these skills only (the weak ones, or the ones the student picked), one per skill round after round (least-used first).""" | |
| used, rng = used or {}, rng or random.Random() | |
| pools = {i: sorted(bank.question_keys(i), key=lambda key: (used.get(key, 0), rng.random())) for i in weak if bank.question_keys(i)} | |
| order, taken, seen = rng.sample(list(pools), len(pools)), [], set() | |
| progress = True | |
| while len(taken) < n and progress: | |
| progress = False | |
| for skill_id in order: | |
| key = next((k for k in pools[skill_id] if k not in seen), None) | |
| if key and len(taken) < n: | |
| seen.add(key) | |
| taken.append({"skill_id": skill_id, "key": key}) | |
| progress = True | |
| rng.shuffle(taken) | |
| return taken | |