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.
Files changed (37) hide show
  1. QuizGenerator/README.md +5 -0
  2. QuizGenerator/__init__.py +2 -1
  3. QuizGenerator/canvas/canvas_interface.py +9 -6
  4. QuizGenerator/canvas/classes.py +0 -1
  5. QuizGenerator/contentast.py +32 -10
  6. QuizGenerator/generate.py +57 -11
  7. QuizGenerator/logging.yaml +55 -0
  8. QuizGenerator/misc.py +0 -8
  9. QuizGenerator/premade_questions/cst334/memory_questions.py +2 -3
  10. QuizGenerator/premade_questions/cst334/process.py +0 -1
  11. QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +10 -1
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +0 -1
  13. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +2 -4
  14. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +22 -20
  15. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +1 -1
  16. QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +11 -1
  17. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +0 -1
  18. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +0 -1
  19. QuizGenerator/premade_questions/cst463/models/attention.py +1 -5
  20. QuizGenerator/premade_questions/cst463/models/cnns.py +1 -5
  21. QuizGenerator/premade_questions/cst463/models/rnns.py +1 -5
  22. QuizGenerator/premade_questions/cst463/models/text.py +1 -5
  23. QuizGenerator/premade_questions/cst463/models/weight_counting.py +20 -3
  24. QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +7 -0
  25. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1 -9
  26. QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +7 -0
  27. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +0 -4
  28. QuizGenerator/qrcode_generator.py +116 -55
  29. QuizGenerator/question.py +30 -16
  30. QuizGenerator/quiz.py +1 -6
  31. QuizGenerator/regenerate.py +23 -9
  32. {quizgenerator-0.9.0.dist-info → quizgenerator-0.10.1.dist-info}/METADATA +26 -17
  33. quizgenerator-0.10.1.dist-info/RECORD +52 -0
  34. quizgenerator-0.9.0.dist-info/RECORD +0 -50
  35. {quizgenerator-0.9.0.dist-info → quizgenerator-0.10.1.dist-info}/WHEEL +0 -0
  36. {quizgenerator-0.9.0.dist-info → quizgenerator-0.10.1.dist-info}/entry_points.txt +0 -0
  37. {quizgenerator-0.9.0.dist-info → quizgenerator-0.10.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,5 @@
1
+
2
+
3
+ ## Installation
4
+
5
+ Note, you will need to install `pandoc` prior to running since it is used to convert to HTML (for canvas) and Latex (for PDF)`
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
- from urllib3.util.retry import Retry # urllib3 v2
21
+ pass # urllib3 v2
23
22
  except Exception:
24
- from urllib3.util import Retry # urllib3 v1 fallback
23
+ pass # urllib3 v1 fallback
25
24
 
26
25
  import os
27
26
  import dotenv
28
27
 
29
- from .classes import LMSWrapper, Student, Submission, Submission__Canvas, FileSubmission__Canvas, TextSubmission__Canvas, QuizSubmission
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
- dotenv.load_dotenv(os.path.join(os.path.expanduser("~"), ".env"))
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:
@@ -6,7 +6,6 @@ import logging
6
6
  import dataclasses
7
7
  import functools
8
8
  import io
9
- import os
10
9
  import urllib.request
11
10
  from typing import Optional, List, Dict
12
11
 
@@ -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') and hasattr(
553
- self, 'question_version'
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
- extra_data['version'] = self.question_version
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') and hasattr(
619
- self, 'question_version'
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
- extra_data['version'] = self.question_version
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("--quiz_yaml", default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "example_files/exam_generation.yaml"))
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 Typst instead of LaTeX for PDF generation")
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
- shutil.copy(f"{tmp_tex.name}", "debug.tex")
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
- shutil.copy(tmp_typ.name, "debug.typ")
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', False),
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, Optional
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
- answer = context["possible_answers"][context["blank_kind"]]
61
-
60
+
62
61
  # Create table data with one blank cell
63
62
  table_data = [{}]
64
63
  for target in list(cls.Target):
@@ -387,7 +387,6 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
387
387
  Returns:
388
388
  Tuple of (body_ast, answers_list)
389
389
  """
390
- from typing import List
391
390
  answers: List[ca.Answer] = []
392
391
 
393
392
  # Create table data for scheduling results
@@ -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 math
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
- sign = "-" if self.minimize else "+"
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 numpy as np
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.data.append(sample)
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.data:
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.data):
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.data):
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.data):
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.data.append(sample)
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.data:
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.data):
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.data):
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.data):
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.data.append(sample)
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.data:
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.data):
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.data):
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.data):
715
+ for i, sample in enumerate(context["data"]):
714
716
  row = []
715
717
 
716
718
  # Input features
@@ -1,5 +1,5 @@
1
1
 
2
- from typing import List, Tuple, Callable, Union, Any
2
+ from typing import List, Any
3
3
  import sympy as sp
4
4
 
5
5
  import QuizGenerator.contentast as ca
@@ -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
+ ]
@@ -1,5 +1,4 @@
1
1
  #!env python
2
- import abc
3
2
  import logging
4
3
  import random
5
4
  from typing import List, Tuple