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,369 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import logging
5
+ from typing import List
6
+ import sympy as sp
7
+
8
+ from QuizGenerator.contentast import ContentAST
9
+ from QuizGenerator.question import Question, Answer, QuestionRegistry
10
+ from .misc import generate_function, format_vector
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ class DerivativeQuestion(Question, abc.ABC):
16
+ """Base class for derivative calculation questions."""
17
+
18
+ def __init__(self, *args, **kwargs):
19
+ kwargs["topic"] = kwargs.get("topic", Question.Topic.ML_OPTIMIZATION)
20
+ super().__init__(*args, **kwargs)
21
+ self.num_variables = kwargs.get("num_variables", 2)
22
+ self.max_degree = kwargs.get("max_degree", 2)
23
+
24
+ def _generate_evaluation_point(self) -> List[float]:
25
+ """Generate a random point for gradient evaluation."""
26
+ return [self.rng.randint(-3, 3) for _ in range(self.num_variables)]
27
+
28
+ def _format_partial_derivative(self, var_index: int) -> str:
29
+ """Format partial derivative symbol for display."""
30
+ if self.num_variables == 1:
31
+ return "\\frac{df}{dx_0}"
32
+ else:
33
+ return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
34
+
35
+ def _create_derivative_answers(self, evaluation_point: List[float]) -> None:
36
+ """Create answer fields for each partial derivative at the evaluation point."""
37
+ self.answers = {}
38
+
39
+ # Evaluate gradient at the specified point
40
+ subs_map = dict(zip(self.variables, evaluation_point))
41
+
42
+ # Create answer for each partial derivative
43
+ for i in range(self.num_variables):
44
+ answer_key = f"partial_derivative_{i}"
45
+ # Evaluate the partial derivative and convert to float
46
+ partial_value = self.gradient_function[i].subs(subs_map)
47
+ try:
48
+ gradient_value = float(partial_value)
49
+ except (TypeError, ValueError):
50
+ # If we get a complex number or other conversion error,
51
+ # this likely means log hit a negative value - regenerate
52
+ raise ValueError("Complex number encountered - need to regenerate")
53
+
54
+ # Use auto_float for Canvas compatibility with integers and decimals
55
+ self.answers[answer_key] = Answer.auto_float(answer_key, gradient_value)
56
+
57
+ def _create_gradient_vector_answer(self) -> None:
58
+ """Create a single gradient vector answer for PDF format."""
59
+ # Format gradient as vector notation
60
+ subs_map = dict(zip(self.variables, self.evaluation_point))
61
+ gradient_values = []
62
+
63
+ for i in range(self.num_variables):
64
+ partial_value = self.gradient_function[i].subs(subs_map)
65
+ try:
66
+ gradient_value = float(partial_value)
67
+ except TypeError:
68
+ gradient_value = float(partial_value.evalf())
69
+ gradient_values.append(gradient_value)
70
+
71
+ # Format as vector for display using consistent formatting
72
+ vector_str = format_vector(gradient_values)
73
+ self.answers["gradient_vector"] = Answer.string("gradient_vector", vector_str, pdf_only=True)
74
+
75
+ def get_body(self, **kwargs) -> ContentAST.Section:
76
+ body = ContentAST.Section()
77
+
78
+ # Display the function
79
+ body.add_element(
80
+ ContentAST.Paragraph([
81
+ "Given the function ",
82
+ ContentAST.Equation(sp.latex(self.equation), inline=True),
83
+ ", calculate the gradient at the point ",
84
+ ContentAST.Equation(format_vector(self.evaluation_point), inline=True),
85
+ "."
86
+ ])
87
+ )
88
+
89
+ # Format evaluation point for LaTeX
90
+ eval_point_str = ", ".join([f"x_{i} = {self.evaluation_point[i]}" for i in range(self.num_variables)])
91
+
92
+ # For PDF: Use OnlyLatex to show gradient vector format (no answer blank)
93
+ body.add_element(
94
+ ContentAST.OnlyLatex([
95
+ ContentAST.Paragraph([
96
+ ContentAST.Equation(
97
+ f"\\left. \\nabla f \\right|_{{{eval_point_str}}} = ",
98
+ inline=True
99
+ )
100
+ ])
101
+ ])
102
+ )
103
+
104
+ # For Canvas: Use OnlyHtml to show individual partial derivatives
105
+ for i in range(self.num_variables):
106
+ body.add_element(
107
+ ContentAST.OnlyHtml([
108
+ ContentAST.Paragraph([
109
+ ContentAST.Equation(
110
+ f"\\left. {self._format_partial_derivative(i)} \\right|_{{{eval_point_str}}} = ",
111
+ inline=True
112
+ ),
113
+ ContentAST.Answer(self.answers[f"partial_derivative_{i}"])
114
+ ])
115
+ ])
116
+ )
117
+
118
+ return body
119
+
120
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
121
+ explanation = ContentAST.Section()
122
+
123
+ # Show the function and its gradient
124
+ explanation.add_element(
125
+ ContentAST.Paragraph([
126
+ "To find the gradient, we calculate the partial derivatives of ",
127
+ ContentAST.Equation(sp.latex(self.equation), inline=True),
128
+ ":"
129
+ ])
130
+ )
131
+
132
+ # Show analytical gradient
133
+ explanation.add_element(
134
+ ContentAST.Equation(f"\\nabla f = {sp.latex(self.gradient_function)}", inline=False)
135
+ )
136
+
137
+ # Show evaluation at the specific point
138
+ explanation.add_element(
139
+ ContentAST.Paragraph([
140
+ f"Evaluating at the point {format_vector(self.evaluation_point)}:"
141
+ ])
142
+ )
143
+
144
+ # Show each partial derivative calculation
145
+ subs_map = dict(zip(self.variables, self.evaluation_point))
146
+ for i in range(self.num_variables):
147
+ partial_expr = self.gradient_function[i]
148
+ partial_value = partial_expr.subs(subs_map)
149
+
150
+ # Use Answer.accepted_strings for clean numerical formatting
151
+ try:
152
+ numerical_value = float(partial_value)
153
+ except (TypeError, ValueError):
154
+ numerical_value = float(partial_value.evalf())
155
+
156
+ # Get clean string representation
157
+ clean_value = sorted(Answer.accepted_strings(numerical_value), key=lambda s: len(s))[0]
158
+
159
+ explanation.add_element(
160
+ ContentAST.Paragraph([
161
+ ContentAST.Equation(
162
+ f"{self._format_partial_derivative(i)} = {sp.latex(partial_expr)} = {clean_value}",
163
+ inline=False
164
+ )
165
+ ])
166
+ )
167
+
168
+ return explanation
169
+
170
+
171
+ @QuestionRegistry.register("DerivativeBasic")
172
+ class DerivativeBasic(DerivativeQuestion):
173
+ """Basic derivative calculation using polynomial functions."""
174
+
175
+ def refresh(self, rng_seed=None, *args, **kwargs):
176
+ super().refresh(rng_seed=rng_seed, *args, **kwargs)
177
+
178
+ # Generate a basic polynomial function
179
+ self.variables, self.function, self.gradient_function, self.equation = generate_function(
180
+ self.rng, self.num_variables, self.max_degree
181
+ )
182
+
183
+ # Generate evaluation point
184
+ self.evaluation_point = self._generate_evaluation_point()
185
+
186
+ # Create answers
187
+ self._create_derivative_answers(self.evaluation_point)
188
+
189
+ # For PDF: Create single gradient vector answer
190
+ self._create_gradient_vector_answer()
191
+
192
+
193
+ @QuestionRegistry.register("DerivativeChain")
194
+ class DerivativeChain(DerivativeQuestion):
195
+ """Chain rule derivative calculation using function composition."""
196
+
197
+ def refresh(self, rng_seed=None, *args, **kwargs):
198
+ super().refresh(rng_seed=rng_seed, *args, **kwargs)
199
+
200
+ # Try to generate a valid function/point combination, regenerating if we hit complex numbers
201
+ max_attempts = 10
202
+ for attempt in range(max_attempts):
203
+ try:
204
+ # Generate inner and outer functions for composition
205
+ self._generate_composed_function()
206
+
207
+ # Generate evaluation point
208
+ self.evaluation_point = self._generate_evaluation_point()
209
+
210
+ # Create answers - this will raise ValueError if we get complex numbers
211
+ self._create_derivative_answers(self.evaluation_point)
212
+
213
+ # For PDF: Create single gradient vector answer
214
+ self._create_gradient_vector_answer()
215
+
216
+ # If we get here, everything worked
217
+ break
218
+
219
+ except ValueError as e:
220
+ if "Complex number encountered" in str(e) and attempt < max_attempts - 1:
221
+ # Advance RNG state by making a dummy call
222
+ _ = self.rng.random()
223
+ continue
224
+ else:
225
+ # If we've exhausted attempts or different error, re-raise
226
+ raise
227
+
228
+ def _generate_composed_function(self) -> None:
229
+ """Generate a composed function f(g(x)) for chain rule practice."""
230
+ # Create variable symbols
231
+ var_names = [f'x_{i}' for i in range(self.num_variables)]
232
+ self.variables = sp.symbols(var_names)
233
+
234
+ # Generate inner function g(x) - simpler polynomial
235
+ inner_terms = [m for m in sp.polys.itermonomials(self.variables, max(1, self.max_degree-1)) if m != 1]
236
+ coeff_pool = [*range(-5, 0), *range(1, 6)] # Smaller coefficients for inner function
237
+
238
+ if inner_terms:
239
+ inner_poly = sp.Add(*(self.rng.choice(coeff_pool) * t for t in inner_terms))
240
+ else:
241
+ inner_poly = sp.Add(*[self.rng.choice(coeff_pool) * v for v in self.variables])
242
+
243
+ # Generate outer function - use polynomials, exp, and ln for reliable evaluation
244
+ u = sp.Symbol('u') # Intermediate variable
245
+ outer_functions = [
246
+ u**2,
247
+ u**3,
248
+ u**4,
249
+ sp.exp(u),
250
+ sp.log(u + 2) # Add 2 to ensure positive argument for evaluation points
251
+ ]
252
+
253
+ outer_func = self.rng.choice(outer_functions)
254
+
255
+ # Compose the functions: f(g(x))
256
+ self.inner_function = inner_poly
257
+ self.outer_function = outer_func
258
+ self.function = outer_func.subs(u, inner_poly)
259
+
260
+ # Calculate gradient using chain rule
261
+ self.gradient_function = sp.Matrix([self.function.diff(v) for v in self.variables])
262
+
263
+ # Create equation for display
264
+ f = sp.Function('f')
265
+ self.equation = sp.Eq(f(*self.variables), self.function)
266
+
267
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
268
+ explanation = ContentAST.Section()
269
+
270
+ # Show the composed function structure
271
+ explanation.add_element(
272
+ ContentAST.Paragraph([
273
+ "This is a composition of functions requiring the chain rule. The function ",
274
+ ContentAST.Equation(sp.latex(self.equation), inline=True),
275
+ " can be written as ",
276
+ ContentAST.Equation(f"f(g(x)) \\text{{ where }} g(x) = {sp.latex(self.inner_function)}", inline=True),
277
+ "."
278
+ ])
279
+ )
280
+
281
+ # Explain chain rule with Leibniz notation
282
+ explanation.add_element(
283
+ ContentAST.Paragraph([
284
+ "The chain rule states that for a composite function ",
285
+ ContentAST.Equation("f(g(x))", inline=True),
286
+ ", the derivative with respect to each variable is found by multiplying the derivative of the outer function with respect to the inner function by the derivative of the inner function with respect to the variable:"
287
+ ])
288
+ )
289
+
290
+ # Show chain rule formula for each variable
291
+ for i in range(self.num_variables):
292
+ var_name = f"x_{i}"
293
+ explanation.add_element(
294
+ ContentAST.Equation(
295
+ f"\\frac{{\\partial f}}{{\\partial {var_name}}} = \\frac{{\\partial f}}{{\\partial g}} \\cdot \\frac{{\\partial g}}{{\\partial {var_name}}}",
296
+ inline=False
297
+ )
298
+ )
299
+
300
+ explanation.add_element(
301
+ ContentAST.Paragraph([
302
+ "Applying this to our specific function:"
303
+ ])
304
+ )
305
+
306
+ # Show the specific derivatives step by step
307
+ for i in range(self.num_variables):
308
+ var_name = f"x_{i}"
309
+
310
+ # Get outer function derivative with respect to inner function
311
+ outer_deriv = self.outer_function.diff(sp.Symbol('u'))
312
+ inner_deriv = self.inner_function.diff(self.variables[i])
313
+
314
+ explanation.add_element(
315
+ ContentAST.Paragraph([
316
+ f"For {var_name}:"
317
+ ])
318
+ )
319
+
320
+ explanation.add_element(
321
+ ContentAST.Equation(
322
+ f"\\frac{{\\partial f}}{{\\partial {var_name}}} = \\left({sp.latex(outer_deriv)}\\right) \\cdot \\left({sp.latex(inner_deriv)}\\right)",
323
+ inline=False
324
+ )
325
+ )
326
+
327
+ # Show analytical gradient
328
+ explanation.add_element(
329
+ ContentAST.Paragraph([
330
+ "This gives us the complete gradient:"
331
+ ])
332
+ )
333
+
334
+ explanation.add_element(
335
+ ContentAST.Equation(f"\\nabla f = {sp.latex(self.gradient_function)}", inline=False)
336
+ )
337
+
338
+ # Show evaluation at the specific point
339
+ explanation.add_element(
340
+ ContentAST.Paragraph([
341
+ f"Evaluating at the point {format_vector(self.evaluation_point)}:"
342
+ ])
343
+ )
344
+
345
+ # Show each partial derivative calculation
346
+ subs_map = dict(zip(self.variables, self.evaluation_point))
347
+ for i in range(self.num_variables):
348
+ partial_expr = self.gradient_function[i]
349
+ partial_value = partial_expr.subs(subs_map)
350
+
351
+ # Use Answer.accepted_strings for clean numerical formatting
352
+ try:
353
+ numerical_value = float(partial_value)
354
+ except (TypeError, ValueError):
355
+ numerical_value = float(partial_value.evalf())
356
+
357
+ # Get clean string representation
358
+ clean_value = sorted(Answer.accepted_strings(numerical_value), key=lambda s: len(s))[0]
359
+
360
+ explanation.add_element(
361
+ ContentAST.Paragraph([
362
+ ContentAST.Equation(
363
+ f"{self._format_partial_derivative(i)} = {sp.latex(partial_expr)} = {clean_value}",
364
+ inline=False
365
+ )
366
+ ])
367
+ )
368
+
369
+ return explanation
@@ -0,0 +1,305 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import logging
5
+ import math
6
+ from typing import List, Tuple, Callable, Union, Any
7
+ import sympy as sp
8
+
9
+ from QuizGenerator.contentast import ContentAST
10
+ from QuizGenerator.question import Question, Answer, QuestionRegistry
11
+ from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
12
+
13
+ from .misc import generate_function, format_vector
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ class GradientDescentQuestion(Question, abc.ABC):
19
+ def __init__(self, *args, **kwargs):
20
+ kwargs["topic"] = kwargs.get("topic", Question.Topic.ML_OPTIMIZATION)
21
+ super().__init__(*args, **kwargs)
22
+
23
+
24
+ @QuestionRegistry.register("GradientDescentWalkthrough")
25
+ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, BodyTemplatesMixin):
26
+
27
+ def __init__(self, *args, **kwargs):
28
+ super().__init__(*args, **kwargs)
29
+ self.num_steps = kwargs.get("num_steps", 4)
30
+ self.num_variables = kwargs.get("num_variables", 2)
31
+ self.max_degree = kwargs.get("max_degree", 2)
32
+ self.single_variable = kwargs.get("single_variable", False)
33
+ self.minimize = kwargs.get("minimize", True) # Default to minimization
34
+
35
+ if self.single_variable:
36
+ self.num_variables = 1
37
+
38
+ def _perform_gradient_descent(self) -> List[dict]:
39
+ """
40
+ Perform gradient descent and return step-by-step results.
41
+ """
42
+ results = []
43
+
44
+ x = list(map(float, self.starting_point)) # current location as floats
45
+
46
+ for step in range(self.num_steps):
47
+ subs_map = dict(zip(self.variables, x))
48
+
49
+ # gradient as floats
50
+ g_syms = self.gradient_function.subs(subs_map)
51
+ g = [float(val) for val in g_syms]
52
+
53
+ # function value
54
+ f_val = float(self.function.subs(subs_map))
55
+
56
+ update = [self.learning_rate * gi for gi in g]
57
+
58
+ results.append(
59
+ {
60
+ "step": step + 1,
61
+ "location": x[:],
62
+ "gradient": g[:],
63
+ "update": update[:],
64
+ "function_value": f_val,
65
+ }
66
+ )
67
+
68
+ x = [xi - ui for xi, ui in zip(x, update)] if self.minimize else \
69
+ [xi + ui for xi, ui in zip(x, update)]
70
+
71
+ return results
72
+
73
+ def refresh(self, rng_seed=None, *args, **kwargs):
74
+ super().refresh(rng_seed=rng_seed, *args, **kwargs)
75
+
76
+ # Generate function and its properties
77
+ self.variables, self.function, self.gradient_function, self.equation = generate_function(self.rng, self.num_variables, self.max_degree)
78
+
79
+ # Generate learning rate (expanded range)
80
+ self.learning_rate = self.rng.choice([0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5])
81
+
82
+ self.starting_point = [self.rng.randint(-3, 3) for _ in range(self.num_variables)]
83
+
84
+ # Perform gradient descent
85
+ self.gradient_descent_results = self._perform_gradient_descent()
86
+
87
+ # Set up answers
88
+ self.answers = {}
89
+
90
+ # Answers for each step
91
+ for i, result in enumerate(self.gradient_descent_results):
92
+ step = result['step']
93
+
94
+ # Location answer
95
+ location_key = f"answer__location_{step}"
96
+ self.answers[location_key] = Answer.vector_value(location_key, list(result['location']))
97
+
98
+ # Gradient answer
99
+ gradient_key = f"answer__gradient_{step}"
100
+ self.answers[gradient_key] = Answer.vector_value(gradient_key, list(result['gradient']))
101
+
102
+ # Update answer
103
+ update_key = f"answer__update_{step}"
104
+ self.answers[update_key] = Answer.vector_value(update_key, list(result['update']))
105
+
106
+ def get_body(self, **kwargs) -> ContentAST.Section:
107
+ body = ContentAST.Section()
108
+
109
+ # Introduction
110
+ objective = "minimize" if self.minimize else "maximize"
111
+ sign = "-" if self.minimize else "+"
112
+
113
+ body.add_element(
114
+ ContentAST.Paragraph(
115
+ [
116
+ f"Use gradient descent to {objective} the function ",
117
+ ContentAST.Equation(sp.latex(self.function), inline=True),
118
+ " with learning rate ",
119
+ ContentAST.Equation(f"\\alpha = {self.learning_rate}", inline=True),
120
+ f" and starting point {self.starting_point[0] if self.num_variables == 1 else tuple(self.starting_point)}. "
121
+ "Fill in the table below with your calculations."
122
+ ]
123
+ )
124
+ )
125
+
126
+ # Create table data - use ContentAST.Equation for proper LaTeX rendering in headers
127
+ headers = [
128
+ "n",
129
+ "location",
130
+ ContentAST.Equation("\\nabla f", inline=True),
131
+ ContentAST.Equation("\\alpha \\cdot \\nabla f", inline=True)
132
+ ]
133
+ table_rows = []
134
+
135
+ for i in range(self.num_steps):
136
+ step = i + 1
137
+ row = {"n": str(step)}
138
+
139
+ if step == 1:
140
+
141
+ # Fill in starting location for first row with default formatting
142
+ row["location"] = f"{format_vector(self.starting_point)}"
143
+ row[headers[2]] = f"answer__gradient_{step}" # gradient column
144
+ row[headers[3]] = f"answer__update_{step}" # update column
145
+ else:
146
+ # Subsequent rows - all answer fields
147
+ row["location"] = f"answer__location_{step}"
148
+ row[headers[2]] = f"answer__gradient_{step}" # gradient column
149
+ row[headers[3]] = f"answer__update_{step}" # update column
150
+ table_rows.append(row)
151
+
152
+ # Create the table using mixin
153
+ gradient_table = self.create_answer_table(
154
+ headers=headers,
155
+ data_rows=table_rows,
156
+ answer_columns=["location", headers[2], headers[3]] # Use actual header objects
157
+ )
158
+
159
+ body.add_element(gradient_table)
160
+
161
+ return body
162
+
163
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
164
+ explanation = ContentAST.Section()
165
+
166
+ explanation.add_element(
167
+ ContentAST.Paragraph(
168
+ [
169
+ "Gradient descent is an optimization algorithm that iteratively moves towards "
170
+ "the minimum of a function by taking steps proportional to the negative of the gradient."
171
+ ]
172
+ )
173
+ )
174
+
175
+ objective = "minimize" if self.minimize else "maximize"
176
+ sign = "-" if self.minimize else "+"
177
+
178
+ explanation.add_element(
179
+ ContentAST.Paragraph(
180
+ [
181
+ f"We want to {objective} the function ",
182
+ ContentAST.Equation(sp.latex(self.function), inline=True),
183
+ ". First, we calculate the analytical gradient:"
184
+ ]
185
+ )
186
+ )
187
+
188
+ # Add analytical gradient calculation as a display equation (vertical vector)
189
+ explanation.add_element(
190
+ ContentAST.Equation(f"\\nabla f = {sp.latex(self.gradient_function)}", inline=False)
191
+ )
192
+
193
+ explanation.add_element(
194
+ ContentAST.Paragraph(
195
+ [
196
+ f"Since we want to {objective}, we use the update rule: ",
197
+ ContentAST.Equation(f"x_{{new}} = x_{{old}} {sign} \\alpha \\nabla f", inline=True),
198
+ f". We start at {tuple(self.starting_point)} with learning rate ",
199
+ ContentAST.Equation(f"\\alpha = {self.learning_rate}", inline=True),
200
+ "."
201
+ ]
202
+ )
203
+ )
204
+
205
+ # Add completed table showing all solutions
206
+ explanation.add_element(
207
+ ContentAST.Paragraph(
208
+ [
209
+ "**Solution Table:**"
210
+ ]
211
+ )
212
+ )
213
+
214
+ # Create filled solution table
215
+ solution_headers = [
216
+ "n",
217
+ "location",
218
+ ContentAST.Equation("\\nabla f", inline=True),
219
+ ContentAST.Equation("\\alpha \\cdot \\nabla f", inline=True)
220
+ ]
221
+
222
+ solution_rows = []
223
+ for i, result in enumerate(self.gradient_descent_results):
224
+ step = result['step']
225
+ row = {"n": str(step)}
226
+
227
+ row["location"] = f"{format_vector(result['location'])}"
228
+ row[solution_headers[2]] = f"{format_vector(result['gradient'])}"
229
+ row[solution_headers[3]] = f"{format_vector(result['update'])}"
230
+
231
+ solution_rows.append(row)
232
+
233
+ # Create solution table (non-answer table, just display)
234
+ solution_table = self.create_answer_table(
235
+ headers=solution_headers,
236
+ data_rows=solution_rows,
237
+ answer_columns=[] # No answer columns since this is just for display
238
+ )
239
+
240
+ explanation.add_element(solution_table)
241
+
242
+ # Step-by-step explanation
243
+ for i, result in enumerate(self.gradient_descent_results):
244
+ step = result['step']
245
+
246
+ explanation.add_element(
247
+ ContentAST.Paragraph(
248
+ [
249
+ f"**Step {step}:**"
250
+ ]
251
+ )
252
+ )
253
+
254
+ explanation.add_element(
255
+ ContentAST.Paragraph(
256
+ [
257
+ f"Location: {format_vector(result['location'])}"
258
+ ]
259
+ )
260
+ )
261
+
262
+ explanation.add_element(
263
+ ContentAST.Paragraph(
264
+ [
265
+ f"Gradient: {format_vector(result['gradient'])}"
266
+ ]
267
+ )
268
+ )
269
+
270
+ explanation.add_element(
271
+ ContentAST.Paragraph(
272
+ [
273
+ "Update: ",
274
+ ContentAST.Equation(
275
+ f"\\alpha \\cdot \\nabla f = {self.learning_rate} \\cdot {format_vector(result['gradient'])} = {format_vector(result['update'])}",
276
+ inline=True
277
+ )
278
+ ]
279
+ )
280
+ )
281
+
282
+ if step < len(self.gradient_descent_results):
283
+ # Calculate next location for display
284
+ current_loc = result['location']
285
+ update = result['update']
286
+ next_loc = [current_loc[j] - update[j] for j in range(len(current_loc))]
287
+
288
+ explanation.add_element(
289
+ ContentAST.Paragraph(
290
+ [
291
+ f"Next location: {format_vector(current_loc)} - {format_vector(result['update'])} = {format_vector(next_loc)}"
292
+ ]
293
+ )
294
+ )
295
+
296
+ function_values = [r['function_value'] for r in self.gradient_descent_results]
297
+ explanation.add_element(
298
+ ContentAST.Paragraph(
299
+ [
300
+ f"Function values: {[f'{v:.4f}' for v in function_values]}"
301
+ ]
302
+ )
303
+ )
304
+
305
+ return explanation