QuizGenerator 0.7.1__py3-none-any.whl → 0.8.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 (30) hide show
  1. QuizGenerator/contentast.py +6 -6
  2. QuizGenerator/generate.py +2 -1
  3. QuizGenerator/mixins.py +14 -100
  4. QuizGenerator/premade_questions/basic.py +24 -29
  5. QuizGenerator/premade_questions/cst334/languages.py +100 -99
  6. QuizGenerator/premade_questions/cst334/math_questions.py +112 -122
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +621 -621
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +137 -163
  9. QuizGenerator/premade_questions/cst334/process.py +312 -322
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +34 -35
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +41 -36
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +48 -41
  13. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +285 -520
  14. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -126
  15. QuizGenerator/premade_questions/cst463/models/attention.py +44 -50
  16. QuizGenerator/premade_questions/cst463/models/cnns.py +43 -47
  17. QuizGenerator/premade_questions/cst463/models/matrices.py +61 -11
  18. QuizGenerator/premade_questions/cst463/models/rnns.py +48 -50
  19. QuizGenerator/premade_questions/cst463/models/text.py +65 -67
  20. QuizGenerator/premade_questions/cst463/models/weight_counting.py +47 -46
  21. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +100 -156
  22. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +93 -141
  23. QuizGenerator/question.py +273 -202
  24. QuizGenerator/quiz.py +8 -5
  25. QuizGenerator/regenerate.py +14 -6
  26. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/METADATA +30 -2
  27. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/RECORD +30 -30
  28. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/WHEEL +0 -0
  29. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/entry_points.txt +0 -0
  30. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,26 +2,28 @@
2
2
  import abc
3
3
  import logging
4
4
  import math
5
+ import random
5
6
  from typing import List
6
7
 
7
8
  from QuizGenerator.question import Question, QuestionRegistry
8
9
  import QuizGenerator.contentast as ca
9
- from QuizGenerator.mixins import MathOperationQuestion
10
10
 
11
11
  log = logging.getLogger(__name__)
12
12
 
13
13
 
14
- class VectorMathQuestion(MathOperationQuestion, Question):
14
+ class VectorMathQuestion(Question):
15
15
 
16
16
  def __init__(self, *args, **kwargs):
17
17
  kwargs["topic"] = kwargs.get("topic", Question.Topic.MATH)
18
18
  super().__init__(*args, **kwargs)
19
19
 
20
- def _generate_vector(self, dimension, min_val=-10, max_val=10):
20
+ @staticmethod
21
+ def _generate_vector(rng, dimension, min_val=-10, max_val=10):
21
22
  """Generate a vector with random integer values."""
22
- return [self.rng.randint(min_val, max_val) for _ in range(dimension)]
23
+ return [rng.randint(min_val, max_val) for _ in range(dimension)]
23
24
 
24
- def _format_vector(self, vector):
25
+ @staticmethod
26
+ def _format_vector(vector):
25
27
  """Return a ca.Matrix element for the vector (format-independent).
26
28
 
27
29
  The Matrix element will render appropriately for each output format:
@@ -33,66 +35,42 @@ class VectorMathQuestion(MathOperationQuestion, Question):
33
35
  matrix_data = [[v] for v in vector]
34
36
  return ca.Matrix(data=matrix_data, bracket_type="b")
35
37
 
36
- def _format_vector_inline(self, vector):
38
+ @staticmethod
39
+ def _format_vector_inline(vector):
37
40
  """Format vector for inline display."""
38
41
  elements = [str(v) for v in vector]
39
42
  return f"({', '.join(elements)})"
40
43
 
41
- # Implement MathOperationQuestion abstract methods
42
-
43
- def generate_operands(self):
44
- """Generate two vectors for the operation."""
45
- if not hasattr(self, 'dimension'):
46
- self.dimension = self.rng.randint(self.MIN_DIMENSION, self.MAX_DIMENSION)
47
- vector_a = self._generate_vector(self.dimension)
48
- vector_b = self._generate_vector(self.dimension)
49
- return vector_a, vector_b
50
-
51
- def format_operand_latex(self, operand):
52
- """Format a vector for LaTeX display."""
53
- return self._format_vector(operand)
54
-
55
- def format_single_equation(self, operand_a, operand_b):
56
- """Format the equation for single questions."""
57
- operand_a_latex = self.format_operand_latex(operand_a)
58
- operand_b_latex = self.format_operand_latex(operand_b)
59
- return f"{operand_a_latex} {self.get_operator()} {operand_b_latex}"
60
-
61
- # Vector-specific overrides
62
-
63
- def refresh(self, *args, **kwargs):
64
- # Generate vector dimension first
65
- self.dimension = self.rng.randint(self.MIN_DIMENSION, self.MAX_DIMENSION)
66
-
67
- # Call parent refresh which will use our generate_operands method
68
- super().refresh(*args, **kwargs)
69
-
70
- self.vector_a = self.operand_a
71
- self.vector_b = self.operand_b
72
-
73
- def generate_subquestion_data(self):
74
- """Generate LaTeX content for each subpart of the question.
75
- Override to handle vector-specific keys in subquestion_data."""
76
- subparts = []
77
- for data in self.subquestion_data:
78
- # Map generic operand names to vector names for compatibility
79
- vector_a = data.get('vector_a', data['operand_a'])
80
- vector_b = data.get('vector_b', data['operand_b'])
81
-
82
- vector_a_latex = self._format_vector(vector_a)
83
- vector_b_latex = self._format_vector(vector_b)
84
- # Return as tuple of (matrix_a, operator, matrix_b)
85
- subparts.append((vector_a_latex, self.get_operator(), vector_b_latex))
86
- return subparts
44
+ @classmethod
45
+ def _build_context(cls, *, rng_seed=None, **kwargs):
46
+ rng = random.Random(rng_seed)
47
+ num_subquestions = kwargs.get("num_subquestions", 1)
48
+ if num_subquestions > 1:
49
+ raise NotImplementedError("Multipart not supported")
50
+
51
+ dimension = kwargs.get("dimension", rng.randint(cls.MIN_DIMENSION, cls.MAX_DIMENSION))
52
+ vector_a = cls._generate_vector(rng, dimension)
53
+ vector_b = cls._generate_vector(rng, dimension)
54
+ result = cls.calculate_single_result(vector_a, vector_b)
55
+
56
+ return {
57
+ "dimension": dimension,
58
+ "vector_a": vector_a,
59
+ "vector_b": vector_b,
60
+ "result": result,
61
+ "num_subquestions": num_subquestions,
62
+ }
87
63
 
88
64
  # Abstract methods that subclasses must still implement
65
+ @staticmethod
89
66
  @abc.abstractmethod
90
- def get_operator(self):
67
+ def get_operator():
91
68
  """Return the LaTeX operator for this operation."""
92
69
  pass
93
70
 
71
+ @staticmethod
94
72
  @abc.abstractmethod
95
- def calculate_single_result(self, vector_a, vector_b):
73
+ def calculate_single_result(vector_a, vector_b):
96
74
  """Calculate the result for a single question with two vectors."""
97
75
  pass
98
76
 
@@ -108,27 +86,32 @@ class VectorAddition(VectorMathQuestion):
108
86
  MIN_DIMENSION = 2
109
87
  MAX_DIMENSION = 4
110
88
 
111
- def get_operator(self):
89
+ @staticmethod
90
+ def get_operator():
112
91
  return "+"
113
92
 
114
- def calculate_single_result(self, vector_a, vector_b):
93
+ @staticmethod
94
+ def calculate_single_result(vector_a, vector_b):
115
95
  return [vector_a[i] + vector_b[i] for i in range(len(vector_a))]
116
96
 
117
97
  def create_subquestion_answers(self, subpart_index, result):
118
98
  raise NotImplementedError("Multipart not supported")
119
99
 
120
100
  def create_single_answers(self, result):
121
- self.answers["result"] = ca.AnswerTypes.Vector(result)
101
+ answer = ca.AnswerTypes.Vector(result)
102
+ self._single_answer = answer
103
+ return [answer]
122
104
 
123
- def _get_body(self):
105
+ @classmethod
106
+ def _build_body(cls, context):
124
107
  """Build question body and collect answers."""
125
108
  body = ca.Section()
126
109
 
127
- body.add_element(ca.Paragraph([self.get_intro_text()]))
110
+ body.add_element(ca.Paragraph(["Calculate the following:"]))
128
111
 
129
112
  # Equation display using MathExpression for format-independent rendering
130
- vector_a_elem = self._format_vector(self.vector_a)
131
- vector_b_elem = self._format_vector(self.vector_b)
113
+ vector_a_elem = cls._format_vector(context["vector_a"])
114
+ vector_b_elem = cls._format_vector(context["vector_b"])
132
115
  body.add_element(ca.MathExpression([
133
116
  vector_a_elem,
134
117
  " + ",
@@ -136,24 +119,27 @@ class VectorAddition(VectorMathQuestion):
136
119
  " = "
137
120
  ]))
138
121
 
139
- # Canvas-only answer field - use stored answer for consistent UUID
140
- answer = self.answers["result"]
122
+ answer = ca.AnswerTypes.Vector(context["result"])
141
123
  body.add_element(ca.OnlyHtml([ca.Paragraph(["Enter your answer as a column vector:"])]))
142
124
  body.add_element(ca.OnlyHtml([answer]))
143
125
 
144
- return body, list(self.answers.values())
126
+ return body, [answer]
145
127
 
146
- def _get_explanation(self):
128
+ @classmethod
129
+ def _build_explanation(cls, context):
147
130
  """Build question explanation."""
148
131
  explanation = ca.Section()
149
132
 
150
133
  explanation.add_element(ca.Paragraph(["To add vectors, we add corresponding components:"]))
151
134
 
152
135
  # Use LaTeX syntax for make_block_equation__multiline_equals
153
- vector_a_str = r" \\ ".join([str(v) for v in self.vector_a])
154
- vector_b_str = r" \\ ".join([str(v) for v in self.vector_b])
155
- result_str = r" \\ ".join([str(v) for v in self.result])
156
- addition_str = r" \\ ".join([f"{self.vector_a[i]}+{self.vector_b[i]}" for i in range(self.dimension)])
136
+ vector_a_str = r" \\ ".join([str(v) for v in context["vector_a"]])
137
+ vector_b_str = r" \\ ".join([str(v) for v in context["vector_b"]])
138
+ result_str = r" \\ ".join([str(v) for v in context["result"]])
139
+ addition_str = r" \\ ".join([
140
+ f"{context['vector_a'][i]}+{context['vector_b'][i]}"
141
+ for i in range(context["dimension"])
142
+ ])
157
143
 
158
144
  explanation.add_element(
159
145
  ca.Equation.make_block_equation__multiline_equals(
@@ -175,66 +161,87 @@ class VectorScalarMultiplication(VectorMathQuestion):
175
161
  MIN_DIMENSION = 2
176
162
  MAX_DIMENSION = 4
177
163
 
178
- def _generate_scalar(self):
164
+ @classmethod
165
+ def _build_context(cls, *, rng_seed=None, **kwargs):
166
+ rng = random.Random(rng_seed)
167
+ num_subquestions = kwargs.get("num_subquestions", 1)
168
+ if num_subquestions > 1:
169
+ raise NotImplementedError("Multipart not supported")
170
+
171
+ dimension = kwargs.get("dimension", rng.randint(cls.MIN_DIMENSION, cls.MAX_DIMENSION))
172
+ vector_a = cls._generate_vector(rng, dimension)
173
+ scalar = cls._generate_scalar(rng)
174
+ result = cls.calculate_single_result(vector_a, scalar)
175
+
176
+ return {
177
+ "dimension": dimension,
178
+ "vector_a": vector_a,
179
+ "scalar": scalar,
180
+ "result": result,
181
+ "num_subquestions": num_subquestions,
182
+ }
183
+
184
+ @staticmethod
185
+ def _generate_scalar(rng):
179
186
  """Generate a non-zero scalar for multiplication."""
180
- scalar = self.rng.randint(-5, 5)
187
+ scalar = rng.randint(-5, 5)
181
188
  while scalar == 0:
182
- scalar = self.rng.randint(-5, 5)
189
+ scalar = rng.randint(-5, 5)
183
190
  return scalar
184
191
 
185
- def refresh(self, *args, **kwargs):
186
- # Generate scalar first, then call parent refresh
187
- self.scalar = self._generate_scalar()
188
- super().refresh(*args, **kwargs)
189
-
190
- def get_operator(self):
191
- return f"{self.scalar} \\cdot"
192
+ @staticmethod
193
+ def get_operator():
194
+ return "\\cdot"
192
195
 
193
- def calculate_single_result(self, vector_a, vector_b):
194
- return [self.scalar * component for component in vector_a]
196
+ @staticmethod
197
+ def calculate_single_result(vector_a, vector_b):
198
+ return [vector_b * component for component in vector_a]
195
199
 
196
200
  def create_subquestion_answers(self, subpart_index, result):
197
201
  raise NotImplementedError("Multipart not supported")
198
202
 
199
203
  def create_single_answers(self, result):
200
- self.answers["result"] = ca.AnswerTypes.Vector(result)
204
+ answer = ca.AnswerTypes.Vector(result)
205
+ self._single_answer = answer
206
+ return [answer]
201
207
 
202
- def _get_body(self):
208
+ @classmethod
209
+ def _build_body(cls, context):
203
210
  """Build question body and collect answers."""
204
211
  body = ca.Section()
205
212
 
206
- body.add_element(ca.Paragraph([self.get_intro_text()]))
213
+ body.add_element(ca.Paragraph(["Calculate the following:"]))
207
214
 
208
215
  # Equation display using MathExpression
209
- vector_elem = self._format_vector(self.vector_a)
216
+ vector_elem = cls._format_vector(context["vector_a"])
210
217
  body.add_element(ca.MathExpression([
211
- f"{self.scalar} \\cdot ",
218
+ f"{context['scalar']} \\cdot ",
212
219
  vector_elem,
213
220
  " = "
214
221
  ]))
215
222
 
216
- # Canvas-only answer field - use stored answer
217
- answer = self.answers["result"]
223
+ answer = ca.AnswerTypes.Vector(context["result"])
218
224
  body.add_element(ca.OnlyHtml([ca.Paragraph(["Enter your answer as a column vector:"])]))
219
225
  body.add_element(ca.OnlyHtml([answer]))
220
226
 
221
- return body, list(self.answers.values())
227
+ return body, [answer]
222
228
 
223
- def _get_explanation(self):
229
+ @classmethod
230
+ def _build_explanation(cls, context):
224
231
  """Build question explanation."""
225
232
  explanation = ca.Section()
226
233
 
227
234
  explanation.add_element(ca.Paragraph(["To multiply a vector by a scalar, we multiply each component by the scalar:"]))
228
235
 
229
- vector_str = r" \\ ".join([str(v) for v in self.vector_a])
230
- multiplication_str = r" \\ ".join([f"{self.scalar} \\cdot {v}" for v in self.vector_a])
231
- result_str = r" \\ ".join([str(v) for v in self.result])
236
+ vector_str = r" \\ ".join([str(v) for v in context["vector_a"]])
237
+ multiplication_str = r" \\ ".join([f"{context['scalar']} \\cdot {v}" for v in context["vector_a"]])
238
+ result_str = r" \\ ".join([str(v) for v in context["result"]])
232
239
 
233
240
  explanation.add_element(
234
241
  ca.Equation.make_block_equation__multiline_equals(
235
- lhs=f"{self.scalar} \\cdot \\vec{{v}}",
242
+ lhs=f"{context['scalar']} \\cdot \\vec{{v}}",
236
243
  rhs=[
237
- f"{self.scalar} \\cdot \\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}",
244
+ f"{context['scalar']} \\cdot \\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}",
238
245
  f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
239
246
  f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
240
247
  ]
@@ -250,27 +257,32 @@ class VectorDotProduct(VectorMathQuestion):
250
257
  MIN_DIMENSION = 2
251
258
  MAX_DIMENSION = 4
252
259
 
253
- def get_operator(self):
260
+ @staticmethod
261
+ def get_operator():
254
262
  return "\\cdot"
255
263
 
256
- def calculate_single_result(self, vector_a, vector_b):
264
+ @staticmethod
265
+ def calculate_single_result(vector_a, vector_b):
257
266
  return sum(vector_a[i] * vector_b[i] for i in range(len(vector_a)))
258
267
 
259
268
  def create_subquestion_answers(self, subpart_index, result):
260
269
  raise NotImplementedError("Multipart not supported")
261
270
 
262
271
  def create_single_answers(self, result):
263
- self.answers["dot_product"] = ca.AnswerTypes.Int(result)
272
+ answer = ca.AnswerTypes.Int(result)
273
+ self._single_answer = answer
274
+ return [answer]
264
275
 
265
- def _get_body(self):
276
+ @classmethod
277
+ def _build_body(cls, context):
266
278
  """Build question body and collect answers."""
267
279
  body = ca.Section()
268
280
 
269
- body.add_element(ca.Paragraph([self.get_intro_text()]))
281
+ body.add_element(ca.Paragraph(["Calculate the following:"]))
270
282
 
271
283
  # Equation display using MathExpression
272
- vector_a_elem = self._format_vector(self.vector_a)
273
- vector_b_elem = self._format_vector(self.vector_b)
284
+ vector_a_elem = cls._format_vector(context["vector_a"])
285
+ vector_b_elem = cls._format_vector(context["vector_b"])
274
286
  body.add_element(ca.MathExpression([
275
287
  vector_a_elem,
276
288
  " \\cdot ",
@@ -278,22 +290,28 @@ class VectorDotProduct(VectorMathQuestion):
278
290
  " = "
279
291
  ]))
280
292
 
281
- # Canvas-only answer field - use stored answer
282
- answer = self.answers["dot_product"]
293
+ answer = ca.AnswerTypes.Int(context["result"])
283
294
  body.add_element(ca.OnlyHtml([answer]))
284
295
 
285
- return body, list(self.answers.values())
296
+ return body, [answer]
286
297
 
287
- def _get_explanation(self):
298
+ @classmethod
299
+ def _build_explanation(cls, context):
288
300
  """Build question explanation."""
289
301
  explanation = ca.Section()
290
302
 
291
303
  explanation.add_element(ca.Paragraph(["The dot product is calculated by multiplying corresponding components and summing the results:"]))
292
304
 
293
- vector_a_str = r" \\ ".join([str(v) for v in self.vector_a])
294
- vector_b_str = r" \\ ".join([str(v) for v in self.vector_b])
295
- products_str = " + ".join([f"({self.vector_a[i]} \\cdot {self.vector_b[i]})" for i in range(self.dimension)])
296
- calculation_str = " + ".join([str(self.vector_a[i] * self.vector_b[i]) for i in range(self.dimension)])
305
+ vector_a_str = r" \\ ".join([str(v) for v in context["vector_a"]])
306
+ vector_b_str = r" \\ ".join([str(v) for v in context["vector_b"]])
307
+ products_str = " + ".join([
308
+ f"({context['vector_a'][i]} \\cdot {context['vector_b'][i]})"
309
+ for i in range(context["dimension"])
310
+ ])
311
+ calculation_str = " + ".join([
312
+ str(context["vector_a"][i] * context["vector_b"][i])
313
+ for i in range(context["dimension"])
314
+ ])
297
315
 
298
316
  explanation.add_element(
299
317
  ca.Equation.make_block_equation__multiline_equals(
@@ -302,7 +320,7 @@ class VectorDotProduct(VectorMathQuestion):
302
320
  f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} \\cdot \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
303
321
  products_str,
304
322
  calculation_str,
305
- str(self.result)
323
+ str(context["result"])
306
324
  ]
307
325
  )
308
326
  )
@@ -316,10 +334,12 @@ class VectorMagnitude(VectorMathQuestion):
316
334
  MIN_DIMENSION = 2
317
335
  MAX_DIMENSION = 3
318
336
 
319
- def get_operator(self):
337
+ @staticmethod
338
+ def get_operator():
320
339
  return "||"
321
340
 
322
- def calculate_single_result(self, vector_a, vector_b):
341
+ @staticmethod
342
+ def calculate_single_result(vector_a, vector_b):
323
343
  magnitude_squared = sum(component ** 2 for component in vector_a)
324
344
  return math.sqrt(magnitude_squared)
325
345
 
@@ -327,29 +347,32 @@ class VectorMagnitude(VectorMathQuestion):
327
347
  raise NotImplementedError("Multipart not supported")
328
348
 
329
349
  def create_single_answers(self, result):
330
- self.answers["magnitude"] = ca.AnswerTypes.Float(result)
350
+ answer = ca.AnswerTypes.Float(result)
351
+ self._single_answer = answer
352
+ return [answer]
331
353
 
332
- def _get_body(self):
354
+ @classmethod
355
+ def _build_body(cls, context):
333
356
  """Build question body and collect answers."""
334
357
  body = ca.Section()
335
358
 
336
- body.add_element(ca.Paragraph([self.get_intro_text()]))
359
+ body.add_element(ca.Paragraph(["Calculate the following:"]))
337
360
 
338
361
  # Equation display using MathExpression
339
- vector_elem = self._format_vector(self.vector_a)
362
+ vector_elem = cls._format_vector(context["vector_a"])
340
363
  body.add_element(ca.MathExpression([
341
364
  "||",
342
365
  vector_elem,
343
366
  "|| = "
344
367
  ]))
345
368
 
346
- # Canvas-only answer field - use stored answer
347
- answer = self.answers["magnitude"]
369
+ answer = ca.AnswerTypes.Float(context["result"])
348
370
  body.add_element(ca.OnlyHtml([answer]))
349
371
 
350
- return body, list(self.answers.values())
372
+ return body, [answer]
351
373
 
352
- def _get_explanation(self):
374
+ @classmethod
375
+ def _build_explanation(cls, context):
353
376
  """Build question explanation."""
354
377
  explanation = ca.Section()
355
378
 
@@ -359,11 +382,11 @@ class VectorMagnitude(VectorMathQuestion):
359
382
  ))
360
383
 
361
384
  # Use LaTeX syntax for make_block_equation__multiline_equals
362
- vector_str = r" \\ ".join([str(v) for v in self.vector_a])
363
- squares_str = " + ".join([f"{v}^2" for v in self.vector_a])
364
- calculation_str = " + ".join([str(v**2) for v in self.vector_a])
365
- sum_of_squares = sum(component ** 2 for component in self.vector_a)
366
- result_formatted = sorted(ca.Answer.accepted_strings(self.result), key=lambda s: len(s))[0]
385
+ vector_str = r" \\ ".join([str(v) for v in context["vector_a"]])
386
+ squares_str = " + ".join([f"{v}^2" for v in context["vector_a"]])
387
+ calculation_str = " + ".join([str(v**2) for v in context["vector_a"]])
388
+ sum_of_squares = sum(component ** 2 for component in context["vector_a"])
389
+ result_formatted = sorted(ca.Answer.accepted_strings(context["result"]), key=lambda s: len(s))[0]
367
390
 
368
391
  explanation.add_element(
369
392
  ca.Equation.make_block_equation__multiline_equals(
@@ -40,33 +40,35 @@ class AttentionForwardPass(MatrixQuestion, TableQuestionMixin):
40
40
 
41
41
  return output, attention_weights
42
42
 
43
- def refresh(self, *args, **kwargs):
44
- super().refresh(*args, **kwargs)
45
-
46
- seq_len = kwargs.get("seq_len", 3)
47
- d_k = kwargs.get("key_dimension", 1) # key/query dimension
48
- d_v = kwargs.get("value_dimension", 1) # value dimension
49
-
50
- # Small integer matrices
51
- self.Q = self.rng.randint(0, 3, size=(seq_len, d_k))
52
- self.K = self.rng.randint(0, 3, size=(seq_len, d_k))
53
- self.V = self.rng.randint(0, 3, size=(seq_len, d_v))
54
-
55
- self.Q = self.get_rounded_matrix((seq_len, d_k), 0, 3)
56
- self.K = self.get_rounded_matrix((seq_len, d_k), 0, 3)
57
- self.V = self.get_rounded_matrix((seq_len, d_v), 0, 3)
58
-
59
- self.output, self.weights = self.simple_attention(self.Q, self.K, self.V)
60
-
61
- ## Answers:
62
- # Q, K, V, output, weights
43
+ @classmethod
44
+ def _build_context(cls, *, rng_seed=None, **kwargs):
45
+ rng = cls.get_rng(rng_seed)
46
+ digits = cls.get_digits_to_round(digits_to_round=kwargs.get("digits_to_round"))
63
47
 
64
- self.answers["weights"] = ca.AnswerTypes.Matrix(self.weights, label="Weights")
65
- self.answers["output"] = ca.AnswerTypes.Matrix(self.output, label="Output")
66
-
67
- return True
68
-
69
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
48
+ seq_len = kwargs.get("seq_len", 3)
49
+ d_k = kwargs.get("key_dimension", 1)
50
+ d_v = kwargs.get("value_dimension", 1)
51
+
52
+ Q = cls.get_rounded_matrix(rng, (seq_len, d_k), 0, 3, digits)
53
+ K = cls.get_rounded_matrix(rng, (seq_len, d_k), 0, 3, digits)
54
+ V = cls.get_rounded_matrix(rng, (seq_len, d_v), 0, 3, digits)
55
+
56
+ output, weights = cls.simple_attention(Q, K, V)
57
+
58
+ return {
59
+ "digits": digits,
60
+ "seq_len": seq_len,
61
+ "d_k": d_k,
62
+ "d_v": d_v,
63
+ "Q": Q,
64
+ "K": K,
65
+ "V": V,
66
+ "output": output,
67
+ "weights": weights,
68
+ }
69
+
70
+ @classmethod
71
+ def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
70
72
  """Build question body and collect answers."""
71
73
  body = ca.Section()
72
74
  answers = []
@@ -75,32 +77,30 @@ class AttentionForwardPass(MatrixQuestion, TableQuestionMixin):
75
77
  ca.Text("Given the below information about a self attention layer, please calculate the output sequence.")
76
78
  )
77
79
  body.add_element(
78
- self.create_info_table(
80
+ cls.create_info_table(
79
81
  {
80
- "Q": ca.Matrix(self.Q),
81
- "K": ca.Matrix(self.K),
82
- "V": ca.Matrix(self.V),
82
+ "Q": ca.Matrix(context["Q"]),
83
+ "K": ca.Matrix(context["K"]),
84
+ "V": ca.Matrix(context["V"]),
83
85
  }
84
86
  )
85
87
  )
86
88
 
87
- answers.append(self.answers["weights"])
88
- answers.append(self.answers["output"])
89
+ weights_answer = ca.AnswerTypes.Matrix(context["weights"], label="Weights")
90
+ output_answer = ca.AnswerTypes.Matrix(context["output"], label="Output")
91
+ answers.append(weights_answer)
92
+ answers.append(output_answer)
89
93
  body.add_elements([
90
94
  ca.LineBreak(),
91
- self.answers["weights"],
95
+ weights_answer,
92
96
  ca.LineBreak(),
93
- self.answers["output"],
97
+ output_answer,
94
98
  ])
95
99
 
96
100
  return body, answers
97
101
 
98
- def get_body(self, **kwargs) -> ca.Section:
99
- """Build question body (backward compatible interface)."""
100
- body, _ = self._get_body(**kwargs)
101
- return body
102
-
103
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
102
+ @classmethod
103
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
104
104
  """Build question explanation."""
105
105
  explanation = ca.Section()
106
106
  digits = ca.Answer.DEFAULT_ROUNDING_DIGITS
@@ -118,12 +118,12 @@ class AttentionForwardPass(MatrixQuestion, TableQuestionMixin):
118
118
  ])
119
119
  )
120
120
 
121
- d_k = self.Q.shape[1]
121
+ d_k = context["Q"].shape[1]
122
122
  explanation.add_element(
123
123
  ca.Equation(f"\\text{{scores}} = \\frac{{Q K^T}}{{\\sqrt{{d_k}}}} = \\frac{{Q K^T}}{{\\sqrt{{{d_k}}}}}")
124
124
  )
125
125
 
126
- scores = self.Q @ self.K.T / np.sqrt(d_k)
126
+ scores = context["Q"] @ context["K"].T / np.sqrt(d_k)
127
127
 
128
128
  explanation.add_element(
129
129
  ca.Paragraph([
@@ -178,7 +178,7 @@ class AttentionForwardPass(MatrixQuestion, TableQuestionMixin):
178
178
  "Complete attention weight matrix:"
179
179
  ])
180
180
  )
181
- explanation.add_element(ca.Matrix(np.round(self.weights, digits)))
181
+ explanation.add_element(ca.Matrix(np.round(context["weights"], digits)))
182
182
 
183
183
  # Step 3: Weighted sum of values
184
184
  explanation.add_element(
@@ -196,12 +196,6 @@ class AttentionForwardPass(MatrixQuestion, TableQuestionMixin):
196
196
  "Final output:"
197
197
  ])
198
198
  )
199
- explanation.add_element(ca.Matrix(np.round(self.output, digits)))
199
+ explanation.add_element(ca.Matrix(np.round(context["output"], digits)))
200
200
 
201
201
  return explanation, []
202
-
203
- def get_explanation(self, **kwargs) -> ca.Section:
204
- """Build question explanation (backward compatible interface)."""
205
- explanation, _ = self._get_explanation(**kwargs)
206
- return explanation
207
-