diff --git a/__pycache__/algebraic_steps.cpython-313.pyc b/__pycache__/algebraic_steps.cpython-313.pyc new file mode 100644 index 0000000..c77c17d Binary files /dev/null and b/__pycache__/algebraic_steps.cpython-313.pyc differ diff --git a/__pycache__/problem_generator.cpython-313.pyc b/__pycache__/problem_generator.cpython-313.pyc index 83b0ce7..48805c7 100644 Binary files a/__pycache__/problem_generator.cpython-313.pyc and b/__pycache__/problem_generator.cpython-313.pyc differ diff --git a/__pycache__/steps.cpython-313.pyc b/__pycache__/steps.cpython-313.pyc new file mode 100644 index 0000000..993e8fd Binary files /dev/null and b/__pycache__/steps.cpython-313.pyc differ diff --git a/__pycache__/steps_generator.cpython-313.pyc b/__pycache__/steps_generator.cpython-313.pyc new file mode 100644 index 0000000..3cae915 Binary files /dev/null and b/__pycache__/steps_generator.cpython-313.pyc differ diff --git a/algebraic_steps.py b/algebraic_steps.py new file mode 100644 index 0000000..3ce887f --- /dev/null +++ b/algebraic_steps.py @@ -0,0 +1,197 @@ +#All Rights Reserved John Salguero +#Steps that are generated + +from sympy import * +from sympy.parsing.sympy_parser import ( + parse_expr, + standard_transformations, + implicit_multiplication_application +) +transformations = standard_transformations + (implicit_multiplication_application,) + +def move_all_to_one_side(equation): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + new_expr = left_expr - right_expr + step["after"] = f"{sstr(new_expr)} = 0" + + step["step"] = f"Subtract both sides by {sstr(right_expr)}" + step["rule"] = "Subtraction Property of Equality" + return step + +def add_both_sides(equation, value): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + new_left_expr = left_expr + value + new_right_expr = right_expr + value + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" + + step["step"] = f"Add both sides by {sstr(value)}" + step["rule"] = "Addition Property of Equality" + return step + +def subtract_both_sides(equation, value): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + new_left_expr = left_expr - value + new_right_expr = right_expr - value + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" + + step["step"] = f"Subtract both sides by {sstr(value)}" + step["rule"] = "Subtraction Property of Equality" + return step + +def divide_both_sides(equation, value): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + new_left_expr = clean(left_expr / value) + new_right_expr = clean(right_expr / value) + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" + + step["step"] = f"Divide both sides by {sstr(value)}" + step["rule"] = "Division Property of Equality" + return step + +def multiply_both_sides(equation, value): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + new_left_expr = left_expr * value + new_right_expr = right_expr * value + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" + + step["step"] = f"Multiply both sides by {sstr(value)}" + step["rule"] = "Multiplication Property of Equality" + return step + +def factor_collect(equation): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + new_left_expr = factor(left_expr) + new_right_expr = factor(right_expr) + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" + + step["step"] = f"Collect the factors" + step["rule"] = "Factoring by grouping" + return step + +def factor_form_collection(equation, factor): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + new_left_expr = collect(left_expr, factor) + new_right_expr = collect(right_expr, factor) + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" + + step["step"] = f"Collect the factors using factor {sstr(factor)}" + step["rule"] = "Factor by grouping" + return step + +def combine_like_terms(equation): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + ## Combine Left Terms + left_terms = left_expr.as_ordered_terms() + # group by base + left_groups = {} + for t in left_terms: + coeff, rest = t.as_coeff_Mul() + left_groups.setdefault(rest, 0) + left_groups[rest] += coeff + # rebuild manually + new_left_terms = [] + for base, coeff in left_groups.items(): + if coeff != 0: + new_left_terms.append(coeff * base) + new_left_expr = sum(new_left_terms) + + ## Comnine Right Terms + right_terms = right_expr.as_ordered_terms() + # group by base + right_groups = {} + for t in right_terms: + coeff, rest = t.as_coeff_Mul() + right_groups.setdefault(rest, 0) + right_groups[rest] += coeff + # rebuild manually + new_right_terms = [] + for base, coeff in right_groups.items(): + if coeff != 0: + new_right_terms.append(coeff * base) + new_right_expr = sum(new_right_terms) + + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" + step["step"] = "Collect Like Terms" + step["rule"] = "Combine the like terms" + return step + +def clean(expr): + + # remove explicit 1 multipliers + expr = expr.replace( + lambda e: isinstance(e, Mul), + lambda e: Mul(*[arg for arg in e.args if arg != 1]) + ) + + return expr \ No newline at end of file diff --git a/main.py b/main.py index 07d947f..b2d253d 100644 --- a/main.py +++ b/main.py @@ -1,44 +1,19 @@ #All Rights Reserved John Salguero #Starts the backend to my Youtube stream -from problem_generator import generate_problem, normalize -from mathhook import parse, solve -from sympy import sympify +from problem_generator import generate_problem +from steps_generator import generate_steps #define the entry point to the programs def main(): problem = generate_problem() - + steps = generate_steps(problem); print("Generated Problem:") print(problem) - print("Solve:") - equation = apply_strategy(problem) - expr = parse(equation) - result = solve(expr, "x") - print(result) - -def square_both_sides(problem): - lhs, rhs = problem["problem"].split("=") - - lhs = sympify(lhs.strip()) - rhs = sympify(rhs.strip()) - - lhs = lhs ** 2 - rhs = rhs ** 2 - - return f"{normalize(lhs)} = {normalize(rhs)}" - -def factor_or_formula(problem) : - return problem["problem"] - -def apply_strategy(problem): - if problem["type"] == "radical": - return square_both_sides(problem) - if problem["type"] == "quadratics": - return factor_or_formula(problem) - return problem["problem"] + print("Steps:") + print(steps) #Starts the program if __name__ == "__main__": diff --git a/problem_generator.py b/problem_generator.py index dd38fbf..1e2af0a 100644 --- a/problem_generator.py +++ b/problem_generator.py @@ -4,17 +4,26 @@ import random from sympy import * +TEMPLATES = {} + +def register_problem_generator(problem_type): + def decorator(func): + TEMPLATES[problem_type] = func + return func + return decorator + +@register_problem_generator("linear") def generate_linear(): #ax + b = c a = random.choice([i for i in range(-10, 16) if i != 0]) ans = random.choice([i for i in range(-10, 11)]) - b = random.choice([i for i in range(-10, 11)]) + b = random.choice([i for i in range(-10, 11) if a != 1 or i != 0]) c = a * ans + b x = symbols('x') expr = a * x + b # expanded = n - s = normalize(expr) + s = sstr(expr) return { "type": "linear", @@ -22,20 +31,21 @@ def generate_linear(): "solution": ans } +@register_problem_generator("hidden_factor") def generate_hidden_factor(): #a(x + b) + c(x + b) = d ans = random.choice([i for i in range(-10, 16)]) b = random.choice([i for i in range(-5, 6) if i != 0]) a = random.choice([i for i in range(-5, 6) if i != 0]) - c = random.choice([i for i in range(-5, 6) if i != 0]) + c = random.choice([i for i in range(-5, 6) if i != 0 and i != -a]) x = symbols('x') inner_expr = x + b inner_value = ans + b right_side = a * inner_value + c * inner_value - problem = f"{a}({normalize(inner_expr)}) + {c}({normalize(inner_expr)}) = {right_side}" + problem = f"{a}({sstr(inner_expr)}) + {c}({sstr(inner_expr)}) = {right_side}" return { "type": "hidden_factor", @@ -43,6 +53,7 @@ def generate_hidden_factor(): "solution": ans } +@register_problem_generator("distribution") def generate_distribution (): #a(x + b) = c ans = random.choice([i for i in range(-10, 16)]) @@ -55,10 +66,11 @@ def generate_distribution (): return { "type": "distribution", - "problem": f"{a}({normalize(inner_expr)}) = {c}", + "problem": f"{a}({sstr(inner_expr)}) = {c}", "solution": ans } +@register_problem_generator("two_sides") def generate_two_sides (): #ax + b = dx + e : a != d ans = random.choice([i for i in range(-10, 16)]) @@ -73,10 +85,11 @@ def generate_two_sides (): return { "type": "two_sides", - "problem": f"{normalize(left_exp)} = {normalize(right_exp)}", + "problem": f"{sstr(left_exp)} = {sstr(right_exp)}", "solution": ans } +@register_problem_generator("like_terms") def generate_like_terms (): #ax + bx + c = d ans = random.choice([i for i in range(-10, 16)]) @@ -90,10 +103,11 @@ def generate_like_terms (): return { "type": "like_terms", - "problem": f"{normalize(expr)} = {d}", + "problem": f"{sstr(expr)} = {d}", "solution": ans } +@register_problem_generator("quadratic") def generate_quadratic (): #ax² + bx + c = 0 r1 = random.choice([i for i in range(-10, 16)]) @@ -103,7 +117,7 @@ def generate_quadratic (): x = symbols('x') expr = n *(x - r1) * (x - r2) - expr = simplify(expr) + expr = expand(expr) if r1 == r2: solution = r1 else: @@ -111,10 +125,11 @@ def generate_quadratic (): return { "type": "quadratic", - "problem": f"{normalize(expr)} = 0", + "problem": f"{sstr(expr)} = 0", "solution": solution } +@register_problem_generator("difference_squares") def generate_difference_squares (): #x² - a² = 0 ans = random.choice([i for i in range(0, 13)]) @@ -130,10 +145,11 @@ def generate_difference_squares (): return { "type": "difference_squares", - "problem": f"{normalize(expr)} = 0", + "problem": f"{sstr(expr)} = 0", "solution": solution } +@register_problem_generator("zero_product") def generate_zero_product (): #(x + a)(x + b) = 0 a = random.choice([i for i in range(-5, 6)]) @@ -144,10 +160,11 @@ def generate_zero_product (): return { "type": "zero_product", - "problem": f"{normalize(expr)} = 0", + "problem": f"{sstr(expr)} = 0", "solution": [-a, -b] } +@register_problem_generator("radical") def generate_radical (): #√(x + a) = b a = random.choice([i for i in range(-10, 16)]) @@ -159,10 +176,11 @@ def generate_radical (): return { "type": "radical", - "problem": f"{normalize(expr)} = {b}", + "problem": f"{sstr(expr)} = {b}", "solution": ans } +@register_problem_generator("fraction") def generate_fraction (): #(x/a) + b = c a = random.choice([i for i in range(-7, 8) if i != 0 and i != 1]) @@ -175,10 +193,11 @@ def generate_fraction (): return { "type": "fraction", - "problem": f"{normalize(expr)} = {c}", + "problem": f"{sstr(expr)} = {c}", "solution": ans } +@register_problem_generator("binomial") def generate_binomial (): #a(x + b) + c(x + d) = e ans = random.choice([i for i in range(-15, 16)]) @@ -197,12 +216,13 @@ def generate_binomial (): return { "type": "binomial", - "problem": f"{normalize(expr)} = {e}", + "problem": f"{sstr(expr)} = {e}", "solution": ans } +@register_problem_generator("tricky") def generate_tricky (): - #generate random numbers + #(x² - x - a) / (x + b) = c n = random.choice([i for i in range(-5, 6) if i != 0]) r1 = random.choice([i for i in range(-10, 16)]) r2 = random.choice([i for i in range(-10, 16) if i != r1]) @@ -214,7 +234,7 @@ def generate_tricky (): expanded = expanded - expr expanded = expanded / (x - r1) # expanded = n - s = normalize(expanded) + s = sstr(expanded) return { "type": "tricky", @@ -222,24 +242,15 @@ def generate_tricky (): "solution": r2 } -def normalize(expr): - return sstr(expr).replace("**", "^") def generate_problem(): - template = random.choice(TEMPLATES) - return template() + types = list(TEMPLATES.keys()) + #print(types) + # ['linear', 'hidden_factor', 'distribution', 'two_sides', 'like_terms', 'quadratic', 'difference_squares', 'zero_product', 'radical', 'fraction', 'binomial', 'tricky'] + weights = [0.80 , 0.90 , 0.70 , 1.00 , 0.9 , 1.0 , 0.80 , 0.70 , 0.70 , 1.00 , 1.00 , 0.65] -TEMPLATES = [ - generate_linear, - generate_hidden_factor, - generate_distribution, - generate_two_sides, - generate_like_terms, - generate_quadratic, - generate_difference_squares, - generate_zero_product, - generate_radical, - generate_fraction, - generate_binomial, - generate_tricky - ] \ No newline at end of file + problem_type = random.choices(types, weights=weights)[0] + template = TEMPLATES[problem_type] + return generate_like_terms() + #return template() + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7fafc8e..2425d3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -sympy -mathhook \ No newline at end of file +sympy \ No newline at end of file diff --git a/steps_generator.py b/steps_generator.py new file mode 100644 index 0000000..8472c2a --- /dev/null +++ b/steps_generator.py @@ -0,0 +1,244 @@ +#All Rights Reserved John Salguero +#Generates steps depending on the problem + +import algebraic_steps +from sympy import * +from sympy.parsing.sympy_parser import ( + parse_expr, + standard_transformations, + implicit_multiplication_application +) +transformations = standard_transformations + (implicit_multiplication_application,) + +STEP_GENERATORS = {} + +def register_steps_generator(problem_type): + def decorator(func): + STEP_GENERATORS[problem_type] = func + return func + return decorator + +@register_steps_generator("linear") +def generate_linear_steps(problem): + #ax + b = c + steps = [] + + x = symbols('x') + current = problem["problem"] + left, right = current.split("=") + expr = parse_expr(left) + a = expr.coeff(x) + b = expr.subs(x, 0) + + ## First Step + if b.is_zero == False: + if b.is_negative: + steps.append(algebraic_steps.add_both_sides(current, -b)) + elif b.is_positive: + steps.append(algebraic_steps.subtract_both_sides(current, b)) + current = steps[-1]["after"] + ## Second Step + if a != 1: + steps.append(algebraic_steps.divide_both_sides(current, a)) + + return steps + +@register_steps_generator("hidden_factor") +def generate_hidden_factor_steps(problem): + #a(x + b) + c(x + b) = d + steps = [] + + x = symbols('x') + current = problem["problem"] + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right) + terms = left_expr.as_ordered_terms() + #factors = [term.as_ordered_factors() for term in terms] + #common = set(factors[0]) & set(factors[1]) + #base = list(common)[0] + base = terms[0].args[1] + + ## First Step + steps.append(algebraic_steps.factor_collect(current)) + current = steps[-1]["after"] + + ## Second Step + div = simplify(left_expr / base) + steps.append(algebraic_steps.divide_both_sides(current, div)) + current = steps[-1]["after"] + + ## Third Step + b = base.subs(x, 0) + if b.is_negative: + steps.append(algebraic_steps.add_both_sides(current, -b)) + elif b.is_positive: + steps.append(algebraic_steps.subtract_both_sides(current, b)) + + current = steps[-1]["after"] + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + right_expr = parse_expr(right) + steps[-1]["after"] = f"{sstr(left_expr)} = {sstr(right_expr)}" + + return steps + + +@register_steps_generator("distribution") +def generate_distribution_steps (problem): + #a(x + b) = c + steps = [] + + x = symbols('x') + current = problem["problem"] + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right) + terms = left_expr.as_ordered_terms() + base = terms[0].args[1] + div = simplify(left_expr / base) + + ## First Step + steps.append(algebraic_steps.divide_both_sides(current, div)) + current = steps[-1]["after"] + + ## Second Step + b = base.subs(x, 0) + if b.is_negative: + steps.append(algebraic_steps.add_both_sides(current, -b)) + elif b.is_positive: + steps.append(algebraic_steps.subtract_both_sides(current, b)) + + current = steps[-1]["after"] + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + right_expr = parse_expr(right) + steps[-1]["after"] = f"{sstr(left_expr)} = {sstr(right_expr)}" + + return steps + +@register_steps_generator("two_sides") +def generate_two_sides_steps (problem): + #ax + b = dx + e : a != d + steps = [] + + x = symbols('x') + current = problem["problem"] + left, right = current.split("=") + left_expr = parse_expr(left) + right_expr = parse_expr(right) + a = left_expr.coeff(x) + b = left_expr.subs(x, 0) + d = right_expr.coeff(x) + e = right_expr.subs(x, 0) + + ## First Step + if d.is_negative: + steps.append(algebraic_steps.add_both_sides(current, -d*x)) + elif d.is_positive: + steps.append(algebraic_steps.subtract_both_sides(current, d*x)) + current = steps[-1]["after"] + left, right = current.split("=") + left_expr = parse_expr(left) + right_expr = parse_expr(right) + steps[-1]["after"] = f"{sstr(left_expr)} = {sstr(right_expr)}" + current = steps[-1]["after"] + + ## Second Step + if b.is_negative: + steps.append(algebraic_steps.add_both_sides(current, -b)) + elif b.is_positive: + steps.append(algebraic_steps.subtract_both_sides(current, b)) + current = steps[-1]["after"] + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + right_expr = parse_expr(right) + steps[-1]["after"] = f"{sstr(left_expr)} = {sstr(right_expr)}" + current = steps[-1]["after"] + + ## Third Step + new_left, new_right = current.split("=") + new_left_expr = parse_expr(left, transformations=transformations, evaluate=False) + new_right_expr = parse_expr(right, transformations=transformations, evaluate=False) + div = left_expr.coeff(x) + if div != 1 and div != -1: + steps.append(algebraic_steps.divide_both_sides(current, div)) + elif div == -1: + steps.append(algebraic_steps.multiply_both_sides(current, div)) + + return steps + +@register_steps_generator("like_terms") +def generate_like_terms_steps (problem): + #ax + bx + c = d + steps = [] + + current = problem["problem"] + + ## First Step + steps.append(algebraic_steps.combine_like_terms(current)) + current = steps[-1]["after"] + + ## Second Step + left, right = current.split("=") + left_expr = parse_expr(left) + b = left_expr.subs(x, 0) + + return steps + +@register_steps_generator("quadratic") +def generate_quadratic_steps (problem): + #ax² + bx + c = 0 + steps = [] + + return steps + +@register_steps_generator("difference_squares") +def generate_difference_squares_steps (problem): + #x² - a² = 0 + steps = [] + + return steps + +@register_steps_generator("zero_product") +def generate_zero_product_steps (problem): + #(x + a)(x + b) = 0 + steps = [] + + return steps + +@register_steps_generator("radical") +def generate_radical_steps (problem): + #√(x + a) = b + steps = [] + + return steps + +@register_steps_generator("fraction") +def generate_fraction_steps (problem): + #(x/a) + b = c + steps = [] + + return steps + +@register_steps_generator("binomial") +def generate_binomial_steps (problem): + #a(x + b) + c(x + d) = e + steps = [] + + return steps + +@register_steps_generator("tricky") +def generate_tricky_steps (problem): + #(x² - x - a) / (x + b) = c + steps = [] + + return steps + +def generate_steps(problem): + problem_type = problem["type"] + + if problem_type not in STEP_GENERATORS: + raise ValueError(f"No step generator for type: {problem_type}") + + return STEP_GENERATORS[problem["type"]](problem) \ No newline at end of file