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.
- QuizGenerator/contentast.py +809 -117
- QuizGenerator/generate.py +219 -11
- QuizGenerator/misc.py +0 -556
- QuizGenerator/mixins.py +50 -29
- QuizGenerator/premade_questions/basic.py +3 -3
- QuizGenerator/premade_questions/cst334/languages.py +183 -175
- QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
- QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
- QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
- QuizGenerator/premade_questions/cst334/process.py +558 -79
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
- QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
- QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
- QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
- QuizGenerator/premade_questions/cst463/models/text.py +29 -15
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
- QuizGenerator/question.py +114 -20
- QuizGenerator/quiz.py +81 -24
- QuizGenerator/regenerate.py +98 -29
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
- QuizGenerator/README.md +0 -5
- QuizGenerator/logging.yaml +0 -55
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,11 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
4
|
import logging
|
|
5
|
-
from typing import List
|
|
5
|
+
from typing import List, Tuple
|
|
6
6
|
import sympy as sp
|
|
7
7
|
|
|
8
|
-
from QuizGenerator.contentast import ContentAST
|
|
9
|
-
from QuizGenerator.question import Question,
|
|
8
|
+
from QuizGenerator.contentast import ContentAST, AnswerTypes
|
|
9
|
+
from QuizGenerator.question import Question, QuestionRegistry
|
|
10
10
|
from .misc import generate_function, format_vector
|
|
11
11
|
|
|
12
12
|
log = logging.getLogger(__name__)
|
|
@@ -39,6 +39,9 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
39
39
|
# Evaluate gradient at the specified point
|
|
40
40
|
subs_map = dict(zip(self.variables, evaluation_point))
|
|
41
41
|
|
|
42
|
+
# Format evaluation point for label
|
|
43
|
+
eval_point_str = ", ".join([f"x_{i} = {evaluation_point[i]}" for i in range(self.num_variables)])
|
|
44
|
+
|
|
42
45
|
# Create answer for each partial derivative
|
|
43
46
|
for i in range(self.num_variables):
|
|
44
47
|
answer_key = f"partial_derivative_{i}"
|
|
@@ -52,7 +55,9 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
52
55
|
raise ValueError("Complex number encountered - need to regenerate")
|
|
53
56
|
|
|
54
57
|
# Use auto_float for Canvas compatibility with integers and decimals
|
|
55
|
-
|
|
58
|
+
# Label includes the partial derivative notation
|
|
59
|
+
label = f"∂f/∂x_{i} at ({eval_point_str})"
|
|
60
|
+
self.answers[answer_key] = AnswerTypes.Float(gradient_value, label=label)
|
|
56
61
|
|
|
57
62
|
def _create_gradient_vector_answer(self) -> None:
|
|
58
63
|
"""Create a single gradient vector answer for PDF format."""
|
|
@@ -70,10 +75,12 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
70
75
|
|
|
71
76
|
# Format as vector for display using consistent formatting
|
|
72
77
|
vector_str = format_vector(gradient_values)
|
|
73
|
-
self.answers["gradient_vector"] =
|
|
78
|
+
self.answers["gradient_vector"] = AnswerTypes.String(vector_str, pdf_only=True)
|
|
74
79
|
|
|
75
|
-
def
|
|
80
|
+
def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
|
|
81
|
+
"""Build question body and collect answers."""
|
|
76
82
|
body = ContentAST.Section()
|
|
83
|
+
answers = []
|
|
77
84
|
|
|
78
85
|
# Display the function
|
|
79
86
|
body.add_element(
|
|
@@ -103,6 +110,8 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
103
110
|
|
|
104
111
|
# For Canvas: Use OnlyHtml to show individual partial derivatives
|
|
105
112
|
for i in range(self.num_variables):
|
|
113
|
+
answer = self.answers[f"partial_derivative_{i}"]
|
|
114
|
+
answers.append(answer)
|
|
106
115
|
body.add_element(
|
|
107
116
|
ContentAST.OnlyHtml([
|
|
108
117
|
ContentAST.Paragraph([
|
|
@@ -110,14 +119,20 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
110
119
|
f"\\left. {self._format_partial_derivative(i)} \\right|_{{{eval_point_str}}} = ",
|
|
111
120
|
inline=True
|
|
112
121
|
),
|
|
113
|
-
|
|
122
|
+
answer
|
|
114
123
|
])
|
|
115
124
|
])
|
|
116
125
|
)
|
|
117
126
|
|
|
127
|
+
return body, answers
|
|
128
|
+
|
|
129
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
130
|
+
"""Build question body (backward compatible interface)."""
|
|
131
|
+
body, _ = self._get_body(**kwargs)
|
|
118
132
|
return body
|
|
119
133
|
|
|
120
|
-
def
|
|
134
|
+
def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
|
|
135
|
+
"""Build question explanation."""
|
|
121
136
|
explanation = ContentAST.Section()
|
|
122
137
|
|
|
123
138
|
# Show the function and its gradient
|
|
@@ -147,14 +162,14 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
147
162
|
partial_expr = self.gradient_function[i]
|
|
148
163
|
partial_value = partial_expr.subs(subs_map)
|
|
149
164
|
|
|
150
|
-
# Use Answer.accepted_strings for clean numerical formatting
|
|
165
|
+
# Use ContentAST.Answer.accepted_strings for clean numerical formatting
|
|
151
166
|
try:
|
|
152
167
|
numerical_value = float(partial_value)
|
|
153
168
|
except (TypeError, ValueError):
|
|
154
169
|
numerical_value = float(partial_value.evalf())
|
|
155
170
|
|
|
156
171
|
# Get clean string representation
|
|
157
|
-
clean_value = sorted(Answer.accepted_strings(numerical_value), key=lambda s: len(s))[0]
|
|
172
|
+
clean_value = sorted(ContentAST.Answer.accepted_strings(numerical_value), key=lambda s: len(s))[0]
|
|
158
173
|
|
|
159
174
|
explanation.add_element(
|
|
160
175
|
ContentAST.Paragraph([
|
|
@@ -165,6 +180,11 @@ class DerivativeQuestion(Question, abc.ABC):
|
|
|
165
180
|
])
|
|
166
181
|
)
|
|
167
182
|
|
|
183
|
+
return explanation, []
|
|
184
|
+
|
|
185
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
186
|
+
"""Build question explanation (backward compatible interface)."""
|
|
187
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
168
188
|
return explanation
|
|
169
189
|
|
|
170
190
|
|
|
@@ -264,7 +284,8 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
264
284
|
f = sp.Function('f')
|
|
265
285
|
self.equation = sp.Eq(f(*self.variables), self.function)
|
|
266
286
|
|
|
267
|
-
def
|
|
287
|
+
def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
|
|
288
|
+
"""Build question explanation."""
|
|
268
289
|
explanation = ContentAST.Section()
|
|
269
290
|
|
|
270
291
|
# Show the composed function structure
|
|
@@ -348,14 +369,14 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
348
369
|
partial_expr = self.gradient_function[i]
|
|
349
370
|
partial_value = partial_expr.subs(subs_map)
|
|
350
371
|
|
|
351
|
-
# Use Answer.accepted_strings for clean numerical formatting
|
|
372
|
+
# Use ContentAST.Answer.accepted_strings for clean numerical formatting
|
|
352
373
|
try:
|
|
353
374
|
numerical_value = float(partial_value)
|
|
354
375
|
except (TypeError, ValueError):
|
|
355
376
|
numerical_value = float(partial_value.evalf())
|
|
356
377
|
|
|
357
378
|
# Get clean string representation
|
|
358
|
-
clean_value = sorted(Answer.accepted_strings(numerical_value), key=lambda s: len(s))[0]
|
|
379
|
+
clean_value = sorted(ContentAST.Answer.accepted_strings(numerical_value), key=lambda s: len(s))[0]
|
|
359
380
|
|
|
360
381
|
explanation.add_element(
|
|
361
382
|
ContentAST.Paragraph([
|
|
@@ -366,4 +387,9 @@ class DerivativeChain(DerivativeQuestion):
|
|
|
366
387
|
])
|
|
367
388
|
)
|
|
368
389
|
|
|
390
|
+
return explanation, []
|
|
391
|
+
|
|
392
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
393
|
+
"""Build question explanation (backward compatible interface)."""
|
|
394
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
369
395
|
return explanation
|
|
@@ -6,8 +6,8 @@ import math
|
|
|
6
6
|
from typing import List, Tuple, Callable, Union, Any
|
|
7
7
|
import sympy as sp
|
|
8
8
|
|
|
9
|
-
from QuizGenerator.contentast import ContentAST
|
|
10
|
-
from QuizGenerator.question import Question,
|
|
9
|
+
from QuizGenerator.contentast import ContentAST, AnswerTypes
|
|
10
|
+
from QuizGenerator.question import Question, QuestionRegistry
|
|
11
11
|
from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
|
|
12
12
|
|
|
13
13
|
from .misc import generate_function, format_vector
|
|
@@ -15,6 +15,11 @@ from .misc import generate_function, format_vector
|
|
|
15
15
|
log = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
# Note: This file does not use ContentAST.Answer wrappers - it uses TableQuestionMixin
|
|
19
|
+
# which handles answer display through create_answer_table(). The answers are created
|
|
20
|
+
# with labels embedded at creation time in refresh().
|
|
21
|
+
|
|
22
|
+
|
|
18
23
|
class GradientDescentQuestion(Question, abc.ABC):
|
|
19
24
|
def __init__(self, *args, **kwargs):
|
|
20
25
|
kwargs["topic"] = kwargs.get("topic", Question.Topic.ML_OPTIMIZATION)
|
|
@@ -86,30 +91,32 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
86
91
|
|
|
87
92
|
# Set up answers
|
|
88
93
|
self.answers = {}
|
|
89
|
-
|
|
94
|
+
|
|
90
95
|
# Answers for each step
|
|
91
96
|
for i, result in enumerate(self.gradient_descent_results):
|
|
92
97
|
step = result['step']
|
|
93
|
-
|
|
98
|
+
|
|
94
99
|
# Location answer
|
|
95
100
|
location_key = f"answer__location_{step}"
|
|
96
|
-
self.answers[location_key] =
|
|
97
|
-
|
|
101
|
+
self.answers[location_key] = AnswerTypes.Vector(list(result['location']), label=f"Location at step {step}")
|
|
102
|
+
|
|
98
103
|
# Gradient answer
|
|
99
104
|
gradient_key = f"answer__gradient_{step}"
|
|
100
|
-
self.answers[gradient_key] =
|
|
101
|
-
|
|
105
|
+
self.answers[gradient_key] = AnswerTypes.Vector(list(result['gradient']), label=f"Gradient at step {step}")
|
|
106
|
+
|
|
102
107
|
# Update answer
|
|
103
108
|
update_key = f"answer__update_{step}"
|
|
104
|
-
self.answers[update_key] =
|
|
109
|
+
self.answers[update_key] = AnswerTypes.Vector(list(result['update']), label=f"Update at step {step}")
|
|
105
110
|
|
|
106
|
-
def
|
|
111
|
+
def _get_body(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
|
|
112
|
+
"""Build question body and collect answers."""
|
|
107
113
|
body = ContentAST.Section()
|
|
108
|
-
|
|
114
|
+
answers = []
|
|
115
|
+
|
|
109
116
|
# Introduction
|
|
110
117
|
objective = "minimize" if self.minimize else "maximize"
|
|
111
118
|
sign = "-" if self.minimize else "+"
|
|
112
|
-
|
|
119
|
+
|
|
113
120
|
body.add_element(
|
|
114
121
|
ContentAST.Paragraph(
|
|
115
122
|
[
|
|
@@ -122,7 +129,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
122
129
|
]
|
|
123
130
|
)
|
|
124
131
|
)
|
|
125
|
-
|
|
132
|
+
|
|
126
133
|
# Create table data - use ContentAST.Equation for proper LaTeX rendering in headers
|
|
127
134
|
headers = [
|
|
128
135
|
"n",
|
|
@@ -131,36 +138,49 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
131
138
|
ContentAST.Equation("\\alpha \\cdot \\nabla f", inline=True)
|
|
132
139
|
]
|
|
133
140
|
table_rows = []
|
|
134
|
-
|
|
141
|
+
|
|
135
142
|
for i in range(self.num_steps):
|
|
136
143
|
step = i + 1
|
|
137
144
|
row = {"n": str(step)}
|
|
138
|
-
|
|
145
|
+
|
|
139
146
|
if step == 1:
|
|
140
|
-
|
|
147
|
+
|
|
141
148
|
# Fill in starting location for first row with default formatting
|
|
142
149
|
row["location"] = f"{format_vector(self.starting_point)}"
|
|
143
150
|
row[headers[2]] = f"answer__gradient_{step}" # gradient column
|
|
144
151
|
row[headers[3]] = f"answer__update_{step}" # update column
|
|
152
|
+
# 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}"])
|
|
145
155
|
else:
|
|
146
156
|
# Subsequent rows - all answer fields
|
|
147
157
|
row["location"] = f"answer__location_{step}"
|
|
148
158
|
row[headers[2]] = f"answer__gradient_{step}" # gradient column
|
|
149
159
|
row[headers[3]] = f"answer__update_{step}" # update column
|
|
160
|
+
# 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}"])
|
|
150
164
|
table_rows.append(row)
|
|
151
|
-
|
|
165
|
+
|
|
152
166
|
# Create the table using mixin
|
|
153
167
|
gradient_table = self.create_answer_table(
|
|
154
168
|
headers=headers,
|
|
155
169
|
data_rows=table_rows,
|
|
156
170
|
answer_columns=["location", headers[2], headers[3]] # Use actual header objects
|
|
157
171
|
)
|
|
158
|
-
|
|
172
|
+
|
|
159
173
|
body.add_element(gradient_table)
|
|
160
|
-
|
|
174
|
+
|
|
175
|
+
return body, answers
|
|
176
|
+
|
|
177
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
178
|
+
"""Build question body (backward compatible interface)."""
|
|
179
|
+
body, _ = self._get_body(**kwargs)
|
|
161
180
|
return body
|
|
162
181
|
|
|
163
|
-
def
|
|
182
|
+
def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Section, List[ContentAST.Answer]]:
|
|
183
|
+
"""Build question explanation."""
|
|
164
184
|
explanation = ContentAST.Section()
|
|
165
185
|
|
|
166
186
|
explanation.add_element(
|
|
@@ -201,7 +221,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
201
221
|
]
|
|
202
222
|
)
|
|
203
223
|
)
|
|
204
|
-
|
|
224
|
+
|
|
205
225
|
# Add completed table showing all solutions
|
|
206
226
|
explanation.add_element(
|
|
207
227
|
ContentAST.Paragraph(
|
|
@@ -210,7 +230,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
210
230
|
]
|
|
211
231
|
)
|
|
212
232
|
)
|
|
213
|
-
|
|
233
|
+
|
|
214
234
|
# Create filled solution table
|
|
215
235
|
solution_headers = [
|
|
216
236
|
"n",
|
|
@@ -218,31 +238,31 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
218
238
|
ContentAST.Equation("\\nabla f", inline=True),
|
|
219
239
|
ContentAST.Equation("\\alpha \\cdot \\nabla f", inline=True)
|
|
220
240
|
]
|
|
221
|
-
|
|
241
|
+
|
|
222
242
|
solution_rows = []
|
|
223
243
|
for i, result in enumerate(self.gradient_descent_results):
|
|
224
244
|
step = result['step']
|
|
225
245
|
row = {"n": str(step)}
|
|
226
|
-
|
|
246
|
+
|
|
227
247
|
row["location"] = f"{format_vector(result['location'])}"
|
|
228
248
|
row[solution_headers[2]] = f"{format_vector(result['gradient'])}"
|
|
229
249
|
row[solution_headers[3]] = f"{format_vector(result['update'])}"
|
|
230
|
-
|
|
250
|
+
|
|
231
251
|
solution_rows.append(row)
|
|
232
|
-
|
|
252
|
+
|
|
233
253
|
# Create solution table (non-answer table, just display)
|
|
234
254
|
solution_table = self.create_answer_table(
|
|
235
255
|
headers=solution_headers,
|
|
236
256
|
data_rows=solution_rows,
|
|
237
257
|
answer_columns=[] # No answer columns since this is just for display
|
|
238
258
|
)
|
|
239
|
-
|
|
259
|
+
|
|
240
260
|
explanation.add_element(solution_table)
|
|
241
|
-
|
|
261
|
+
|
|
242
262
|
# Step-by-step explanation
|
|
243
263
|
for i, result in enumerate(self.gradient_descent_results):
|
|
244
264
|
step = result['step']
|
|
245
|
-
|
|
265
|
+
|
|
246
266
|
explanation.add_element(
|
|
247
267
|
ContentAST.Paragraph(
|
|
248
268
|
[
|
|
@@ -250,7 +270,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
250
270
|
]
|
|
251
271
|
)
|
|
252
272
|
)
|
|
253
|
-
|
|
273
|
+
|
|
254
274
|
explanation.add_element(
|
|
255
275
|
ContentAST.Paragraph(
|
|
256
276
|
[
|
|
@@ -258,7 +278,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
258
278
|
]
|
|
259
279
|
)
|
|
260
280
|
)
|
|
261
|
-
|
|
281
|
+
|
|
262
282
|
explanation.add_element(
|
|
263
283
|
ContentAST.Paragraph(
|
|
264
284
|
[
|
|
@@ -266,7 +286,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
266
286
|
]
|
|
267
287
|
)
|
|
268
288
|
)
|
|
269
|
-
|
|
289
|
+
|
|
270
290
|
explanation.add_element(
|
|
271
291
|
ContentAST.Paragraph(
|
|
272
292
|
[
|
|
@@ -278,13 +298,13 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
278
298
|
]
|
|
279
299
|
)
|
|
280
300
|
)
|
|
281
|
-
|
|
301
|
+
|
|
282
302
|
if step < len(self.gradient_descent_results):
|
|
283
303
|
# Calculate next location for display
|
|
284
304
|
current_loc = result['location']
|
|
285
305
|
update = result['update']
|
|
286
306
|
next_loc = [current_loc[j] - update[j] for j in range(len(current_loc))]
|
|
287
|
-
|
|
307
|
+
|
|
288
308
|
explanation.add_element(
|
|
289
309
|
ContentAST.Paragraph(
|
|
290
310
|
[
|
|
@@ -292,7 +312,7 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
292
312
|
]
|
|
293
313
|
)
|
|
294
314
|
)
|
|
295
|
-
|
|
315
|
+
|
|
296
316
|
function_values = [r['function_value'] for r in self.gradient_descent_results]
|
|
297
317
|
explanation.add_element(
|
|
298
318
|
ContentAST.Paragraph(
|
|
@@ -301,5 +321,10 @@ class GradientDescentWalkthrough(GradientDescentQuestion, TableQuestionMixin, Bo
|
|
|
301
321
|
]
|
|
302
322
|
)
|
|
303
323
|
)
|
|
304
|
-
|
|
324
|
+
|
|
325
|
+
return explanation, []
|
|
326
|
+
|
|
327
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
328
|
+
"""Build question explanation (backward compatible interface)."""
|
|
329
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
305
330
|
return explanation
|
|
@@ -6,13 +6,16 @@ import math
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from typing import List, Tuple, Dict, Any
|
|
8
8
|
|
|
9
|
-
from QuizGenerator.contentast import ContentAST
|
|
10
|
-
from QuizGenerator.question import Question,
|
|
9
|
+
from QuizGenerator.contentast import ContentAST, AnswerTypes
|
|
10
|
+
from QuizGenerator.question import Question, QuestionRegistry
|
|
11
11
|
from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
|
|
12
12
|
|
|
13
13
|
log = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
# Note: This file migrates to the _get_body()/_get_explanation() pattern
|
|
17
|
+
|
|
18
|
+
|
|
16
19
|
class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
17
20
|
"""Base class for loss function calculation questions."""
|
|
18
21
|
|
|
@@ -70,14 +73,15 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
|
70
73
|
|
|
71
74
|
# Individual loss answers
|
|
72
75
|
for i in range(self.num_samples):
|
|
73
|
-
self.answers[f"loss_{i}"] =
|
|
76
|
+
self.answers[f"loss_{i}"] = AnswerTypes.Float(self.individual_losses[i], label=f"Sample {i + 1} loss")
|
|
74
77
|
|
|
75
78
|
# Overall loss answer
|
|
76
|
-
self.answers["overall_loss"] =
|
|
79
|
+
self.answers["overall_loss"] = AnswerTypes.Float(self.overall_loss, label="Overall loss")
|
|
77
80
|
|
|
78
|
-
def
|
|
79
|
-
"""
|
|
81
|
+
def _get_body(self, **kwargs) -> Tuple[ContentAST.Element, List[ContentAST.Answer]]:
|
|
82
|
+
"""Build question body and collect answers."""
|
|
80
83
|
body = ContentAST.Section()
|
|
84
|
+
answers = []
|
|
81
85
|
|
|
82
86
|
# Question description
|
|
83
87
|
body.add_element(ContentAST.Paragraph([
|
|
@@ -85,15 +89,25 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
|
85
89
|
f"and the overall {self._get_loss_function_short_name()}."
|
|
86
90
|
]))
|
|
87
91
|
|
|
88
|
-
# Data table
|
|
92
|
+
# Data table (contains individual loss answers)
|
|
89
93
|
body.add_element(self._create_data_table())
|
|
90
94
|
|
|
95
|
+
# Collect individual loss answers
|
|
96
|
+
for i in range(self.num_samples):
|
|
97
|
+
answers.append(self.answers[f"loss_{i}"])
|
|
98
|
+
|
|
91
99
|
# Overall loss question
|
|
92
100
|
body.add_element(ContentAST.Paragraph([
|
|
93
101
|
f"Overall {self._get_loss_function_short_name()}: "
|
|
94
102
|
]))
|
|
95
|
-
|
|
103
|
+
answers.append(self.answers["overall_loss"])
|
|
104
|
+
body.add_element(self.answers["overall_loss"])
|
|
105
|
+
|
|
106
|
+
return body, answers
|
|
96
107
|
|
|
108
|
+
def get_body(self, **kwargs) -> ContentAST.Element:
|
|
109
|
+
"""Build question body (backward compatible interface)."""
|
|
110
|
+
body, _ = self._get_body(**kwargs)
|
|
97
111
|
return body
|
|
98
112
|
|
|
99
113
|
@abc.abstractmethod
|
|
@@ -101,8 +115,8 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
|
101
115
|
"""Create the data table with answer fields."""
|
|
102
116
|
pass
|
|
103
117
|
|
|
104
|
-
def
|
|
105
|
-
"""
|
|
118
|
+
def _get_explanation(self, **kwargs) -> Tuple[ContentAST.Element, List[ContentAST.Answer]]:
|
|
119
|
+
"""Build question explanation."""
|
|
106
120
|
explanation = ContentAST.Section()
|
|
107
121
|
|
|
108
122
|
explanation.add_element(ContentAST.Paragraph([
|
|
@@ -121,6 +135,11 @@ class LossQuestion(Question, TableQuestionMixin, BodyTemplatesMixin, abc.ABC):
|
|
|
121
135
|
# Overall loss calculation
|
|
122
136
|
explanation.add_element(self._create_overall_loss_explanation())
|
|
123
137
|
|
|
138
|
+
return explanation, []
|
|
139
|
+
|
|
140
|
+
def get_explanation(self, **kwargs) -> ContentAST.Element:
|
|
141
|
+
"""Build question explanation (backward compatible interface)."""
|
|
142
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
124
143
|
return explanation
|
|
125
144
|
|
|
126
145
|
@abc.abstractmethod
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from typing import List, Tuple, Callable, Union, Any
|
|
3
3
|
import sympy as sp
|
|
4
4
|
|
|
5
|
-
from QuizGenerator.
|
|
5
|
+
from QuizGenerator.contentast import ContentAST
|
|
6
6
|
|
|
7
7
|
def generate_function(rng, num_variables: int, max_degree: int, use_quadratic: bool = True) -> tuple[Any, sp.Expr, sp.MutableDenseMatrix, sp.Equality]:
|
|
8
8
|
"""
|
|
@@ -61,7 +61,7 @@ def format_vector(vec: List[float]) -> str:
|
|
|
61
61
|
|
|
62
62
|
vector_string = ', '.join(
|
|
63
63
|
[
|
|
64
|
-
sorted(Answer.accepted_strings(v), key=lambda s: len(s))[0]
|
|
64
|
+
sorted(ContentAST.Answer.accepted_strings(v), key=lambda s: len(s))[0]
|
|
65
65
|
for v in vec
|
|
66
66
|
]
|
|
67
67
|
)
|