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,534 @@
|
|
|
1
|
+
#!env python
|
|
2
|
+
import abc
|
|
3
|
+
import logging
|
|
4
|
+
import math
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from QuizGenerator.question import Question, QuestionRegistry, Answer
|
|
8
|
+
from QuizGenerator.contentast import ContentAST
|
|
9
|
+
from QuizGenerator.mixins import MathOperationQuestion
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VectorMathQuestion(MathOperationQuestion, Question):
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
kwargs["topic"] = kwargs.get("topic", Question.Topic.MATH)
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
|
|
20
|
+
def _generate_vector(self, dimension, min_val=-10, max_val=10):
|
|
21
|
+
"""Generate a vector with random integer values."""
|
|
22
|
+
return [self.rng.randint(min_val, max_val) for _ in range(dimension)]
|
|
23
|
+
|
|
24
|
+
def _format_vector(self, vector):
|
|
25
|
+
"""Format vector for display as column vector using ContentAST.Matrix."""
|
|
26
|
+
# Convert to column matrix format
|
|
27
|
+
matrix_data = [[v] for v in vector]
|
|
28
|
+
return ContentAST.Matrix.to_latex(matrix_data, "b")
|
|
29
|
+
|
|
30
|
+
def _format_vector_inline(self, vector):
|
|
31
|
+
"""Format vector for inline display."""
|
|
32
|
+
elements = [str(v) for v in vector]
|
|
33
|
+
return f"({', '.join(elements)})"
|
|
34
|
+
|
|
35
|
+
# Implement MathOperationQuestion abstract methods
|
|
36
|
+
|
|
37
|
+
def generate_operands(self):
|
|
38
|
+
"""Generate two vectors for the operation."""
|
|
39
|
+
if not hasattr(self, 'dimension'):
|
|
40
|
+
self.dimension = self.rng.randint(self.MIN_DIMENSION, self.MAX_DIMENSION)
|
|
41
|
+
vector_a = self._generate_vector(self.dimension)
|
|
42
|
+
vector_b = self._generate_vector(self.dimension)
|
|
43
|
+
return vector_a, vector_b
|
|
44
|
+
|
|
45
|
+
def format_operand_latex(self, operand):
|
|
46
|
+
"""Format a vector for LaTeX display."""
|
|
47
|
+
return self._format_vector(operand)
|
|
48
|
+
|
|
49
|
+
def format_single_equation(self, operand_a, operand_b):
|
|
50
|
+
"""Format the equation for single questions."""
|
|
51
|
+
operand_a_latex = self.format_operand_latex(operand_a)
|
|
52
|
+
operand_b_latex = self.format_operand_latex(operand_b)
|
|
53
|
+
return f"{operand_a_latex} {self.get_operator()} {operand_b_latex}"
|
|
54
|
+
|
|
55
|
+
# Vector-specific overrides
|
|
56
|
+
|
|
57
|
+
def refresh(self, *args, **kwargs):
|
|
58
|
+
# Generate vector dimension first
|
|
59
|
+
self.dimension = self.rng.randint(self.MIN_DIMENSION, self.MAX_DIMENSION)
|
|
60
|
+
|
|
61
|
+
# Call parent refresh which will use our generate_operands method
|
|
62
|
+
super().refresh(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
# For backward compatibility, set vector_a/vector_b for single questions
|
|
65
|
+
if not self.is_multipart():
|
|
66
|
+
self.vector_a = self.operand_a
|
|
67
|
+
self.vector_b = self.operand_b
|
|
68
|
+
|
|
69
|
+
def generate_subquestion_data(self):
|
|
70
|
+
"""Generate LaTeX content for each subpart of the question.
|
|
71
|
+
Override to handle vector-specific keys in subquestion_data."""
|
|
72
|
+
subparts = []
|
|
73
|
+
for data in self.subquestion_data:
|
|
74
|
+
# Map generic operand names to vector names for compatibility
|
|
75
|
+
vector_a = data.get('vector_a', data['operand_a'])
|
|
76
|
+
vector_b = data.get('vector_b', data['operand_b'])
|
|
77
|
+
|
|
78
|
+
vector_a_latex = self._format_vector(vector_a)
|
|
79
|
+
vector_b_latex = self._format_vector(vector_b)
|
|
80
|
+
# Return as tuple of (matrix_a, operator, matrix_b)
|
|
81
|
+
subparts.append((vector_a_latex, self.get_operator(), vector_b_latex))
|
|
82
|
+
return subparts
|
|
83
|
+
|
|
84
|
+
def _add_single_question_answers(self, body):
|
|
85
|
+
"""Add Canvas-only answer fields for single questions."""
|
|
86
|
+
# Check if it's a scalar result (like dot product)
|
|
87
|
+
if hasattr(self, 'answers') and len(self.answers) == 1:
|
|
88
|
+
# Single scalar answer
|
|
89
|
+
answer_key = list(self.answers.keys())[0]
|
|
90
|
+
body.add_element(ContentAST.OnlyHtml([ContentAST.Answer(answer=self.answers[answer_key])]))
|
|
91
|
+
else:
|
|
92
|
+
# Vector results (like addition) - show table
|
|
93
|
+
body.add_element(ContentAST.OnlyHtml([ContentAST.Paragraph(["Enter your answer as a column vector:"])]))
|
|
94
|
+
table_data = []
|
|
95
|
+
for i in range(self.dimension):
|
|
96
|
+
if f"result_{i}" in self.answers:
|
|
97
|
+
table_data.append([ContentAST.Answer(answer=self.answers[f"result_{i}"])])
|
|
98
|
+
if table_data:
|
|
99
|
+
body.add_element(ContentAST.OnlyHtml([ContentAST.Table(data=table_data, padding=True)]))
|
|
100
|
+
|
|
101
|
+
# Abstract methods that subclasses must still implement
|
|
102
|
+
@abc.abstractmethod
|
|
103
|
+
def get_operator(self):
|
|
104
|
+
"""Return the LaTeX operator for this operation."""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
@abc.abstractmethod
|
|
108
|
+
def calculate_single_result(self, vector_a, vector_b):
|
|
109
|
+
"""Calculate the result for a single question with two vectors."""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@abc.abstractmethod
|
|
113
|
+
def create_subquestion_answers(self, subpart_index, result):
|
|
114
|
+
"""Create answer objects for a subquestion result."""
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@QuestionRegistry.register()
|
|
119
|
+
class VectorAddition(VectorMathQuestion):
|
|
120
|
+
|
|
121
|
+
MIN_DIMENSION = 2
|
|
122
|
+
MAX_DIMENSION = 4
|
|
123
|
+
|
|
124
|
+
def get_operator(self):
|
|
125
|
+
return "+"
|
|
126
|
+
|
|
127
|
+
def calculate_single_result(self, vector_a, vector_b):
|
|
128
|
+
return [vector_a[i] + vector_b[i] for i in range(len(vector_a))]
|
|
129
|
+
|
|
130
|
+
def create_subquestion_answers(self, subpart_index, result):
|
|
131
|
+
letter = chr(ord('a') + subpart_index)
|
|
132
|
+
for j in range(len(result)):
|
|
133
|
+
self.answers[f"subpart_{letter}_{j}"] = Answer.integer(f"subpart_{letter}_{j}", result[j])
|
|
134
|
+
|
|
135
|
+
def create_single_answers(self, result):
|
|
136
|
+
for i in range(len(result)):
|
|
137
|
+
self.answers[f"result_{i}"] = Answer.integer(f"result_{i}", result[i])
|
|
138
|
+
|
|
139
|
+
def get_explanation(self):
|
|
140
|
+
explanation = ContentAST.Section()
|
|
141
|
+
|
|
142
|
+
explanation.add_element(ContentAST.Paragraph(["To add vectors, we add corresponding components:"]))
|
|
143
|
+
|
|
144
|
+
if self.is_multipart():
|
|
145
|
+
# Handle multipart explanations
|
|
146
|
+
for i, data in enumerate(self.subquestion_data):
|
|
147
|
+
letter = chr(ord('a') + i)
|
|
148
|
+
vector_a = data['vector_a']
|
|
149
|
+
vector_b = data['vector_b']
|
|
150
|
+
result = data['result']
|
|
151
|
+
|
|
152
|
+
# Create LaTeX strings for multiline equation
|
|
153
|
+
vector_a_str = r" \\ ".join([str(v) for v in vector_a])
|
|
154
|
+
vector_b_str = r" \\ ".join([str(v) for v in vector_b])
|
|
155
|
+
addition_str = r" \\ ".join([f"{vector_a[j]}+{vector_b[j]}" for j in range(self.dimension)])
|
|
156
|
+
result_str = r" \\ ".join([str(v) for v in result])
|
|
157
|
+
|
|
158
|
+
# Add explanation for this subpart
|
|
159
|
+
explanation.add_element(ContentAST.Paragraph([f"Part ({letter}):"]))
|
|
160
|
+
explanation.add_element(
|
|
161
|
+
ContentAST.Equation.make_block_equation__multiline_equals(
|
|
162
|
+
lhs="\\vec{a} + \\vec{b}",
|
|
163
|
+
rhs=[
|
|
164
|
+
f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} + \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
|
|
165
|
+
f"\\begin{{bmatrix}} {addition_str} \\end{{bmatrix}}",
|
|
166
|
+
f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
# Single part explanation (original behavior)
|
|
172
|
+
vector_a_str = r" \\ ".join([str(v) for v in self.vector_a])
|
|
173
|
+
vector_b_str = r" \\ ".join([str(v) for v in self.vector_b])
|
|
174
|
+
addition_str = r" \\ ".join([f"{self.vector_a[i]}+{self.vector_b[i]}" for i in range(self.dimension)])
|
|
175
|
+
result_str = r" \\ ".join([str(v) for v in self.result])
|
|
176
|
+
|
|
177
|
+
explanation.add_element(
|
|
178
|
+
ContentAST.Equation.make_block_equation__multiline_equals(
|
|
179
|
+
lhs="\\vec{a} + \\vec{b}",
|
|
180
|
+
rhs=[
|
|
181
|
+
f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} + \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
|
|
182
|
+
f"\\begin{{bmatrix}} {addition_str} \\end{{bmatrix}}",
|
|
183
|
+
f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return explanation
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@QuestionRegistry.register()
|
|
192
|
+
class VectorScalarMultiplication(VectorMathQuestion):
|
|
193
|
+
|
|
194
|
+
MIN_DIMENSION = 2
|
|
195
|
+
MAX_DIMENSION = 4
|
|
196
|
+
|
|
197
|
+
def _generate_scalar(self):
|
|
198
|
+
"""Generate a non-zero scalar for multiplication."""
|
|
199
|
+
scalar = self.rng.randint(-5, 5)
|
|
200
|
+
while scalar == 0: # Avoid zero scalar for more interesting problems
|
|
201
|
+
scalar = self.rng.randint(-5, 5)
|
|
202
|
+
return scalar
|
|
203
|
+
|
|
204
|
+
def generate_operands(self):
|
|
205
|
+
"""Override to generate scalar and vector."""
|
|
206
|
+
if not hasattr(self, 'dimension'):
|
|
207
|
+
self.dimension = self.rng.randint(self.MIN_DIMENSION, self.MAX_DIMENSION)
|
|
208
|
+
vector_a = self._generate_vector(self.dimension)
|
|
209
|
+
vector_b = self._generate_vector(self.dimension) # Not used, but kept for consistency
|
|
210
|
+
return vector_a, vector_b
|
|
211
|
+
|
|
212
|
+
def refresh(self, *args, **kwargs):
|
|
213
|
+
if self.is_multipart():
|
|
214
|
+
# For multipart questions, we handle everything ourselves
|
|
215
|
+
# Don't call super() because we need different scalars per subpart
|
|
216
|
+
|
|
217
|
+
# Call Question.refresh() directly to get basic setup
|
|
218
|
+
Question.refresh(self, *args, **kwargs)
|
|
219
|
+
|
|
220
|
+
# Generate vector dimension
|
|
221
|
+
self.dimension = self.rng.randint(self.MIN_DIMENSION, self.MAX_DIMENSION)
|
|
222
|
+
|
|
223
|
+
# Clear any existing data
|
|
224
|
+
self.answers = {}
|
|
225
|
+
|
|
226
|
+
# Generate multiple subquestions with their own scalars
|
|
227
|
+
self.subquestion_data = []
|
|
228
|
+
for i in range(self.num_subquestions):
|
|
229
|
+
# Generate unique vectors and scalar for each subquestion
|
|
230
|
+
vector_a = self._generate_vector(self.dimension)
|
|
231
|
+
vector_b = self._generate_vector(self.dimension) # Not used, but kept for consistency
|
|
232
|
+
scalar = self._generate_scalar()
|
|
233
|
+
result = [scalar * component for component in vector_a]
|
|
234
|
+
|
|
235
|
+
self.subquestion_data.append({
|
|
236
|
+
'operand_a': vector_a,
|
|
237
|
+
'operand_b': vector_b,
|
|
238
|
+
'vector_a': vector_a,
|
|
239
|
+
'vector_b': vector_b,
|
|
240
|
+
'scalar': scalar,
|
|
241
|
+
'result': result
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
# Create answers for this subpart
|
|
245
|
+
self.create_subquestion_answers(i, result)
|
|
246
|
+
else:
|
|
247
|
+
# For single questions, generate scalar first
|
|
248
|
+
self.scalar = self._generate_scalar()
|
|
249
|
+
# Then call super() normally
|
|
250
|
+
super().refresh(*args, **kwargs)
|
|
251
|
+
|
|
252
|
+
def get_operator(self):
|
|
253
|
+
return f"{self.scalar} \\cdot"
|
|
254
|
+
|
|
255
|
+
def calculate_single_result(self, vector_a, vector_b):
|
|
256
|
+
# For scalar multiplication, we only use vector_a and ignore vector_b
|
|
257
|
+
return [self.scalar * component for component in vector_a]
|
|
258
|
+
|
|
259
|
+
def create_subquestion_answers(self, subpart_index, result):
|
|
260
|
+
letter = chr(ord('a') + subpart_index)
|
|
261
|
+
for j in range(len(result)):
|
|
262
|
+
self.answers[f"subpart_{letter}_{j}"] = Answer.integer(f"subpart_{letter}_{j}", result[j])
|
|
263
|
+
|
|
264
|
+
def create_single_answers(self, result):
|
|
265
|
+
for i in range(len(result)):
|
|
266
|
+
self.answers[f"result_{i}"] = Answer.integer(f"result_{i}", result[i])
|
|
267
|
+
|
|
268
|
+
def generate_subquestion_data(self):
|
|
269
|
+
"""Override to handle scalar multiplication format."""
|
|
270
|
+
subparts = []
|
|
271
|
+
for data in self.subquestion_data:
|
|
272
|
+
vector_a_latex = self._format_vector(data['vector_a'])
|
|
273
|
+
# For scalar multiplication, we show scalar * vector as a single string
|
|
274
|
+
# Use the scalar from this specific subquestion's data
|
|
275
|
+
scalar = data['scalar']
|
|
276
|
+
subparts.append(f"{scalar} \\cdot {vector_a_latex}")
|
|
277
|
+
return subparts
|
|
278
|
+
|
|
279
|
+
def get_body(self):
|
|
280
|
+
body = ContentAST.Section()
|
|
281
|
+
|
|
282
|
+
body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
|
|
283
|
+
|
|
284
|
+
if self.is_multipart():
|
|
285
|
+
# Use multipart formatting with repeated problem parts
|
|
286
|
+
subpart_data = self.generate_subquestion_data()
|
|
287
|
+
repeated_part = self.create_repeated_problem_part(subpart_data)
|
|
288
|
+
body.add_element(repeated_part)
|
|
289
|
+
else:
|
|
290
|
+
# Single equation display
|
|
291
|
+
vector_a_latex = self._format_vector(self.vector_a)
|
|
292
|
+
body.add_element(ContentAST.Equation(f"{self.scalar} \\cdot {vector_a_latex} = ", inline=False))
|
|
293
|
+
|
|
294
|
+
# Canvas-only answer fields (hidden from PDF)
|
|
295
|
+
self._add_single_question_answers(body)
|
|
296
|
+
|
|
297
|
+
return body
|
|
298
|
+
|
|
299
|
+
def get_explanation(self):
|
|
300
|
+
explanation = ContentAST.Section()
|
|
301
|
+
|
|
302
|
+
explanation.add_element(ContentAST.Paragraph(["To multiply a vector by a scalar, we multiply each component by the scalar:"]))
|
|
303
|
+
|
|
304
|
+
if self.is_multipart():
|
|
305
|
+
# Handle multipart explanations
|
|
306
|
+
for i, data in enumerate(self.subquestion_data):
|
|
307
|
+
letter = chr(ord('a') + i)
|
|
308
|
+
vector_a = data['vector_a']
|
|
309
|
+
result = data['result']
|
|
310
|
+
|
|
311
|
+
# Get the scalar for this specific subpart
|
|
312
|
+
scalar = data['scalar']
|
|
313
|
+
|
|
314
|
+
# Create LaTeX strings for multiline equation
|
|
315
|
+
vector_str = r" \\ ".join([str(v) for v in vector_a])
|
|
316
|
+
multiplication_str = r" \\ ".join([f"{scalar} \\cdot {v}" for v in vector_a])
|
|
317
|
+
result_str = r" \\ ".join([str(v) for v in result])
|
|
318
|
+
|
|
319
|
+
# Add explanation for this subpart
|
|
320
|
+
explanation.add_element(ContentAST.Paragraph([f"Part ({letter}):"]))
|
|
321
|
+
explanation.add_element(
|
|
322
|
+
ContentAST.Equation.make_block_equation__multiline_equals(
|
|
323
|
+
lhs=f"{scalar} \\cdot \\vec{{v}}",
|
|
324
|
+
rhs=[
|
|
325
|
+
f"{scalar} \\cdot \\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}",
|
|
326
|
+
f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
|
|
327
|
+
f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
|
|
328
|
+
]
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
# Single part explanation - use the correct attributes
|
|
333
|
+
vector_str = r" \\ ".join([str(v) for v in self.vector_a])
|
|
334
|
+
multiplication_str = r" \\ ".join([f"{self.scalar} \\cdot {v}" for v in self.vector_a])
|
|
335
|
+
result_str = r" \\ ".join([str(v) for v in self.result])
|
|
336
|
+
|
|
337
|
+
explanation.add_element(
|
|
338
|
+
ContentAST.Equation.make_block_equation__multiline_equals(
|
|
339
|
+
lhs=f"{self.scalar} \\cdot \\vec{{v}}",
|
|
340
|
+
rhs=[
|
|
341
|
+
f"{self.scalar} \\cdot \\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}",
|
|
342
|
+
f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
|
|
343
|
+
f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
|
|
344
|
+
]
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return explanation
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@QuestionRegistry.register()
|
|
352
|
+
class VectorDotProduct(VectorMathQuestion):
|
|
353
|
+
|
|
354
|
+
MIN_DIMENSION = 2
|
|
355
|
+
MAX_DIMENSION = 4
|
|
356
|
+
|
|
357
|
+
def get_operator(self):
|
|
358
|
+
return "\\cdot"
|
|
359
|
+
|
|
360
|
+
def calculate_single_result(self, vector_a, vector_b):
|
|
361
|
+
return sum(vector_a[i] * vector_b[i] for i in range(len(vector_a)))
|
|
362
|
+
|
|
363
|
+
def create_subquestion_answers(self, subpart_index, result):
|
|
364
|
+
letter = chr(ord('a') + subpart_index)
|
|
365
|
+
self.answers[f"subpart_{letter}"] = Answer.integer(f"subpart_{letter}", result)
|
|
366
|
+
|
|
367
|
+
def create_single_answers(self, result):
|
|
368
|
+
self.answers = {
|
|
369
|
+
"dot_product": Answer.integer("dot_product", result)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
def get_explanation(self):
|
|
373
|
+
explanation = ContentAST.Section()
|
|
374
|
+
|
|
375
|
+
explanation.add_element(ContentAST.Paragraph(["The dot product is calculated by multiplying corresponding components and summing the results:"]))
|
|
376
|
+
|
|
377
|
+
if self.is_multipart():
|
|
378
|
+
# Handle multipart explanations
|
|
379
|
+
for i, data in enumerate(self.subquestion_data):
|
|
380
|
+
letter = chr(ord('a') + i)
|
|
381
|
+
vector_a = data['vector_a']
|
|
382
|
+
vector_b = data['vector_b']
|
|
383
|
+
result = data['result']
|
|
384
|
+
|
|
385
|
+
# Create LaTeX strings for multiline equation
|
|
386
|
+
vector_a_str = r" \\ ".join([str(v) for v in vector_a])
|
|
387
|
+
vector_b_str = r" \\ ".join([str(v) for v in vector_b])
|
|
388
|
+
products_str = " + ".join([f"({vector_a[j]} \\cdot {vector_b[j]})" for j in range(self.dimension)])
|
|
389
|
+
calculation_str = " + ".join([str(vector_a[j] * vector_b[j]) for j in range(self.dimension)])
|
|
390
|
+
|
|
391
|
+
# Add explanation for this subpart
|
|
392
|
+
explanation.add_element(ContentAST.Paragraph([f"Part ({letter}):"]))
|
|
393
|
+
explanation.add_element(
|
|
394
|
+
ContentAST.Equation.make_block_equation__multiline_equals(
|
|
395
|
+
lhs="\\vec{a} \\cdot \\vec{b}",
|
|
396
|
+
rhs=[
|
|
397
|
+
f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} \\cdot \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
|
|
398
|
+
products_str,
|
|
399
|
+
calculation_str,
|
|
400
|
+
str(result)
|
|
401
|
+
]
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
# Single part explanation (original behavior)
|
|
406
|
+
vector_a_str = r" \\ ".join([str(v) for v in self.vector_a])
|
|
407
|
+
vector_b_str = r" \\ ".join([str(v) for v in self.vector_b])
|
|
408
|
+
products_str = " + ".join([f"({self.vector_a[i]} \\cdot {self.vector_b[i]})" for i in range(self.dimension)])
|
|
409
|
+
calculation_str = " + ".join([str(self.vector_a[i] * self.vector_b[i]) for i in range(self.dimension)])
|
|
410
|
+
|
|
411
|
+
explanation.add_element(
|
|
412
|
+
ContentAST.Equation.make_block_equation__multiline_equals(
|
|
413
|
+
lhs="\\vec{a} \\cdot \\vec{b}",
|
|
414
|
+
rhs=[
|
|
415
|
+
f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} \\cdot \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
|
|
416
|
+
products_str,
|
|
417
|
+
calculation_str,
|
|
418
|
+
str(self.result)
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
return explanation
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@QuestionRegistry.register()
|
|
427
|
+
class VectorMagnitude(VectorMathQuestion):
|
|
428
|
+
|
|
429
|
+
MIN_DIMENSION = 2
|
|
430
|
+
MAX_DIMENSION = 3
|
|
431
|
+
|
|
432
|
+
def get_operator(self):
|
|
433
|
+
# Magnitude uses ||...|| notation, not an operator between vectors
|
|
434
|
+
return "||"
|
|
435
|
+
|
|
436
|
+
def calculate_single_result(self, vector_a, vector_b):
|
|
437
|
+
# For magnitude, we only use vector_a and ignore vector_b
|
|
438
|
+
magnitude_squared = sum(component ** 2 for component in vector_a)
|
|
439
|
+
return math.sqrt(magnitude_squared)
|
|
440
|
+
|
|
441
|
+
def create_subquestion_answers(self, subpart_index, result):
|
|
442
|
+
letter = chr(ord('a') + subpart_index)
|
|
443
|
+
self.answers[f"subpart_{letter}"] = Answer.auto_float(f"subpart_{letter}", result)
|
|
444
|
+
|
|
445
|
+
def create_single_answers(self, result):
|
|
446
|
+
self.answers = {
|
|
447
|
+
"magnitude": Answer.auto_float("magnitude", result)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
def generate_subquestion_data(self):
|
|
451
|
+
"""Override to handle magnitude format ||vector||."""
|
|
452
|
+
subparts = []
|
|
453
|
+
for data in self.subquestion_data:
|
|
454
|
+
vector_a_latex = self._format_vector(data['vector_a'])
|
|
455
|
+
# For magnitude, we show ||vector|| as a single string
|
|
456
|
+
subparts.append(f"\\left\\|{vector_a_latex}\\right\\|")
|
|
457
|
+
return subparts
|
|
458
|
+
|
|
459
|
+
def get_body(self):
|
|
460
|
+
body = ContentAST.Section()
|
|
461
|
+
|
|
462
|
+
body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
|
|
463
|
+
|
|
464
|
+
if self.is_multipart():
|
|
465
|
+
# Use multipart formatting with repeated problem parts
|
|
466
|
+
subpart_data = self.generate_subquestion_data()
|
|
467
|
+
repeated_part = self.create_repeated_problem_part(subpart_data)
|
|
468
|
+
body.add_element(repeated_part)
|
|
469
|
+
else:
|
|
470
|
+
# Single equation display
|
|
471
|
+
vector_a_latex = self._format_vector(self.vector_a)
|
|
472
|
+
body.add_element(ContentAST.Equation(f"\\left\\|{vector_a_latex}\\right\\| = ", inline=False))
|
|
473
|
+
|
|
474
|
+
# Canvas-only answer field (hidden from PDF)
|
|
475
|
+
self._add_single_question_answers(body)
|
|
476
|
+
|
|
477
|
+
return body
|
|
478
|
+
|
|
479
|
+
def get_explanation(self):
|
|
480
|
+
explanation = ContentAST.Section()
|
|
481
|
+
|
|
482
|
+
explanation.add_element(ContentAST.Paragraph(["The magnitude of a vector is calculated using the formula:"]))
|
|
483
|
+
explanation.add_element(ContentAST.Equation("\\left\\|\\vec{v}\\right\\| = \\sqrt{v_1^2 + v_2^2 + \\ldots + v_n^2}", inline=False))
|
|
484
|
+
|
|
485
|
+
if self.is_multipart():
|
|
486
|
+
# Handle multipart explanations
|
|
487
|
+
for i, data in enumerate(self.subquestion_data):
|
|
488
|
+
letter = chr(ord('a') + i)
|
|
489
|
+
vector_a = data['vector_a']
|
|
490
|
+
result = data['result']
|
|
491
|
+
|
|
492
|
+
# Create LaTeX strings for multiline equation
|
|
493
|
+
vector_str = r" \\ ".join([str(v) for v in vector_a])
|
|
494
|
+
squares_str = " + ".join([f"{v}^2" for v in vector_a])
|
|
495
|
+
calculation_str = " + ".join([str(v**2) for v in vector_a])
|
|
496
|
+
sum_of_squares = sum(component ** 2 for component in vector_a)
|
|
497
|
+
result_formatted = sorted(Answer.accepted_strings(result), key=lambda s: len(s))[0]
|
|
498
|
+
|
|
499
|
+
# Add explanation for this subpart
|
|
500
|
+
explanation.add_element(ContentAST.Paragraph([f"Part ({letter}):"]))
|
|
501
|
+
explanation.add_element(
|
|
502
|
+
ContentAST.Equation.make_block_equation__multiline_equals(
|
|
503
|
+
lhs="\\left\\|\\vec{v}\\right\\|",
|
|
504
|
+
rhs=[
|
|
505
|
+
f"\\left\\|\\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}\\right\\|",
|
|
506
|
+
f"\\sqrt{{{squares_str}}}",
|
|
507
|
+
f"\\sqrt{{{calculation_str}}}",
|
|
508
|
+
f"\\sqrt{{{sum_of_squares}}}",
|
|
509
|
+
result_formatted
|
|
510
|
+
]
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
else:
|
|
514
|
+
# Single part explanation - use the correct attributes
|
|
515
|
+
vector_str = r" \\ ".join([str(v) for v in self.vector_a])
|
|
516
|
+
squares_str = " + ".join([f"{v}^2" for v in self.vector_a])
|
|
517
|
+
calculation_str = " + ".join([str(v**2) for v in self.vector_a])
|
|
518
|
+
sum_of_squares = sum(component ** 2 for component in self.vector_a)
|
|
519
|
+
result_formatted = sorted(Answer.accepted_strings(self.result), key=lambda s: len(s))[0]
|
|
520
|
+
|
|
521
|
+
explanation.add_element(
|
|
522
|
+
ContentAST.Equation.make_block_equation__multiline_equals(
|
|
523
|
+
lhs="\\left\\|\\vec{v}\\right\\|",
|
|
524
|
+
rhs=[
|
|
525
|
+
f"\\left\\|\\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}\\right\\|",
|
|
526
|
+
f"\\sqrt{{{squares_str}}}",
|
|
527
|
+
f"\\sqrt{{{calculation_str}}}",
|
|
528
|
+
f"\\sqrt{{{sum_of_squares}}}",
|
|
529
|
+
result_formatted
|
|
530
|
+
]
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return explanation
|
|
File without changes
|