QuizGenerator 0.7.0__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 +128 -19
  26. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/METADATA +30 -2
  27. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/RECORD +30 -30
  28. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/WHEEL +0 -0
  29. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/entry_points.txt +0 -0
  30. {quizgenerator-0.7.0.dist-info → quizgenerator-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,16 @@
1
1
  #!env python
2
2
  import abc
3
3
  import logging
4
+ import random
5
+ from typing import List, Tuple
4
6
 
5
7
  from QuizGenerator.question import Question, QuestionRegistry
6
8
  import QuizGenerator.contentast as ca
7
- from QuizGenerator.mixins import MathOperationQuestion
8
9
 
9
10
  log = logging.getLogger(__name__)
10
11
 
11
12
 
12
- class MatrixMathQuestion(MathOperationQuestion, Question):
13
+ class MatrixMathQuestion(Question):
13
14
  """
14
15
  Base class for matrix mathematics questions with multipart support.
15
16
 
@@ -27,15 +28,18 @@ class MatrixMathQuestion(MathOperationQuestion, Question):
27
28
  kwargs["topic"] = kwargs.get("topic", Question.Topic.MATH)
28
29
  super().__init__(*args, **kwargs)
29
30
 
30
- def _generate_matrix(self, rows, cols, min_val=1, max_val=9):
31
+ @staticmethod
32
+ def _generate_matrix(rng, rows, cols, min_val=1, max_val=9):
31
33
  """Generate a matrix with random integer values."""
32
- return [[self.rng.randint(min_val, max_val) for _ in range(cols)] for _ in range(rows)]
34
+ return [[rng.randint(min_val, max_val) for _ in range(cols)] for _ in range(rows)]
33
35
 
34
- def _matrix_to_table(self, matrix, prefix=""):
36
+ @staticmethod
37
+ def _matrix_to_table(matrix, prefix=""):
35
38
  """Convert a matrix to content AST table format."""
36
39
  return [[f"{prefix}{matrix[i][j]}" for j in range(len(matrix[0]))] for i in range(len(matrix))]
37
40
 
38
- def _create_answer_table(self, rows, cols, answers_dict, answer_prefix="answer"):
41
+ @staticmethod
42
+ def _create_answer_table(answer_matrix):
39
43
  """Create a table with answer blanks for matrix results.
40
44
 
41
45
  Returns:
@@ -43,94 +47,16 @@ class MatrixMathQuestion(MathOperationQuestion, Question):
43
47
  """
44
48
  table_data = []
45
49
  answers = []
46
- for i in range(rows):
47
- row = []
48
- for j in range(cols):
49
- answer_key = f"{answer_prefix}_{i}_{j}"
50
- ans = answers_dict[answer_key]
51
- row.append(ans)
52
- answers.append(ans)
53
- table_data.append(row)
50
+ for row in answer_matrix:
51
+ table_row = []
52
+ for ans in row:
53
+ table_row.append(ans)
54
+ if isinstance(ans, ca.Answer):
55
+ answers.append(ans)
56
+ table_data.append(table_row)
54
57
  return ca.Table(data=table_data, padding=True), answers
55
58
 
56
- # Implement MathOperationQuestion abstract methods
57
-
58
- @abc.abstractmethod
59
- def generate_operands(self):
60
- """Generate matrices for the operation. Subclasses must implement."""
61
- pass
62
-
63
- def format_operand_latex(self, operand):
64
- """Format a matrix for LaTeX display."""
65
- return ca.Matrix.to_latex(operand, "b")
66
-
67
- def format_single_equation(self, operand_a, operand_b):
68
- """Format the equation for single questions."""
69
- operand_a_latex = self.format_operand_latex(operand_a)
70
- operand_b_latex = self.format_operand_latex(operand_b)
71
- return f"{operand_a_latex} {self.get_operator()} {operand_b_latex}"
72
-
73
- def _add_single_question_answers(self, body):
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
-
81
- # For matrices, we typically show result dimensions and answer table
82
- if hasattr(self, 'result_rows') and hasattr(self, 'result_cols'):
83
- # Matrix multiplication case with dimension answers
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])
88
- body.add_element(
89
- ca.OnlyHtml([
90
- ca.AnswerBlock([rows_ans, cols_ans])
91
- ])
92
- )
93
-
94
- # Matrix result table
95
- if hasattr(self, 'result') and self.result:
96
- rows = len(self.result)
97
- cols = len(self.result[0])
98
- table, table_answers = self._create_answer_table(rows, cols, self.answers)
99
- answers.extend(table_answers)
100
- body.add_element(
101
- ca.OnlyHtml([
102
- ca.Paragraph(["Result matrix:"]),
103
- table
104
- ])
105
- )
106
- elif hasattr(self, 'max_dim'):
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)
110
- body.add_element(
111
- ca.OnlyHtml([
112
- ca.Paragraph(["Result matrix (use '-' if cell doesn't exist):"]),
113
- table
114
- ])
115
- )
116
-
117
- return answers
118
-
119
- # Abstract methods that subclasses must implement
120
- @abc.abstractmethod
121
- def get_operator(self):
122
- """Return the LaTeX operator for this operation."""
123
- pass
124
-
125
- @abc.abstractmethod
126
- def calculate_single_result(self, matrix_a, matrix_b):
127
- """Calculate the result for a single question with two matrices."""
128
- pass
129
-
130
- @abc.abstractmethod
131
- def create_subquestion_answers(self, subpart_index, result):
132
- """Create answer objects for a subquestion result."""
133
- pass
59
+ # Abstract methods retained for compatibility; subclasses handle build directly.
134
60
 
135
61
 
136
62
  @QuestionRegistry.register()
@@ -139,58 +65,54 @@ class MatrixAddition(MatrixMathQuestion):
139
65
  MIN_SIZE = 2
140
66
  MAX_SIZE = 4
141
67
 
142
- def generate_operands(self):
143
- """Generate two matrices with the same dimensions for addition."""
144
- # Generate matrix dimensions (same for both matrices in addition)
145
- self.rows = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
146
- self.cols = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
147
-
148
- # Generate two matrices
149
- matrix_a = self._generate_matrix(self.rows, self.cols)
150
- matrix_b = self._generate_matrix(self.rows, self.cols)
151
- return matrix_a, matrix_b
152
-
153
- def get_operator(self):
154
- """Return the addition operator."""
155
- return "+"
156
-
157
- def calculate_single_result(self, matrix_a, matrix_b):
158
- """Calculate matrix addition result."""
159
- rows = len(matrix_a)
160
- cols = len(matrix_a[0])
161
- return [[matrix_a[i][j] + matrix_b[i][j] for j in range(cols)] for i in range(rows)]
162
-
163
- def create_subquestion_answers(self, subpart_index, result):
164
- """Create answer objects for matrix addition result."""
165
- if subpart_index == 0 and not self.is_multipart():
166
- # For single questions, use the old answer format
167
- rows = len(result)
168
- cols = len(result[0])
169
- for i in range(rows):
170
- for j in range(cols):
171
- answer_key = f"answer_{i}_{j}"
172
- self.answers[answer_key] = ca.AnswerTypes.Int(result[i][j])
173
- else:
174
- # For multipart questions, use subpart letter format
175
- letter = chr(ord('a') + subpart_index)
176
- rows = len(result)
177
- cols = len(result[0])
178
- for i in range(rows):
179
- for j in range(cols):
180
- answer_key = f"subpart_{letter}_{i}_{j}"
181
- self.answers[answer_key] = ca.AnswerTypes.Int(result[i][j])
182
-
183
- def refresh(self, *args, **kwargs):
184
- """Override refresh to set rows/cols for compatibility."""
185
- super().refresh(*args, **kwargs)
186
-
187
- # For backward compatibility, set matrix attributes for single questions
188
- if not self.is_multipart():
189
- self.matrix_a = self.operand_a
190
- self.matrix_b = self.operand_b
191
- # rows and cols should already be set by generate_operands
192
-
193
- def get_explanation(self, **kwargs) -> ca.Section:
68
+ @classmethod
69
+ def _build_context(cls, *, rng_seed=None, **kwargs):
70
+ rng = random.Random(rng_seed)
71
+ num_subquestions = kwargs.get("num_subquestions", 1)
72
+ if num_subquestions > 1:
73
+ raise NotImplementedError("Multipart not supported")
74
+
75
+ rows = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
76
+ cols = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
77
+
78
+ matrix_a = cls._generate_matrix(rng, rows, cols)
79
+ matrix_b = cls._generate_matrix(rng, rows, cols)
80
+ result = [[matrix_a[i][j] + matrix_b[i][j] for j in range(cols)] for i in range(rows)]
81
+
82
+ return {
83
+ "rows": rows,
84
+ "cols": cols,
85
+ "matrix_a": matrix_a,
86
+ "matrix_b": matrix_b,
87
+ "result": result,
88
+ "num_subquestions": num_subquestions,
89
+ }
90
+
91
+ @classmethod
92
+ def _build_body(cls, context):
93
+ body = ca.Section()
94
+ body.add_element(ca.Paragraph(["Calculate the following:"]))
95
+
96
+ matrix_a_elem = ca.Matrix(data=context["matrix_a"], bracket_type="b")
97
+ matrix_b_elem = ca.Matrix(data=context["matrix_b"], bracket_type="b")
98
+ body.add_element(ca.MathExpression([matrix_a_elem, " + ", matrix_b_elem, " = "]))
99
+
100
+ answer_matrix = [
101
+ [ca.AnswerTypes.Int(value) for value in row]
102
+ for row in context["result"]
103
+ ]
104
+ table, table_answers = cls._create_answer_table(answer_matrix)
105
+ body.add_element(
106
+ ca.OnlyHtml([
107
+ ca.Paragraph(["Result matrix:"]),
108
+ table
109
+ ])
110
+ )
111
+
112
+ return body, table_answers
113
+
114
+ @classmethod
115
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
194
116
  explanation = ca.Section()
195
117
 
196
118
  explanation.add_element(
@@ -200,57 +122,38 @@ class MatrixAddition(MatrixMathQuestion):
200
122
  ])
201
123
  )
202
124
 
203
- if self.is_multipart():
204
- # Handle multipart explanations
205
- explanation.add_element(ca.Paragraph(["Step-by-step calculation for each part:"]))
206
- for i, data in enumerate(self.subquestion_data):
207
- letter = chr(ord('a') + i)
208
- matrix_a = data.get('matrix_a', data['operand_a'])
209
- matrix_b = data.get('matrix_b', data['operand_b'])
210
- result = data['result']
211
-
212
- # Create LaTeX strings for multiline equation
213
- rows = len(matrix_a)
214
- cols = len(matrix_a[0])
215
- matrix_a_str = r" \\ ".join([" & ".join([str(matrix_a[row][col]) for col in range(cols)]) for row in range(rows)])
216
- matrix_b_str = r" \\ ".join([" & ".join([str(matrix_b[row][col]) for col in range(cols)]) for row in range(rows)])
217
- addition_str = r" \\ ".join([" & ".join([f"{matrix_a[row][col]}+{matrix_b[row][col]}" for col in range(cols)]) for row in range(rows)])
218
- result_str = r" \\ ".join([" & ".join([str(result[row][col]) for col in range(cols)]) for row in range(rows)])
219
-
220
- # Add explanation for this subpart
221
- explanation.add_element(ca.Paragraph([f"Part ({letter}):"]))
222
- explanation.add_element(
223
- ca.Equation.make_block_equation__multiline_equals(
224
- lhs="A + B",
225
- rhs=[
226
- f"\\begin{{bmatrix}} {matrix_a_str} \\end{{bmatrix}} + \\begin{{bmatrix}} {matrix_b_str} \\end{{bmatrix}}",
227
- f"\\begin{{bmatrix}} {addition_str} \\end{{bmatrix}}",
228
- f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
229
- ]
230
- )
231
- )
232
- else:
233
- # Single part explanation (original behavior)
234
- explanation.add_element(ca.Paragraph(["Step-by-step calculation:"]))
235
-
236
- # Create properly formatted matrix strings
237
- matrix_a_str = r" \\ ".join([" & ".join([str(self.matrix_a[i][j]) for j in range(self.cols)]) for i in range(self.rows)])
238
- matrix_b_str = r" \\ ".join([" & ".join([str(self.matrix_b[i][j]) for j in range(self.cols)]) for i in range(self.rows)])
239
- addition_str = r" \\ ".join([" & ".join([f"{self.matrix_a[i][j]}+{self.matrix_b[i][j]}" for j in range(self.cols)]) for i in range(self.rows)])
240
- result_str = r" \\ ".join([" & ".join([str(self.result[i][j]) for j in range(self.cols)]) for i in range(self.rows)])
125
+ explanation.add_element(ca.Paragraph(["Step-by-step calculation:"]))
126
+
127
+ # Create properly formatted matrix strings
128
+ matrix_a_str = r" \\ ".join([
129
+ " & ".join([str(context["matrix_a"][i][j]) for j in range(context["cols"])])
130
+ for i in range(context["rows"])
131
+ ])
132
+ matrix_b_str = r" \\ ".join([
133
+ " & ".join([str(context["matrix_b"][i][j]) for j in range(context["cols"])])
134
+ for i in range(context["rows"])
135
+ ])
136
+ addition_str = r" \\ ".join([
137
+ " & ".join([f"{context['matrix_a'][i][j]}+{context['matrix_b'][i][j]}" for j in range(context["cols"])])
138
+ for i in range(context["rows"])
139
+ ])
140
+ result_str = r" \\ ".join([
141
+ " & ".join([str(context["result"][i][j]) for j in range(context["cols"])])
142
+ for i in range(context["rows"])
143
+ ])
241
144
 
242
- explanation.add_element(
243
- ca.Equation.make_block_equation__multiline_equals(
244
- lhs="A + B",
245
- rhs=[
246
- f"\\begin{{bmatrix}} {matrix_a_str} \\end{{bmatrix}} + \\begin{{bmatrix}} {matrix_b_str} \\end{{bmatrix}}",
247
- f"\\begin{{bmatrix}} {addition_str} \\end{{bmatrix}}",
248
- f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
249
- ]
250
- )
145
+ explanation.add_element(
146
+ ca.Equation.make_block_equation__multiline_equals(
147
+ lhs="A + B",
148
+ rhs=[
149
+ f"\\begin{{bmatrix}} {matrix_a_str} \\end{{bmatrix}} + \\begin{{bmatrix}} {matrix_b_str} \\end{{bmatrix}}",
150
+ f"\\begin{{bmatrix}} {addition_str} \\end{{bmatrix}}",
151
+ f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
152
+ ]
251
153
  )
154
+ )
252
155
 
253
- return explanation
156
+ return explanation, []
254
157
 
255
158
 
256
159
  @QuestionRegistry.register()
@@ -261,112 +164,57 @@ class MatrixScalarMultiplication(MatrixMathQuestion):
261
164
  MIN_SCALAR = 2
262
165
  MAX_SCALAR = 9
263
166
 
264
- def _generate_scalar(self):
167
+ @staticmethod
168
+ def _generate_scalar(rng, min_scalar, max_scalar):
265
169
  """Generate a scalar for multiplication."""
266
- return self.rng.randint(self.MIN_SCALAR, self.MAX_SCALAR)
267
-
268
- def generate_operands(self):
269
- """Generate scalar and matrix for scalar multiplication."""
270
- # Generate matrix dimensions
271
- self.rows = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
272
- self.cols = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
273
-
274
- # Generate matrix (we'll generate scalar per subpart in refresh)
275
- matrix = self._generate_matrix(self.rows, self.cols)
276
- dummy_matrix = matrix # Not used but needed for interface compatibility
277
- return matrix, dummy_matrix
278
-
279
- def get_operator(self):
280
- """Return scalar multiplication operator with current scalar."""
281
- if hasattr(self, 'scalar'):
282
- return f"{self.scalar} \\cdot"
283
- else:
284
- return "k \\cdot" # Fallback for multipart case
285
-
286
- def calculate_single_result(self, matrix_a, matrix_b):
287
- """Calculate scalar multiplication result."""
288
- # For scalar multiplication, we only use matrix_a and need self.scalar
289
- rows = len(matrix_a)
290
- cols = len(matrix_a[0])
291
- return [[self.scalar * matrix_a[i][j] for j in range(cols)] for i in range(rows)]
292
-
293
- def create_subquestion_answers(self, subpart_index, result):
294
- """Create answer objects for matrix scalar multiplication result."""
295
- if subpart_index == 0 and not self.is_multipart():
296
- # For single questions, use the old answer format
297
- rows = len(result)
298
- cols = len(result[0])
299
- for i in range(rows):
300
- for j in range(cols):
301
- answer_key = f"answer_{i}_{j}"
302
- self.answers[answer_key] = ca.AnswerTypes.Int(result[i][j])
303
- else:
304
- # For multipart questions, use subpart letter format
305
- letter = chr(ord('a') + subpart_index)
306
- rows = len(result)
307
- cols = len(result[0])
308
- for i in range(rows):
309
- for j in range(cols):
310
- answer_key = f"subpart_{letter}_{i}_{j}"
311
- self.answers[answer_key] = ca.AnswerTypes.Int(result[i][j])
312
-
313
- def refresh(self, *args, **kwargs):
314
- """Override refresh to handle different scalars per subpart."""
315
- if self.is_multipart():
316
- # For multipart questions, handle everything ourselves like VectorScalarMultiplication
317
- Question.refresh(self, *args, **kwargs)
318
-
319
- # Generate matrix dimensions
320
- self.rows = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
321
- self.cols = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
322
-
323
- # Clear any existing data
324
- self.answers = {}
325
-
326
- # Generate multiple subquestions with different scalars
327
- self.subquestion_data = []
328
- for i in range(self.num_subquestions):
329
- # Generate matrix and scalar for each subquestion
330
- matrix = self._generate_matrix(self.rows, self.cols)
331
- scalar = self._generate_scalar()
332
- result = [[scalar * matrix[i][j] for j in range(self.cols)] for i in range(self.rows)]
333
-
334
- self.subquestion_data.append({
335
- 'operand_a': matrix,
336
- 'operand_b': matrix, # Not used but kept for consistency
337
- 'matrix': matrix, # For compatibility
338
- 'scalar': scalar,
339
- 'result': result
340
- })
341
-
342
- # Create answers for this subpart
343
- self.create_subquestion_answers(i, result)
344
- else:
345
- # For single questions, generate scalar first
346
- self.scalar = self._generate_scalar()
347
- # Then call super() normally
348
- super().refresh(*args, **kwargs)
349
-
350
- # For backward compatibility
351
- if hasattr(self, 'operand_a'):
352
- self.matrix = self.operand_a
353
-
354
- def generate_subquestion_data(self):
355
- """Override to handle scalar multiplication format."""
356
- subparts = []
357
- for data in self.subquestion_data:
358
- matrix_latex = ca.Matrix.to_latex(data['matrix'], "b")
359
- scalar = data['scalar']
360
- # Return scalar * matrix as a single string
361
- subparts.append(f"{scalar} \\cdot {matrix_latex}")
362
- return subparts
363
-
364
- def format_single_equation(self, operand_a, operand_b):
365
- """Format the equation for single questions."""
366
- matrix_latex = ca.Matrix.to_latex(operand_a, "b")
367
- return f"{self.scalar} \\cdot {matrix_latex}"
368
-
369
- def get_explanation(self, **kwargs) -> ca.Section:
170
+ return rng.randint(min_scalar, max_scalar)
171
+
172
+ @classmethod
173
+ def _build_context(cls, *, rng_seed=None, **kwargs):
174
+ rng = random.Random(rng_seed)
175
+ num_subquestions = kwargs.get("num_subquestions", 1)
176
+ if num_subquestions > 1:
177
+ raise NotImplementedError("Multipart not supported")
178
+
179
+ rows = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
180
+ cols = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
181
+ matrix = cls._generate_matrix(rng, rows, cols)
182
+ scalar = cls._generate_scalar(rng, cls.MIN_SCALAR, cls.MAX_SCALAR)
183
+ result = [[scalar * matrix[i][j] for j in range(cols)] for i in range(rows)]
184
+
185
+ return {
186
+ "rows": rows,
187
+ "cols": cols,
188
+ "matrix": matrix,
189
+ "scalar": scalar,
190
+ "result": result,
191
+ "num_subquestions": num_subquestions,
192
+ }
193
+
194
+ @classmethod
195
+ def _build_body(cls, context):
196
+ body = ca.Section()
197
+ body.add_element(ca.Paragraph(["Calculate the following:"]))
198
+
199
+ matrix_elem = ca.Matrix(data=context["matrix"], bracket_type="b")
200
+ body.add_element(ca.MathExpression([f"{context['scalar']} \\cdot ", matrix_elem, " = "]))
201
+
202
+ answer_matrix = [
203
+ [ca.AnswerTypes.Int(value) for value in row]
204
+ for row in context["result"]
205
+ ]
206
+ table, table_answers = cls._create_answer_table(answer_matrix)
207
+ body.add_element(
208
+ ca.OnlyHtml([
209
+ ca.Paragraph(["Result matrix:"]),
210
+ table
211
+ ])
212
+ )
213
+
214
+ return body, table_answers
215
+
216
+ @classmethod
217
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
370
218
  explanation = ca.Section()
371
219
 
372
220
  explanation.add_element(
@@ -375,55 +223,33 @@ class MatrixScalarMultiplication(MatrixMathQuestion):
375
223
  ])
376
224
  )
377
225
 
378
- if self.is_multipart():
379
- # Handle multipart explanations
380
- explanation.add_element(ca.Paragraph(["Step-by-step calculation for each part:"]))
381
- for i, data in enumerate(self.subquestion_data):
382
- letter = chr(ord('a') + i)
383
- matrix = data.get('matrix', data['operand_a'])
384
- scalar = data['scalar']
385
- result = data['result']
386
-
387
- # Create LaTeX strings for multiline equation
388
- rows = len(matrix)
389
- cols = len(matrix[0])
390
- matrix_str = r" \\ ".join([" & ".join([str(matrix[row][col]) for col in range(cols)]) for row in range(rows)])
391
- multiplication_str = r" \\ ".join([" & ".join([f"{scalar} \\cdot {matrix[row][col]}" for col in range(cols)]) for row in range(rows)])
392
- result_str = r" \\ ".join([" & ".join([str(result[row][col]) for col in range(cols)]) for row in range(rows)])
393
-
394
- # Add explanation for this subpart
395
- explanation.add_element(ca.Paragraph([f"Part ({letter}):"]))
396
- explanation.add_element(
397
- ca.Equation.make_block_equation__multiline_equals(
398
- lhs=f"{scalar} \\cdot A",
399
- rhs=[
400
- f"{scalar} \\cdot \\begin{{bmatrix}} {matrix_str} \\end{{bmatrix}}",
401
- f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
402
- f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
403
- ]
404
- )
405
- )
406
- else:
407
- # Single part explanation
408
- explanation.add_element(ca.Paragraph(["Step-by-step calculation:"]))
409
-
410
- # Create properly formatted matrix strings
411
- matrix_str = r" \\ ".join([" & ".join([str(self.matrix[i][j]) for j in range(self.cols)]) for i in range(self.rows)])
412
- multiplication_str = r" \\ ".join([" & ".join([f"{self.scalar} \\cdot {self.matrix[i][j]}" for j in range(self.cols)]) for i in range(self.rows)])
413
- result_str = r" \\ ".join([" & ".join([str(self.result[i][j]) for j in range(self.cols)]) for i in range(self.rows)])
226
+ explanation.add_element(ca.Paragraph(["Step-by-step calculation:"]))
227
+
228
+ matrix_str = r" \\ ".join([
229
+ " & ".join([str(context["matrix"][row][col]) for col in range(context["cols"])])
230
+ for row in range(context["rows"])
231
+ ])
232
+ multiplication_str = r" \\ ".join([
233
+ " & ".join([f"{context['scalar']} \\cdot {context['matrix'][row][col]}" for col in range(context["cols"])])
234
+ for row in range(context["rows"])
235
+ ])
236
+ result_str = r" \\ ".join([
237
+ " & ".join([str(context["result"][row][col]) for col in range(context["cols"])])
238
+ for row in range(context["rows"])
239
+ ])
414
240
 
415
- explanation.add_element(
416
- ca.Equation.make_block_equation__multiline_equals(
417
- lhs=f"{self.scalar} \\cdot A",
418
- rhs=[
419
- f"{self.scalar} \\cdot \\begin{{bmatrix}} {matrix_str} \\end{{bmatrix}}",
420
- f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
421
- f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
422
- ]
423
- )
241
+ explanation.add_element(
242
+ ca.Equation.make_block_equation__multiline_equals(
243
+ lhs=f"{context['scalar']} \\cdot A",
244
+ rhs=[
245
+ f"{context['scalar']} \\cdot \\begin{{bmatrix}} {matrix_str} \\end{{bmatrix}}",
246
+ f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
247
+ f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
248
+ ]
424
249
  )
250
+ )
425
251
 
426
- return explanation
252
+ return explanation, []
427
253
 
428
254
 
429
255
  @QuestionRegistry.register()
@@ -433,129 +259,96 @@ class MatrixMultiplication(MatrixMathQuestion):
433
259
  MAX_SIZE = 4
434
260
  PROBABILITY_OF_VALID = 0.875 # 7/8 chance of success, 1/8 chance of failure
435
261
 
436
- def generate_operands(self):
437
- """Generate two matrices for multiplication."""
438
- # For multipart questions, always generate valid multiplications
439
- # For single questions, use probability to determine validity
440
- if self.is_multipart():
441
- should_be_valid = True # Always valid for multipart
442
- else:
443
- should_be_valid = self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1-self.PROBABILITY_OF_VALID], k=1)[0]
262
+ @classmethod
263
+ def _build_context(cls, *, rng_seed=None, **kwargs):
264
+ rng = random.Random(rng_seed)
265
+ num_subquestions = kwargs.get("num_subquestions", 1)
266
+ if num_subquestions > 1:
267
+ raise NotImplementedError("Multipart not supported")
268
+
269
+ should_be_valid = rng.choices(
270
+ [True, False],
271
+ weights=[cls.PROBABILITY_OF_VALID, 1 - cls.PROBABILITY_OF_VALID],
272
+ k=1,
273
+ )[0]
444
274
 
445
275
  if should_be_valid:
446
- # Generate dimensions that allow multiplication
447
- self.rows_a = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
448
- self.cols_a = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
449
- self.rows_b = self.cols_a # Ensure multiplication is possible
450
- self.cols_b = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
276
+ rows_a = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
277
+ cols_a = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
278
+ rows_b = cols_a
279
+ cols_b = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
451
280
  else:
452
- # Generate dimensions that don't allow multiplication
453
- self.rows_a = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
454
- self.cols_a = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
455
- self.rows_b = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
456
- self.cols_b = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
457
- # Ensure they don't match by chance
458
- while self.cols_a == self.rows_b:
459
- self.rows_b = self.rng.randint(self.MIN_SIZE, self.MAX_SIZE)
460
-
461
- # Store multiplication possibility
462
- self.multiplication_possible = (self.cols_a == self.rows_b)
463
-
464
- # Generate matrices
465
- matrix_a = self._generate_matrix(self.rows_a, self.cols_a)
466
- matrix_b = self._generate_matrix(self.rows_b, self.cols_b)
467
-
468
- # Calculate max dimensions for answer table
469
- self.max_dim = max(self.rows_a, self.cols_a, self.rows_b, self.cols_b)
470
-
471
- return matrix_a, matrix_b
472
-
473
- def get_operator(self):
474
- """Return the multiplication operator."""
475
- return "\\cdot"
476
-
477
- def calculate_single_result(self, matrix_a, matrix_b):
478
- """Calculate matrix multiplication result."""
479
- rows_a = len(matrix_a)
480
- cols_a = len(matrix_a[0])
481
- rows_b = len(matrix_b)
482
- cols_b = len(matrix_b[0])
483
-
484
- # Check if multiplication is possible
485
- if cols_a != rows_b:
486
- return None # Multiplication not possible
487
-
488
- # Calculate result
489
- result = [[sum(matrix_a[i][k] * matrix_b[k][j] for k in range(cols_a))
490
- for j in range(cols_b)] for i in range(rows_a)]
491
-
492
- # Store result dimensions
493
- self.result_rows = rows_a
494
- self.result_cols = cols_b
495
-
496
- return result
497
-
498
- def create_subquestion_answers(self, subpart_index, result):
499
- """Create answer objects for matrix multiplication result."""
500
- if subpart_index == 0 and not self.is_multipart():
501
- # For single questions, use the old answer format
502
- # Dimension answers
503
- if result is not None:
504
- self.answers["result_rows"] = ca.AnswerTypes.Int(self.result_rows, label="Number of rows in result")
505
- self.answers["result_cols"] = ca.AnswerTypes.Int(self.result_cols, label="Number of columns in result")
506
-
507
- # Matrix element answers
508
- for i in range(self.max_dim):
509
- for j in range(self.max_dim):
510
- answer_key = f"answer_{i}_{j}"
511
- if i < self.result_rows and j < self.result_cols:
512
- self.answers[answer_key] = ca.AnswerTypes.Int(result[i][j])
513
- else:
514
- self.answers[answer_key] = ca.AnswerTypes.String("-")
515
- else:
516
- # Multiplication not possible
517
- self.answers["result_rows"] = ca.AnswerTypes.String("-", label="Number of rows in result")
518
- self.answers["result_cols"] = ca.AnswerTypes.String("-", label="Number of columns in result")
519
-
520
- # All matrix elements are "-"
521
- for i in range(self.max_dim):
522
- for j in range(self.max_dim):
523
- answer_key = f"answer_{i}_{j}"
524
- self.answers[answer_key] = ca.AnswerTypes.String("-")
281
+ rows_a = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
282
+ cols_a = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
283
+ rows_b = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
284
+ cols_b = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
285
+ while cols_a == rows_b:
286
+ rows_b = rng.randint(cls.MIN_SIZE, cls.MAX_SIZE)
287
+
288
+ multiplication_possible = (cols_a == rows_b)
289
+
290
+ matrix_a = cls._generate_matrix(rng, rows_a, cols_a)
291
+ matrix_b = cls._generate_matrix(rng, rows_b, cols_b)
292
+ max_dim = max(rows_a, cols_a, rows_b, cols_b)
293
+
294
+ result = None
295
+ result_rows = None
296
+ result_cols = None
297
+ if multiplication_possible:
298
+ result = [[sum(matrix_a[i][k] * matrix_b[k][j] for k in range(cols_a))
299
+ for j in range(cols_b)] for i in range(rows_a)]
300
+ result_rows = rows_a
301
+ result_cols = cols_b
302
+
303
+ return {
304
+ "rows_a": rows_a,
305
+ "cols_a": cols_a,
306
+ "rows_b": rows_b,
307
+ "cols_b": cols_b,
308
+ "matrix_a": matrix_a,
309
+ "matrix_b": matrix_b,
310
+ "multiplication_possible": multiplication_possible,
311
+ "result": result,
312
+ "result_rows": result_rows,
313
+ "result_cols": result_cols,
314
+ "max_dim": max_dim,
315
+ "num_subquestions": num_subquestions,
316
+ }
317
+
318
+ @classmethod
319
+ def _build_body(cls, context):
320
+ body = ca.Section()
321
+ body.add_element(ca.Paragraph(["Calculate the following:"]))
322
+
323
+ matrix_a_elem = ca.Matrix(data=context["matrix_a"], bracket_type="b")
324
+ matrix_b_elem = ca.Matrix(data=context["matrix_b"], bracket_type="b")
325
+ body.add_element(ca.MathExpression([matrix_a_elem, " \cdot ", matrix_b_elem, " = "]))
326
+
327
+ if context["result"] is not None:
328
+ rows_ans = ca.AnswerTypes.Int(context["result_rows"], label="Number of rows in result")
329
+ cols_ans = ca.AnswerTypes.Int(context["result_cols"], label="Number of columns in result")
525
330
  else:
526
- # For multipart questions, use subpart letter format
527
- letter = chr(ord('a') + subpart_index)
528
-
529
- # For multipart, result should always be valid
530
- if result is not None:
531
- rows = len(result)
532
- cols = len(result[0])
533
- for i in range(rows):
534
- for j in range(cols):
535
- answer_key = f"subpart_{letter}_{i}_{j}"
536
- self.answers[answer_key] = ca.AnswerTypes.Int(result[i][j])
537
-
538
- def _add_single_question_answers(self, body):
539
- """Add Canvas-only answer fields for MatrixMultiplication with dash instruction.
331
+ rows_ans = ca.AnswerTypes.String("-", label="Number of rows in result")
332
+ cols_ans = ca.AnswerTypes.String("-", label="Number of columns in result")
540
333
 
541
- Returns:
542
- List of Answer objects that were added to the body.
543
- """
544
- answers = []
545
-
546
- # Dimension answers for matrix multiplication
547
- if hasattr(self, 'answers') and "result_rows" in self.answers:
548
- rows_ans = self.answers["result_rows"]
549
- cols_ans = self.answers["result_cols"]
550
- answers.extend([rows_ans, cols_ans])
551
- body.add_element(
552
- ca.OnlyHtml([
553
- ca.AnswerBlock([rows_ans, cols_ans])
554
- ])
555
- )
334
+ answers = [rows_ans, cols_ans]
335
+ body.add_element(
336
+ ca.OnlyHtml([
337
+ ca.AnswerBlock([rows_ans, cols_ans])
338
+ ])
339
+ )
556
340
 
557
- # Matrix result table with dash instruction
558
- table, table_answers = self._create_answer_table(self.max_dim, self.max_dim, self.answers)
341
+ answer_matrix = []
342
+ for i in range(context["max_dim"]):
343
+ row = []
344
+ for j in range(context["max_dim"]):
345
+ if context["result"] is not None and i < context["result_rows"] and j < context["result_cols"]:
346
+ row.append(ca.AnswerTypes.Int(context["result"][i][j]))
347
+ else:
348
+ row.append(ca.AnswerTypes.String("-"))
349
+ answer_matrix.append(row)
350
+
351
+ table, table_answers = cls._create_answer_table(answer_matrix)
559
352
  answers.extend(table_answers)
560
353
  body.add_element(
561
354
  ca.OnlyHtml([
@@ -564,85 +357,57 @@ class MatrixMultiplication(MatrixMathQuestion):
564
357
  ])
565
358
  )
566
359
 
567
- return answers
568
-
569
- def refresh(self, *args, **kwargs):
570
- """Override refresh to handle matrix attributes."""
571
- super().refresh(*args, **kwargs)
572
-
573
- # For backward compatibility, set matrix attributes for single questions
574
- if not self.is_multipart():
575
- self.matrix_a = self.operand_a
576
- self.matrix_b = self.operand_b
360
+ return body, answers
577
361
 
578
- def get_explanation(self, **kwargs) -> ca.Section:
362
+ @classmethod
363
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
579
364
  explanation = ca.Section()
580
365
 
581
- if self.is_multipart():
582
- # For multipart questions, provide simpler explanations
583
- explanation.add_element(
584
- ca.Paragraph([
585
- "Matrix multiplication: Each element in the result is the dot product of "
586
- "the corresponding row from the first matrix and column from the second matrix."
587
- ])
588
- )
589
-
590
- for i, data in enumerate(self.subquestion_data):
591
- letter = chr(ord('a') + i)
592
- matrix_a = data.get('matrix_a', data['operand_a'])
593
- matrix_b = data.get('matrix_b', data['operand_b'])
594
- result = data['result']
595
-
596
- explanation.add_element(ca.Paragraph([f"Part ({letter}): Matrices multiplied successfully."]))
597
-
598
- elif hasattr(self, 'multiplication_possible') and self.multiplication_possible:
599
- # Single question with successful multiplication
366
+ if context["multiplication_possible"]:
600
367
  explanation.add_element(ca.Paragraph(["Given matrices:"]))
601
- matrix_a_latex = ca.Matrix.to_latex(self.matrix_a, "b")
602
- matrix_b_latex = ca.Matrix.to_latex(self.matrix_b, "b")
603
- explanation.add_element(ca.Equation(f"A = {matrix_a_latex}, \\quad B = {matrix_b_latex}"))
368
+ matrix_a_latex = ca.Matrix.to_latex(context["matrix_a"], "b")
369
+ matrix_b_latex = ca.Matrix.to_latex(context["matrix_b"], "b")
370
+ explanation.add_element(ca.Equation(f"A = {matrix_a_latex}, \quad B = {matrix_b_latex}"))
604
371
 
605
372
  explanation.add_element(
606
373
  ca.Paragraph([
607
- f"Matrix multiplication is possible because the number of columns in Matrix A ({self.cols_a}) "
608
- f"equals the number of rows in Matrix B ({self.rows_b}). "
609
- f"The result is a {self.result_rows}×{self.result_cols} matrix."
374
+ f"Matrix multiplication is possible because the number of columns in Matrix A ({context['cols_a']}) "
375
+ f"equals the number of rows in Matrix B ({context['rows_b']}). "
376
+ f"The result is a {context['result_rows']}×{context['result_cols']} matrix."
610
377
  ])
611
378
  )
612
379
 
613
- # Comprehensive matrix multiplication walkthrough
614
380
  explanation.add_element(ca.Paragraph(["Step-by-step calculation:"]))
615
-
616
- # Show detailed multiplication process using row×column visualization
617
- explanation.add_element(ca.Paragraph(["Each element is calculated as the dot product of a row from Matrix A and a column from Matrix B:"]))
618
-
619
- # Show calculation for first few elements with row×column visualization
620
- for i in range(min(2, self.result_rows)):
621
- for j in range(min(2, self.result_cols)):
622
- # Get the row from matrix A and column from matrix B
623
- row_a = [str(self.matrix_a[i][k]) for k in range(self.cols_a)]
624
- col_b = [str(self.matrix_b[k][j]) for k in range(self.cols_a)]
625
-
626
- # Create row and column vectors in LaTeX
627
- row_latex = f"\\begin{{bmatrix}} {' & '.join(row_a)} \\end{{bmatrix}}"
628
- col_latex = f"\\begin{{bmatrix}} {' \\\\\\\\ '.join(col_b)} \\end{{bmatrix}}"
629
-
630
- # Show the calculation
631
- element_calc = " + ".join([f"{self.matrix_a[i][k]} \\cdot {self.matrix_b[k][j]}" for k in range(self.cols_a)])
381
+ explanation.add_element(ca.Paragraph([
382
+ "Each element is calculated as the dot product of a row from Matrix A and a column from Matrix B:"
383
+ ]))
384
+
385
+ for i in range(min(2, context["result_rows"])):
386
+ for j in range(min(2, context["result_cols"])):
387
+ row_a = [str(context["matrix_a"][i][k]) for k in range(context["cols_a"])]
388
+ col_b = [str(context["matrix_b"][k][j]) for k in range(context["cols_a"])]
389
+
390
+ row_latex = f"\begin{{bmatrix}} {' & '.join(row_a)} \end{{bmatrix}}"
391
+ col_latex = f"\begin{{bmatrix}} {' \\ '.join(col_b)} \end{{bmatrix}}"
392
+ element_calc = " + ".join([
393
+ f"{context['matrix_a'][i][k]} \cdot {context['matrix_b'][k][j]}"
394
+ for k in range(context["cols_a"])
395
+ ])
632
396
 
633
397
  explanation.add_element(
634
- ca.Equation(f"({i+1},{j+1}): {row_latex} \\cdot {col_latex} = {element_calc} = {self.result[i][j]}")
398
+ ca.Equation(
399
+ f"({i+1},{j+1}): {row_latex} \cdot {col_latex} = {element_calc} = {context['result'][i][j]}"
400
+ )
635
401
  )
636
402
 
637
403
  explanation.add_element(ca.Paragraph(["Final result:"]))
638
- explanation.add_element(ca.Matrix(data=self.result, bracket_type="b"))
404
+ explanation.add_element(ca.Matrix(data=context["result"], bracket_type="b"))
639
405
  else:
640
- # Single question with failed multiplication
641
406
  explanation.add_element(
642
407
  ca.Paragraph([
643
- f"Matrix multiplication is not possible because the number of columns in Matrix A ({getattr(self, 'cols_a', 'unknown')}) "
644
- f"does not equal the number of rows in Matrix B ({getattr(self, 'rows_b', 'unknown')})."
408
+ f"Matrix multiplication is not possible because the number of columns in Matrix A ({context['cols_a']}) "
409
+ f"does not equal the number of rows in Matrix B ({context['rows_b']})."
645
410
  ])
646
411
  )
647
412
 
648
- return explanation
413
+ return explanation, []