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