QuizGenerator 0.8.1__tar.gz → 0.9.0__tar.gz
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-0.8.1 → quizgenerator-0.9.0}/PKG-INFO +9 -6
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/contentast.py +1 -1
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/generate.py +1 -1
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/mixins.py +6 -2
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/basic.py +49 -7
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +92 -82
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +68 -45
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +235 -162
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +51 -45
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +212 -215
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/question.py +139 -18
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/README.md +8 -5
- quizgenerator-0.8.1/pyproject_prev.toml → quizgenerator-0.9.0/pyproject.toml +1 -1
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/uv.lock +1 -1
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/.envrc +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/.gitignore +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/CODEOWNERS +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/LICENSE +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/__main__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/canvas/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/canvas/canvas_interface.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/canvas/classes.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/constants.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/misc.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/performance.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/languages.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/math_questions.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/memory_questions.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/persistence_questions.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst334/process.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/attention.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/cnns.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/matrices.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/rnns.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/text.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/models/weight_counting.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/qrcode_generator.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/quiz.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/regenerate.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/QuizGenerator/typst_utils.py +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/examples/web_ui_integration_example.py +0 -0
- /quizgenerator-0.8.1/pyproject.toml → /quizgenerator-0.9.0/pyproject_prev.toml +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/scripts/generate_practice_yaml.sh +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/scripts/print.sh +0 -0
- {quizgenerator-0.8.1 → quizgenerator-0.9.0}/scripts/vendor_lms_interface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: QuizGenerator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Generate randomized quiz questions for Canvas LMS and PDF exams
|
|
5
5
|
Project-URL: Homepage, https://github.com/OtterDen-Lab/QuizGenerator
|
|
6
6
|
Project-URL: Documentation, https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation
|
|
@@ -150,26 +150,29 @@ All questions follow the same three‑method flow:
|
|
|
150
150
|
|
|
151
151
|
```python
|
|
152
152
|
class MyQuestion(Question):
|
|
153
|
-
|
|
153
|
+
@classmethod
|
|
154
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
154
155
|
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
155
|
-
rng = context
|
|
156
|
+
rng = context.rng
|
|
156
157
|
context["value"] = rng.randint(1, 10)
|
|
157
158
|
return context
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
@classmethod
|
|
161
|
+
def _build_body(cls, context):
|
|
160
162
|
body = ca.Section()
|
|
161
163
|
body.add_element(ca.Paragraph([f"Value: {context['value']}"]))
|
|
162
164
|
body.add_element(ca.AnswerTypes.Int(context["value"], label="Value"))
|
|
163
165
|
return body
|
|
164
166
|
|
|
165
|
-
|
|
167
|
+
@classmethod
|
|
168
|
+
def _build_explanation(cls, context):
|
|
166
169
|
explanation = ca.Section()
|
|
167
170
|
explanation.add_element(ca.Paragraph([f"Answer: {context['value']}"]))
|
|
168
171
|
return explanation
|
|
169
172
|
```
|
|
170
173
|
|
|
171
174
|
Notes:
|
|
172
|
-
- Always use `context["rng"]` for deterministic randomness.
|
|
175
|
+
- Always use `context.rng` (or `context["rng"]`) for deterministic randomness.
|
|
173
176
|
- Avoid `refresh()`; it is no longer part of the API.
|
|
174
177
|
|
|
175
178
|
## Built-in Question Types
|
|
@@ -668,7 +668,7 @@ class Section(Container):
|
|
|
668
668
|
- Organizing complex question content
|
|
669
669
|
|
|
670
670
|
Example:
|
|
671
|
-
def _build_body(
|
|
671
|
+
def _build_body(cls, context):
|
|
672
672
|
body = Section()
|
|
673
673
|
answers = []
|
|
674
674
|
body.add_element(Paragraph(["Calculate the determinant:"]))
|
|
@@ -153,7 +153,7 @@ def test_all_questions(
|
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
# Generate the question (this calls refresh and builds the AST)
|
|
156
|
-
instance = question.instantiate(rng_seed=seed)
|
|
156
|
+
instance = question.instantiate(rng_seed=seed, max_backoff_attempts=200)
|
|
157
157
|
question_ast = question._build_question_ast(instance)
|
|
158
158
|
|
|
159
159
|
# Try rendering to both formats to catch format-specific issues
|
|
@@ -419,8 +419,10 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
|
|
|
419
419
|
subparts.append((operand_a_latex, self.get_operator(), operand_b_latex))
|
|
420
420
|
return subparts
|
|
421
421
|
|
|
422
|
-
|
|
422
|
+
@classmethod
|
|
423
|
+
def _build_body(cls, context):
|
|
423
424
|
"""Build question body and collect answers."""
|
|
425
|
+
self = context
|
|
424
426
|
body = ca.Section()
|
|
425
427
|
answers = []
|
|
426
428
|
|
|
@@ -453,8 +455,10 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
|
|
|
453
455
|
# Default implementation - subclasses should override for specific answer formats
|
|
454
456
|
return []
|
|
455
457
|
|
|
456
|
-
|
|
458
|
+
@classmethod
|
|
459
|
+
def _build_explanation(cls, context):
|
|
457
460
|
"""Default explanation structure. Subclasses should override for specific explanations."""
|
|
461
|
+
self = context
|
|
458
462
|
explanation = ca.Section()
|
|
459
463
|
|
|
460
464
|
explanation.add_element(ca.Paragraph([self.get_explanation_intro()]))
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from typing import Tuple, List
|
|
5
|
+
from types import SimpleNamespace
|
|
5
6
|
import random
|
|
6
7
|
|
|
7
8
|
import logging
|
|
@@ -17,19 +18,23 @@ log = logging.getLogger(__name__)
|
|
|
17
18
|
class FromText(Question):
|
|
18
19
|
|
|
19
20
|
def __init__(self, *args, text, **kwargs):
|
|
21
|
+
kwargs["text"] = text
|
|
20
22
|
super().__init__(*args, **kwargs)
|
|
21
23
|
self.text = text
|
|
22
24
|
self.possible_variations = 1
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
@classmethod
|
|
27
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
25
28
|
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
26
|
-
context["text"] =
|
|
29
|
+
context["text"] = kwargs.get("text", "")
|
|
27
30
|
return context
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
@classmethod
|
|
33
|
+
def _build_body(cls, context) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
30
34
|
return ca.Section([ca.Text(context["text"])]), []
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
@classmethod
|
|
37
|
+
def _build_explanation(cls, context) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
33
38
|
return ca.Section(), []
|
|
34
39
|
|
|
35
40
|
|
|
@@ -43,6 +48,7 @@ class FromGenerator(FromText, TableQuestionMixin):
|
|
|
43
48
|
if generator is None:
|
|
44
49
|
generator = text
|
|
45
50
|
|
|
51
|
+
kwargs["generator"] = generator
|
|
46
52
|
super().__init__(*args, text="", **kwargs)
|
|
47
53
|
self.possible_variations = kwargs.get("possible_variations", float('inf'))
|
|
48
54
|
|
|
@@ -72,15 +78,51 @@ class FromGenerator(FromText, TableQuestionMixin):
|
|
|
72
78
|
# Attach the function dynamically
|
|
73
79
|
attach_function_to_object(self, generator, "generator")
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _compile_generator(function_code, function_name="generator"):
|
|
83
|
+
# Provide a deterministic RNG handle for generator snippets.
|
|
84
|
+
function_code = "rng = self.rng\n" + function_code
|
|
85
|
+
|
|
86
|
+
local_namespace = {
|
|
87
|
+
'ca': ca,
|
|
88
|
+
'Section': ca.Section,
|
|
89
|
+
'Text': ca.Text,
|
|
90
|
+
'Table': ca.Table,
|
|
91
|
+
'Paragraph': ca.Paragraph
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exec_globals = {**globals(), **local_namespace}
|
|
95
|
+
exec(
|
|
96
|
+
f"def {function_name}(self):\n" + "\n".join(f" {line}" for line in function_code.splitlines()),
|
|
97
|
+
exec_globals,
|
|
98
|
+
local_namespace
|
|
99
|
+
)
|
|
100
|
+
return local_namespace[function_name]
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def _build_context(cls, *, rng_seed=None, **kwargs):
|
|
76
104
|
context = super()._build_context(rng_seed=rng_seed, **kwargs)
|
|
105
|
+
for key, value in kwargs.items():
|
|
106
|
+
if key not in context:
|
|
107
|
+
context[key] = value
|
|
77
108
|
# Preserve prior behavior for generators that use the global random module.
|
|
78
109
|
random.seed(rng_seed)
|
|
110
|
+
generator_text = kwargs.get("generator")
|
|
111
|
+
if generator_text is not None:
|
|
112
|
+
context["generator_fn"] = cls._compile_generator(generator_text)
|
|
113
|
+
context["generator_scope"] = SimpleNamespace(
|
|
114
|
+
rng=context.rng,
|
|
115
|
+
**context.data
|
|
116
|
+
)
|
|
79
117
|
return context
|
|
80
118
|
|
|
81
|
-
|
|
119
|
+
@classmethod
|
|
120
|
+
def _build_body(cls, context) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
82
121
|
try:
|
|
83
|
-
|
|
122
|
+
generator_fn = context.get("generator_fn")
|
|
123
|
+
if generator_fn is None:
|
|
124
|
+
raise TypeError("No generator provided for FromGenerator.")
|
|
125
|
+
generated_content = generator_fn(context.get("generator_scope"))
|
|
84
126
|
if isinstance(generated_content, ca.Section):
|
|
85
127
|
body = generated_content
|
|
86
128
|
elif isinstance(generated_content, str):
|
|
@@ -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",
|
|
22
|
-
self.max_degree = kwargs.get("max_degree",
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
+
@classmethod
|
|
35
|
+
def _generate_evaluation_point(cls, context) -> List[float]:
|
|
35
36
|
"""Generate a random point for gradient evaluation."""
|
|
36
|
-
return [
|
|
37
|
+
return [context.rng.randint(-3, 3) for _ in range(context.num_variables)]
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _format_partial_derivative(var_index: int, num_variables: int) -> str:
|
|
39
41
|
"""Format partial derivative symbol for display."""
|
|
40
|
-
if
|
|
42
|
+
if num_variables == 1:
|
|
41
43
|
return "\\frac{df}{dx_0}"
|
|
42
|
-
|
|
43
|
-
return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
|
|
44
|
+
return f"\\frac{{\\partial f}}{{\\partial x_{var_index}}}"
|
|
44
45
|
|
|
45
|
-
|
|
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(
|
|
52
|
+
subs_map = dict(zip(context.variables, context.evaluation_point))
|
|
51
53
|
|
|
52
54
|
# Format evaluation point for label
|
|
53
|
-
eval_point_str = ", ".join(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
82
|
+
subs_map = dict(zip(context.variables, context.evaluation_point))
|
|
78
83
|
gradient_values = []
|
|
79
84
|
|
|
80
|
-
for i in range(
|
|
81
|
-
partial_value =
|
|
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
|
-
|
|
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(
|
|
107
|
+
ca.Equation(sp.latex(context.equation), inline=True),
|
|
102
108
|
", calculate the gradient at the point ",
|
|
103
|
-
ca.Equation(format_vector(
|
|
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(
|
|
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 =
|
|
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. {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
168
|
-
for i in range(
|
|
169
|
-
partial_expr =
|
|
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"{
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
216
|
+
context.evaluation_point = cls._generate_evaluation_point(context)
|
|
207
217
|
|
|
208
218
|
# Create answers for evaluation point (used in _build_body)
|
|
209
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
237
|
+
cls._generate_composed_function(context)
|
|
229
238
|
|
|
230
239
|
# Generate evaluation point
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
_ =
|
|
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
|
-
|
|
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(
|
|
256
|
-
|
|
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(
|
|
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(*(
|
|
270
|
+
inner_poly = sp.Add(*(context.rng.choice(coeff_pool) * t for t in inner_terms))
|
|
264
271
|
else:
|
|
265
|
-
inner_poly = sp.Add(*[
|
|
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 =
|
|
284
|
+
outer_func = context.rng.choice(outer_functions)
|
|
278
285
|
|
|
279
286
|
# Compose the functions: f(g(x))
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
+
context.equation = sp.Eq(f(*context.variables), context.function)
|
|
297
|
+
|
|
298
|
+
return context
|
|
290
299
|
|
|
291
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
337
|
-
inner_deriv =
|
|
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(
|
|
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(
|
|
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(
|
|
372
|
-
for i in range(
|
|
373
|
-
partial_expr =
|
|
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"{
|
|
398
|
+
f"{cls._format_partial_derivative(i, context.num_variables)} = {sp.latex(partial_expr)} = {clean_value}",
|
|
389
399
|
inline=False
|
|
390
400
|
)
|
|
391
401
|
])
|