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.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +627 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1955 -0
- QuizGenerator/generate.py +253 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +579 -0
- QuizGenerator/mixins.py +548 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +391 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
- QuizGenerator/premade_questions/cst334/process.py +648 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
- QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
- QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
- QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
- QuizGenerator/premade_questions/cst463/models/text.py +203 -0
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +715 -0
- QuizGenerator/quiz.py +467 -0
- QuizGenerator/regenerate.py +472 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.4.2.dist-info/METADATA +265 -0
- quizgenerator-0.4.2.dist-info/RECORD +52 -0
- quizgenerator-0.4.2.dist-info/WHEEL +4 -0
- quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
- 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
|