QuizGenerator 0.8.1__py3-none-any.whl → 0.10.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 (25) hide show
  1. QuizGenerator/README.md +5 -0
  2. QuizGenerator/canvas/canvas_interface.py +6 -2
  3. QuizGenerator/contentast.py +33 -11
  4. QuizGenerator/generate.py +51 -10
  5. QuizGenerator/logging.yaml +55 -0
  6. QuizGenerator/mixins.py +6 -2
  7. QuizGenerator/premade_questions/basic.py +49 -7
  8. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +92 -82
  9. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +68 -45
  10. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +238 -162
  11. QuizGenerator/premade_questions/cst463/models/attention.py +0 -1
  12. QuizGenerator/premade_questions/cst463/models/cnns.py +0 -1
  13. QuizGenerator/premade_questions/cst463/models/rnns.py +0 -1
  14. QuizGenerator/premade_questions/cst463/models/text.py +0 -1
  15. QuizGenerator/premade_questions/cst463/models/weight_counting.py +20 -1
  16. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +51 -45
  17. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +212 -215
  18. QuizGenerator/qrcode_generator.py +116 -54
  19. QuizGenerator/question.py +168 -23
  20. QuizGenerator/regenerate.py +23 -9
  21. {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/METADATA +34 -22
  22. {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/RECORD +25 -23
  23. {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/WHEEL +0 -0
  24. {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/entry_points.txt +0 -0
  25. {quizgenerator-0.8.1.dist-info → quizgenerator-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -15,48 +15,52 @@ log = logging.getLogger(__name__)
15
15
  class DerivativeQuestion(Question, abc.ABC):
16
16
  """Base class for derivative calculation questions."""
17
17
 
18
+ DEFAULT_NUM_VARIABLES = 2
19
+ DEFAULT_MAX_DEGREE = 2
20
+
18
21
  def __init__(self, *args, **kwargs):
19
22
  kwargs["topic"] = kwargs.get("topic", Question.Topic.ML_OPTIMIZATION)
20
23
  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 _build_context(self, *, rng_seed=None, **kwargs):
25
- if "num_variables" in kwargs:
26
- self.num_variables = kwargs.get("num_variables", self.num_variables)
27
- if "max_degree" in kwargs:
28
- self.max_degree = kwargs.get("max_degree", self.max_degree)
29
- self.rng.seed(rng_seed)
30
- context = dict(kwargs)
31
- context["rng_seed"] = rng_seed
24
+ self.num_variables = kwargs.get("num_variables", self.DEFAULT_NUM_VARIABLES)
25
+ self.max_degree = kwargs.get("max_degree", self.DEFAULT_MAX_DEGREE)
26
+
27
+ @classmethod
28
+ def _build_context(cls, *, rng_seed=None, **kwargs):
29
+ context = super()._build_context(rng_seed=rng_seed, **kwargs)
30
+ context["num_variables"] = kwargs.get("num_variables", cls.DEFAULT_NUM_VARIABLES)
31
+ context["max_degree"] = kwargs.get("max_degree", cls.DEFAULT_MAX_DEGREE)
32
32
  return context
33
33
 
34
- def _generate_evaluation_point(self) -> List[float]:
34
+ @classmethod
35
+ def _generate_evaluation_point(cls, context) -> List[float]:
35
36
  """Generate a random point for gradient evaluation."""
36
- return [self.rng.randint(-3, 3) for _ in range(self.num_variables)]
37
+ return [context.rng.randint(-3, 3) for _ in range(context.num_variables)]
37
38
 
38
- def _format_partial_derivative(self, var_index: int) -> str:
39
+ @staticmethod
40
+ def _format_partial_derivative(var_index: int, num_variables: int) -> str:
39
41
  """Format partial derivative symbol for display."""
40
- if self.num_variables == 1:
42
+ if num_variables == 1:
41
43
  return "\\frac{df}{dx_0}"
42
- else:
43
- return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
44
+ return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
44
45
 
45
- def _create_derivative_answers(self, evaluation_point: List[float]) -> List[ca.Answer]:
46
+ @staticmethod
47
+ def _create_derivative_answers(context) -> List[ca.Answer]:
46
48
  """Create answer fields for each partial derivative at the evaluation point."""
47
49
  answers: List[ca.Answer] = []
48
50
 
49
51
  # Evaluate gradient at the specified point
50
- subs_map = dict(zip(self.variables, evaluation_point))
52
+ subs_map = dict(zip(context.variables, context.evaluation_point))
51
53
 
52
54
  # Format evaluation point for label
53
- eval_point_str = ", ".join([f"x_{i} = {evaluation_point[i]}" for i in range(self.num_variables)])
55
+ eval_point_str = ", ".join(
56
+ [f"x_{i} = {context.evaluation_point[i]}" for i in range(context.num_variables)]
57
+ )
54
58
 
55
59
  # Create answer for each partial derivative
56
- for i in range(self.num_variables):
60
+ for i in range(context.num_variables):
57
61
  answer_key = f"partial_derivative_{i}"
58
62
  # Evaluate the partial derivative and convert to float
59
- partial_value = self.gradient_function[i].subs(subs_map)
63
+ partial_value = context.gradient_function[i].subs(subs_map)
60
64
  try:
61
65
  gradient_value = float(partial_value)
62
66
  except (TypeError, ValueError):
@@ -71,14 +75,15 @@ class DerivativeQuestion(Question, abc.ABC):
71
75
 
72
76
  return answers
73
77
 
74
- def _create_gradient_vector_answer(self) -> ca.Answer:
78
+ @staticmethod
79
+ def _create_gradient_vector_answer(context) -> ca.Answer:
75
80
  """Create a single gradient vector answer for PDF format."""
76
81
  # Format gradient as vector notation
77
- subs_map = dict(zip(self.variables, self.evaluation_point))
82
+ subs_map = dict(zip(context.variables, context.evaluation_point))
78
83
  gradient_values = []
79
84
 
80
- for i in range(self.num_variables):
81
- partial_value = self.gradient_function[i].subs(subs_map)
85
+ for i in range(context.num_variables):
86
+ partial_value = context.gradient_function[i].subs(subs_map)
82
87
  try:
83
88
  gradient_value = float(partial_value)
84
89
  except TypeError:
@@ -89,7 +94,8 @@ class DerivativeQuestion(Question, abc.ABC):
89
94
  vector_str = format_vector(gradient_values)
90
95
  return ca.AnswerTypes.String(vector_str, pdf_only=True)
91
96
 
92
- def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
97
+ @classmethod
98
+ def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
93
99
  """Build question body and collect answers."""
94
100
  body = ca.Section()
95
101
  answers = []
@@ -98,15 +104,17 @@ class DerivativeQuestion(Question, abc.ABC):
98
104
  body.add_element(
99
105
  ca.Paragraph([
100
106
  "Given the function ",
101
- ca.Equation(sp.latex(self.equation), inline=True),
107
+ ca.Equation(sp.latex(context.equation), inline=True),
102
108
  ", calculate the gradient at the point ",
103
- ca.Equation(format_vector(self.evaluation_point), inline=True),
109
+ ca.Equation(format_vector(context.evaluation_point), inline=True),
104
110
  "."
105
111
  ])
106
112
  )
107
113
 
108
114
  # Format evaluation point for LaTeX
109
- eval_point_str = ", ".join([f"x_{i} = {self.evaluation_point[i]}" for i in range(self.num_variables)])
115
+ eval_point_str = ", ".join(
116
+ [f"x_{i} = {context.evaluation_point[i]}" for i in range(context.num_variables)]
117
+ )
110
118
 
111
119
  # For PDF: Use OnlyLatex to show gradient vector format (no answer blank)
112
120
  body.add_element(
@@ -121,14 +129,14 @@ class DerivativeQuestion(Question, abc.ABC):
121
129
  )
122
130
 
123
131
  # For Canvas: Use OnlyHtml to show individual partial derivatives
124
- derivative_answers = self._create_derivative_answers(self.evaluation_point)
132
+ derivative_answers = cls._create_derivative_answers(context)
125
133
  for i, answer in enumerate(derivative_answers):
126
134
  answers.append(answer)
127
135
  body.add_element(
128
136
  ca.OnlyHtml([
129
137
  ca.Paragraph([
130
138
  ca.Equation(
131
- f"\\left. {self._format_partial_derivative(i)} \\right|_{{{eval_point_str}}} = ",
139
+ f"\\left. {cls._format_partial_derivative(i, context.num_variables)} \\right|_{{{eval_point_str}}} = ",
132
140
  inline=True
133
141
  ),
134
142
  answer
@@ -138,7 +146,8 @@ class DerivativeQuestion(Question, abc.ABC):
138
146
 
139
147
  return body, answers
140
148
 
141
- def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
149
+ @classmethod
150
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
142
151
  """Build question explanation."""
143
152
  explanation = ca.Section()
144
153
 
@@ -146,27 +155,27 @@ class DerivativeQuestion(Question, abc.ABC):
146
155
  explanation.add_element(
147
156
  ca.Paragraph([
148
157
  "To find the gradient, we calculate the partial derivatives of ",
149
- ca.Equation(sp.latex(self.equation), inline=True),
158
+ ca.Equation(sp.latex(context.equation), inline=True),
150
159
  ":"
151
160
  ])
152
161
  )
153
162
 
154
163
  # Show analytical gradient
155
164
  explanation.add_element(
156
- ca.Equation(f"\\nabla f = {sp.latex(self.gradient_function)}", inline=False)
165
+ ca.Equation(f"\\nabla f = {sp.latex(context.gradient_function)}", inline=False)
157
166
  )
158
167
 
159
168
  # Show evaluation at the specific point
160
169
  explanation.add_element(
161
170
  ca.Paragraph([
162
- f"Evaluating at the point {format_vector(self.evaluation_point)}:"
171
+ f"Evaluating at the point {format_vector(context.evaluation_point)}:"
163
172
  ])
164
173
  )
165
174
 
166
175
  # Show each partial derivative calculation
167
- subs_map = dict(zip(self.variables, self.evaluation_point))
168
- for i in range(self.num_variables):
169
- partial_expr = self.gradient_function[i]
176
+ subs_map = dict(zip(context.variables, context.evaluation_point))
177
+ for i in range(context.num_variables):
178
+ partial_expr = context.gradient_function[i]
170
179
  partial_value = partial_expr.subs(subs_map)
171
180
 
172
181
  # Use ca.Answer.accepted_strings for clean numerical formatting
@@ -181,7 +190,7 @@ class DerivativeQuestion(Question, abc.ABC):
181
190
  explanation.add_element(
182
191
  ca.Paragraph([
183
192
  ca.Equation(
184
- f"{self._format_partial_derivative(i)} = {sp.latex(partial_expr)} = {clean_value}",
193
+ f"{cls._format_partial_derivative(i, context.num_variables)} = {sp.latex(partial_expr)} = {clean_value}",
185
194
  inline=False
186
195
  )
187
196
  ])
@@ -194,22 +203,21 @@ class DerivativeQuestion(Question, abc.ABC):
194
203
  class DerivativeBasic(DerivativeQuestion):
195
204
  """Basic derivative calculation using polynomial functions."""
196
205
 
197
- def _build_context(self, *, rng_seed=None, **kwargs):
198
- super()._build_context(rng_seed=rng_seed, **kwargs)
206
+ @classmethod
207
+ def _build_context(cls, *, rng_seed=None, **kwargs):
208
+ context = super()._build_context(rng_seed=rng_seed, **kwargs)
199
209
 
200
210
  # Generate a basic polynomial function
201
- self.variables, self.function, self.gradient_function, self.equation = generate_function(
202
- self.rng, self.num_variables, self.max_degree
211
+ context.variables, context.function, context.gradient_function, context.equation = generate_function(
212
+ context.rng, context.num_variables, context.max_degree
203
213
  )
204
214
 
205
215
  # Generate evaluation point
206
- self.evaluation_point = self._generate_evaluation_point()
216
+ context.evaluation_point = cls._generate_evaluation_point(context)
207
217
 
208
218
  # Create answers for evaluation point (used in _build_body)
209
- self._create_derivative_answers(self.evaluation_point)
219
+ cls._create_derivative_answers(context)
210
220
 
211
- context = dict(kwargs)
212
- context["rng_seed"] = rng_seed
213
221
  return context
214
222
 
215
223
 
@@ -217,21 +225,22 @@ class DerivativeBasic(DerivativeQuestion):
217
225
  class DerivativeChain(DerivativeQuestion):
218
226
  """Chain rule derivative calculation using function composition."""
219
227
 
220
- def _build_context(self, *, rng_seed=None, **kwargs):
221
- super()._build_context(rng_seed=rng_seed, **kwargs)
228
+ @classmethod
229
+ def _build_context(cls, *, rng_seed=None, **kwargs):
230
+ context = super()._build_context(rng_seed=rng_seed, **kwargs)
222
231
 
223
232
  # Try to generate a valid function/point combination, regenerating if we hit complex numbers
224
233
  max_attempts = 10
225
234
  for attempt in range(max_attempts):
226
235
  try:
227
236
  # Generate inner and outer functions for composition
228
- self._generate_composed_function()
237
+ cls._generate_composed_function(context)
229
238
 
230
239
  # Generate evaluation point
231
- self.evaluation_point = self._generate_evaluation_point()
240
+ context.evaluation_point = cls._generate_evaluation_point(context)
232
241
 
233
242
  # Create answers - this will raise ValueError if we get complex numbers
234
- self._create_derivative_answers(self.evaluation_point)
243
+ cls._create_derivative_answers(context)
235
244
 
236
245
  # If we get here, everything worked
237
246
  break
@@ -239,30 +248,28 @@ class DerivativeChain(DerivativeQuestion):
239
248
  except ValueError as e:
240
249
  if "Complex number encountered" in str(e) and attempt < max_attempts - 1:
241
250
  # Advance RNG state by making a dummy call
242
- _ = self.rng.random()
251
+ _ = context.rng.random()
243
252
  continue
244
253
  else:
245
254
  # If we've exhausted attempts or different error, re-raise
246
255
  raise
247
-
248
- context = dict(kwargs)
249
- context["rng_seed"] = rng_seed
250
256
  return context
251
257
 
252
- def _generate_composed_function(self) -> None:
258
+ @staticmethod
259
+ def _generate_composed_function(context) -> None:
253
260
  """Generate a composed function f(g(x)) for chain rule practice."""
254
261
  # Create variable symbols
255
- var_names = [f'x_{i}' for i in range(self.num_variables)]
256
- self.variables = sp.symbols(var_names)
262
+ var_names = [f'x_{i}' for i in range(context.num_variables)]
263
+ context.variables = sp.symbols(var_names)
257
264
 
258
265
  # Generate inner function g(x) - simpler polynomial
259
- inner_terms = [m for m in sp.polys.itermonomials(self.variables, max(1, self.max_degree-1)) if m != 1]
266
+ inner_terms = [m for m in sp.polys.itermonomials(context.variables, max(1, context.max_degree-1)) if m != 1]
260
267
  coeff_pool = [*range(-5, 0), *range(1, 6)] # Smaller coefficients for inner function
261
268
 
262
269
  if inner_terms:
263
- inner_poly = sp.Add(*(self.rng.choice(coeff_pool) * t for t in inner_terms))
270
+ inner_poly = sp.Add(*(context.rng.choice(coeff_pool) * t for t in inner_terms))
264
271
  else:
265
- inner_poly = sp.Add(*[self.rng.choice(coeff_pool) * v for v in self.variables])
272
+ inner_poly = sp.Add(*[context.rng.choice(coeff_pool) * v for v in context.variables])
266
273
 
267
274
  # Generate outer function - use polynomials, exp, and ln for reliable evaluation
268
275
  u = sp.Symbol('u') # Intermediate variable
@@ -274,21 +281,24 @@ class DerivativeChain(DerivativeQuestion):
274
281
  sp.log(u + 2) # Add 2 to ensure positive argument for evaluation points
275
282
  ]
276
283
 
277
- outer_func = self.rng.choice(outer_functions)
284
+ outer_func = context.rng.choice(outer_functions)
278
285
 
279
286
  # Compose the functions: f(g(x))
280
- self.inner_function = inner_poly
281
- self.outer_function = outer_func
282
- self.function = outer_func.subs(u, inner_poly)
287
+ context.inner_function = inner_poly
288
+ context.outer_function = outer_func
289
+ context.function = outer_func.subs(u, inner_poly)
283
290
 
284
291
  # Calculate gradient using chain rule
285
- self.gradient_function = sp.Matrix([self.function.diff(v) for v in self.variables])
292
+ context.gradient_function = sp.Matrix([context.function.diff(v) for v in context.variables])
286
293
 
287
294
  # Create equation for display
288
295
  f = sp.Function('f')
289
- self.equation = sp.Eq(f(*self.variables), self.function)
296
+ context.equation = sp.Eq(f(*context.variables), context.function)
297
+
298
+ return context
290
299
 
291
- def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
300
+ @classmethod
301
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
292
302
  """Build question explanation."""
293
303
  explanation = ca.Section()
294
304
 
@@ -296,9 +306,9 @@ class DerivativeChain(DerivativeQuestion):
296
306
  explanation.add_element(
297
307
  ca.Paragraph([
298
308
  "This is a composition of functions requiring the chain rule. The function ",
299
- ca.Equation(sp.latex(self.equation), inline=True),
309
+ ca.Equation(sp.latex(context.equation), inline=True),
300
310
  " can be written as ",
301
- ca.Equation(f"f(g(x)) \\text{{ where }} g(x) = {sp.latex(self.inner_function)}", inline=True),
311
+ ca.Equation(f"f(g(x)) \\text{{ where }} g(x) = {sp.latex(context.inner_function)}", inline=True),
302
312
  "."
303
313
  ])
304
314
  )
@@ -313,7 +323,7 @@ class DerivativeChain(DerivativeQuestion):
313
323
  )
314
324
 
315
325
  # Show chain rule formula for each variable
316
- for i in range(self.num_variables):
326
+ for i in range(context.num_variables):
317
327
  var_name = f"x_{i}"
318
328
  explanation.add_element(
319
329
  ca.Equation(
@@ -329,12 +339,12 @@ class DerivativeChain(DerivativeQuestion):
329
339
  )
330
340
 
331
341
  # Show the specific derivatives step by step
332
- for i in range(self.num_variables):
342
+ for i in range(context.num_variables):
333
343
  var_name = f"x_{i}"
334
344
 
335
345
  # Get outer function derivative with respect to inner function
336
- outer_deriv = self.outer_function.diff(sp.Symbol('u'))
337
- inner_deriv = self.inner_function.diff(self.variables[i])
346
+ outer_deriv = context.outer_function.diff(sp.Symbol('u'))
347
+ inner_deriv = context.inner_function.diff(context.variables[i])
338
348
 
339
349
  explanation.add_element(
340
350
  ca.Paragraph([
@@ -357,20 +367,20 @@ class DerivativeChain(DerivativeQuestion):
357
367
  )
358
368
 
359
369
  explanation.add_element(
360
- ca.Equation(f"\\nabla f = {sp.latex(self.gradient_function)}", inline=False)
370
+ ca.Equation(f"\\nabla f = {sp.latex(context.gradient_function)}", inline=False)
361
371
  )
362
372
 
363
373
  # Show evaluation at the specific point
364
374
  explanation.add_element(
365
375
  ca.Paragraph([
366
- f"Evaluating at the point {format_vector(self.evaluation_point)}:"
376
+ f"Evaluating at the point {format_vector(context.evaluation_point)}:"
367
377
  ])
368
378
  )
369
379
 
370
380
  # Show each partial derivative calculation
371
- subs_map = dict(zip(self.variables, self.evaluation_point))
372
- for i in range(self.num_variables):
373
- partial_expr = self.gradient_function[i]
381
+ subs_map = dict(zip(context.variables, context.evaluation_point))
382
+ for i in range(context.num_variables):
383
+ partial_expr = context.gradient_function[i]
374
384
  partial_value = partial_expr.subs(subs_map)
375
385
 
376
386
  # Use ca.Answer.accepted_strings for clean numerical formatting
@@ -385,7 +395,7 @@ class DerivativeChain(DerivativeQuestion):
385
395
  explanation.add_element(
386
396
  ca.Paragraph([
387
397
  ca.Equation(
388
- f"{self._format_partial_derivative(i)} = {sp.latex(partial_expr)} = {clean_value}",
398
+ f"{cls._format_partial_derivative(i, context.num_variables)} = {sp.latex(partial_expr)} = {clean_value}",
389
399
  inline=False
390
400
  )
391
401
  ])
@@ -4,6 +4,8 @@ import abc
4
4
  import logging
5
5
  import math
6
6
  from typing import List, Tuple, Callable, Union, Any
7
+
8
+ import sympy
7
9
  import sympy as sp
8
10
 
9
11
  import QuizGenerator.contentast as ca
@@ -28,37 +30,52 @@ class GradientDescentQuestion(Question, abc.ABC):
28
30
 
29
31
  @QuestionRegistry.register("GradientDescentWalkthrough")
30
32
  class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, BodyTemplatesMixin):
31
-
33
+ DEFAULT_NUM_STEPS = 4
34
+ DEFAULT_NUM_VARIABLES = 2
35
+ DEFAULT_MAX_DEGREE = 2
36
+ DEFAULT_SINGLE_VARIABLE = False
37
+ DEFAULT_MINIMIZE = True
38
+
32
39
  def __init__(self, *args, **kwargs):
33
40
  super().__init__(*args, **kwargs)
34
- self.num_steps = kwargs.get("num_steps", 4)
35
- self.num_variables = kwargs.get("num_variables", 2)
36
- self.max_degree = kwargs.get("max_degree", 2)
37
- self.single_variable = kwargs.get("single_variable", False)
38
- self.minimize = kwargs.get("minimize", True) # Default to minimization
41
+ self.num_steps = kwargs.get("num_steps", self.DEFAULT_NUM_STEPS)
42
+ self.num_variables = kwargs.get("num_variables", self.DEFAULT_NUM_VARIABLES)
43
+ self.max_degree = kwargs.get("max_degree", self.DEFAULT_MAX_DEGREE)
44
+ self.single_variable = kwargs.get("single_variable", self.DEFAULT_SINGLE_VARIABLE)
45
+ self.minimize = kwargs.get("minimize", self.DEFAULT_MINIMIZE) # Default to minimization
39
46
 
40
47
  if self.single_variable:
41
48
  self.num_variables = 1
42
-
43
- def _perform_gradient_descent(self) -> List[dict]:
49
+
50
+ @classmethod
51
+ def _perform_gradient_descent(
52
+ cls,
53
+ function: sympy.Function,
54
+ gradient_function,
55
+ starting_point,
56
+ num_steps,
57
+ variables,
58
+ learning_rate,
59
+ minimize=True,
60
+ ) -> List[dict]:
44
61
  """
45
62
  Perform gradient descent and return step-by-step results.
46
63
  """
47
64
  results = []
48
65
 
49
- x = list(map(float, self.starting_point)) # current location as floats
66
+ x = list(map(float, starting_point)) # current location as floats
50
67
 
51
- for step in range(self.num_steps):
52
- subs_map = dict(zip(self.variables, x))
68
+ for step in range(num_steps):
69
+ subs_map = dict(zip(variables, x))
53
70
 
54
71
  # gradient as floats
55
- g_syms = self.gradient_function.subs(subs_map)
72
+ g_syms = gradient_function.subs(subs_map)
56
73
  g = [float(val) for val in g_syms]
57
74
 
58
75
  # function value
59
- f_val = float(self.function.subs(subs_map))
76
+ f_val = float(function.subs(subs_map))
60
77
 
61
- update = [self.learning_rate * gi for gi in g]
78
+ update = [learning_rate * gi for gi in g]
62
79
 
63
80
  results.append(
64
81
  {
@@ -70,61 +87,65 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
70
87
  }
71
88
  )
72
89
 
73
- x = [xi - ui for xi, ui in zip(x, update)] if self.minimize else \
90
+ x = [xi - ui for xi, ui in zip(x, update)] if minimize else \
74
91
  [xi + ui for xi, ui in zip(x, update)]
75
92
 
76
93
  return results
77
94
 
78
- def _build_context(self, *, rng_seed=None, **kwargs):
79
- if "num_steps" in kwargs:
80
- self.num_steps = kwargs.get("num_steps", self.num_steps)
81
- if "num_variables" in kwargs:
82
- self.num_variables = kwargs.get("num_variables", self.num_variables)
83
- if "max_degree" in kwargs:
84
- self.max_degree = kwargs.get("max_degree", self.max_degree)
85
- if "single_variable" in kwargs:
86
- self.single_variable = kwargs.get("single_variable", self.single_variable)
87
- if self.single_variable:
88
- self.num_variables = 1
89
- if "minimize" in kwargs:
90
- self.minimize = kwargs.get("minimize", self.minimize)
91
-
92
- self.rng.seed(rng_seed)
95
+ @classmethod
96
+ def _build_context(cls, *, rng_seed=None, **kwargs):
97
+ context = super()._build_context(rng_seed=rng_seed, **kwargs)
98
+ context.num_steps = kwargs.get("num_steps", cls.DEFAULT_NUM_STEPS)
99
+ context.num_variables = kwargs.get("num_variables", cls.DEFAULT_NUM_VARIABLES)
100
+ context.max_degree = kwargs.get("max_degree", cls.DEFAULT_MAX_DEGREE)
101
+ context.single_variable = kwargs.get("single_variable", cls.DEFAULT_SINGLE_VARIABLE)
102
+ if context.single_variable:
103
+ context.num_variables = 1
104
+ context.minimize = kwargs.get("minimize", cls.DEFAULT_MINIMIZE)
93
105
 
94
106
  # Generate function and its properties
95
- self.variables, self.function, self.gradient_function, self.equation = generate_function(self.rng, self.num_variables, self.max_degree)
107
+ context.variables, context.function, context.gradient_function, context.equation = generate_function(
108
+ context.rng, context.num_variables, context.max_degree
109
+ )
96
110
 
97
111
  # Generate learning rate (expanded range)
98
- self.learning_rate = self.rng.choice([0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5])
112
+ context.learning_rate = context.rng.choice([0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5])
99
113
 
100
- self.starting_point = [self.rng.randint(-3, 3) for _ in range(self.num_variables)]
114
+ context.starting_point = [context.rng.randint(-3, 3) for _ in range(context.num_variables)]
101
115
 
102
116
  # Perform gradient descent
103
- self.gradient_descent_results = self._perform_gradient_descent()
117
+ context.gradient_descent_results = cls._perform_gradient_descent(
118
+ context.function,
119
+ context.gradient_function,
120
+ context.starting_point,
121
+ context.num_steps,
122
+ context.variables,
123
+ context.learning_rate,
124
+ minimize=context.minimize,
125
+ )
104
126
 
105
127
  # Build answers for each step
106
- self.step_answers = {}
107
- for i, result in enumerate(self.gradient_descent_results):
128
+ context.step_answers = {}
129
+ for i, result in enumerate(context.gradient_descent_results):
108
130
  step = result['step']
109
131
 
110
132
  # Location answer
111
133
  location_key = f"answer__location_{step}"
112
- self.step_answers[location_key] = ca.AnswerTypes.Vector(list(result['location']), label=f"Location at step {step}")
134
+ context.step_answers[location_key] = ca.AnswerTypes.Vector(list(result['location']), label=f"Location at step {step}")
113
135
 
114
136
  # Gradient answer
115
137
  gradient_key = f"answer__gradient_{step}"
116
- self.step_answers[gradient_key] = ca.AnswerTypes.Vector(list(result['gradient']), label=f"Gradient at step {step}")
138
+ context.step_answers[gradient_key] = ca.AnswerTypes.Vector(list(result['gradient']), label=f"Gradient at step {step}")
117
139
 
118
140
  # Update answer
119
141
  update_key = f"answer__update_{step}"
120
- self.step_answers[update_key] = ca.AnswerTypes.Vector(list(result['update']), label=f"Update at step {step}")
121
-
122
- context = dict(kwargs)
123
- context["rng_seed"] = rng_seed
142
+ context.step_answers[update_key] = ca.AnswerTypes.Vector(list(result['update']), label=f"Update at step {step}")
124
143
  return context
125
144
 
126
- def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
145
+ @classmethod
146
+ def _build_body(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
127
147
  """Build question body and collect answers."""
148
+ self = context
128
149
  body = ca.Section()
129
150
  answers = []
130
151
 
@@ -179,7 +200,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
179
200
  table_rows.append(row)
180
201
 
181
202
  # Create the table using mixin
182
- gradient_table = self.create_answer_table(
203
+ gradient_table = cls.create_answer_table(
183
204
  headers=headers,
184
205
  data_rows=table_rows,
185
206
  answer_columns=["location", headers[2], headers[3]] # Use actual header objects
@@ -189,8 +210,10 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
189
210
 
190
211
  return body, answers
191
212
 
192
- def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
213
+ @classmethod
214
+ def _build_explanation(cls, context) -> Tuple[ca.Section, List[ca.Answer]]:
193
215
  """Build question explanation."""
216
+ self = context
194
217
  explanation = ca.Section()
195
218
 
196
219
  explanation.add_element(