File size: 12,954 Bytes
f8319a8
 
973cd6f
f8319a8
 
973cd6f
 
 
 
 
 
 
 
 
 
 
f8319a8
 
973cd6f
f8319a8
 
 
 
 
 
973cd6f
 
 
 
 
 
 
 
 
 
 
f8319a8
973cd6f
 
 
 
 
 
 
 
 
 
f8319a8
 
 
 
 
973cd6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8319a8
973cd6f
f8319a8
 
 
973cd6f
 
 
 
 
 
 
f8319a8
 
973cd6f
f8319a8
973cd6f
 
 
 
 
 
f8319a8
 
 
 
973cd6f
 
 
 
f8319a8
 
 
 
 
 
973cd6f
 
 
 
 
 
 
 
 
 
 
 
 
f8319a8
 
 
 
 
 
 
 
973cd6f
 
 
 
 
 
 
 
 
 
 
f8319a8
 
 
 
 
 
 
 
973cd6f
 
 
f8319a8
 
973cd6f
f8319a8
 
 
973cd6f
 
f8319a8
 
 
 
 
 
 
 
 
 
 
 
 
 
973cd6f
 
 
 
 
 
 
 
f8319a8
973cd6f
f8319a8
973cd6f
f8319a8
 
973cd6f
 
 
f8319a8
 
 
 
 
 
 
 
973cd6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import sympy as sp
import random
from typing import Dict, Any, Tuple, List, Optional

class TaskGenerationEngine:
    """
    Symbolic calculus task generator with scaffold hints and technique metadata.
    
    Improvements over v1:
    1. Stores which integration technique is needed (u-sub, by-parts, etc.)
    2. Generates scaffold hints (first step of solution) for Scaf-GRPO
    3. Better prompt formatting using LaTeX-style notation
    4. More diverse function compositions
    5. Technique-aware variant generation
    """
    
    def __init__(self):
        self.x = sp.Symbol('x')
        
        # Components for generating random functions F(x)
        self.basic_functions = [
            lambda x, c: x**c,
            lambda x, c: sp.sin(c*x),
            lambda x, c: sp.cos(c*x),
            lambda x, c: sp.exp(c*x),
            lambda x, c: sp.ln(sp.Abs(c*x + 1)),  # +1 avoids log(0)
        ]
        
        # Additional functions for higher difficulty
        self.advanced_functions = [
            lambda x, c: sp.tan(c*x),
            lambda x, c: sp.atan(c*x),
            lambda x, c: sp.sinh(c*x),
            lambda x, c: sp.cosh(c*x),
            lambda x, c: x**c * sp.exp(x),         # Requires integration by parts
            lambda x, c: sp.sin(x) * sp.cos(c*x),  # Product of trig
        ]
        
        # Technique detection patterns
        self._technique_detectors = {
            'power_rule': self._is_power_rule,
            'u_substitution': self._is_u_substitution,
            'by_parts': self._is_by_parts,
            'trigonometric': self._is_trig_integral,
            'exponential': self._is_exponential,
            'logarithmic': self._is_logarithmic,
        }

    def _score_difficulty(self, components: int, nesting: int) -> float:
        """D = num_components + degree_of_nesting * 2"""
        return float(components + nesting * 2.0)

    def _detect_technique(self, f_expr) -> str:
        """Detect which integration technique is most appropriate for f(x)."""
        for technique, detector in self._technique_detectors.items():
            if detector(f_expr):
                return technique
        return 'power_rule'  # Default fallback
    
    def _is_power_rule(self, expr) -> bool:
        """Check if expression is a simple polynomial."""
        return expr.is_polynomial(self.x)
    
    def _is_u_substitution(self, expr) -> bool:
        """Check if expression likely needs u-substitution."""
        # Composition of functions suggests u-sub
        if isinstance(expr, sp.Mul):
            args = expr.args
            # Look for f(g(x)) * g'(x) pattern
            for arg in args:
                if arg.has(sp.sin, sp.cos, sp.exp, sp.log) and not arg.is_polynomial(self.x):
                    return True
        return False
    
    def _is_by_parts(self, expr) -> bool:
        """Check if expression likely needs integration by parts."""
        if isinstance(expr, sp.Mul):
            has_poly = any(a.is_polynomial(self.x) for a in expr.args)
            has_transcendental = any(a.has(sp.sin, sp.cos, sp.exp, sp.log) for a in expr.args)
            return has_poly and has_transcendental
        return False
    
    def _is_trig_integral(self, expr) -> bool:
        """Check if expression is primarily trigonometric."""
        return expr.has(sp.sin, sp.cos, sp.tan) and not expr.has(sp.exp, sp.log)
    
    def _is_exponential(self, expr) -> bool:
        """Check if expression is primarily exponential."""
        return expr.has(sp.exp) and not expr.has(sp.sin, sp.cos)
    
    def _is_logarithmic(self, expr) -> bool:
        """Check if expression involves logarithms."""
        return expr.has(sp.log, sp.ln)
    
    def _generate_scaffold_hint(self, f_expr, F_expr, technique: str) -> Dict[str, str]:
        """
        Generate a scaffold hint for the problem.
        
        Returns a dict with:
        - 'technique': which technique to use
        - 'hint_level_1': gentle nudge (technique name)
        - 'hint_level_2': first step of solution
        - 'hint_level_3': most of the solution
        """
        hints = {
            'technique': technique,
            'hint_level_1': '',
            'hint_level_2': '',
            'hint_level_3': '',
        }
        
        technique_descriptions = {
            'power_rule': "Try applying the power rule: ∫x^n dx = x^(n+1)/(n+1) + C",
            'u_substitution': "Try u-substitution. Look for a composite function and its derivative.",
            'by_parts': "Try integration by parts: ∫u dv = uv - ∫v du",
            'trigonometric': "Try using trigonometric identities to simplify first.",
            'exponential': "Remember that ∫e^(ax) dx = (1/a)e^(ax) + C",
            'logarithmic': "Remember that ∫(1/x) dx = ln|x| + C",
        }
        
        hints['hint_level_1'] = technique_descriptions.get(
            technique, "Try identifying the integration technique needed."
        )
        
        # Level 2: Show the substitution or setup
        try:
            if technique == 'u_substitution':
                # Try to identify the inner function for u-sub hint
                hints['hint_level_2'] = f"Hint: Try {hints['hint_level_1']}. The integrand has a composite structure."
            elif technique == 'by_parts':
                hints['hint_level_2'] = f"Hint: {hints['hint_level_1']}. Identify which part to differentiate (u) and which to integrate (dv)."
            else:
                hints['hint_level_2'] = f"Hint: {hints['hint_level_1']}"
        except Exception:
            hints['hint_level_2'] = hints['hint_level_1']
        
        # Level 3: Show the first term of the answer
        try:
            simplified = sp.simplify(F_expr)
            if isinstance(simplified, sp.Add):
                first_term = simplified.args[0]
                hints['hint_level_3'] = f"The answer starts with: {sp.pretty(first_term)} + ..."
            else:
                hints['hint_level_3'] = f"The answer has the form: {type(simplified).__name__} expression"
        except Exception:
            hints['hint_level_3'] = hints['hint_level_2']
        
        return hints
    
    def generate_random_function(self, complexity: int) -> Tuple[Any, float]:
        """Generates a random F(x) with appropriate complexity."""
        num_components = max(1, int(complexity / 2))
        nesting = max(0, int(complexity / 4))
        
        # Use advanced functions at higher complexity
        available_funcs = list(self.basic_functions)
        if complexity >= 4:
            available_funcs.extend(self.advanced_functions[:3])
        if complexity >= 6:
            available_funcs.extend(self.advanced_functions[3:])
        
        f_expr = 0
        for _ in range(num_components):
            comp_func = random.choice(available_funcs)
            coeff = random.randint(1, 5)
            
            try:
                term = comp_func(self.x, coeff)
            except Exception:
                # Fallback to simple polynomial
                term = self.x ** coeff
            
            # Apply nesting
            for _ in range(nesting):
                outer = random.choice(self.basic_functions)
                try:
                    term = outer(term, 1)
                except Exception:
                    break
            
            f_expr += random.randint(1, 10) * term
            
        return f_expr, self._score_difficulty(num_components, nesting)

    def generate_task(self, target_difficulty_band: float) -> Dict[str, Any]:
        """
        Provides an indefinite integral task with technique hints and scaffold support.
        
        Returns dict with:
        - problem: formatted problem text
        - solution: ground truth solution string
        - difficulty: computed difficulty score
        - type: 'integration'
        - sympy_F: SymPy expression for F(x) (antiderivative)
        - sympy_f: SymPy expression for f(x) (integrand)
        - technique: detected integration technique
        - scaffold_hints: dict of progressive hints
        """
        complexity = max(1, int(target_difficulty_band))
        
        # 1. Generate F(x)
        F_expr, diff = self.generate_random_function(complexity)
        
        # 2. Differentiate to get the problem f(x)
        f_expr = sp.diff(F_expr, self.x)
        
        # 3. Detect technique and generate hints
        technique = self._detect_technique(f_expr)
        scaffold_hints = self._generate_scaffold_hint(f_expr, F_expr, technique)
        
        # 4. Format strings — use cleaner formatting for LLM consumption
        try:
            pretty_f = sp.pretty(f_expr, use_unicode=True)
        except Exception:
            pretty_f = str(f_expr)
        
        problem_text = f"Find the indefinite integral: ∫ ({pretty_f}) dx"
        solution_text = f"{sp.simplify(F_expr)} + C"
        
        return {
            "problem": problem_text,
            "difficulty": diff,
            "solution": solution_text,
            "type": "integration",
            "sympy_F": F_expr,
            "sympy_f": f_expr,
            "technique": technique,
            "scaffold_hints": scaffold_hints,
        }

    def generate_variants(self, task: Dict[str, Any], count: int = 2) -> List[Dict[str, Any]]:
        """
        LADDER Component: Recursive Decomposition for Integration.
        Breaks down sums or simplifies coefficients.
        
        Improved: preserves technique hints and scaffold data through decomposition.
        """
        variants = []
        F_expr = task.get("sympy_F")
        
        if F_expr is None:
             # Fallback if task was not generated by us
             return [self.generate_task(max(1, task.get("difficulty", 2) - 2))]

        # Recursive Rule 1: Linearity (split sums)
        if isinstance(F_expr, sp.Add):
            args = F_expr.args
            for arg in args[:count]:
                sub_F = arg
                sub_f = sp.diff(sub_F, self.x)
                technique = self._detect_technique(sub_f)
                scaffold = self._generate_scaffold_hint(sub_f, sub_F, technique)
                
                try:
                    pretty_sub_f = sp.pretty(sub_f, use_unicode=True)
                except Exception:
                    pretty_sub_f = str(sub_f)
                
                variants.append({
                    "problem": f"Integrate step-variant: ∫ ({pretty_sub_f}) dx",
                    "solution": f"{sub_F} + C",
                    "difficulty": max(0.5, task["difficulty"] - 1.0),
                    "type": "integration",
                    "sympy_F": sub_F,
                    "sympy_f": sub_f,
                    "technique": technique,
                    "scaffold_hints": scaffold,
                })

        # Recursive Rule 2: Constant simplification
        if not variants:
            # Just return a simpler integral by reducing difficulty
            variants.append(self.generate_task(max(1.0, task["difficulty"] - 2.0)))

        return variants[:count]
    
    def generate_technique_focused_task(self, technique: str, difficulty: float = 2.0) -> Dict[str, Any]:
        """
        Generate a task that specifically targets a given integration technique.
        Useful for curriculum learning when the model struggles with a technique.
        """
        x = self.x
        
        technique_generators = {
            'power_rule': lambda: random.randint(1, 5) * x**random.randint(1, 6),
            'u_substitution': lambda: sp.sin(random.randint(1, 3) * x**2) * x,
            'by_parts': lambda: x * sp.exp(random.randint(1, 3) * x),
            'trigonometric': lambda: sp.sin(x)**random.randint(1, 3) * sp.cos(x),
            'exponential': lambda: random.randint(1, 5) * sp.exp(random.randint(1, 4) * x),
            'logarithmic': lambda: sp.ln(sp.Abs(x + 1)),
        }
        
        generator = technique_generators.get(technique)
        if generator is None:
            return self.generate_task(difficulty)
        
        try:
            F_expr = generator()
            f_expr = sp.diff(F_expr, x)
            scaffold = self._generate_scaffold_hint(f_expr, F_expr, technique)
            
            try:
                pretty_f = sp.pretty(f_expr, use_unicode=True)
            except Exception:
                pretty_f = str(f_expr)
            
            return {
                "problem": f"Find the indefinite integral: ∫ ({pretty_f}) dx",
                "solution": f"{sp.simplify(F_expr)} + C",
                "difficulty": difficulty,
                "type": "integration",
                "sympy_F": F_expr,
                "sympy_f": f_expr,
                "technique": technique,
                "scaffold_hints": scaffold,
            }
        except Exception:
            return self.generate_task(difficulty)