QuizGenerator 0.7.1__py3-none-any.whl → 0.8.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 (30) hide show
  1. QuizGenerator/contentast.py +6 -6
  2. QuizGenerator/generate.py +2 -1
  3. QuizGenerator/mixins.py +14 -100
  4. QuizGenerator/premade_questions/basic.py +24 -29
  5. QuizGenerator/premade_questions/cst334/languages.py +100 -99
  6. QuizGenerator/premade_questions/cst334/math_questions.py +112 -122
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +621 -621
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +137 -163
  9. QuizGenerator/premade_questions/cst334/process.py +312 -322
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +34 -35
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +41 -36
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +48 -41
  13. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +285 -520
  14. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -126
  15. QuizGenerator/premade_questions/cst463/models/attention.py +44 -50
  16. QuizGenerator/premade_questions/cst463/models/cnns.py +43 -47
  17. QuizGenerator/premade_questions/cst463/models/matrices.py +61 -11
  18. QuizGenerator/premade_questions/cst463/models/rnns.py +48 -50
  19. QuizGenerator/premade_questions/cst463/models/text.py +65 -67
  20. QuizGenerator/premade_questions/cst463/models/weight_counting.py +47 -46
  21. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +100 -156
  22. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +93 -141
  23. QuizGenerator/question.py +273 -202
  24. QuizGenerator/quiz.py +8 -5
  25. QuizGenerator/regenerate.py +14 -6
  26. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/METADATA +30 -2
  27. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/RECORD +30 -30
  28. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/WHEEL +0 -0
  29. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/entry_points.txt +0 -0
  30. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,6 +21,16 @@ class DerivativeQuestion(Question, abc.ABC):
21
21
  self.num_variables = kwargs.get("num_variables", 2)
22
22
  self.max_degree = kwargs.get("max_degree", 2)
23
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
32
+ return context
33
+
24
34
  def _generate_evaluation_point(self) -> List[float]:
25
35
  """Generate a random point for gradient evaluation."""
26
36
  return [self.rng.randint(-3, 3) for _ in range(self.num_variables)]
@@ -32,9 +42,9 @@ class DerivativeQuestion(Question, abc.ABC):
32
42
  else:
33
43
  return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
34
44
 
35
- def _create_derivative_answers(self, evaluation_point: List[float]) -> None:
45
+ def _create_derivative_answers(self, evaluation_point: List[float]) -> List[ca.Answer]:
36
46
  """Create answer fields for each partial derivative at the evaluation point."""
37
- self.answers = {}
47
+ answers: List[ca.Answer] = []
38
48
 
39
49
  # Evaluate gradient at the specified point
40
50
  subs_map = dict(zip(self.variables, evaluation_point))
@@ -57,9 +67,11 @@ class DerivativeQuestion(Question, abc.ABC):
57
67
  # Use auto_float for Canvas compatibility with integers and decimals
58
68
  # Label includes the partial derivative notation
59
69
  label = f"∂f/∂x_{i} at ({eval_point_str})"
60
- self.answers[answer_key] = ca.AnswerTypes.Float(gradient_value, label=label)
70
+ answers.append(ca.AnswerTypes.Float(gradient_value, label=label))
71
+
72
+ return answers
61
73
 
62
- def _create_gradient_vector_answer(self) -> None:
74
+ def _create_gradient_vector_answer(self) -> ca.Answer:
63
75
  """Create a single gradient vector answer for PDF format."""
64
76
  # Format gradient as vector notation
65
77
  subs_map = dict(zip(self.variables, self.evaluation_point))
@@ -75,9 +87,9 @@ class DerivativeQuestion(Question, abc.ABC):
75
87
 
76
88
  # Format as vector for display using consistent formatting
77
89
  vector_str = format_vector(gradient_values)
78
- self.answers["gradient_vector"] = ca.AnswerTypes.String(vector_str, pdf_only=True)
90
+ return ca.AnswerTypes.String(vector_str, pdf_only=True)
79
91
 
80
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
92
+ def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
81
93
  """Build question body and collect answers."""
82
94
  body = ca.Section()
83
95
  answers = []
@@ -109,8 +121,8 @@ class DerivativeQuestion(Question, abc.ABC):
109
121
  )
110
122
 
111
123
  # For Canvas: Use OnlyHtml to show individual partial derivatives
112
- for i in range(self.num_variables):
113
- answer = self.answers[f"partial_derivative_{i}"]
124
+ derivative_answers = self._create_derivative_answers(self.evaluation_point)
125
+ for i, answer in enumerate(derivative_answers):
114
126
  answers.append(answer)
115
127
  body.add_element(
116
128
  ca.OnlyHtml([
@@ -126,12 +138,7 @@ class DerivativeQuestion(Question, abc.ABC):
126
138
 
127
139
  return body, answers
128
140
 
129
- def get_body(self, **kwargs) -> ca.Section:
130
- """Build question body (backward compatible interface)."""
131
- body, _ = self._get_body(**kwargs)
132
- return body
133
-
134
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
141
+ def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
135
142
  """Build question explanation."""
136
143
  explanation = ca.Section()
137
144
 
@@ -182,18 +189,13 @@ class DerivativeQuestion(Question, abc.ABC):
182
189
 
183
190
  return explanation, []
184
191
 
185
- def get_explanation(self, **kwargs) -> ca.Section:
186
- """Build question explanation (backward compatible interface)."""
187
- explanation, _ = self._get_explanation(**kwargs)
188
- return explanation
189
-
190
192
 
191
193
  @QuestionRegistry.register("DerivativeBasic")
192
194
  class DerivativeBasic(DerivativeQuestion):
193
195
  """Basic derivative calculation using polynomial functions."""
194
196
 
195
- def refresh(self, rng_seed=None, *args, **kwargs):
196
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
197
+ def _build_context(self, *, rng_seed=None, **kwargs):
198
+ super()._build_context(rng_seed=rng_seed, **kwargs)
197
199
 
198
200
  # Generate a basic polynomial function
199
201
  self.variables, self.function, self.gradient_function, self.equation = generate_function(
@@ -203,19 +205,20 @@ class DerivativeBasic(DerivativeQuestion):
203
205
  # Generate evaluation point
204
206
  self.evaluation_point = self._generate_evaluation_point()
205
207
 
206
- # Create answers
208
+ # Create answers for evaluation point (used in _build_body)
207
209
  self._create_derivative_answers(self.evaluation_point)
208
210
 
209
- # For PDF: Create single gradient vector answer
210
- self._create_gradient_vector_answer()
211
+ context = dict(kwargs)
212
+ context["rng_seed"] = rng_seed
213
+ return context
211
214
 
212
215
 
213
216
  @QuestionRegistry.register("DerivativeChain")
214
217
  class DerivativeChain(DerivativeQuestion):
215
218
  """Chain rule derivative calculation using function composition."""
216
219
 
217
- def refresh(self, rng_seed=None, *args, **kwargs):
218
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
220
+ def _build_context(self, *, rng_seed=None, **kwargs):
221
+ super()._build_context(rng_seed=rng_seed, **kwargs)
219
222
 
220
223
  # Try to generate a valid function/point combination, regenerating if we hit complex numbers
221
224
  max_attempts = 10
@@ -230,9 +233,6 @@ class DerivativeChain(DerivativeQuestion):
230
233
  # Create answers - this will raise ValueError if we get complex numbers
231
234
  self._create_derivative_answers(self.evaluation_point)
232
235
 
233
- # For PDF: Create single gradient vector answer
234
- self._create_gradient_vector_answer()
235
-
236
236
  # If we get here, everything worked
237
237
  break
238
238
 
@@ -245,6 +245,10 @@ class DerivativeChain(DerivativeQuestion):
245
245
  # If we've exhausted attempts or different error, re-raise
246
246
  raise
247
247
 
248
+ context = dict(kwargs)
249
+ context["rng_seed"] = rng_seed
250
+ return context
251
+
248
252
  def _generate_composed_function(self) -> None:
249
253
  """Generate a composed function f(g(x)) for chain rule practice."""
250
254
  # Create variable symbols
@@ -284,7 +288,7 @@ class DerivativeChain(DerivativeQuestion):
284
288
  f = sp.Function('f')
285
289
  self.equation = sp.Eq(f(*self.variables), self.function)
286
290
 
287
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
291
+ def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
288
292
  """Build question explanation."""
289
293
  explanation = ca.Section()
290
294
 
@@ -388,8 +392,3 @@ class DerivativeChain(DerivativeQuestion):
388
392
  )
389
393
 
390
394
  return explanation, []
391
-
392
- def get_explanation(self, **kwargs) -> ca.Section:
393
- """Build question explanation (backward compatible interface)."""
394
- explanation, _ = self._get_explanation(**kwargs)
395
- return explanation
@@ -17,7 +17,7 @@ log = logging.getLogger(__name__)
17
17
 
18
18
  # Note: This file does not use ca.Answer wrappers - it uses TableQuestionMixin
19
19
  # which handles answer display through create_answer_table(). The answers are created
20
- # with labels embedded at creation time in refresh().
20
+ # with labels embedded at creation time in _build_context().
21
21
 
22
22
 
23
23
  class GradientDescentQuestion(Question, abc.ABC):
@@ -75,40 +75,55 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
75
75
 
76
76
  return results
77
77
 
78
- def refresh(self, rng_seed=None, *args, **kwargs):
79
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
80
-
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)
93
+
81
94
  # Generate function and its properties
82
95
  self.variables, self.function, self.gradient_function, self.equation = generate_function(self.rng, self.num_variables, self.max_degree)
83
-
96
+
84
97
  # Generate learning rate (expanded range)
85
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])
86
-
99
+
87
100
  self.starting_point = [self.rng.randint(-3, 3) for _ in range(self.num_variables)]
88
-
101
+
89
102
  # Perform gradient descent
90
103
  self.gradient_descent_results = self._perform_gradient_descent()
91
-
92
- # Set up answers
93
- self.answers = {}
94
104
 
95
- # Answers for each step
105
+ # Build answers for each step
106
+ self.step_answers = {}
96
107
  for i, result in enumerate(self.gradient_descent_results):
97
108
  step = result['step']
98
109
 
99
110
  # Location answer
100
111
  location_key = f"answer__location_{step}"
101
- self.answers[location_key] = ca.AnswerTypes.Vector(list(result['location']), label=f"Location at step {step}")
112
+ self.step_answers[location_key] = ca.AnswerTypes.Vector(list(result['location']), label=f"Location at step {step}")
102
113
 
103
114
  # Gradient answer
104
115
  gradient_key = f"answer__gradient_{step}"
105
- self.answers[gradient_key] = ca.AnswerTypes.Vector(list(result['gradient']), label=f"Gradient at step {step}")
116
+ self.step_answers[gradient_key] = ca.AnswerTypes.Vector(list(result['gradient']), label=f"Gradient at step {step}")
106
117
 
107
118
  # Update answer
108
119
  update_key = f"answer__update_{step}"
109
- self.answers[update_key] = ca.AnswerTypes.Vector(list(result['update']), label=f"Update at step {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
124
+ return context
110
125
 
111
- def _get_body(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
126
+ def _build_body(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
112
127
  """Build question body and collect answers."""
113
128
  body = ca.Section()
114
129
  answers = []
@@ -147,20 +162,20 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
147
162
 
148
163
  # Fill in starting location for first row with default formatting
149
164
  row["location"] = f"{format_vector(self.starting_point)}"
150
- row[headers[2]] = f"answer__gradient_{step}" # gradient column
151
- row[headers[3]] = f"answer__update_{step}" # update column
165
+ row[headers[2]] = self.step_answers[f"answer__gradient_{step}"] # gradient column
166
+ row[headers[3]] = self.step_answers[f"answer__update_{step}"] # update column
152
167
  # Collect answers for this step (no location answer for step 1)
153
- answers.append(self.answers[f"answer__gradient_{step}"])
154
- answers.append(self.answers[f"answer__update_{step}"])
168
+ answers.append(self.step_answers[f"answer__gradient_{step}"])
169
+ answers.append(self.step_answers[f"answer__update_{step}"])
155
170
  else:
156
171
  # Subsequent rows - all answer fields
157
- row["location"] = f"answer__location_{step}"
158
- row[headers[2]] = f"answer__gradient_{step}" # gradient column
159
- row[headers[3]] = f"answer__update_{step}" # update column
172
+ row["location"] = self.step_answers[f"answer__location_{step}"]
173
+ row[headers[2]] = self.step_answers[f"answer__gradient_{step}"]
174
+ row[headers[3]] = self.step_answers[f"answer__update_{step}"]
160
175
  # Collect all answers for this step
161
- answers.append(self.answers[f"answer__location_{step}"])
162
- answers.append(self.answers[f"answer__gradient_{step}"])
163
- answers.append(self.answers[f"answer__update_{step}"])
176
+ answers.append(self.step_answers[f"answer__location_{step}"])
177
+ answers.append(self.step_answers[f"answer__gradient_{step}"])
178
+ answers.append(self.step_answers[f"answer__update_{step}"])
164
179
  table_rows.append(row)
165
180
 
166
181
  # Create the table using mixin
@@ -174,12 +189,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
174
189
 
175
190
  return body, answers
176
191
 
177
- def get_body(self, **kwargs) -> ca.Section:
178
- """Build question body (backward compatible interface)."""
179
- body, _ = self._get_body(**kwargs)
180
- return body
181
-
182
- def _get_explanation(self, **kwargs) -> Tuple[ca.Section, List[ca.Answer]]:
192
+ def _build_explanation(self, context) -> Tuple[ca.Section, List[ca.Answer]]:
183
193
  """Build question explanation."""
184
194
  explanation = ca.Section()
185
195
 
@@ -323,8 +333,3 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
323
333
  )
324
334
 
325
335
  return explanation, []
326
-
327
- def get_explanation(self, **kwargs) -> ca.Section:
328
- """Build question explanation (backward compatible interface)."""
329
- explanation, _ = self._get_explanation(**kwargs)
330
- return explanation
@@ -13,7 +13,7 @@ from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
13
13
  log = logging.getLogger(__name__)
14
14
 
15
15
 
16
- # Note: This file migrates to the _get_body()/_get_explanation() pattern
16
+ # Note: This file migrates to the _build_body()/_build_explanation() pattern
17
17
 
18
18
 
19
19
  class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
@@ -35,12 +35,24 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
35
35
  self.individual_losses = []
36
36
  self.overall_loss = 0.0
37
37
 
38
- def refresh(self, rng_seed=None, *args, **kwargs):
38
+ def _build_context(self, *, rng_seed=None, **kwargs):
39
39
  """Generate new random data and calculate losses."""
40
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
40
+ # Update configurable parameters if provided
41
+ if "num_samples" in kwargs:
42
+ self.num_samples = max(3, min(10, kwargs.get("num_samples", self.num_samples)))
43
+ if "num_input_features" in kwargs:
44
+ self.num_input_features = max(1, min(5, kwargs.get("num_input_features", self.num_input_features)))
45
+ if "vector_inputs" in kwargs:
46
+ self.vector_inputs = kwargs.get("vector_inputs", self.vector_inputs)
47
+
48
+ # Seed RNG and generate data
49
+ self.rng.seed(rng_seed)
41
50
  self._generate_data()
42
51
  self._calculate_losses()
43
- self._create_answers()
52
+
53
+ context = dict(kwargs)
54
+ context["rng_seed"] = rng_seed
55
+ return context
44
56
 
45
57
  @abc.abstractmethod
46
58
  def _generate_data(self):
@@ -67,18 +79,15 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
67
79
  """Return the short name of the loss function (used in question body)."""
68
80
  pass
69
81
 
70
- def _create_answers(self):
71
- """Create answer objects for individual losses and overall loss."""
72
- self.answers = {}
73
-
74
- # Individual loss answers
75
- for i in range(self.num_samples):
76
- self.answers[f"loss_{i}"] = ca.AnswerTypes.Float(self.individual_losses[i], label=f"Sample {i + 1} loss")
77
-
78
- # Overall loss answer
79
- self.answers["overall_loss"] = ca.AnswerTypes.Float(self.overall_loss, label="Overall loss")
82
+ def _build_loss_answers(self) -> Tuple[List[ca.Answer], ca.Answer]:
83
+ answers = [
84
+ ca.AnswerTypes.Float(self.individual_losses[i], label=f"Sample {i + 1} loss")
85
+ for i in range(self.num_samples)
86
+ ]
87
+ overall = ca.AnswerTypes.Float(self.overall_loss, label="Overall loss")
88
+ return answers, overall
80
89
 
81
- def _get_body(self, **kwargs) -> Tuple[ca.Element, List[ca.Answer]]:
90
+ def _build_body(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
82
91
  """Build question body and collect answers."""
83
92
  body = ca.Section()
84
93
  answers = []
@@ -90,32 +99,25 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
90
99
  ]))
91
100
 
92
101
  # Data table (contains individual loss answers)
93
- body.add_element(self._create_data_table())
94
-
95
- # Collect individual loss answers
96
- for i in range(self.num_samples):
97
- answers.append(self.answers[f"loss_{i}"])
102
+ loss_answers, overall_answer = self._build_loss_answers()
103
+ body.add_element(self._create_data_table(loss_answers))
104
+ answers.extend(loss_answers)
98
105
 
99
106
  # Overall loss question
100
107
  body.add_element(ca.Paragraph([
101
108
  f"Overall {self._get_loss_function_short_name()}: "
102
109
  ]))
103
- answers.append(self.answers["overall_loss"])
104
- body.add_element(self.answers["overall_loss"])
110
+ answers.append(overall_answer)
111
+ body.add_element(overall_answer)
105
112
 
106
113
  return body, answers
107
114
 
108
- def get_body(self, **kwargs) -> ca.Element:
109
- """Build question body (backward compatible interface)."""
110
- body, _ = self._get_body(**kwargs)
111
- return body
112
-
113
115
  @abc.abstractmethod
114
- def _create_data_table(self) -> ca.Element:
116
+ def _create_data_table(self, loss_answers: List[ca.Answer]) -> ca.Element:
115
117
  """Create the data table with answer fields."""
116
118
  pass
117
119
 
118
- def _get_explanation(self, **kwargs) -> Tuple[ca.Element, List[ca.Answer]]:
120
+ def _build_explanation(self, context) -> Tuple[ca.Element, List[ca.Answer]]:
119
121
  """Build question explanation."""
120
122
  explanation = ca.Section()
121
123
 
@@ -137,11 +139,6 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
137
139
 
138
140
  return explanation, []
139
141
 
140
- def get_explanation(self, **kwargs) -> ca.Element:
141
- """Build question explanation (backward compatible interface)."""
142
- explanation, _ = self._get_explanation(**kwargs)
143
- return explanation
144
-
145
142
  @abc.abstractmethod
146
143
  def _create_calculation_steps(self) -> ca.Element:
147
144
  """Create step-by-step calculation explanations."""
@@ -167,6 +164,11 @@ class LossQuestion_Linear(LossQuestion):
167
164
  self.num_output_vars = max(1, min(5, self.num_output_vars)) # Constrain to 1-5 range
168
165
  super().__init__(*args, **kwargs)
169
166
 
167
+ def _build_context(self, *, rng_seed=None, **kwargs):
168
+ if "num_output_vars" in kwargs:
169
+ self.num_output_vars = max(1, min(5, kwargs.get("num_output_vars", self.num_output_vars)))
170
+ return super()._build_context(rng_seed=rng_seed, **kwargs)
171
+
170
172
  def _generate_data(self):
171
173
  """Generate regression data with continuous target values."""
172
174
  self.data = []
@@ -225,7 +227,7 @@ class LossQuestion_Linear(LossQuestion):
225
227
  else:
226
228
  return r"L(\mathbf{y}, \mathbf{p}) = \sum_{i=1}^{k} (y_i - p_i)^2"
227
229
 
228
- def _create_data_table(self) -> ca.Element:
230
+ def _create_data_table(self, loss_answers: List[ca.Answer]) -> ca.Element:
229
231
  """Create table with input features, true values, predictions, and loss fields."""
230
232
  headers = ["x"]
231
233
 
@@ -262,7 +264,7 @@ class LossQuestion_Linear(LossQuestion):
262
264
  row[f"p_{j}"] = f"{sample['predictions'][j]:.2f}"
263
265
 
264
266
  # Loss answer field
265
- row["loss"] = self.answers[f"loss_{i}"]
267
+ row["loss"] = loss_answers[i]
266
268
 
267
269
  rows.append(row)
268
270
 
@@ -416,7 +418,7 @@ class LossQuestion_Logistic(LossQuestion):
416
418
  def _get_loss_function_formula(self) -> str:
417
419
  return r"L(y, p) = -[y \ln(p) + (1-y) \ln(1-p)]"
418
420
 
419
- def _create_data_table(self) -> ca.Element:
421
+ def _create_data_table(self, loss_answers: List[ca.Answer]) -> ca.Element:
420
422
  """Create table with features, true labels, predicted probabilities, and loss fields."""
421
423
  headers = ["x", "y", "p", "loss"]
422
424
 
@@ -435,7 +437,7 @@ class LossQuestion_Logistic(LossQuestion):
435
437
  row["p"] = f"{sample['predictions']:.3f}"
436
438
 
437
439
  # Loss answer field
438
- row["loss"] = self.answers[f"loss_{i}"]
440
+ row["loss"] = loss_answers[i]
439
441
 
440
442
  rows.append(row)
441
443
 
@@ -511,6 +513,11 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
511
513
  self.num_classes = max(3, min(5, self.num_classes)) # Constrain to 3-5 classes
512
514
  super().__init__(*args, **kwargs)
513
515
 
516
+ def _build_context(self, *, rng_seed=None, **kwargs):
517
+ if "num_classes" in kwargs:
518
+ self.num_classes = max(3, min(5, kwargs.get("num_classes", self.num_classes)))
519
+ return super()._build_context(rng_seed=rng_seed, **kwargs)
520
+
514
521
  def _generate_data(self):
515
522
  """Generate multi-class classification data."""
516
523
  self.data = []
@@ -560,7 +567,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
560
567
  def _get_loss_function_formula(self) -> str:
561
568
  return r"L(\mathbf{y}, \mathbf{p}) = -\sum_{i=1}^{K} y_i \ln(p_i)"
562
569
 
563
- def _create_data_table(self) -> ca.Element:
570
+ def _create_data_table(self, loss_answers: List[ca.Answer]) -> ca.Element:
564
571
  """Create table with features, true class vectors, predicted probabilities, and loss fields."""
565
572
  headers = ["x", "y", "p", "loss"]
566
573
 
@@ -581,7 +588,7 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
581
588
  row["p"] = p_vector
582
589
 
583
590
  # Loss answer field
584
- row["loss"] = self.answers[f"loss_{i}"]
591
+ row["loss"] = loss_answers[i]
585
592
 
586
593
  rows.append(row)
587
594
 
@@ -666,4 +673,4 @@ class LossQuestion_MulticlassLogistic(LossQuestion):
666
673
 
667
674
  explanation.add_element(ca.Equation(calculation, inline=False))
668
675
 
669
- return explanation
676
+ return explanation