QuizGenerator 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. QuizGenerator/README.md +5 -0
  2. QuizGenerator/__init__.py +27 -0
  3. QuizGenerator/__main__.py +7 -0
  4. QuizGenerator/canvas/__init__.py +13 -0
  5. QuizGenerator/canvas/canvas_interface.py +627 -0
  6. QuizGenerator/canvas/classes.py +235 -0
  7. QuizGenerator/constants.py +149 -0
  8. QuizGenerator/contentast.py +1955 -0
  9. QuizGenerator/generate.py +253 -0
  10. QuizGenerator/logging.yaml +55 -0
  11. QuizGenerator/misc.py +579 -0
  12. QuizGenerator/mixins.py +548 -0
  13. QuizGenerator/performance.py +202 -0
  14. QuizGenerator/premade_questions/__init__.py +0 -0
  15. QuizGenerator/premade_questions/basic.py +103 -0
  16. QuizGenerator/premade_questions/cst334/__init__.py +1 -0
  17. QuizGenerator/premade_questions/cst334/languages.py +391 -0
  18. QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
  19. QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
  20. QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
  21. QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
  22. QuizGenerator/premade_questions/cst334/process.py +648 -0
  23. QuizGenerator/premade_questions/cst463/__init__.py +0 -0
  24. QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
  25. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
  26. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
  27. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
  28. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
  29. QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
  30. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
  31. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
  32. QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
  33. QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
  34. QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
  35. QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
  36. QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
  37. QuizGenerator/premade_questions/cst463/models/text.py +203 -0
  38. QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
  39. QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
  40. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
  41. QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
  42. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
  43. QuizGenerator/qrcode_generator.py +293 -0
  44. QuizGenerator/question.py +715 -0
  45. QuizGenerator/quiz.py +467 -0
  46. QuizGenerator/regenerate.py +472 -0
  47. QuizGenerator/typst_utils.py +113 -0
  48. quizgenerator-0.4.2.dist-info/METADATA +265 -0
  49. quizgenerator-0.4.2.dist-info/RECORD +52 -0
  50. quizgenerator-0.4.2.dist-info/WHEEL +4 -0
  51. quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
  52. quizgenerator-0.4.2.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,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