QuizGenerator 0.9.0__py3-none-any.whl → 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +2 -1
- QuizGenerator/canvas/canvas_interface.py +9 -6
- QuizGenerator/canvas/classes.py +0 -1
- QuizGenerator/contentast.py +32 -10
- QuizGenerator/generate.py +57 -11
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +0 -8
- QuizGenerator/premade_questions/cst334/memory_questions.py +2 -3
- QuizGenerator/premade_questions/cst334/process.py +0 -1
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +10 -1
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +0 -1
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +2 -4
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +22 -20
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +1 -1
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +11 -1
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +0 -1
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +0 -1
- QuizGenerator/premade_questions/cst463/models/attention.py +1 -5
- QuizGenerator/premade_questions/cst463/models/cnns.py +1 -5
- QuizGenerator/premade_questions/cst463/models/rnns.py +1 -5
- QuizGenerator/premade_questions/cst463/models/text.py +1 -5
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +20 -3
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +7 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1 -9
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +7 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +0 -4
- QuizGenerator/qrcode_generator.py +116 -55
- QuizGenerator/question.py +30 -16
- QuizGenerator/quiz.py +1 -6
- QuizGenerator/regenerate.py +23 -9
- {quizgenerator-0.9.0.dist-info → quizgenerator-0.10.1.dist-info}/METADATA +26 -17
- quizgenerator-0.10.1.dist-info/RECORD +52 -0
- quizgenerator-0.9.0.dist-info/RECORD +0 -50
- {quizgenerator-0.9.0.dist-info → quizgenerator-0.10.1.dist-info}/WHEEL +0 -0
- {quizgenerator-0.9.0.dist-info → quizgenerator-0.10.1.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.9.0.dist-info → quizgenerator-0.10.1.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/README.md
ADDED
QuizGenerator/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import re
|
|
6
6
|
|
|
7
7
|
def setup_logging() -> None:
|
|
8
|
+
os.makedirs(os.path.join("out", "logs"), exist_ok=True)
|
|
8
9
|
config_path = os.path.join(os.path.dirname(__file__), 'logging.yaml')
|
|
9
10
|
if os.path.exists(config_path):
|
|
10
11
|
with open(config_path, 'r') as f:
|
|
@@ -24,4 +25,4 @@ def setup_logging() -> None:
|
|
|
24
25
|
logging.basicConfig(level=logging.INFO)
|
|
25
26
|
|
|
26
27
|
# Call this once when your application starts
|
|
27
|
-
setup_logging()
|
|
28
|
+
setup_logging()
|
|
@@ -16,17 +16,16 @@ import canvasapi.submission
|
|
|
16
16
|
import canvasapi.exceptions
|
|
17
17
|
import dotenv, os
|
|
18
18
|
import requests
|
|
19
|
-
from canvasapi.util import combine_kwargs
|
|
20
19
|
|
|
21
20
|
try:
|
|
22
|
-
|
|
21
|
+
pass # urllib3 v2
|
|
23
22
|
except Exception:
|
|
24
|
-
|
|
23
|
+
pass # urllib3 v1 fallback
|
|
25
24
|
|
|
26
25
|
import os
|
|
27
26
|
import dotenv
|
|
28
27
|
|
|
29
|
-
from .classes import LMSWrapper, Student, Submission,
|
|
28
|
+
from .classes import LMSWrapper, Student, Submission, FileSubmission__Canvas, TextSubmission__Canvas, QuizSubmission
|
|
30
29
|
|
|
31
30
|
import logging
|
|
32
31
|
|
|
@@ -37,8 +36,12 @@ NUM_WORKERS = 4
|
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
class CanvasInterface:
|
|
40
|
-
def __init__(self, *, prod=False):
|
|
41
|
-
|
|
39
|
+
def __init__(self, *, prod=False, env_path: str | None = None):
|
|
40
|
+
default_env = os.path.join(os.path.expanduser("~"), ".env")
|
|
41
|
+
if env_path and os.path.exists(env_path):
|
|
42
|
+
dotenv.load_dotenv(env_path)
|
|
43
|
+
elif os.path.exists(default_env):
|
|
44
|
+
dotenv.load_dotenv(default_env)
|
|
42
45
|
|
|
43
46
|
self.prod = prod
|
|
44
47
|
if self.prod:
|
QuizGenerator/canvas/classes.py
CHANGED
QuizGenerator/contentast.py
CHANGED
|
@@ -23,6 +23,21 @@ import numpy as np
|
|
|
23
23
|
|
|
24
24
|
log = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
|
+
_PANDOC_OK = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ensure_pandoc() -> bool:
|
|
30
|
+
global _PANDOC_OK
|
|
31
|
+
if _PANDOC_OK is not None:
|
|
32
|
+
return _PANDOC_OK
|
|
33
|
+
try:
|
|
34
|
+
pypandoc.get_pandoc_version()
|
|
35
|
+
_PANDOC_OK = True
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
_PANDOC_OK = False
|
|
38
|
+
log.warning(f"Pandoc not found or unusable: {exc}")
|
|
39
|
+
return _PANDOC_OK
|
|
40
|
+
|
|
26
41
|
|
|
27
42
|
"""
|
|
28
43
|
Content Abstract Syntax Tree - The core content system for quiz generation.
|
|
@@ -221,6 +236,9 @@ class Leaf(Element):
|
|
|
221
236
|
return html_output.strip()
|
|
222
237
|
|
|
223
238
|
case _:
|
|
239
|
+
if not _ensure_pandoc():
|
|
240
|
+
log.warning(f"Pandoc unavailable; returning raw markdown for {output_format} output.")
|
|
241
|
+
return str_to_convert
|
|
224
242
|
output = pypandoc.convert_text(
|
|
225
243
|
str_to_convert,
|
|
226
244
|
output_format,
|
|
@@ -549,16 +567,18 @@ class Question(Container):
|
|
|
549
567
|
|
|
550
568
|
# Build extra_data dict with regeneration metadata if available
|
|
551
569
|
extra_data = {}
|
|
552
|
-
if hasattr(self, 'question_class_name') and hasattr(self, 'generation_seed')
|
|
553
|
-
|
|
554
|
-
):
|
|
555
|
-
if self.question_class_name and self.generation_seed is not None and self.question_version:
|
|
570
|
+
if hasattr(self, 'question_class_name') and hasattr(self, 'generation_seed'):
|
|
571
|
+
if self.question_class_name and self.generation_seed is not None:
|
|
556
572
|
extra_data['question_type'] = self.question_class_name
|
|
557
573
|
extra_data['seed'] = self.generation_seed
|
|
558
|
-
|
|
574
|
+
if hasattr(self, 'question_version') and self.question_version:
|
|
575
|
+
extra_data['version'] = self.question_version
|
|
559
576
|
# Include question-specific configuration parameters if available
|
|
560
577
|
if hasattr(self, 'config_params') and self.config_params:
|
|
561
578
|
extra_data['config'] = self.config_params
|
|
579
|
+
# Include context-derived extras if available
|
|
580
|
+
if hasattr(self, 'qr_context_extras') and self.qr_context_extras:
|
|
581
|
+
extra_data['context'] = self.qr_context_extras
|
|
562
582
|
|
|
563
583
|
qr_path = QuestionQRCode.generate_qr_pdf(
|
|
564
584
|
self.question_number,
|
|
@@ -615,16 +635,18 @@ class Question(Container):
|
|
|
615
635
|
|
|
616
636
|
# Build extra_data dict with regeneration metadata if available
|
|
617
637
|
extra_data = {}
|
|
618
|
-
if hasattr(self, 'question_class_name') and hasattr(self, 'generation_seed')
|
|
619
|
-
|
|
620
|
-
):
|
|
621
|
-
if self.question_class_name and self.generation_seed is not None and self.question_version:
|
|
638
|
+
if hasattr(self, 'question_class_name') and hasattr(self, 'generation_seed'):
|
|
639
|
+
if self.question_class_name and self.generation_seed is not None:
|
|
622
640
|
extra_data['question_type'] = self.question_class_name
|
|
623
641
|
extra_data['seed'] = self.generation_seed
|
|
624
|
-
|
|
642
|
+
if hasattr(self, 'question_version') and self.question_version:
|
|
643
|
+
extra_data['version'] = self.question_version
|
|
625
644
|
# Include question-specific configuration parameters if available
|
|
626
645
|
if hasattr(self, 'config_params') and self.config_params:
|
|
627
646
|
extra_data['config'] = self.config_params
|
|
647
|
+
# Include context-derived extras if available
|
|
648
|
+
if hasattr(self, 'qr_context_extras') and self.qr_context_extras:
|
|
649
|
+
extra_data['context'] = self.qr_context_extras
|
|
628
650
|
|
|
629
651
|
# Generate QR code PNG
|
|
630
652
|
qr_path = QuestionQRCode.generate_qr_pdf(
|
QuizGenerator/generate.py
CHANGED
|
@@ -6,6 +6,7 @@ import random
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import tempfile
|
|
9
|
+
from datetime import datetime
|
|
9
10
|
import traceback
|
|
10
11
|
import re
|
|
11
12
|
from pathlib import Path
|
|
@@ -32,7 +33,18 @@ def parse_args():
|
|
|
32
33
|
|
|
33
34
|
parser.add_argument("--debug", action="store_true", help="Set logging level to debug")
|
|
34
35
|
|
|
35
|
-
parser.add_argument(
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--yaml",
|
|
38
|
+
dest="quiz_yaml",
|
|
39
|
+
default=None,
|
|
40
|
+
help="Path to quiz YAML configuration"
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--quiz_yaml",
|
|
44
|
+
dest="quiz_yaml",
|
|
45
|
+
default=None,
|
|
46
|
+
help=argparse.SUPPRESS # Backwards-compatible alias for --yaml
|
|
47
|
+
)
|
|
36
48
|
parser.add_argument("--seed", type=int, default=None,
|
|
37
49
|
help="Random seed for quiz generation (default: None for random)")
|
|
38
50
|
|
|
@@ -45,7 +57,10 @@ def parse_args():
|
|
|
45
57
|
|
|
46
58
|
# PDF Flags
|
|
47
59
|
parser.add_argument("--num_pdfs", default=0, type=int, help="How many PDF quizzes to create")
|
|
48
|
-
parser.add_argument("--latex", action="store_false", dest="typst", help="Use
|
|
60
|
+
parser.add_argument("--latex", action="store_false", dest="typst", help="Use LaTeX instead of Typst for PDF generation")
|
|
61
|
+
parser.set_defaults(typst=True)
|
|
62
|
+
parser.add_argument("--typst_measurement", action="store_true",
|
|
63
|
+
help="Use Typst measurement for layout optimization (experimental)")
|
|
49
64
|
|
|
50
65
|
# Testing flags
|
|
51
66
|
parser.add_argument("--test_all", type=int, default=0, metavar="N",
|
|
@@ -54,6 +69,8 @@ def parse_args():
|
|
|
54
69
|
help="Only test specific question types by name (use with --test_all)")
|
|
55
70
|
parser.add_argument("--strict", action="store_true",
|
|
56
71
|
help="With --test_all, skip PDF/Canvas generation if any questions fail")
|
|
72
|
+
parser.add_argument("--skip_missing_extras", action="store_true",
|
|
73
|
+
help="With --test_all, skip questions that fail due to missing optional dependencies")
|
|
57
74
|
|
|
58
75
|
subparsers = parser.add_subparsers(dest='command')
|
|
59
76
|
test_parser = subparsers.add_parser("TEST")
|
|
@@ -65,6 +82,10 @@ def parse_args():
|
|
|
65
82
|
log.error("Must provide course_id when pushing to canvas")
|
|
66
83
|
exit(8)
|
|
67
84
|
|
|
85
|
+
if args.test_all <= 0 and not args.quiz_yaml:
|
|
86
|
+
log.error("Must provide --yaml unless using --test_all")
|
|
87
|
+
exit(8)
|
|
88
|
+
|
|
68
89
|
return args
|
|
69
90
|
|
|
70
91
|
|
|
@@ -82,7 +103,8 @@ def test_all_questions(
|
|
|
82
103
|
use_typst: bool = True,
|
|
83
104
|
canvas_course=None,
|
|
84
105
|
strict: bool = False,
|
|
85
|
-
question_filter: list = None
|
|
106
|
+
question_filter: list = None,
|
|
107
|
+
skip_missing_extras: bool = False
|
|
86
108
|
):
|
|
87
109
|
"""
|
|
88
110
|
Test all registered questions by generating N variations of each.
|
|
@@ -129,6 +151,7 @@ def test_all_questions(
|
|
|
129
151
|
print("=" * 70)
|
|
130
152
|
|
|
131
153
|
failed_questions = []
|
|
154
|
+
skipped_questions = []
|
|
132
155
|
successful_questions = []
|
|
133
156
|
# Collect question instances for PDF/Canvas generation
|
|
134
157
|
test_question_instances = []
|
|
@@ -174,6 +197,14 @@ def test_all_questions(
|
|
|
174
197
|
# If we got here, the question works - save the instance
|
|
175
198
|
test_question_instances.append(question)
|
|
176
199
|
|
|
200
|
+
except ImportError as e:
|
|
201
|
+
if skip_missing_extras:
|
|
202
|
+
skipped_questions.append(question_name)
|
|
203
|
+
log.warning(f"Skipping {question_name} due to missing optional dependency: {e}")
|
|
204
|
+
question_failures = []
|
|
205
|
+
break
|
|
206
|
+
tb = traceback.format_exc()
|
|
207
|
+
question_failures.append(f" Variation {variation+1}: Generation failed - {e}\n{tb}")
|
|
177
208
|
except Exception as e:
|
|
178
209
|
tb = traceback.format_exc()
|
|
179
210
|
question_failures.append(f" Variation {variation+1}: Generation failed - {e}\n{tb}")
|
|
@@ -183,6 +214,8 @@ def test_all_questions(
|
|
|
183
214
|
for failure in question_failures:
|
|
184
215
|
print(failure)
|
|
185
216
|
failed_questions.append((question_name, question_failures))
|
|
217
|
+
elif question_name in skipped_questions:
|
|
218
|
+
print(" SKIPPED (missing optional dependency)")
|
|
186
219
|
else:
|
|
187
220
|
print(f" OK ({num_variations}/{num_variations} variations)")
|
|
188
221
|
successful_questions.append(question_name)
|
|
@@ -194,11 +227,17 @@ def test_all_questions(
|
|
|
194
227
|
print(f"Total question types: {total_questions}")
|
|
195
228
|
print(f"Successful: {len(successful_questions)}")
|
|
196
229
|
print(f"Failed: {len(failed_questions)}")
|
|
230
|
+
if skipped_questions:
|
|
231
|
+
print(f"Skipped (missing extras): {len(set(skipped_questions))}")
|
|
197
232
|
|
|
198
233
|
if failed_questions:
|
|
199
234
|
print("\nFailed questions:")
|
|
200
235
|
for name, failures in failed_questions:
|
|
201
236
|
print(f" - {name}: {len(failures)} failures")
|
|
237
|
+
if skipped_questions:
|
|
238
|
+
print("\nSkipped questions (missing extras):")
|
|
239
|
+
for name in sorted(set(skipped_questions)):
|
|
240
|
+
print(f" - {name}")
|
|
202
241
|
|
|
203
242
|
print("=" * 70)
|
|
204
243
|
|
|
@@ -258,7 +297,9 @@ def generate_latex(latex_text, remove_previous=False, name_prefix=None):
|
|
|
258
297
|
tmp_tex.write(latex_text)
|
|
259
298
|
|
|
260
299
|
tmp_tex.flush()
|
|
261
|
-
|
|
300
|
+
os.makedirs(os.path.join("out", "debug"), exist_ok=True)
|
|
301
|
+
debug_name = f"debug-{datetime.now().strftime('%Y%m%d-%H%M%S')}.tex"
|
|
302
|
+
shutil.copy(f"{tmp_tex.name}", os.path.join("out", "debug", debug_name))
|
|
262
303
|
p = subprocess.Popen(
|
|
263
304
|
f"latexmk -pdf -output-directory={os.path.join(os.getcwd(), 'out')} {tmp_tex.name}",
|
|
264
305
|
shell=True,
|
|
@@ -331,7 +372,9 @@ def generate_typst(typst_text, remove_previous=False, name_prefix=None):
|
|
|
331
372
|
tmp_typ.close()
|
|
332
373
|
|
|
333
374
|
# Save debug copy
|
|
334
|
-
|
|
375
|
+
os.makedirs(os.path.join("out", "debug"), exist_ok=True)
|
|
376
|
+
debug_name = f"debug-{datetime.now().strftime('%Y%m%d-%H%M%S')}.typ"
|
|
377
|
+
shutil.copy(tmp_typ.name, os.path.join("out", "debug", debug_name))
|
|
335
378
|
|
|
336
379
|
# Compile with typst
|
|
337
380
|
output_pdf = os.path.join(os.getcwd(), 'out', os.path.basename(tmp_typ.name).replace('.typ', '.pdf'))
|
|
@@ -367,14 +410,15 @@ def generate_quiz(
|
|
|
367
410
|
delete_assignment_group=False,
|
|
368
411
|
use_typst=False,
|
|
369
412
|
use_typst_measurement=False,
|
|
370
|
-
base_seed=None
|
|
413
|
+
base_seed=None,
|
|
414
|
+
env_path=None
|
|
371
415
|
):
|
|
372
416
|
|
|
373
417
|
quizzes = Quiz.from_yaml(path_to_quiz_yaml)
|
|
374
418
|
|
|
375
419
|
# Handle Canvas uploads with shared assignment group
|
|
376
420
|
if num_canvas > 0:
|
|
377
|
-
canvas_interface = CanvasInterface(prod=use_prod)
|
|
421
|
+
canvas_interface = CanvasInterface(prod=use_prod, env_path=env_path)
|
|
378
422
|
canvas_course = canvas_interface.get_course(course_id=course_id)
|
|
379
423
|
|
|
380
424
|
# Create assignment group once, with delete flag if specified
|
|
@@ -448,7 +492,7 @@ def main():
|
|
|
448
492
|
# Set up Canvas course if course_id provided
|
|
449
493
|
canvas_course = None
|
|
450
494
|
if args.course_id:
|
|
451
|
-
canvas_interface = CanvasInterface(prod=args.prod)
|
|
495
|
+
canvas_interface = CanvasInterface(prod=args.prod, env_path=args.env)
|
|
452
496
|
canvas_course = canvas_interface.get_course(course_id=args.course_id)
|
|
453
497
|
|
|
454
498
|
success = test_all_questions(
|
|
@@ -457,7 +501,8 @@ def main():
|
|
|
457
501
|
use_typst=getattr(args, 'typst', True),
|
|
458
502
|
canvas_course=canvas_course,
|
|
459
503
|
strict=args.strict,
|
|
460
|
-
question_filter=args.test_questions
|
|
504
|
+
question_filter=args.test_questions,
|
|
505
|
+
skip_missing_extras=args.skip_missing_extras
|
|
461
506
|
)
|
|
462
507
|
exit(0 if success else 1)
|
|
463
508
|
|
|
@@ -471,9 +516,10 @@ def main():
|
|
|
471
516
|
use_prod=args.prod,
|
|
472
517
|
course_id=args.course_id,
|
|
473
518
|
delete_assignment_group=getattr(args, 'delete_assignment_group', False),
|
|
474
|
-
use_typst=getattr(args, 'typst',
|
|
519
|
+
use_typst=getattr(args, 'typst', True),
|
|
475
520
|
use_typst_measurement=getattr(args, 'typst_measurement', False),
|
|
476
|
-
base_seed=getattr(args, 'seed', None)
|
|
521
|
+
base_seed=getattr(args, 'seed', None),
|
|
522
|
+
env_path=args.env
|
|
477
523
|
)
|
|
478
524
|
|
|
479
525
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
disable_existing_loggers: false
|
|
3
|
+
|
|
4
|
+
formatters:
|
|
5
|
+
standard:
|
|
6
|
+
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
7
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
8
|
+
|
|
9
|
+
detailed:
|
|
10
|
+
format: '%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s:%(lineno)d - %(message)s'
|
|
11
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
12
|
+
|
|
13
|
+
simple:
|
|
14
|
+
format: '%(levelname)s - %(message)s'
|
|
15
|
+
|
|
16
|
+
handlers:
|
|
17
|
+
console:
|
|
18
|
+
class: logging.StreamHandler
|
|
19
|
+
level: INFO
|
|
20
|
+
formatter: standard
|
|
21
|
+
stream: ext://sys.stdout
|
|
22
|
+
|
|
23
|
+
file:
|
|
24
|
+
class: logging.FileHandler
|
|
25
|
+
level: INFO
|
|
26
|
+
formatter: detailed
|
|
27
|
+
filename: ${LOG_FILE:-out/logs/teachingtools.log}
|
|
28
|
+
mode: a
|
|
29
|
+
|
|
30
|
+
error_file:
|
|
31
|
+
class: logging.FileHandler
|
|
32
|
+
level: ERROR
|
|
33
|
+
formatter: detailed
|
|
34
|
+
filename: ${ERROR_LOG_FILE:-out/logs/teachingtools_errors.log}
|
|
35
|
+
mode: a
|
|
36
|
+
|
|
37
|
+
loggers:
|
|
38
|
+
QuizGenerator:
|
|
39
|
+
level: INFO
|
|
40
|
+
handlers: [console, file]
|
|
41
|
+
propagate: false
|
|
42
|
+
|
|
43
|
+
lms_interface:
|
|
44
|
+
level: INFO
|
|
45
|
+
handlers: [console, file]
|
|
46
|
+
propagate: false
|
|
47
|
+
|
|
48
|
+
canvasapi:
|
|
49
|
+
level: WARNING
|
|
50
|
+
handlers: [console]
|
|
51
|
+
propagate: false
|
|
52
|
+
|
|
53
|
+
root:
|
|
54
|
+
level: INFO
|
|
55
|
+
handlers: [console, file, error_file]
|
QuizGenerator/misc.py
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
#!env python
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import decimal
|
|
5
|
-
import enum
|
|
6
|
-
import itertools
|
|
7
4
|
import logging
|
|
8
|
-
import math
|
|
9
|
-
import numpy as np
|
|
10
|
-
from typing import List, Dict, Tuple, Any
|
|
11
5
|
|
|
12
|
-
import fractions
|
|
13
6
|
|
|
14
|
-
import QuizGenerator.contentast as ca
|
|
15
7
|
|
|
16
8
|
log = logging.getLogger(__name__)
|
|
17
9
|
|
|
@@ -8,7 +8,7 @@ import enum
|
|
|
8
8
|
import logging
|
|
9
9
|
import random
|
|
10
10
|
import math
|
|
11
|
-
from typing import List
|
|
11
|
+
from typing import List
|
|
12
12
|
|
|
13
13
|
import QuizGenerator.contentast as ca
|
|
14
14
|
from QuizGenerator.question import Question, QuestionRegistry, RegenerableChoiceMixin
|
|
@@ -57,8 +57,7 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
|
|
|
57
57
|
@classmethod
|
|
58
58
|
def _build_body(cls, context):
|
|
59
59
|
"""Build question body and collect answers."""
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
|
|
62
61
|
# Create table data with one blank cell
|
|
63
62
|
table_data = [{}]
|
|
64
63
|
for target in list(cls.Target):
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
from .gradient_descent_questions import GradientDescentWalkthrough
|
|
2
2
|
from .gradient_calculation import DerivativeBasic, DerivativeChain
|
|
3
|
-
from .loss_calculations import LossQuestion_Linear, LossQuestion_Logistic, LossQuestion_MulticlassLogistic
|
|
3
|
+
from .loss_calculations import LossQuestion_Linear, LossQuestion_Logistic, LossQuestion_MulticlassLogistic
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"GradientDescentWalkthrough",
|
|
7
|
+
"DerivativeBasic",
|
|
8
|
+
"DerivativeChain",
|
|
9
|
+
"LossQuestion_Linear",
|
|
10
|
+
"LossQuestion_Logistic",
|
|
11
|
+
"LossQuestion_MulticlassLogistic",
|
|
12
|
+
]
|
|
@@ -58,7 +58,6 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
58
58
|
|
|
59
59
|
# Create answer for each partial derivative
|
|
60
60
|
for i in range(context.num_variables):
|
|
61
|
-
answer_key = f"partial_derivative_{i}"
|
|
62
61
|
# Evaluate the partial derivative and convert to float
|
|
63
62
|
partial_value = context.gradient_function[i].subs(subs_map)
|
|
64
63
|
try:
|
|
@@ -2,8 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
4
|
import logging
|
|
5
|
-
import
|
|
6
|
-
from typing import List, Tuple, Callable, Union, Any
|
|
5
|
+
from typing import List, Tuple
|
|
7
6
|
|
|
8
7
|
import sympy
|
|
9
8
|
import sympy as sp
|
|
@@ -151,8 +150,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
151
150
|
|
|
152
151
|
# Introduction
|
|
153
152
|
objective = "minimize" if self.minimize else "maximize"
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
|
|
156
154
|
body.add_element(
|
|
157
155
|
ca.Paragraph(
|
|
158
156
|
[
|
|
@@ -3,8 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import abc
|
|
4
4
|
import logging
|
|
5
5
|
import math
|
|
6
|
-
import
|
|
7
|
-
from typing import List, Tuple, Dict, Any
|
|
6
|
+
from typing import List, Tuple
|
|
8
7
|
|
|
9
8
|
import QuizGenerator.contentast as ca
|
|
10
9
|
from QuizGenerator.question import Question, QuestionRegistry
|
|
@@ -198,7 +197,8 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
198
197
|
@classmethod
|
|
199
198
|
def _generate_data(cls, context):
|
|
200
199
|
"""Generate regression data with continuous target values."""
|
|
201
|
-
context.data =
|
|
200
|
+
context.data = {}
|
|
201
|
+
context["data"] = []
|
|
202
202
|
|
|
203
203
|
for _ in range(context.num_samples):
|
|
204
204
|
sample = {}
|
|
@@ -227,7 +227,7 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
227
227
|
for _ in range(context.num_output_vars)
|
|
228
228
|
]
|
|
229
229
|
|
|
230
|
-
context
|
|
230
|
+
context["data"].append(sample)
|
|
231
231
|
|
|
232
232
|
@classmethod
|
|
233
233
|
def _calculate_losses(cls, context):
|
|
@@ -235,7 +235,7 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
235
235
|
context.individual_losses = []
|
|
236
236
|
total_loss = 0.0
|
|
237
237
|
|
|
238
|
-
for sample in context
|
|
238
|
+
for sample in context["data"]:
|
|
239
239
|
if context.num_output_vars == 1:
|
|
240
240
|
# Single output MSE: (y - p)^2
|
|
241
241
|
loss = (sample['true_values'] - sample['predictions']) ** 2
|
|
@@ -282,7 +282,7 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
282
282
|
headers.append("loss")
|
|
283
283
|
|
|
284
284
|
rows = []
|
|
285
|
-
for i, sample in enumerate(context
|
|
285
|
+
for i, sample in enumerate(context["data"]):
|
|
286
286
|
row = {}
|
|
287
287
|
|
|
288
288
|
# Input features as vector
|
|
@@ -315,7 +315,7 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
315
315
|
"""Show step-by-step MSE calculations."""
|
|
316
316
|
steps = ca.Section()
|
|
317
317
|
|
|
318
|
-
for i, sample in enumerate(context
|
|
318
|
+
for i, sample in enumerate(context["data"]):
|
|
319
319
|
steps.add_element(ca.Paragraph([f"Sample {i+1}:"]))
|
|
320
320
|
|
|
321
321
|
if context.num_output_vars == 1:
|
|
@@ -364,7 +364,7 @@ class LossQuestion_Linear(LossQuestion):
|
|
|
364
364
|
headers.append("loss")
|
|
365
365
|
|
|
366
366
|
rows = []
|
|
367
|
-
for i, sample in enumerate(context
|
|
367
|
+
for i, sample in enumerate(context["data"]):
|
|
368
368
|
row = []
|
|
369
369
|
|
|
370
370
|
# Input features
|
|
@@ -416,7 +416,8 @@ class LossQuestion_Logistic(LossQuestion):
|
|
|
416
416
|
@classmethod
|
|
417
417
|
def _generate_data(cls, context):
|
|
418
418
|
"""Generate binary classification data."""
|
|
419
|
-
context.data =
|
|
419
|
+
context.data = {}
|
|
420
|
+
context["data"] = []
|
|
420
421
|
|
|
421
422
|
for _ in range(context.num_samples):
|
|
422
423
|
sample = {}
|
|
@@ -433,7 +434,7 @@ class LossQuestion_Logistic(LossQuestion):
|
|
|
433
434
|
# Generate predicted probabilities (between 0 and 1, rounded to 3 decimal places)
|
|
434
435
|
sample['predictions'] = round(context.rng.uniform(0.1, 0.9), 3) # Avoid extreme values
|
|
435
436
|
|
|
436
|
-
context
|
|
437
|
+
context["data"].append(sample)
|
|
437
438
|
|
|
438
439
|
@classmethod
|
|
439
440
|
def _calculate_losses(cls, context):
|
|
@@ -441,7 +442,7 @@ class LossQuestion_Logistic(LossQuestion):
|
|
|
441
442
|
context.individual_losses = []
|
|
442
443
|
total_loss = 0.0
|
|
443
444
|
|
|
444
|
-
for sample in context
|
|
445
|
+
for sample in context["data"]:
|
|
445
446
|
y = sample['true_values']
|
|
446
447
|
p = sample['predictions']
|
|
447
448
|
|
|
@@ -475,7 +476,7 @@ class LossQuestion_Logistic(LossQuestion):
|
|
|
475
476
|
headers = ["x", "y", "p", "loss"]
|
|
476
477
|
|
|
477
478
|
rows = []
|
|
478
|
-
for i, sample in enumerate(context
|
|
479
|
+
for i, sample in enumerate(context["data"]):
|
|
479
480
|
row = {}
|
|
480
481
|
|
|
481
482
|
# Input features as vector
|
|
@@ -500,7 +501,7 @@ class LossQuestion_Logistic(LossQuestion):
|
|
|
500
501
|
"""Show step-by-step log-loss calculations."""
|
|
501
502
|
steps = ca.Section()
|
|
502
503
|
|
|
503
|
-
for i, sample in enumerate(context
|
|
504
|
+
for i, sample in enumerate(context["data"]):
|
|
504
505
|
y = sample['true_values']
|
|
505
506
|
p = sample['predictions']
|
|
506
507
|
loss = context.individual_losses[i]
|
|
@@ -522,7 +523,7 @@ class LossQuestion_Logistic(LossQuestion):
|
|
|
522
523
|
headers = ["x_0", "x_1", "y", "p", "loss"]
|
|
523
524
|
|
|
524
525
|
rows = []
|
|
525
|
-
for i, sample in enumerate(context
|
|
526
|
+
for i, sample in enumerate(context["data"]):
|
|
526
527
|
row = []
|
|
527
528
|
|
|
528
529
|
# Input features
|
|
@@ -585,7 +586,8 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
585
586
|
@classmethod
|
|
586
587
|
def _generate_data(cls, context):
|
|
587
588
|
"""Generate multi-class classification data."""
|
|
588
|
-
context.data =
|
|
589
|
+
context.data = {}
|
|
590
|
+
context["data"] = []
|
|
589
591
|
|
|
590
592
|
for _ in range(context.num_samples):
|
|
591
593
|
sample = {}
|
|
@@ -606,7 +608,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
606
608
|
prob_sum = sum(raw_probs)
|
|
607
609
|
sample['predictions'] = [round(p / prob_sum, 3) for p in raw_probs]
|
|
608
610
|
|
|
609
|
-
context
|
|
611
|
+
context["data"].append(sample)
|
|
610
612
|
|
|
611
613
|
@classmethod
|
|
612
614
|
def _calculate_losses(cls, context):
|
|
@@ -614,7 +616,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
614
616
|
context.individual_losses = []
|
|
615
617
|
total_loss = 0.0
|
|
616
618
|
|
|
617
|
-
for sample in context
|
|
619
|
+
for sample in context["data"]:
|
|
618
620
|
y_vec = sample['true_values']
|
|
619
621
|
p_vec = sample['predictions']
|
|
620
622
|
|
|
@@ -645,7 +647,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
645
647
|
headers = ["x", "y", "p", "loss"]
|
|
646
648
|
|
|
647
649
|
rows = []
|
|
648
|
-
for i, sample in enumerate(context
|
|
650
|
+
for i, sample in enumerate(context["data"]):
|
|
649
651
|
row = {}
|
|
650
652
|
|
|
651
653
|
# Input features as vector
|
|
@@ -672,7 +674,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
672
674
|
"""Show step-by-step cross-entropy calculations."""
|
|
673
675
|
steps = ca.Section()
|
|
674
676
|
|
|
675
|
-
for i, sample in enumerate(context
|
|
677
|
+
for i, sample in enumerate(context["data"]):
|
|
676
678
|
y_vec = sample['true_values']
|
|
677
679
|
p_vec = sample['predictions']
|
|
678
680
|
loss = context.individual_losses[i]
|
|
@@ -710,7 +712,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
|
|
|
710
712
|
headers = ["x_0", "x_1", "y", "p", "loss"]
|
|
711
713
|
|
|
712
714
|
rows = []
|
|
713
|
-
for i, sample in enumerate(context
|
|
715
|
+
for i, sample in enumerate(context["data"]):
|
|
714
716
|
row = []
|
|
715
717
|
|
|
716
718
|
# Input features
|
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
from .matrix_questions import MatrixAddition, MatrixScalarMultiplication, MatrixMultiplication
|
|
2
|
-
from .vector_questions import VectorAddition, VectorScalarMultiplication, VectorDotProduct, VectorMagnitude
|
|
2
|
+
from .vector_questions import VectorAddition, VectorScalarMultiplication, VectorDotProduct, VectorMagnitude
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"MatrixAddition",
|
|
6
|
+
"MatrixScalarMultiplication",
|
|
7
|
+
"MatrixMultiplication",
|
|
8
|
+
"VectorAddition",
|
|
9
|
+
"VectorScalarMultiplication",
|
|
10
|
+
"VectorDotProduct",
|
|
11
|
+
"VectorMagnitude",
|
|
12
|
+
]
|