QuizGenerator 0.5.1__py3-none-any.whl → 0.6.1__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 (29) hide show
  1. QuizGenerator/contentast.py +1056 -1231
  2. QuizGenerator/generate.py +174 -2
  3. QuizGenerator/misc.py +0 -6
  4. QuizGenerator/mixins.py +7 -8
  5. QuizGenerator/premade_questions/basic.py +3 -3
  6. QuizGenerator/premade_questions/cst334/languages.py +45 -51
  7. QuizGenerator/premade_questions/cst334/math_questions.py +9 -10
  8. QuizGenerator/premade_questions/cst334/memory_questions.py +39 -56
  9. QuizGenerator/premade_questions/cst334/persistence_questions.py +12 -27
  10. QuizGenerator/premade_questions/cst334/process.py +11 -22
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +11 -11
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +7 -7
  13. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +6 -6
  14. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  15. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +15 -19
  16. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -442
  17. QuizGenerator/premade_questions/cst463/models/attention.py +7 -8
  18. QuizGenerator/premade_questions/cst463/models/cnns.py +6 -7
  19. QuizGenerator/premade_questions/cst463/models/rnns.py +6 -6
  20. QuizGenerator/premade_questions/cst463/models/text.py +7 -8
  21. QuizGenerator/premade_questions/cst463/models/weight_counting.py +5 -9
  22. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +22 -22
  23. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +25 -25
  24. QuizGenerator/question.py +13 -14
  25. {quizgenerator-0.5.1.dist-info → quizgenerator-0.6.1.dist-info}/METADATA +1 -1
  26. {quizgenerator-0.5.1.dist-info → quizgenerator-0.6.1.dist-info}/RECORD +29 -29
  27. {quizgenerator-0.5.1.dist-info → quizgenerator-0.6.1.dist-info}/WHEEL +0 -0
  28. {quizgenerator-0.5.1.dist-info → quizgenerator-0.6.1.dist-info}/entry_points.txt +0 -0
  29. {quizgenerator-0.5.1.dist-info → quizgenerator-0.6.1.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__)
@@ -67,10 +67,8 @@ class VectorMathQuestion(MathOperationQuestion, Question):
67
67
  # Call parent refresh which will use our generate_operands method
68
68
  super().refresh(*args, **kwargs)
69
69
 
70
- # For backward compatibility, set vector_a/vector_b for single questions
71
- if not self.is_multipart():
72
- self.vector_a = self.operand_a
73
- self.vector_b = self.operand_b
70
+ self.vector_a = self.operand_a
71
+ self.vector_b = self.operand_b
74
72
 
75
73
  def generate_subquestion_data(self):
76
74
  """Generate LaTeX content for each subpart of the question.
@@ -117,121 +115,58 @@ class VectorAddition(VectorMathQuestion):
117
115
  return [vector_a[i] + vector_b[i] for i in range(len(vector_a))]
118
116
 
119
117
  def create_subquestion_answers(self, subpart_index, result):
120
- letter = chr(ord('a') + subpart_index)
121
- for j in range(len(result)):
122
- self.answers[f"subpart_{letter}_{j}"] = Answer.integer(f"subpart_{letter}_{j}", result[j])
118
+ raise NotImplementedError("Multipart not supported")
123
119
 
124
120
  def create_single_answers(self, result):
125
- # Backward compatibility - still populate dict for old pattern
126
- for i in range(len(result)):
127
- self.answers[f"result_{i}"] = Answer.integer(f"result_{i}", result[i])
128
-
129
- def get_body(self):
130
- """Override parent get_body() to use our custom formatting."""
131
- body, _ = self._get_body()
132
- return body
121
+ self.answers["result"] = AnswerTypes.Vector(result)
133
122
 
134
123
  def _get_body(self):
135
- """Build question body and collect answers (new pattern)."""
136
- from typing import Tuple, List
137
-
124
+ """Build question body and collect answers."""
138
125
  body = ContentAST.Section()
139
- answers = []
140
126
 
141
127
  body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
142
128
 
143
- if self.is_multipart():
144
- # Use multipart formatting with repeated problem parts
145
- subpart_data = self.generate_subquestion_data()
146
- repeated_part = self.create_repeated_problem_part(subpart_data)
147
- body.add_element(repeated_part)
148
-
149
- # Collect all subpart answers
150
- for i, data in enumerate(self.subquestion_data):
151
- letter = chr(ord('a') + i)
152
- result = data['result']
153
- for j in range(len(result)):
154
- ans = Answer.integer(f"subpart_{letter}_{j}", result[j])
155
- answers.append(ans)
156
- else:
157
- # Single equation display using MathExpression for format-independent rendering
158
- vector_a_elem = self._format_vector(self.vector_a)
159
- vector_b_elem = self._format_vector(self.vector_b)
160
- body.add_element(ContentAST.MathExpression([
161
- vector_a_elem,
162
- " + ",
163
- vector_b_elem,
164
- " = "
165
- ]))
166
-
167
- # Canvas-only answer fields (hidden from PDF)
168
- body.add_element(ContentAST.OnlyHtml([ContentAST.Paragraph(["Enter your answer as a column vector:"])]))
169
- table_data = []
170
- for i in range(self.dimension):
171
- ans = Answer.integer(f"result_{i}", self.result[i])
172
- answers.append(ans)
173
- table_data.append([ans])
174
- body.add_element(ContentAST.OnlyHtml([ContentAST.Table(data=table_data, padding=True)]))
175
-
176
- return body, answers
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())
177
145
 
178
146
  def _get_explanation(self):
179
- """Build question explanation (new pattern)."""
147
+ """Build question explanation."""
180
148
  explanation = ContentAST.Section()
181
149
 
182
150
  explanation.add_element(ContentAST.Paragraph(["To add vectors, we add corresponding components:"]))
183
151
 
184
- if self.is_multipart():
185
- # Handle multipart explanations
186
- for i, data in enumerate(self.subquestion_data):
187
- letter = chr(ord('a') + i)
188
- vector_a = data['vector_a']
189
- vector_b = data['vector_b']
190
- result = data['result']
191
-
192
- # Format vectors using Typst syntax
193
- vector_a_str = self._format_vector(vector_a)
194
- vector_b_str = self._format_vector(vector_b)
195
- result_str = self._format_vector(result)
196
-
197
- # Build addition step-by-step
198
- addition_elements = "; ".join([f"{vector_a[j]}+{vector_b[j]}" for j in range(self.dimension)])
199
- addition_str = f"mat({addition_elements})"
200
-
201
- # Add explanation for this subpart
202
- explanation.add_element(ContentAST.Paragraph([f"Part ({letter}):"]))
203
- explanation.add_element(
204
- ContentAST.Equation.make_block_equation__multiline_equals(
205
- lhs="arrow(a) + arrow(b)", # Typst uses arrow() for vectors
206
- rhs=[
207
- f"{vector_a_str} + {vector_b_str}",
208
- addition_str,
209
- result_str
210
- ]
211
- )
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
+ ]
212
166
  )
213
- else:
214
- # Single part explanation - use Typst syntax
215
- vector_a_str = self._format_vector(self.vector_a)
216
- vector_b_str = self._format_vector(self.vector_b)
217
- result_str = self._format_vector(self.result)
218
-
219
- # Build addition step-by-step
220
- addition_elements = "; ".join([f"{self.vector_a[i]}+{self.vector_b[i]}" for i in range(self.dimension)])
221
- addition_str = f"mat({addition_elements})"
222
-
223
- explanation.add_element(
224
- ContentAST.Equation.make_block_equation__multiline_equals(
225
- lhs="arrow(a) + arrow(b)",
226
- rhs=[
227
- f"{vector_a_str} + {vector_b_str}",
228
- addition_str,
229
- result_str
230
- ]
231
- )
232
- )
167
+ )
233
168
 
234
- return explanation, [] # Explanations don't have answers
169
+ return explanation, []
235
170
 
236
171
 
237
172
  @QuestionRegistry.register()
@@ -243,179 +178,70 @@ class VectorScalarMultiplication(VectorMathQuestion):
243
178
  def _generate_scalar(self):
244
179
  """Generate a non-zero scalar for multiplication."""
245
180
  scalar = self.rng.randint(-5, 5)
246
- while scalar == 0: # Avoid zero scalar for more interesting problems
181
+ while scalar == 0:
247
182
  scalar = self.rng.randint(-5, 5)
248
183
  return scalar
249
184
 
250
- def generate_operands(self):
251
- """Override to generate scalar and vector."""
252
- if not hasattr(self, 'dimension'):
253
- self.dimension = self.rng.randint(self.MIN_DIMENSION, self.MAX_DIMENSION)
254
- vector_a = self._generate_vector(self.dimension)
255
- vector_b = self._generate_vector(self.dimension) # Not used, but kept for consistency
256
- return vector_a, vector_b
257
-
258
185
  def refresh(self, *args, **kwargs):
259
- if self.is_multipart():
260
- # For multipart questions, we handle everything ourselves
261
- # Don't call super() because we need different scalars per subpart
262
-
263
- # Call Question.refresh() directly to get basic setup
264
- Question.refresh(self, *args, **kwargs)
265
-
266
- # Generate vector dimension
267
- self.dimension = self.rng.randint(self.MIN_DIMENSION, self.MAX_DIMENSION)
268
-
269
- # Clear any existing data
270
- self.answers = {}
271
-
272
- # Generate multiple subquestions with their own scalars
273
- self.subquestion_data = []
274
- for i in range(self.num_subquestions):
275
- # Generate unique vectors and scalar for each subquestion
276
- vector_a = self._generate_vector(self.dimension)
277
- vector_b = self._generate_vector(self.dimension) # Not used, but kept for consistency
278
- scalar = self._generate_scalar()
279
- result = [scalar * component for component in vector_a]
280
-
281
- self.subquestion_data.append({
282
- 'operand_a': vector_a,
283
- 'operand_b': vector_b,
284
- 'vector_a': vector_a,
285
- 'vector_b': vector_b,
286
- 'scalar': scalar,
287
- 'result': result
288
- })
289
-
290
- # Create answers for this subpart
291
- self.create_subquestion_answers(i, result)
292
- else:
293
- # For single questions, generate scalar first
294
- self.scalar = self._generate_scalar()
295
- # Then call super() normally
296
- super().refresh(*args, **kwargs)
186
+ # Generate scalar first, then call parent refresh
187
+ self.scalar = self._generate_scalar()
188
+ super().refresh(*args, **kwargs)
297
189
 
298
190
  def get_operator(self):
299
191
  return f"{self.scalar} \\cdot"
300
192
 
301
193
  def calculate_single_result(self, vector_a, vector_b):
302
- # For scalar multiplication, we only use vector_a and ignore vector_b
303
194
  return [self.scalar * component for component in vector_a]
304
195
 
305
196
  def create_subquestion_answers(self, subpart_index, result):
306
- letter = chr(ord('a') + subpart_index)
307
- for j in range(len(result)):
308
- self.answers[f"subpart_{letter}_{j}"] = Answer.integer(f"subpart_{letter}_{j}", result[j])
197
+ raise NotImplementedError("Multipart not supported")
309
198
 
310
199
  def create_single_answers(self, result):
311
- for i in range(len(result)):
312
- self.answers[f"result_{i}"] = Answer.integer(f"result_{i}", result[i])
313
-
314
- def generate_subquestion_data(self):
315
- """Override to handle scalar multiplication format."""
316
- subparts = []
317
- for data in self.subquestion_data:
318
- vector_elem = self._format_vector(data['vector_a'])
319
- scalar = data['scalar']
320
- # Return MathExpression for format-independent rendering
321
- subparts.append(ContentAST.MathExpression([
322
- f"{scalar} \\cdot ",
323
- vector_elem
324
- ], inline=True))
325
- return subparts
326
-
327
- def get_body(self):
328
- """Override parent get_body() to use our custom formatting."""
329
- body, _ = self._get_body()
330
- return body
200
+ self.answers["result"] = AnswerTypes.Vector(result)
331
201
 
332
202
  def _get_body(self):
333
- """Build question body and collect answers (new pattern)."""
203
+ """Build question body and collect answers."""
334
204
  body = ContentAST.Section()
335
- answers = []
336
205
 
337
206
  body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
338
207
 
339
- if self.is_multipart():
340
- # Use multipart formatting with repeated problem parts
341
- subpart_data = self.generate_subquestion_data()
342
- repeated_part = self.create_repeated_problem_part(subpart_data)
343
- body.add_element(repeated_part)
344
-
345
- # Collect all subpart answers
346
- for i, data in enumerate(self.subquestion_data):
347
- letter = chr(ord('a') + i)
348
- result = data['result']
349
- for j in range(len(result)):
350
- ans = Answer.integer(f"subpart_{letter}_{j}", result[j])
351
- answers.append(ans)
352
- else:
353
- # Single equation display using MathExpression
354
- vector_elem = self._format_vector(self.vector_a)
355
- body.add_element(ContentAST.MathExpression([
356
- f"{self.scalar} \\cdot ",
357
- vector_elem,
358
- " = "
359
- ]))
360
-
361
- # Canvas-only answer fields (hidden from PDF)
362
- body.add_element(ContentAST.OnlyHtml([ContentAST.Paragraph(["Enter your answer as a column vector:"])]))
363
- table_data = []
364
- for i in range(self.dimension):
365
- ans = Answer.integer(f"result_{i}", self.result[i])
366
- answers.append(ans)
367
- table_data.append([ans])
368
- body.add_element(ContentAST.OnlyHtml([ContentAST.Table(data=table_data, padding=True)]))
369
-
370
- return body, answers
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
+ ]))
215
+
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]))
220
+
221
+ return body, list(self.answers.values())
371
222
 
372
223
  def _get_explanation(self):
373
- """Build question explanation (new pattern)."""
224
+ """Build question explanation."""
374
225
  explanation = ContentAST.Section()
375
226
 
376
227
  explanation.add_element(ContentAST.Paragraph(["To multiply a vector by a scalar, we multiply each component by the scalar:"]))
377
228
 
378
- if self.is_multipart():
379
- # Handle multipart explanations
380
- for i, data in enumerate(self.subquestion_data):
381
- letter = chr(ord('a') + i)
382
- vector_a = data['vector_a']
383
- result = data['result']
384
- scalar = data['scalar']
385
-
386
- # Use LaTeX syntax (will be converted by Equation._latex_to_typst for Typst output)
387
- vector_str = r" \\ ".join([str(v) for v in vector_a])
388
- multiplication_str = r" \\ ".join([f"{scalar} \\cdot {v}" for v in vector_a])
389
- result_str = r" \\ ".join([str(v) for v in result])
390
-
391
- explanation.add_element(ContentAST.Paragraph([f"Part ({letter}):"]))
392
- explanation.add_element(
393
- ContentAST.Equation.make_block_equation__multiline_equals(
394
- lhs=f"{scalar} \\cdot \\vec{{v}}",
395
- rhs=[
396
- f"{scalar} \\cdot \\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}",
397
- f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
398
- f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
399
- ]
400
- )
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
+ ]
401
241
  )
402
- else:
403
- vector_str = r" \\ ".join([str(v) for v in self.vector_a])
404
- multiplication_str = r" \\ ".join([f"{self.scalar} \\cdot {v}" for v in self.vector_a])
405
- result_str = r" \\ ".join([str(v) for v in self.result])
406
-
407
- explanation.add_element(
408
- ContentAST.Equation.make_block_equation__multiline_equals(
409
- lhs=f"{self.scalar} \\cdot \\vec{{v}}",
410
- rhs=[
411
- f"{self.scalar} \\cdot \\begin{{bmatrix}} {vector_str} \\end{{bmatrix}}",
412
- f"\\begin{{bmatrix}} {multiplication_str} \\end{{bmatrix}}",
413
- f"\\begin{{bmatrix}} {result_str} \\end{{bmatrix}}"
414
- ]
415
- )
416
- )
242
+ )
417
243
 
418
- return explanation, [] # Explanations don't have answers
244
+ return explanation, []
419
245
 
420
246
 
421
247
  @QuestionRegistry.register()
@@ -431,101 +257,53 @@ class VectorDotProduct(VectorMathQuestion):
431
257
  return sum(vector_a[i] * vector_b[i] for i in range(len(vector_a)))
432
258
 
433
259
  def create_subquestion_answers(self, subpart_index, result):
434
- letter = chr(ord('a') + subpart_index)
435
- self.answers[f"subpart_{letter}"] = Answer.integer(f"subpart_{letter}", result)
260
+ raise NotImplementedError("Multipart not supported")
436
261
 
437
262
  def create_single_answers(self, result):
438
- self.answers = {
439
- "dot_product": Answer.integer("dot_product", result)
440
- }
441
-
442
- def get_body(self):
443
- """Override parent get_body() to use our custom formatting."""
444
- body, _ = self._get_body()
445
- return body
263
+ self.answers["dot_product"] = AnswerTypes.Int(result)
446
264
 
447
265
  def _get_body(self):
448
- """Build question body and collect answers (new pattern)."""
266
+ """Build question body and collect answers."""
449
267
  body = ContentAST.Section()
450
- answers = []
451
268
 
452
269
  body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
453
270
 
454
- if self.is_multipart():
455
- # Use multipart formatting with repeated problem parts
456
- subpart_data = self.generate_subquestion_data()
457
- repeated_part = self.create_repeated_problem_part(subpart_data)
458
- body.add_element(repeated_part)
459
-
460
- # Collect all subpart answers (scalar results)
461
- for i, data in enumerate(self.subquestion_data):
462
- letter = chr(ord('a') + i)
463
- result = data['result']
464
- ans = Answer.integer(f"subpart_{letter}", result)
465
- answers.append(ans)
466
- else:
467
- # Single equation display using MathExpression
468
- vector_a_elem = self._format_vector(self.vector_a)
469
- vector_b_elem = self._format_vector(self.vector_b)
470
- body.add_element(ContentAST.MathExpression([
471
- vector_a_elem,
472
- " \\cdot ",
473
- vector_b_elem,
474
- " = "
475
- ]))
476
-
477
- # Canvas-only answer field (single scalar result)
478
- ans = Answer.integer("dot_product", self.result)
479
- answers.append(ans)
480
- body.add_element(ContentAST.OnlyHtml([ans]))
481
-
482
- return body, answers
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
+ ]))
280
+
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())
483
286
 
484
287
  def _get_explanation(self):
485
- """Build question explanation (new pattern)."""
288
+ """Build question explanation."""
486
289
  explanation = ContentAST.Section()
487
290
 
488
291
  explanation.add_element(ContentAST.Paragraph(["The dot product is calculated by multiplying corresponding components and summing the results:"]))
489
292
 
490
- if self.is_multipart():
491
- for i, data in enumerate(self.subquestion_data):
492
- letter = chr(ord('a') + i)
493
- vector_a = data['vector_a']
494
- vector_b = data['vector_b']
495
- result = data['result']
496
-
497
- vector_a_str = r" \\ ".join([str(v) for v in vector_a])
498
- vector_b_str = r" \\ ".join([str(v) for v in vector_b])
499
- products_str = " + ".join([f"({vector_a[j]} \\cdot {vector_b[j]})" for j in range(self.dimension)])
500
- calculation_str = " + ".join([str(vector_a[j] * vector_b[j]) for j in range(self.dimension)])
501
-
502
- explanation.add_element(ContentAST.Paragraph([f"Part ({letter}):"]))
503
- explanation.add_element(
504
- ContentAST.Equation.make_block_equation__multiline_equals(
505
- lhs="\\vec{a} \\cdot \\vec{b}",
506
- rhs=[
507
- f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} \\cdot \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
508
- products_str,
509
- calculation_str,
510
- str(result)
511
- ]
512
- )
513
- )
514
- else:
515
- vector_a_str = r" \\ ".join([str(v) for v in self.vector_a])
516
- vector_b_str = r" \\ ".join([str(v) for v in self.vector_b])
517
- products_str = " + ".join([f"({self.vector_a[i]} \\cdot {self.vector_b[i]})" for i in range(self.dimension)])
518
- calculation_str = " + ".join([str(self.vector_a[i] * self.vector_b[i]) for i in range(self.dimension)])
519
-
520
- explanation.add_element(
521
- ContentAST.Equation.make_block_equation__multiline_equals(
522
- lhs="\\vec{a} \\cdot \\vec{b}",
523
- rhs=[
524
- f"\\begin{{bmatrix}} {vector_a_str} \\end{{bmatrix}} \\cdot \\begin{{bmatrix}} {vector_b_str} \\end{{bmatrix}}",
525
- products_str,
526
- calculation_str,
527
- str(self.result)
528
- ]
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
+ ]
529
307
  )
530
308
  )
531
309
 
@@ -539,136 +317,65 @@ class VectorMagnitude(VectorMathQuestion):
539
317
  MAX_DIMENSION = 3
540
318
 
541
319
  def get_operator(self):
542
- # Magnitude uses ||...|| notation, not an operator between vectors
543
320
  return "||"
544
321
 
545
322
  def calculate_single_result(self, vector_a, vector_b):
546
- # For magnitude, we only use vector_a and ignore vector_b
547
323
  magnitude_squared = sum(component ** 2 for component in vector_a)
548
324
  return math.sqrt(magnitude_squared)
549
325
 
550
326
  def create_subquestion_answers(self, subpart_index, result):
551
- # Backward compatibility
552
- letter = chr(ord('a') + subpart_index)
553
- self.answers[f"subpart_{letter}"] = Answer.auto_float(f"subpart_{letter}", result)
327
+ raise NotImplementedError("Multipart not supported")
554
328
 
555
329
  def create_single_answers(self, result):
556
- # Backward compatibility
557
- self.answers = {
558
- "magnitude": Answer.auto_float("magnitude", result)
559
- }
560
-
561
- def generate_subquestion_data(self):
562
- """Override to handle magnitude format ||vector||.
563
-
564
- Returns MathExpression elements for format-independent rendering.
565
- """
566
- subparts = []
567
- for data in self.subquestion_data:
568
- vector_elem = self._format_vector(data['vector_a'])
569
- # Create MathExpression for ||vector||
570
- subparts.append(ContentAST.MathExpression([
571
- "||",
572
- vector_elem,
573
- "||"
574
- ], inline=True))
575
- return subparts
576
-
577
- def get_body(self):
578
- """Override parent get_body() to use our custom formatting."""
579
- body, _ = self._get_body()
580
- return body
330
+ self.answers["magnitude"] = AnswerTypes.Float(result)
581
331
 
582
332
  def _get_body(self):
583
- """Build question body and collect answers (new pattern)."""
584
- from typing import Tuple, List
585
-
333
+ """Build question body and collect answers."""
586
334
  body = ContentAST.Section()
587
- answers = []
588
335
 
589
336
  body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
590
337
 
591
- if self.is_multipart():
592
- # Use multipart formatting with repeated problem parts
593
- subpart_data = self.generate_subquestion_data()
594
- repeated_part = self.create_repeated_problem_part(subpart_data)
595
- body.add_element(repeated_part)
596
-
597
- # Collect all subpart answers
598
- for i, data in enumerate(self.subquestion_data):
599
- letter = chr(ord('a') + i)
600
- result = data['result']
601
- ans = Answer.auto_float(f"subpart_{letter}", result)
602
- answers.append(ans)
603
- else:
604
- # Single equation display using MathExpression for format-independent rendering
605
- vector_elem = self._format_vector(self.vector_a)
606
- body.add_element(ContentAST.MathExpression([
607
- "||",
608
- vector_elem,
609
- "|| = "
610
- ]))
611
-
612
- # Canvas-only answer field (hidden from PDF)
613
- ans = Answer.auto_float("magnitude", self.result)
614
- answers.append(ans)
615
- body.add_element(ContentAST.OnlyHtml([ans]))
616
-
617
- return body, answers
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
+ ]))
345
+
346
+ # Canvas-only answer field - use stored answer
347
+ answer = self.answers["magnitude"]
348
+ body.add_element(ContentAST.OnlyHtml([answer]))
349
+
350
+ return body, list(self.answers.values())
618
351
 
619
352
  def _get_explanation(self):
353
+ """Build question explanation."""
620
354
  explanation = ContentAST.Section()
621
355
 
622
356
  explanation.add_element(ContentAST.Paragraph(["The magnitude of a vector is calculated using the formula:"]))
623
- explanation.add_element(ContentAST.Equation("||arrow(v)|| = sqrt(v_1^2 + v_2^2 + ... + v_n^2)", inline=False))
624
-
625
- if self.is_multipart():
626
- # Handle multipart explanations
627
- for i, data in enumerate(self.subquestion_data):
628
- letter = chr(ord('a') + i)
629
- vector_a = data['vector_a']
630
- result = data['result']
631
-
632
- # Format vectors using Typst syntax
633
- vector_str = self._format_vector(vector_a)
634
- squares_str = " + ".join([f"{v}^2" for v in vector_a])
635
- calculation_str = " + ".join([str(v**2) for v in vector_a])
636
- sum_of_squares = sum(component ** 2 for component in vector_a)
637
- result_formatted = sorted(Answer.accepted_strings(result), key=lambda s: len(s))[0]
638
-
639
- # Add explanation for this subpart
640
- explanation.add_element(ContentAST.Paragraph([f"Part ({letter}):"]))
641
- explanation.add_element(
642
- ContentAST.Equation.make_block_equation__multiline_equals(
643
- lhs="||arrow(v)||",
644
- rhs=[
645
- f"||{vector_str}||",
646
- f"sqrt({squares_str})",
647
- f"sqrt({calculation_str})",
648
- f"sqrt({sum_of_squares})",
649
- result_formatted
650
- ]
651
- )
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
+ ]
652
378
  )
653
- else:
654
- # Single part explanation - use Typst syntax
655
- vector_str = self._format_vector(self.vector_a)
656
- squares_str = " + ".join([f"{v}^2" for v in self.vector_a])
657
- calculation_str = " + ".join([str(v**2) for v in self.vector_a])
658
- sum_of_squares = sum(component ** 2 for component in self.vector_a)
659
- result_formatted = sorted(Answer.accepted_strings(self.result), key=lambda s: len(s))[0]
660
-
661
- explanation.add_element(
662
- ContentAST.Equation.make_block_equation__multiline_equals(
663
- lhs="||arrow(v)||",
664
- rhs=[
665
- f"||{vector_str}||",
666
- f"sqrt({squares_str})",
667
- f"sqrt({calculation_str})",
668
- f"sqrt({sum_of_squares})",
669
- result_formatted
670
- ]
671
- )
672
- )
379
+ )
673
380
 
674
- return explanation, [] # Explanations don't have answers
381
+ return explanation, []