QuizGenerator 0.4.3__py3-none-any.whl → 0.5.0__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 (31) hide show
  1. QuizGenerator/contentast.py +949 -80
  2. QuizGenerator/generate.py +44 -7
  3. QuizGenerator/misc.py +4 -554
  4. QuizGenerator/mixins.py +47 -25
  5. QuizGenerator/premade_questions/cst334/languages.py +139 -125
  6. QuizGenerator/premade_questions/cst334/math_questions.py +78 -66
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +258 -144
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +71 -33
  9. QuizGenerator/premade_questions/cst334/process.py +51 -20
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +32 -6
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +59 -34
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +27 -8
  13. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +53 -32
  14. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +228 -88
  15. QuizGenerator/premade_questions/cst463/models/attention.py +26 -10
  16. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -19
  17. QuizGenerator/premade_questions/cst463/models/rnns.py +25 -12
  18. QuizGenerator/premade_questions/cst463/models/text.py +26 -11
  19. QuizGenerator/premade_questions/cst463/models/weight_counting.py +36 -22
  20. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +89 -109
  21. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +126 -53
  22. QuizGenerator/question.py +110 -15
  23. QuizGenerator/quiz.py +74 -23
  24. QuizGenerator/regenerate.py +98 -29
  25. {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/METADATA +1 -1
  26. {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/RECORD +29 -31
  27. QuizGenerator/README.md +0 -5
  28. QuizGenerator/logging.yaml +0 -55
  29. {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/WHEEL +0 -0
  30. {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/entry_points.txt +0 -0
  31. {quizgenerator-0.4.3.dist-info → quizgenerator-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -15,6 +15,11 @@ from .misc import generate_function, format_vector
15
15
  log = logging.getLogger(__name__)
16
16
 
17
17
 
18
+ # Note: This file does not use ContentAST.Answer wrappers - it uses TableQuestionMixin
19
+ # which handles answer display through create_answer_table(). The answers are created
20
+ # with labels embedded at creation time in refresh().
21
+
22
+
18
23
  class GradientDescentQuestion(Question, abc.ABC):
19
24
  def __init__(self, *args, **kwargs):
20
25
  kwargs["topic"] = kwargs.get("topic", Question.Topic.ML_OPTIMIZATION)
@@ -86,30 +91,32 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
86
91
 
87
92
  # Set up answers
88
93
  self.answers = {}
89
-
94
+
90
95
  # Answers for each step
91
96
  for i, result in enumerate(self.gradient_descent_results):
92
97
  step = result['step']
93
-
98
+
94
99
  # Location answer
95
100
  location_key = f"answer__location_{step}"
96
- self.answers[location_key] = Answer.vector_value(location_key, list(result['location']))
97
-
101
+ self.answers[location_key] = Answer.vector_value(location_key, list(result['location']), label=f"Location at step {step}")
102
+
98
103
  # Gradient answer
99
104
  gradient_key = f"answer__gradient_{step}"
100
- self.answers[gradient_key] = Answer.vector_value(gradient_key, list(result['gradient']))
101
-
105
+ self.answers[gradient_key] = Answer.vector_value(gradient_key, list(result['gradient']), label=f"Gradient at step {step}")
106
+
102
107
  # Update answer
103
108
  update_key = f"answer__update_{step}"
104
- self.answers[update_key] = Answer.vector_value(update_key, list(result['update']))
109
+ self.answers[update_key] = Answer.vector_value(update_key, list(result['update']), label=f"Update at step {step}")
105
110
 
106
- def get_body(self, **kwargs) -> ContentAST.Section:
111
+ def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
112
+ """Build question body and collect answers."""
107
113
  body = ContentAST.Section()
108
-
114
+ answers = []
115
+
109
116
  # Introduction
110
117
  objective = "minimize" if self.minimize else "maximize"
111
118
  sign = "-" if self.minimize else "+"
112
-
119
+
113
120
  body.add_element(
114
121
  ContentAST.Paragraph(
115
122
  [
@@ -122,7 +129,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
122
129
  ]
123
130
  )
124
131
  )
125
-
132
+
126
133
  # Create table data - use ContentAST.Equation for proper LaTeX rendering in headers
127
134
  headers = [
128
135
  "n",
@@ -131,36 +138,49 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
131
138
  ContentAST.Equation("\\alpha \\cdot \\nabla f", inline=True)
132
139
  ]
133
140
  table_rows = []
134
-
141
+
135
142
  for i in range(self.num_steps):
136
143
  step = i + 1
137
144
  row = {"n": str(step)}
138
-
145
+
139
146
  if step == 1:
140
-
147
+
141
148
  # Fill in starting location for first row with default formatting
142
149
  row["location"] = f"{format_vector(self.starting_point)}"
143
150
  row[headers[2]] = f"answer__gradient_{step}" # gradient column
144
151
  row[headers[3]] = f"answer__update_{step}" # update column
152
+ # Collect answers for this step (no location answer for step 1)
153
+ answers.append(self.answers[f"answer__gradient_{step}"])
154
+ answers.append(self.answers[f"answer__update_{step}"])
145
155
  else:
146
156
  # Subsequent rows - all answer fields
147
157
  row["location"] = f"answer__location_{step}"
148
158
  row[headers[2]] = f"answer__gradient_{step}" # gradient column
149
159
  row[headers[3]] = f"answer__update_{step}" # update column
160
+ # Collect all answers for this step
161
+ answers.append(self.answers[f"answer__location_{step}"])
162
+ answers.append(self.answers[f"answer__gradient_{step}"])
163
+ answers.append(self.answers[f"answer__update_{step}"])
150
164
  table_rows.append(row)
151
-
165
+
152
166
  # Create the table using mixin
153
167
  gradient_table = self.create_answer_table(
154
168
  headers=headers,
155
169
  data_rows=table_rows,
156
170
  answer_columns=["location", headers[2], headers[3]] # Use actual header objects
157
171
  )
158
-
172
+
159
173
  body.add_element(gradient_table)
160
-
174
+
175
+ return body, answers
176
+
177
+ def get_body(self, **kwargs) -> ContentAST.Section:
178
+ """Build question body (backward compatible interface)."""
179
+ body, _ = self._get_body(**kwargs)
161
180
  return body
162
181
 
163
- def get_explanation(self, **kwargs) -> ContentAST.Section:
182
+ def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[Answer]]:
183
+ """Build question explanation."""
164
184
  explanation = ContentAST.Section()
165
185
 
166
186
  explanation.add_element(
@@ -201,7 +221,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
201
221
  ]
202
222
  )
203
223
  )
204
-
224
+
205
225
  # Add completed table showing all solutions
206
226
  explanation.add_element(
207
227
  ContentAST.Paragraph(
@@ -210,7 +230,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
210
230
  ]
211
231
  )
212
232
  )
213
-
233
+
214
234
  # Create filled solution table
215
235
  solution_headers = [
216
236
  "n",
@@ -218,31 +238,31 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
218
238
  ContentAST.Equation("\\nabla f", inline=True),
219
239
  ContentAST.Equation("\\alpha \\cdot \\nabla f", inline=True)
220
240
  ]
221
-
241
+
222
242
  solution_rows = []
223
243
  for i, result in enumerate(self.gradient_descent_results):
224
244
  step = result['step']
225
245
  row = {"n": str(step)}
226
-
246
+
227
247
  row["location"] = f"{format_vector(result['location'])}"
228
248
  row[solution_headers[2]] = f"{format_vector(result['gradient'])}"
229
249
  row[solution_headers[3]] = f"{format_vector(result['update'])}"
230
-
250
+
231
251
  solution_rows.append(row)
232
-
252
+
233
253
  # Create solution table (non-answer table, just display)
234
254
  solution_table = self.create_answer_table(
235
255
  headers=solution_headers,
236
256
  data_rows=solution_rows,
237
257
  answer_columns=[] # No answer columns since this is just for display
238
258
  )
239
-
259
+
240
260
  explanation.add_element(solution_table)
241
-
261
+
242
262
  # Step-by-step explanation
243
263
  for i, result in enumerate(self.gradient_descent_results):
244
264
  step = result['step']
245
-
265
+
246
266
  explanation.add_element(
247
267
  ContentAST.Paragraph(
248
268
  [
@@ -250,7 +270,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
250
270
  ]
251
271
  )
252
272
  )
253
-
273
+
254
274
  explanation.add_element(
255
275
  ContentAST.Paragraph(
256
276
  [
@@ -258,7 +278,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
258
278
  ]
259
279
  )
260
280
  )
261
-
281
+
262
282
  explanation.add_element(
263
283
  ContentAST.Paragraph(
264
284
  [
@@ -266,7 +286,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
266
286
  ]
267
287
  )
268
288
  )
269
-
289
+
270
290
  explanation.add_element(
271
291
  ContentAST.Paragraph(
272
292
  [
@@ -278,13 +298,13 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
278
298
  ]
279
299
  )
280
300
  )
281
-
301
+
282
302
  if step < len(self.gradient_descent_results):
283
303
  # Calculate next location for display
284
304
  current_loc = result['location']
285
305
  update = result['update']
286
306
  next_loc = [current_loc[j] - update[j] for j in range(len(current_loc))]
287
-
307
+
288
308
  explanation.add_element(
289
309
  ContentAST.Paragraph(
290
310
  [
@@ -292,7 +312,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
292
312
  ]
293
313
  )
294
314
  )
295
-
315
+
296
316
  function_values = [r['function_value'] for r in self.gradient_descent_results]
297
317
  explanation.add_element(
298
318
  ContentAST.Paragraph(
@@ -301,5 +321,10 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
301
321
  ]
302
322
  )
303
323
  )
304
-
324
+
325
+ return explanation, []
326
+
327
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
328
+ """Build question explanation (backward compatible interface)."""
329
+ explanation, _ = self._get_explanation(**kwargs)
305
330
  return explanation
@@ -13,6 +13,9 @@ from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
13
13
  log = logging.getLogger(__name__)
14
14
 
15
15
 
16
+ # Note: This file migrates to the _get_body()/_get_explanation() pattern
17
+
18
+
16
19
  class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
17
20
  """Base class for loss function calculation questions."""
18
21
 
@@ -70,14 +73,15 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
70
73
 
71
74
  # Individual loss answers
72
75
  for i in range(self.num_samples):
73
- self.answers[f"loss_{i}"] = Answer.float_value(f"loss_{i}", self.individual_losses[i])
76
+ self.answers[f"loss_{i}"] = Answer.float_value(f"loss_{i}", self.individual_losses[i], label=f"Sample {i+1} loss")
74
77
 
75
78
  # Overall loss answer
76
- self.answers["overall_loss"] = Answer.float_value("overall_loss", self.overall_loss)
79
+ self.answers["overall_loss"] = Answer.float_value("overall_loss", self.overall_loss, label="Overall loss")
77
80
 
78
- def get_body(self) -> ContentAST.Element:
79
- """Generate the question body with data table."""
81
+ def _get_body(self, **kwargs) -> Tuple[ContentAST.Element, List[Answer]]:
82
+ """Build question body and collect answers."""
80
83
  body = ContentAST.Section()
84
+ answers = []
81
85
 
82
86
  # Question description
83
87
  body.add_element(ContentAST.Paragraph([
@@ -85,15 +89,25 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
85
89
  f"and the overall {self._get_loss_function_short_name()}."
86
90
  ]))
87
91
 
88
- # Data table
92
+ # Data table (contains individual loss answers)
89
93
  body.add_element(self._create_data_table())
90
94
 
95
+ # Collect individual loss answers
96
+ for i in range(self.num_samples):
97
+ answers.append(self.answers[f"loss_{i}"])
98
+
91
99
  # Overall loss question
92
100
  body.add_element(ContentAST.Paragraph([
93
101
  f"Overall {self._get_loss_function_short_name()}: "
94
102
  ]))
95
- body.add_element(ContentAST.Answer(self.answers["overall_loss"]))
103
+ answers.append(self.answers["overall_loss"])
104
+ body.add_element(self.answers["overall_loss"])
105
+
106
+ return body, answers
96
107
 
108
+ def get_body(self, **kwargs) -> ContentAST.Element:
109
+ """Build question body (backward compatible interface)."""
110
+ body, _ = self._get_body(**kwargs)
97
111
  return body
98
112
 
99
113
  @abc.abstractmethod
@@ -101,8 +115,8 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
101
115
  """Create the data table with answer fields."""
102
116
  pass
103
117
 
104
- def get_explanation(self) -> ContentAST.Element:
105
- """Generate detailed explanation of the loss calculations."""
118
+ def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Element, List[Answer]]:
119
+ """Build question explanation."""
106
120
  explanation = ContentAST.Section()
107
121
 
108
122
  explanation.add_element(ContentAST.Paragraph([
@@ -121,6 +135,11 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
121
135
  # Overall loss calculation
122
136
  explanation.add_element(self._create_overall_loss_explanation())
123
137
 
138
+ return explanation, []
139
+
140
+ def get_explanation(self, **kwargs) -> ContentAST.Element:
141
+ """Build question explanation (backward compatible interface)."""
142
+ explanation, _ = self._get_explanation(**kwargs)
124
143
  return explanation
125
144
 
126
145
  @abc.abstractmethod
@@ -36,15 +36,22 @@ class MatrixMathQuestion(MathOperationQuestion, Question):
36
36
  return [[f"{prefix}{matrix[i][j]}" for j in range(len(matrix[0]))] for i in range(len(matrix))]
37
37
 
38
38
  def _create_answer_table(self, rows, cols, answers_dict, answer_prefix="answer"):
39
- """Create a table with answer blanks for matrix results."""
39
+ """Create a table with answer blanks for matrix results.
40
+
41
+ Returns:
42
+ Tuple of (table, answers_list)
43
+ """
40
44
  table_data = []
45
+ answers = []
41
46
  for i in range(rows):
42
47
  row = []
43
48
  for j in range(cols):
44
49
  answer_key = f"{answer_prefix}_{i}_{j}"
45
- row.append(ContentAST.Answer(answer=answers_dict[answer_key]))
50
+ ans = answers_dict[answer_key]
51
+ row.append(ans)
52
+ answers.append(ans)
46
53
  table_data.append(row)
47
- return ContentAST.Table(data=table_data, padding=True)
54
+ return ContentAST.Table(data=table_data, padding=True), answers
48
55
 
49
56
  # Implement MathOperationQuestion abstract methods
50
57
 
@@ -64,23 +71,23 @@ class MatrixMathQuestion(MathOperationQuestion, Question):
64
71
  return f"{operand_a_latex} {self.get_operator()} {operand_b_latex}"
65
72
 
66
73
  def _add_single_question_answers(self, body):
67
- """Add Canvas-only answer fields for single questions."""
74
+ """Add Canvas-only answer fields for single questions.
75
+
76
+ Returns:
77
+ List of Answer objects that were added to the body.
78
+ """
79
+ answers = []
80
+
68
81
  # For matrices, we typically show result dimensions and answer table
69
82
  if hasattr(self, 'result_rows') and hasattr(self, 'result_cols'):
70
83
  # Matrix multiplication case with dimension answers
71
84
  if hasattr(self, 'answers') and "result_rows" in self.answers:
85
+ rows_ans = self.answers["result_rows"]
86
+ cols_ans = self.answers["result_cols"]
87
+ answers.extend([rows_ans, cols_ans])
72
88
  body.add_element(
73
89
  ContentAST.OnlyHtml([
74
- ContentAST.AnswerBlock([
75
- ContentAST.Answer(
76
- answer=self.answers["result_rows"],
77
- label="Number of rows in result"
78
- ),
79
- ContentAST.Answer(
80
- answer=self.answers["result_cols"],
81
- label="Number of columns in result"
82
- )
83
- ])
90
+ ContentAST.AnswerBlock([rows_ans, cols_ans])
84
91
  ])
85
92
  )
86
93
 
@@ -88,21 +95,27 @@ class MatrixMathQuestion(MathOperationQuestion, Question):
88
95
  if hasattr(self, 'result') and self.result:
89
96
  rows = len(self.result)
90
97
  cols = len(self.result[0])
98
+ table, table_answers = self._create_answer_table(rows, cols, self.answers)
99
+ answers.extend(table_answers)
91
100
  body.add_element(
92
101
  ContentAST.OnlyHtml([
93
102
  ContentAST.Paragraph(["Result matrix:"]),
94
- self._create_answer_table(rows, cols, self.answers)
103
+ table
95
104
  ])
96
105
  )
97
106
  elif hasattr(self, 'max_dim'):
98
107
  # Matrix multiplication with max dimensions
108
+ table, table_answers = self._create_answer_table(self.max_dim, self.max_dim, self.answers)
109
+ answers.extend(table_answers)
99
110
  body.add_element(
100
111
  ContentAST.OnlyHtml([
101
112
  ContentAST.Paragraph(["Result matrix (use '-' if cell doesn't exist):"]),
102
- self._create_answer_table(self.max_dim, self.max_dim, self.answers)
113
+ table
103
114
  ])
104
115
  )
105
116
 
117
+ return answers
118
+
106
119
  # Abstract methods that subclasses must implement
107
120
  @abc.abstractmethod
108
121
  def get_operator(self):
@@ -488,8 +501,10 @@ class MatrixMultiplication(MatrixMathQuestion):
488
501
  # For single questions, use the old answer format
489
502
  # Dimension answers
490
503
  if result is not None:
491
- self.answers["result_rows"] = Answer.integer("result_rows", self.result_rows)
492
- self.answers["result_cols"] = Answer.integer("result_cols", self.result_cols)
504
+ self.answers["result_rows"] = Answer.integer("result_rows", self.result_rows,
505
+ label="Number of rows in result")
506
+ self.answers["result_cols"] = Answer.integer("result_cols", self.result_cols,
507
+ label="Number of columns in result")
493
508
 
494
509
  # Matrix element answers
495
510
  for i in range(self.max_dim):
@@ -501,8 +516,10 @@ class MatrixMultiplication(MatrixMathQuestion):
501
516
  self.answers[answer_key] = Answer.string(answer_key, "-")
502
517
  else:
503
518
  # Multiplication not possible
504
- self.answers["result_rows"] = Answer.string("result_rows", "-")
505
- self.answers["result_cols"] = Answer.string("result_cols", "-")
519
+ self.answers["result_rows"] = Answer.string("result_rows", "-",
520
+ label="Number of rows in result")
521
+ self.answers["result_cols"] = Answer.string("result_cols", "-",
522
+ label="Number of columns in result")
506
523
 
507
524
  # All matrix elements are "-"
508
525
  for i in range(self.max_dim):
@@ -523,32 +540,36 @@ class MatrixMultiplication(MatrixMathQuestion):
523
540
  self.answers[answer_key] = Answer.integer(answer_key, result[i][j])
524
541
 
525
542
  def _add_single_question_answers(self, body):
526
- """Add Canvas-only answer fields for MatrixMultiplication with dash instruction."""
543
+ """Add Canvas-only answer fields for MatrixMultiplication with dash instruction.
544
+
545
+ Returns:
546
+ List of Answer objects that were added to the body.
547
+ """
548
+ answers = []
549
+
527
550
  # Dimension answers for matrix multiplication
528
551
  if hasattr(self, 'answers') and "result_rows" in self.answers:
552
+ rows_ans = self.answers["result_rows"]
553
+ cols_ans = self.answers["result_cols"]
554
+ answers.extend([rows_ans, cols_ans])
529
555
  body.add_element(
530
556
  ContentAST.OnlyHtml([
531
- ContentAST.AnswerBlock([
532
- ContentAST.Answer(
533
- answer=self.answers["result_rows"],
534
- label="Number of rows in result"
535
- ),
536
- ContentAST.Answer(
537
- answer=self.answers["result_cols"],
538
- label="Number of columns in result"
539
- )
540
- ])
557
+ ContentAST.AnswerBlock([rows_ans, cols_ans])
541
558
  ])
542
559
  )
543
560
 
544
561
  # Matrix result table with dash instruction
562
+ table, table_answers = self._create_answer_table(self.max_dim, self.max_dim, self.answers)
563
+ answers.extend(table_answers)
545
564
  body.add_element(
546
565
  ContentAST.OnlyHtml([
547
566
  ContentAST.Paragraph(["Result matrix (use '-' if cell doesn't exist):"]),
548
- self._create_answer_table(self.max_dim, self.max_dim, self.answers)
567
+ table
549
568
  ])
550
569
  )
551
570
 
571
+ return answers
572
+
552
573
  def refresh(self, *args, **kwargs):
553
574
  """Override refresh to handle matrix attributes."""
554
575
  super().refresh(*args, **kwargs)