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
@@ -8,8 +8,8 @@ import numpy as np
8
8
  import sympy as sp
9
9
  from typing import List, Tuple, Dict, Any
10
10
 
11
- from QuizGenerator.contentast import ContentAST
12
- from QuizGenerator.question import Question, Answer, QuestionRegistry
11
+ from QuizGenerator.contentast import ContentAST, AnswerTypes
12
+ from QuizGenerator.question import Question, QuestionRegistry
13
13
  from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
14
14
 
15
15
  # Import gradient descent utilities
@@ -85,16 +85,18 @@ class ParameterCountingQuestion(Question):
85
85
  """Create answer fields."""
86
86
  self.answers = {}
87
87
 
88
- self.answers["total_weights"] = Answer.integer("total_weights", self.total_weights)
88
+ self.answers["total_weights"] = AnswerTypes.Int(self.total_weights, label="Total weights")
89
89
 
90
90
  if self.include_biases:
91
- self.answers["total_biases"] = Answer.integer("total_biases", self.total_biases)
92
- self.answers["total_params"] = Answer.integer("total_params", self.total_params)
91
+ self.answers["total_biases"] = AnswerTypes.Int(self.total_biases, label="Total biases")
92
+ self.answers["total_params"] = AnswerTypes.Int(self.total_params, label="Total trainable parameters")
93
93
  else:
94
- self.answers["total_params"] = Answer.integer("total_params", self.total_params)
94
+ self.answers["total_params"] = AnswerTypes.Int(self.total_params, label="Total trainable parameters")
95
95
 
96
- def get_body(self, **kwargs) -> ContentAST.Section:
96
+ def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
97
+ """Build question body and collect answers."""
97
98
  body = ContentAST.Section()
99
+ answers = []
98
100
 
99
101
  # Question description
100
102
  body.add_element(ContentAST.Paragraph([
@@ -122,27 +124,36 @@ class ParameterCountingQuestion(Question):
122
124
  table_data = []
123
125
  table_data.append(["Parameter Type", "Count"])
124
126
 
127
+ answers.append(self.answers["total_weights"])
125
128
  table_data.append([
126
129
  "Total weights (connections between layers)",
127
- ContentAST.Answer(self.answers["total_weights"])
130
+ self.answers["total_weights"]
128
131
  ])
129
132
 
130
133
  if self.include_biases:
134
+ answers.append(self.answers["total_biases"])
131
135
  table_data.append([
132
136
  "Total biases",
133
- ContentAST.Answer(self.answers["total_biases"])
137
+ self.answers["total_biases"]
134
138
  ])
135
139
 
140
+ answers.append(self.answers["total_params"])
136
141
  table_data.append([
137
142
  "Total trainable parameters",
138
- ContentAST.Answer(self.answers["total_params"])
143
+ self.answers["total_params"]
139
144
  ])
140
145
 
141
146
  body.add_element(ContentAST.Table(data=table_data))
142
147
 
148
+ return body, answers
149
+
150
+ def get_body(self, **kwargs) -> ContentAST.Section:
151
+ """Build question body (backward compatible interface)."""
152
+ body, _ = self._get_body(**kwargs)
143
153
  return body
144
154
 
145
- def get_explanation(self, **kwargs) -> ContentAST.Section:
155
+ def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
156
+ """Build question explanation."""
146
157
  explanation = ContentAST.Section()
147
158
 
148
159
  explanation.add_element(ContentAST.Paragraph([
@@ -150,7 +161,7 @@ class ParameterCountingQuestion(Question):
150
161
  ]))
151
162
 
152
163
  explanation.add_element(ContentAST.Paragraph([
153
- "**Weights calculation:**"
164
+ ContentAST.Text("Weights calculation:", emphasis=True)
154
165
  ]))
155
166
 
156
167
  for i in range(len(self.layer_sizes) - 1):
@@ -174,7 +185,7 @@ class ParameterCountingQuestion(Question):
174
185
 
175
186
  if self.include_biases:
176
187
  explanation.add_element(ContentAST.Paragraph([
177
- "**Biases calculation:**"
188
+ ContentAST.Text("Biases calculation:", emphasis=True)
178
189
  ]))
179
190
 
180
191
  for i in range(len(self.layer_sizes) - 1):
@@ -194,7 +205,7 @@ class ParameterCountingQuestion(Question):
194
205
  ]))
195
206
 
196
207
  explanation.add_element(ContentAST.Paragraph([
197
- "**Total trainable parameters:**"
208
+ ContentAST.Text("Total trainable parameters:", emphasis=True)
198
209
  ]))
199
210
 
200
211
  if self.include_biases:
@@ -208,6 +219,11 @@ class ParameterCountingQuestion(Question):
208
219
  inline=False
209
220
  ))
210
221
 
222
+ return explanation, []
223
+
224
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
225
+ """Build question explanation (backward compatible interface)."""
226
+ explanation, _ = self._get_explanation(**kwargs)
211
227
  return explanation
212
228
 
213
229
 
@@ -315,15 +331,17 @@ class ActivationFunctionComputationQuestion(Question):
315
331
 
316
332
  if self.activation == self.ACTIVATION_SOFTMAX:
317
333
  # Softmax: single vector answer
318
- self.answers["output"] = Answer.vector_value("output", self.output_vector)
334
+ self.answers["output"] = AnswerTypes.Vector(self.output_vector, label="Output vector")
319
335
  else:
320
336
  # Element-wise: individual answers
321
337
  for i, output in enumerate(self.output_vector):
322
338
  key = f"output_{i}"
323
- self.answers[key] = Answer.float_value(key, float(output))
339
+ self.answers[key] = AnswerTypes.Float(float(output), label=f"Output for input {self.input_vector[i]:.1f}")
324
340
 
325
- def get_body(self, **kwargs) -> ContentAST.Section:
341
+ def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
342
+ """Build question body and collect answers."""
326
343
  body = ContentAST.Section()
344
+ answers = []
327
345
 
328
346
  # Question description
329
347
  body.add_element(ContentAST.Paragraph([
@@ -349,9 +367,10 @@ class ActivationFunctionComputationQuestion(Question):
349
367
  "Compute the output vector:"
350
368
  ]))
351
369
 
370
+ answers.append(self.answers["output"])
352
371
  table_data = []
353
372
  table_data.append(["Output Vector"])
354
- table_data.append([ContentAST.Answer(self.answers["output"])])
373
+ table_data.append([self.answers["output"]])
355
374
 
356
375
  body.add_element(ContentAST.Table(data=table_data))
357
376
 
@@ -364,16 +383,24 @@ class ActivationFunctionComputationQuestion(Question):
364
383
  table_data.append(["Input", "Output"])
365
384
 
366
385
  for i, x in enumerate(self.input_vector):
386
+ answer = self.answers[f"output_{i}"]
387
+ answers.append(answer)
367
388
  table_data.append([
368
389
  ContentAST.Equation(f"{x:.1f}", inline=True),
369
- ContentAST.Answer(self.answers[f"output_{i}"])
390
+ answer
370
391
  ])
371
392
 
372
393
  body.add_element(ContentAST.Table(data=table_data))
373
394
 
395
+ return body, answers
396
+
397
+ def get_body(self, **kwargs) -> ContentAST.Section:
398
+ """Build question body (backward compatible interface)."""
399
+ body, _ = self._get_body(**kwargs)
374
400
  return body
375
401
 
376
- def get_explanation(self, **kwargs) -> ContentAST.Section:
402
+ def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
403
+ """Build question explanation."""
377
404
  explanation = ContentAST.Section()
378
405
 
379
406
  explanation.add_element(ContentAST.Paragraph([
@@ -382,7 +409,7 @@ class ActivationFunctionComputationQuestion(Question):
382
409
 
383
410
  if self.activation == self.ACTIVATION_SOFTMAX:
384
411
  explanation.add_element(ContentAST.Paragraph([
385
- "**Softmax computation:**"
412
+ ContentAST.Text("Softmax computation:", emphasis=True)
386
413
  ]))
387
414
 
388
415
  # Show exponentials
@@ -419,7 +446,7 @@ class ActivationFunctionComputationQuestion(Question):
419
446
 
420
447
  else:
421
448
  explanation.add_element(ContentAST.Paragraph([
422
- "**Element-wise computation:**"
449
+ ContentAST.Text("Element-wise computation:", emphasis=True)
423
450
  ]))
424
451
 
425
452
  for i, (x, y) in enumerate(zip(self.input_vector, self.output_vector)):
@@ -441,6 +468,11 @@ class ActivationFunctionComputationQuestion(Question):
441
468
  inline=False
442
469
  ))
443
470
 
471
+ return explanation, []
472
+
473
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
474
+ """Build question explanation (backward compatible interface)."""
475
+ explanation, _ = self._get_explanation(**kwargs)
444
476
  return explanation
445
477
 
446
478
 
@@ -513,14 +545,16 @@ class RegularizationCalculationQuestion(Question):
513
545
  """Create answer fields."""
514
546
  self.answers = {}
515
547
 
516
- self.answers["prediction"] = Answer.float_value("prediction", float(self.prediction))
517
- self.answers["base_loss"] = Answer.float_value("base_loss", float(self.base_loss))
518
- self.answers["l2_penalty"] = Answer.float_value("l2_penalty", float(self.l2_penalty))
519
- self.answers["total_loss"] = Answer.float_value("total_loss", float(self.total_loss))
520
- self.answers["grad_total_w0"] = Answer.auto_float("grad_total_w0", float(self.grad_total_w0))
548
+ self.answers["prediction"] = AnswerTypes.Float(float(self.prediction), label="Prediction ŷ")
549
+ self.answers["base_loss"] = AnswerTypes.Float(float(self.base_loss), label="Base MSE loss")
550
+ self.answers["l2_penalty"] = AnswerTypes.Float(float(self.l2_penalty), label="L2 penalty")
551
+ self.answers["total_loss"] = AnswerTypes.Float(float(self.total_loss), label="Total loss")
552
+ self.answers["grad_total_w0"] = AnswerTypes.Float(float(self.grad_total_w0), label="Gradient ∂L/∂w₀")
521
553
 
522
- def get_body(self, **kwargs) -> ContentAST.Section:
554
+ def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
555
+ """Build question body and collect answers."""
523
556
  body = ContentAST.Section()
557
+ answers = []
524
558
 
525
559
  # Question description
526
560
  body.add_element(ContentAST.Paragraph([
@@ -570,36 +604,47 @@ class RegularizationCalculationQuestion(Question):
570
604
  table_data = []
571
605
  table_data.append(["Calculation", "Value"])
572
606
 
607
+ answers.append(self.answers["prediction"])
573
608
  table_data.append([
574
609
  ContentAST.Paragraph(["Prediction ", ContentAST.Equation(r"\hat{y}", inline=True)]),
575
- ContentAST.Answer(self.answers["prediction"])
610
+ self.answers["prediction"]
576
611
  ])
577
612
 
613
+ answers.append(self.answers["base_loss"])
578
614
  table_data.append([
579
615
  ContentAST.Paragraph(["Base MSE loss: ", ContentAST.Equation(r"L_{base} = (1/2)(y - \hat{y})^2", inline=True)]),
580
- ContentAST.Answer(self.answers["base_loss"])
616
+ self.answers["base_loss"]
581
617
  ])
582
618
 
619
+ answers.append(self.answers["l2_penalty"])
583
620
  table_data.append([
584
621
  ContentAST.Paragraph(["L2 penalty: ", ContentAST.Equation(r"L_{reg} = (\lambda/2)\sum w_i^2", inline=True)]),
585
- ContentAST.Answer(self.answers["l2_penalty"])
622
+ self.answers["l2_penalty"]
586
623
  ])
587
624
 
625
+ answers.append(self.answers["total_loss"])
588
626
  table_data.append([
589
627
  ContentAST.Paragraph(["Total loss: ", ContentAST.Equation(r"L_{total} = L_{base} + L_{reg}", inline=True)]),
590
- ContentAST.Answer(self.answers["total_loss"])
628
+ self.answers["total_loss"]
591
629
  ])
592
630
 
631
+ answers.append(self.answers["grad_total_w0"])
593
632
  table_data.append([
594
633
  ContentAST.Paragraph(["Gradient: ", ContentAST.Equation(r"\frac{\partial L_{total}}{\partial w_0}", inline=True)]),
595
- ContentAST.Answer(self.answers["grad_total_w0"])
634
+ self.answers["grad_total_w0"]
596
635
  ])
597
636
 
598
637
  body.add_element(ContentAST.Table(data=table_data))
599
638
 
639
+ return body, answers
640
+
641
+ def get_body(self, **kwargs) -> ContentAST.Section:
642
+ """Build question body (backward compatible interface)."""
643
+ body, _ = self._get_body(**kwargs)
600
644
  return body
601
645
 
602
- def get_explanation(self, **kwargs) -> ContentAST.Section:
646
+ def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
647
+ """Build question explanation."""
603
648
  explanation = ContentAST.Section()
604
649
 
605
650
  explanation.add_element(ContentAST.Paragraph([
@@ -608,7 +653,7 @@ class RegularizationCalculationQuestion(Question):
608
653
 
609
654
  # Step 1: Forward pass
610
655
  explanation.add_element(ContentAST.Paragraph([
611
- "**Step 1: Compute prediction**"
656
+ ContentAST.Text("Step 1: Compute prediction", emphasis=True)
612
657
  ]))
613
658
 
614
659
  terms = []
@@ -626,7 +671,7 @@ class RegularizationCalculationQuestion(Question):
626
671
 
627
672
  # Step 2: Base loss
628
673
  explanation.add_element(ContentAST.Paragraph([
629
- "**Step 2: Compute base MSE loss**"
674
+ ContentAST.Text("Step 2: Compute base MSE loss", emphasis=True)
630
675
  ]))
631
676
 
632
677
  explanation.add_element(ContentAST.Equation(
@@ -636,7 +681,7 @@ class RegularizationCalculationQuestion(Question):
636
681
 
637
682
  # Step 3: L2 penalty
638
683
  explanation.add_element(ContentAST.Paragraph([
639
- "**Step 3: Compute L2 penalty**"
684
+ ContentAST.Text("Step 3: Compute L2 penalty", emphasis=True)
640
685
  ]))
641
686
 
642
687
  weight_squares = [f"{w:.1f}^2" for w in self.weights]
@@ -649,7 +694,7 @@ class RegularizationCalculationQuestion(Question):
649
694
 
650
695
  # Step 4: Total loss
651
696
  explanation.add_element(ContentAST.Paragraph([
652
- "**Step 4: Compute total loss**"
697
+ ContentAST.Text("Step 4: Compute total loss", emphasis=True)
653
698
  ]))
654
699
 
655
700
  explanation.add_element(ContentAST.Equation(
@@ -659,7 +704,7 @@ class RegularizationCalculationQuestion(Question):
659
704
 
660
705
  # Step 5: Gradient with regularization
661
706
  explanation.add_element(ContentAST.Paragraph([
662
- "**Step 5: Compute gradient with regularization**"
707
+ ContentAST.Text("Step 5: Compute gradient with regularization", emphasis=True)
663
708
  ]))
664
709
 
665
710
  explanation.add_element(ContentAST.Paragraph([
@@ -688,6 +733,11 @@ class RegularizationCalculationQuestion(Question):
688
733
  " to the gradient, pushing the weight toward zero."
689
734
  ]))
690
735
 
736
+ return explanation, []
737
+
738
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
739
+ """Build question explanation (backward compatible interface)."""
740
+ explanation, _ = self._get_explanation(**kwargs)
691
741
  return explanation
692
742
 
693
743
 
@@ -767,17 +817,19 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
767
817
  self.answers = {}
768
818
 
769
819
  # New velocity
770
- self.answers["velocity"] = Answer.vector_value("velocity", self.new_velocity)
820
+ self.answers["velocity"] = AnswerTypes.Vector(self.new_velocity, label="New velocity")
771
821
 
772
822
  # New weights with momentum
773
- self.answers["weights_momentum"] = Answer.vector_value("weights_momentum", self.new_weights)
823
+ self.answers["weights_momentum"] = AnswerTypes.Vector(self.new_weights, label="Weights (momentum)")
774
824
 
775
825
  # Vanilla SGD weights for comparison
776
826
  if self.show_vanilla_sgd:
777
- self.answers["weights_sgd"] = Answer.vector_value("weights_sgd", self.sgd_weights)
827
+ self.answers["weights_sgd"] = AnswerTypes.Vector(self.sgd_weights, label="Weights (vanilla SGD)")
778
828
 
779
- def get_body(self, **kwargs) -> ContentAST.Section:
829
+ def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
830
+ """Build question body and collect answers."""
780
831
  body = ContentAST.Section()
832
+ answers = []
781
833
 
782
834
  # Question description
783
835
  body.add_element(ContentAST.Paragraph([
@@ -800,14 +852,14 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
800
852
 
801
853
  # Current state
802
854
  body.add_element(ContentAST.Paragraph([
803
- "**Current optimization state:**"
855
+ ContentAST.Text("Current optimization state:", emphasis=True)
804
856
  ]))
805
857
 
806
858
  body.add_element(ContentAST.Paragraph([
807
859
  "Current weights: ",
808
860
  ContentAST.Equation(f"{format_vector(self.current_weights)}", inline=True)
809
861
  ]))
810
-
862
+
811
863
  body.add_element(ContentAST.Paragraph([
812
864
  "Previous velocity: ",
813
865
  ContentAST.Equation(f"{format_vector(self.prev_velocity)}", inline=True)
@@ -815,7 +867,7 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
815
867
 
816
868
  # Hyperparameters
817
869
  body.add_element(ContentAST.Paragraph([
818
- "**Hyperparameters:**"
870
+ ContentAST.Text("Hyperparameters:", emphasis=True)
819
871
  ]))
820
872
 
821
873
  body.add_element(ContentAST.Paragraph([
@@ -837,30 +889,39 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
837
889
  table_data = []
838
890
  table_data.append(["Update Type", "Formula", "Result"])
839
891
 
892
+ answers.append(self.answers["velocity"])
840
893
  table_data.append([
841
894
  "New velocity",
842
895
  ContentAST.Equation(r"v' = \beta v + (1-\beta)\nabla f", inline=True),
843
- ContentAST.Answer(self.answers["velocity"])
896
+ self.answers["velocity"]
844
897
  ])
845
898
 
899
+ answers.append(self.answers["weights_momentum"])
846
900
  table_data.append([
847
901
  "Weights (momentum)",
848
902
  ContentAST.Equation(r"w' = w - \alpha v'", inline=True),
849
- ContentAST.Answer(self.answers["weights_momentum"])
903
+ self.answers["weights_momentum"]
850
904
  ])
851
905
 
852
906
  if self.show_vanilla_sgd:
907
+ answers.append(self.answers["weights_sgd"])
853
908
  table_data.append([
854
909
  "Weights (vanilla SGD)",
855
910
  ContentAST.Equation(r"w' = w - \alpha \nabla f", inline=True),
856
- ContentAST.Answer(self.answers["weights_sgd"])
911
+ self.answers["weights_sgd"]
857
912
  ])
858
913
 
859
914
  body.add_element(ContentAST.Table(data=table_data))
860
915
 
916
+ return body, answers
917
+
918
+ def get_body(self, **kwargs) -> ContentAST.Section:
919
+ """Build question body (backward compatible interface)."""
920
+ body, _ = self._get_body(**kwargs)
861
921
  return body
862
922
 
863
- def get_explanation(self, **kwargs) -> ContentAST.Section:
923
+ def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
924
+ """Build question explanation."""
864
925
  explanation = ContentAST.Section()
865
926
 
866
927
  explanation.add_element(ContentAST.Paragraph([
@@ -870,7 +931,7 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
870
931
 
871
932
  # Step 1: Calculate new velocity
872
933
  explanation.add_element(ContentAST.Paragraph([
873
- "**Step 1: Update velocity using momentum**"
934
+ ContentAST.Text("Step 1: Update velocity using momentum", emphasis=True)
874
935
  ]))
875
936
 
876
937
  explanation.add_element(ContentAST.Paragraph([
@@ -883,17 +944,24 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
883
944
  ))
884
945
 
885
946
  # Show calculation for each component
947
+ digits = ContentAST.Answer.DEFAULT_ROUNDING_DIGITS
886
948
  for i in range(self.num_variables):
887
949
  var_name = f"x_{i}"
950
+ # Round all intermediate values to avoid floating point precision issues
951
+ beta_times_v = round(self.momentum_beta * self.prev_velocity[i], digits)
952
+ one_minus_beta = round(1 - self.momentum_beta, digits)
953
+ one_minus_beta_times_grad = round((1 - self.momentum_beta) * self.gradients[i], digits)
954
+
888
955
  explanation.add_element(ContentAST.Equation(
889
- f"v'[{i}] = {self.momentum_beta} \\times {self.prev_velocity[i]:.2f} + "
890
- f"{1 - self.momentum_beta} \\times {self.gradients[i]:.4f} = {self.new_velocity[i]:.4f}",
956
+ f"v'[{i}] = {self.momentum_beta} \\times {self.prev_velocity[i]:.{digits}f} + "
957
+ f"{one_minus_beta:.{digits}f} \\times {self.gradients[i]:.{digits}f} = "
958
+ f"{beta_times_v:.{digits}f} + {one_minus_beta_times_grad:.{digits}f} = {self.new_velocity[i]:.{digits}f}",
891
959
  inline=False
892
960
  ))
893
961
 
894
962
  # Step 2: Update weights with momentum
895
963
  explanation.add_element(ContentAST.Paragraph([
896
- "**Step 2: Update weights using new velocity**"
964
+ ContentAST.Text("Step 2: Update weights using new velocity", emphasis=True)
897
965
  ]))
898
966
 
899
967
  explanation.add_element(ContentAST.Equation(
@@ -910,7 +978,7 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
910
978
  # Comparison with vanilla SGD
911
979
  if self.show_vanilla_sgd:
912
980
  explanation.add_element(ContentAST.Paragraph([
913
- "**Comparison with vanilla SGD:**"
981
+ ContentAST.Text("Comparison with vanilla SGD:", emphasis=True)
914
982
  ]))
915
983
 
916
984
  explanation.add_element(ContentAST.Paragraph([
@@ -933,4 +1001,9 @@ class MomentumOptimizerQuestion(Question, TableQuestionMixin, BodyTemplatesMixin
933
1001
  "which can help accelerate learning and smooth out noisy gradients."
934
1002
  ]))
935
1003
 
1004
+ return explanation, []
1005
+
1006
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
1007
+ """Build question explanation (backward compatible interface)."""
1008
+ explanation, _ = self._get_explanation(**kwargs)
936
1009
  return explanation