QuizGenerator 0.4.2__py3-none-any.whl → 0.6.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.
Files changed (33) hide show
  1. QuizGenerator/contentast.py +809 -117
  2. QuizGenerator/generate.py +219 -11
  3. QuizGenerator/misc.py +0 -556
  4. QuizGenerator/mixins.py +50 -29
  5. QuizGenerator/premade_questions/basic.py +3 -3
  6. QuizGenerator/premade_questions/cst334/languages.py +183 -175
  7. QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
  8. QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
  9. QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
  10. QuizGenerator/premade_questions/cst334/process.py +558 -79
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
  13. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
  14. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  15. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
  16. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
  17. QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
  18. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
  19. QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
  20. QuizGenerator/premade_questions/cst463/models/text.py +29 -15
  21. QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
  22. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
  23. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
  24. QuizGenerator/question.py +114 -20
  25. QuizGenerator/quiz.py +81 -24
  26. QuizGenerator/regenerate.py +98 -29
  27. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
  28. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
  29. QuizGenerator/README.md +0 -5
  30. QuizGenerator/logging.yaml +0 -55
  31. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
  32. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
  33. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,8 +4,8 @@ import logging
4
4
  import math
5
5
  from typing import List
6
6
 
7
- from QuizGenerator.question import Question, QuestionRegistry, Answer
8
- from QuizGenerator.contentast import ContentAST
7
+ from QuizGenerator.question import Question, QuestionRegistry
8
+ from QuizGenerator.contentast import ContentAST, AnswerTypes
9
9
  from QuizGenerator.mixins import MathOperationQuestion
10
10
 
11
11
  log = logging.getLogger(__name__)
@@ -22,10 +22,16 @@ class VectorMathQuestion(MathOperationQuestion, Question):
22
22
  return [self.rng.randint(min_val, max_val) for _ in range(dimension)]
23
23
 
24
24
  def _format_vector(self, vector):
25
- """Format vector for display as column vector using ContentAST.Matrix."""
26
- # Convert to column matrix format
25
+ """Return a ContentAST.Matrix element for the vector (format-independent).
26
+
27
+ The Matrix element will render appropriately for each output format:
28
+ - HTML: LaTeX bmatrix (for MathJax)
29
+ - Typst: mat() with square bracket delimiter
30
+ - LaTeX: bmatrix environment
31
+ """
32
+ # Convert to column matrix format: [[v1], [v2], [v3]]
27
33
  matrix_data = [[v] for v in vector]
28
- return ContentAST.Matrix.to_latex(matrix_data, "b")
34
+ return ContentAST.Matrix(data=matrix_data, bracket_type="b")
29
35
 
30
36
  def _format_vector_inline(self, vector):
31
37
  """Format vector for inline display."""
@@ -61,10 +67,8 @@ class VectorMathQuestion(MathOperationQuestion, Question):
61
67
  # Call parent refresh which will use our generate_operands method
62
68
  super().refresh(*args, **kwargs)
63
69
 
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
70
+ self.vector_a = self.operand_a
71
+ self.vector_b = self.operand_b
68
72
 
69
73
  def generate_subquestion_data(self):
70
74
  """Generate LaTeX content for each subpart of the question.
@@ -81,23 +85,6 @@ class VectorMathQuestion(MathOperationQuestion, Question):
81
85
  subparts.append((vector_a_latex, self.get_operator(), vector_b_latex))
82
86
  return subparts
83
87
 
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
88
  # Abstract methods that subclasses must still implement
102
89
  @abc.abstractmethod
103
90
  def get_operator(self):
@@ -128,64 +115,58 @@ class VectorAddition(VectorMathQuestion):
128
115
  return [vector_a[i] + vector_b[i] for i in range(len(vector_a))]
129
116
 
130
117
  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])
118
+ raise NotImplementedError("Multipart not supported")
134
119
 
135
120
  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])
121
+ self.answers["result"] = AnswerTypes.Vector(result)
122
+
123
+ def _get_body(self):
124
+ """Build question body and collect answers."""
125
+ body = ContentAST.Section()
126
+
127
+ body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
138
128
 
139
- def get_explanation(self):
129
+ # Equation display using MathExpression for format-independent rendering
130
+ vector_a_elem = self._format_vector(self.vector_a)
131
+ vector_b_elem = self._format_vector(self.vector_b)
132
+ body.add_element(ContentAST.MathExpression([
133
+ vector_a_elem,
134
+ " + ",
135
+ vector_b_elem,
136
+ " = "
137
+ ]))
138
+
139
+ # Canvas-only answer field - use stored answer for consistent UUID
140
+ answer = self.answers["result"]
141
+ body.add_element(ContentAST.OnlyHtml([ContentAST.Paragraph(["Enter your answer as a column vector:"])]))
142
+ body.add_element(ContentAST.OnlyHtml([answer]))
143
+
144
+ return body, list(self.answers.values())
145
+
146
+ def _get_explanation(self):
147
+ """Build question explanation."""
140
148
  explanation = ContentAST.Section()
141
149
 
142
150
  explanation.add_element(ContentAST.Paragraph(["To add vectors, we add corresponding components:"]))
143
151
 
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
- )
152
+ # Use LaTeX syntax for make_block_equation__multiline_equals
153
+ vector_a_str = r" \\ ".join([str(v) for v in self.vector_a])
154
+ vector_b_str = r" \\ ".join([str(v) for v in self.vector_b])
155
+ result_str = r" \\ ".join([str(v) for v in self.result])
156
+ addition_str = r" \\ ".join([f"{self.vector_a[i]}+{self.vector_b[i]}" for i in range(self.dimension)])
157
+
158
+ explanation.add_element(
159
+ ContentAST.Equation.make_block_equation__multiline_equals(
160
+ lhs=r"\vec{a} + \vec{b}",
161
+ rhs=[
162
+ f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} + \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
163
+ f"\\begin{{bmatrix}} {addition_str} \\end{{bmatrix}}",
164
+ f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
165
+ ]
169
166
  )
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
- )
167
+ )
187
168
 
188
- return explanation
169
+ return explanation, []
189
170
 
190
171
 
191
172
  @QuestionRegistry.register()
@@ -197,155 +178,70 @@ class VectorScalarMultiplication(VectorMathQuestion):
197
178
  def _generate_scalar(self):
198
179
  """Generate a non-zero scalar for multiplication."""
199
180
  scalar = self.rng.randint(-5, 5)
200
- while scalar == 0: # Avoid zero scalar for more interesting problems
181
+ while scalar == 0:
201
182
  scalar = self.rng.randint(-5, 5)
202
183
  return scalar
203
184
 
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
185
  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)
186
+ # Generate scalar first, then call parent refresh
187
+ self.scalar = self._generate_scalar()
188
+ super().refresh(*args, **kwargs)
251
189
 
252
190
  def get_operator(self):
253
191
  return f"{self.scalar} \\cdot"
254
192
 
255
193
  def calculate_single_result(self, vector_a, vector_b):
256
- # For scalar multiplication, we only use vector_a and ignore vector_b
257
194
  return [self.scalar * component for component in vector_a]
258
195
 
259
196
  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])
197
+ raise NotImplementedError("Multipart not supported")
263
198
 
264
199
  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
200
+ self.answers["result"] = AnswerTypes.Vector(result)
278
201
 
279
- def get_body(self):
202
+ def _get_body(self):
203
+ """Build question body and collect answers."""
280
204
  body = ContentAST.Section()
281
205
 
282
206
  body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
283
207
 
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))
208
+ # Equation display using MathExpression
209
+ vector_elem = self._format_vector(self.vector_a)
210
+ body.add_element(ContentAST.MathExpression([
211
+ f"{self.scalar} \\cdot ",
212
+ vector_elem,
213
+ " = "
214
+ ]))
293
215
 
294
- # Canvas-only answer fields (hidden from PDF)
295
- self._add_single_question_answers(body)
216
+ # Canvas-only answer field - use stored answer
217
+ answer = self.answers["result"]
218
+ body.add_element(ContentAST.OnlyHtml([ContentAST.Paragraph(["Enter your answer as a column vector:"])]))
219
+ body.add_element(ContentAST.OnlyHtml([answer]))
296
220
 
297
- return body
221
+ return body, list(self.answers.values())
298
222
 
299
- def get_explanation(self):
223
+ def _get_explanation(self):
224
+ """Build question explanation."""
300
225
  explanation = ContentAST.Section()
301
226
 
302
227
  explanation.add_element(ContentAST.Paragraph(["To multiply a vector by a scalar, we multiply each component by the scalar:"]))
303
228
 
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
- )
229
+ vector_str = r" \\ ".join([str(v) for v in self.vector_a])
230
+ multiplication_str = r" \\ ".join([f"{self.scalar} \\cdot {v}" for v in self.vector_a])
231
+ result_str = r" \\ ".join([str(v) for v in self.result])
232
+
233
+ explanation.add_element(
234
+ ContentAST.Equation.make_block_equation__multiline_equals(
235
+ lhs=f"{self.scalar} \\cdot \\vec{{v}}",
236
+ rhs=[
237
+ f"{self.scalar} \\cdot \\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}",
238
+ f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
239
+ f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
240
+ ]
330
241
  )
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
- )
242
+ )
347
243
 
348
- return explanation
244
+ return explanation, []
349
245
 
350
246
 
351
247
  @QuestionRegistry.register()
@@ -361,66 +257,57 @@ class VectorDotProduct(VectorMathQuestion):
361
257
  return sum(vector_a[i] * vector_b[i] for i in range(len(vector_a)))
362
258
 
363
259
  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)
260
+ raise NotImplementedError("Multipart not supported")
366
261
 
367
262
  def create_single_answers(self, result):
368
- self.answers = {
369
- "dot_product": Answer.integer("dot_product", result)
370
- }
263
+ self.answers["dot_product"] = AnswerTypes.Int(result)
264
+
265
+ def _get_body(self):
266
+ """Build question body and collect answers."""
267
+ body = ContentAST.Section()
268
+
269
+ body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
270
+
271
+ # Equation display using MathExpression
272
+ vector_a_elem = self._format_vector(self.vector_a)
273
+ vector_b_elem = self._format_vector(self.vector_b)
274
+ body.add_element(ContentAST.MathExpression([
275
+ vector_a_elem,
276
+ " \\cdot ",
277
+ vector_b_elem,
278
+ " = "
279
+ ]))
371
280
 
372
- def get_explanation(self):
281
+ # Canvas-only answer field - use stored answer
282
+ answer = self.answers["dot_product"]
283
+ body.add_element(ContentAST.OnlyHtml([answer]))
284
+
285
+ return body, list(self.answers.values())
286
+
287
+ def _get_explanation(self):
288
+ """Build question explanation."""
373
289
  explanation = ContentAST.Section()
374
290
 
375
291
  explanation.add_element(ContentAST.Paragraph(["The dot product is calculated by multiplying corresponding components and summing the results:"]))
376
292
 
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
- ]
293
+ vector_a_str = r" \\ ".join([str(v) for v in self.vector_a])
294
+ vector_b_str = r" \\ ".join([str(v) for v in self.vector_b])
295
+ products_str = " + ".join([f"({self.vector_a[i]} \\cdot {self.vector_b[i]})" for i in range(self.dimension)])
296
+ calculation_str = " + ".join([str(self.vector_a[i] * self.vector_b[i]) for i in range(self.dimension)])
297
+
298
+ explanation.add_element(
299
+ ContentAST.Equation.make_block_equation__multiline_equals(
300
+ lhs="\\vec{a} \\cdot \\vec{b}",
301
+ rhs=[
302
+ f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} \\cdot \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
303
+ products_str,
304
+ calculation_str,
305
+ str(self.result)
306
+ ]
420
307
  )
421
308
  )
422
309
 
423
- return explanation
310
+ return explanation, [] # Explanations don't have answers
424
311
 
425
312
 
426
313
  @QuestionRegistry.register()
@@ -430,105 +317,65 @@ class VectorMagnitude(VectorMathQuestion):
430
317
  MAX_DIMENSION = 3
431
318
 
432
319
  def get_operator(self):
433
- # Magnitude uses ||...|| notation, not an operator between vectors
434
320
  return "||"
435
321
 
436
322
  def calculate_single_result(self, vector_a, vector_b):
437
- # For magnitude, we only use vector_a and ignore vector_b
438
323
  magnitude_squared = sum(component ** 2 for component in vector_a)
439
324
  return math.sqrt(magnitude_squared)
440
325
 
441
326
  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)
327
+ raise NotImplementedError("Multipart not supported")
444
328
 
445
329
  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
330
+ self.answers["magnitude"] = AnswerTypes.Float(result)
458
331
 
459
- def get_body(self):
332
+ def _get_body(self):
333
+ """Build question body and collect answers."""
460
334
  body = ContentAST.Section()
461
335
 
462
336
  body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
463
337
 
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))
338
+ # Equation display using MathExpression
339
+ vector_elem = self._format_vector(self.vector_a)
340
+ body.add_element(ContentAST.MathExpression([
341
+ "||",
342
+ vector_elem,
343
+ "|| = "
344
+ ]))
473
345
 
474
- # Canvas-only answer field (hidden from PDF)
475
- self._add_single_question_answers(body)
346
+ # Canvas-only answer field - use stored answer
347
+ answer = self.answers["magnitude"]
348
+ body.add_element(ContentAST.OnlyHtml([answer]))
476
349
 
477
- return body
350
+ return body, list(self.answers.values())
478
351
 
479
- def get_explanation(self):
352
+ def _get_explanation(self):
353
+ """Build question explanation."""
480
354
  explanation = ContentAST.Section()
481
355
 
482
356
  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
- )
357
+ explanation.add_element(ContentAST.Equation(
358
+ r"||\vec{v}|| = \sqrt{v_1^2 + v_2^2 + \cdots + v_n^2}", inline=False
359
+ ))
360
+
361
+ # Use LaTeX syntax for make_block_equation__multiline_equals
362
+ vector_str = r" \\ ".join([str(v) for v in self.vector_a])
363
+ squares_str = " + ".join([f"{v}^2" for v in self.vector_a])
364
+ calculation_str = " + ".join([str(v**2) for v in self.vector_a])
365
+ sum_of_squares = sum(component ** 2 for component in self.vector_a)
366
+ result_formatted = sorted(ContentAST.Answer.accepted_strings(self.result), key=lambda s: len(s))[0]
367
+
368
+ explanation.add_element(
369
+ ContentAST.Equation.make_block_equation__multiline_equals(
370
+ lhs=r"||\vec{v}||",
371
+ rhs=[
372
+ f"\\left|\\left| \\begin{{bmatrix}} {vector_str} \\end{{bmatrix}} \\right|\\right|",
373
+ f"\\sqrt{{{squares_str}}}",
374
+ f"\\sqrt{{{calculation_str}}}",
375
+ f"\\sqrt{{{sum_of_squares}}}",
376
+ result_formatted
377
+ ]
512
378
  )
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
- )
379
+ )
533
380
 
534
- return explanation
381
+ return explanation, []