QuizGenerator 0.7.1__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- QuizGenerator/contentast.py +6 -6
- QuizGenerator/generate.py +2 -1
- QuizGenerator/mixins.py +14 -100
- QuizGenerator/premade_questions/basic.py +24 -29
- QuizGenerator/premade_questions/cst334/languages.py +100 -99
- QuizGenerator/premade_questions/cst334/math_questions.py +112 -122
- QuizGenerator/premade_questions/cst334/memory_questions.py +621 -621
- QuizGenerator/premade_questions/cst334/persistence_questions.py +137 -163
- QuizGenerator/premade_questions/cst334/process.py +312 -322
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +34 -35
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +41 -36
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +48 -41
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +285 -520
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -126
- QuizGenerator/premade_questions/cst463/models/attention.py +44 -50
- QuizGenerator/premade_questions/cst463/models/cnns.py +43 -47
- QuizGenerator/premade_questions/cst463/models/matrices.py +61 -11
- QuizGenerator/premade_questions/cst463/models/rnns.py +48 -50
- QuizGenerator/premade_questions/cst463/models/text.py +65 -67
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +47 -46
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +100 -156
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +93 -141
- QuizGenerator/question.py +273 -202
- QuizGenerator/quiz.py +8 -5
- QuizGenerator/regenerate.py +14 -6
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/METADATA +30 -2
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/RECORD +30 -30
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.7.1.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(
|
|
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
|
-
|
|
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 [[
|
|
34
|
+
return [[rng.randint(min_val, max_val) for _ in range(cols)] for _ in range(rows)]
|
|
33
35
|
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
for
|
|
49
|
-
|
|
50
|
-
ans
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _generate_scalar(rng, min_scalar, max_scalar):
|
|
265
169
|
"""Generate a scalar for multiplication."""
|
|
266
|
-
return
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
for
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
for
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
558
|
-
|
|
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
|
-
|
|
362
|
+
@classmethod
|
|
363
|
+
def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
|
|
579
364
|
explanation = ca.Section()
|
|
580
365
|
|
|
581
|
-
if
|
|
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(
|
|
602
|
-
matrix_b_latex = ca.Matrix.to_latex(
|
|
603
|
-
explanation.add_element(ca.Equation(f"A = {matrix_a_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 ({
|
|
608
|
-
f"equals the number of rows in Matrix B ({
|
|
609
|
-
f"The result is a {
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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(
|
|
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=
|
|
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 ({
|
|
644
|
-
f"does not equal the number of rows in Matrix B ({
|
|
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, []
|