diff --git a/__pycache__/algebraic_steps.cpython-313.pyc b/__pycache__/algebraic_steps.cpython-313.pyc index a54cb75..061f4cd 100644 Binary files a/__pycache__/algebraic_steps.cpython-313.pyc 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 09722cd..cf098ce 100644 Binary files a/__pycache__/problem_generator.cpython-313.pyc and b/__pycache__/problem_generator.cpython-313.pyc differ diff --git a/__pycache__/steps_generator.cpython-313.pyc b/__pycache__/steps_generator.cpython-313.pyc index ca800d9..5755285 100644 Binary files a/__pycache__/steps_generator.cpython-313.pyc and b/__pycache__/steps_generator.cpython-313.pyc differ diff --git a/algebraic_steps.py b/algebraic_steps.py index 9e532fa..c17dc4d 100644 --- a/algebraic_steps.py +++ b/algebraic_steps.py @@ -2,6 +2,7 @@ #Steps that are generated from sympy import * +import re from sympy.parsing.sympy_parser import ( parse_expr, standard_transformations, @@ -94,8 +95,8 @@ def multiply_both_sides(equation, value): 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 + new_left_expr = cancel(left_expr * value) + new_right_expr = Mul(right_expr, value, evaluate=False) step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" step["step"] = f"Multiply both sides by {sstr(value)}" @@ -253,8 +254,12 @@ def trinomial_by_grouping(equation, inner): new_left_expr = left_expr steps[-1]["after"] = f"{sstr(new_left_expr)} = {sstr(right_expr)}" - steps[-1]["step"] = f"Seperate the x coeficients equal to the factors of {sstr(ac)} that {action} {sstr(Abs(b))}" - steps[-1]["rule"] = "Split Coeficients" + steps[-1]["step"] = ( + f"Seperate the x coefficient to equal the factors of the first times last coefficients" + f"({sstr(Abs(a))} x {sstr(Abs(c))} = {sstr(ac)}) that {action} {sstr(Abs(b))}" + f"({sstr(factor1)},{sstr(factor2)})" + ) + steps[-1]["rule"] = "Split Coefficients" ## Factor Out X on left term steps.append({}) @@ -262,6 +267,7 @@ def trinomial_by_grouping(equation, inner): terms = left_expr.as_ordered_terms() t1, t2, t3, t4 = terms[0], terms[1], terms[2], terms[3] + div = gcd(t3, t4) factored_part1 = factor(t1 + t2) factored_part2 = factor(t3 + t4) rest = sum(terms[2:]) @@ -280,7 +286,7 @@ def trinomial_by_grouping(equation, inner): new_left_expr = Mul(n, left_expr, evaluate=False) steps[-1]["after"] = f"{sstr(new_left_expr)} = {sstr(right_expr)}" - steps[-1]["step"] = f"Factor out the GCD from the right two terms of the inner polynomial" + steps[-1]["step"] = f"Factor out the GCD({sstr(div)}) from the right two terms of the inner polynomial" steps[-1]["rule"] = "Reverse Distributive Property" ## Add Like Terms @@ -311,9 +317,30 @@ def solve_roots(equation): x = symbols('x') left_expr = parse_expr(left, transformations=transformations, evaluate=False) - factors = left_expr.as_ordered_factors() - x_factors = [f for f in factors if f.has(x)] + ## Get the roots + factors = left_expr.as_ordered_factors() + x_factors = [] + + i = 0 + while i < len(factors): + f = factors[i] + + if f.has(x): + # If it's already something like 2*x or (x+...) + x_factors.append(f) + i += 1 + else: + # Check if next factor has x → combine them + if i + 1 < len(factors) and factors[i + 1].has(x) and not factors[i + 1].is_Add: + combined = Mul(f, factors[i + 1], evaluate=False) + x_factors.append(combined) + i += 2 + else: + i += 1 + + + ## Iterate through the roots solutions = "" for factor in x_factors: clean_factor = clean(factor) @@ -399,7 +426,7 @@ def combine_like_terms(equation): step["rule"] = "Combine the like terms" return step -def distribute_step(equation): +def distribute_left_step(equation): steps = [] current = equation @@ -416,12 +443,101 @@ def distribute_step(equation): if distributed != None: steps.append({}) steps[-1]["before"] = current - steps[-1]["after"] = f"{sstr(safe_format(new_left_expr))} = {sstr(right_expr)}" + steps[-1]["after"] = f"{sstr(safe_format(new_left_expr))} = {sstr(safe_format(right_expr))}" steps[-1]["step"] = f"Distribute out {sstr(distributed)}" steps[-1]["rule"] = "Distributive Law of Multiplication" return steps +def distribute_right_step(equation): + steps = [] + + current = equation + left, right = current.split("=") + left = left.replace("-(", "-1*(") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + #print(f"calling distribute_once with expression: {sstr(left_expr)}") + new_right_expr, distributed = distribute_once(right_expr) + + if distributed != None: + steps.append({}) + steps[-1]["before"] = current + steps[-1]["after"] = f"{sstr(safe_format(left_expr))} = {sstr(safe_format(new_right_expr))}" + steps[-1]["step"] = f"Distribute out {sstr(distributed)}" + steps[-1]["rule"] = "Distributive Law of Multiplication" + + return steps + +def check_roots(equation, roots): + steps = [] + + valid_roots = "" + str_values = [r.split("=")[1].strip() for r in roots.split(",")] + values = [sympify(value) for value in str_values] + current = equation + 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) + + # Check the roots + for value in values: + ## Substitution + left_subbed = substitute_var(left, 'x', f"{value}") + right_subbed = substitute_var(right, 'x', f"{value}") + left_subbed_exp = parse_expr(left_subbed) + right_subbed_exp = parse_expr(right_subbed) + steps.append({}) + steps[-1]["before"] = equation + steps[-1]["after"] = f"{left_subbed} = {right_subbed}" + steps[-1]["step"] = f"Substitute x with {value}" + steps[-1]["rule"] = "Substitution" + ## Check + l_result = simplify(left_subbed_exp) + r_result = simplify(right_subbed_exp) + if l_result in (zoo, oo, -oo, nan) or r_result in (zoo, oo, -oo, nan): + steps.append({}) + steps[-1]["before"] = steps[-2]["after"] + steps[-1]["after"] = f"Undefined" + steps[-1]["step"] = f"Found Extraneous Root, {value} is incorrect" + steps[-1]["rule"] = "Extraneous Root" + continue + + if l_result != r_result: + steps.append({}) + steps[-1]["before"] = steps[-2]["after"] + steps[-1]["after"] = f"{sstr(l_result)} ≠ {sstr(r_result)}" + steps[-1]["step"] = f"Found Extraneous Root, {value} is incorrect" + steps[-1]["rule"] = "Extraneous Root" + continue + + else: + steps.append({}) + steps[-1]["before"] = steps[-2]["after"] + steps[-1]["after"] = f"{sstr(l_result)} = {sstr(r_result)}" + steps[-1]["step"] = f"{value} is correct" + steps[-1]["rule"] = "Found Root" + if len(valid_roots) > 0: + valid_roots += "," + valid_roots += f"x = {value}" + + + steps.append({}) + steps[-1]["before"] = equation + steps[-1]["after"] = valid_roots + steps[-1]["step"] = f"Found Valid Roots" + steps[-1]["rule"] = "Found Solution" + + return steps + +def substitute_var(expr, var, value): + pattern = rf'\b{re.escape(var)}\b' + return re.sub(pattern, f'({value})', expr) + def build_ordered_add(args): flat_args = [] @@ -521,12 +637,35 @@ def clean(expr): def safe_format(expr): if expr.is_Mul: args = [] + sign = 1 + for a in expr.args: a = safe_format(a) + if a == 1: continue - args.append(a) - return Mul(*args, evaluate=False) + elif a == -1: + sign *= -1 + else: + args.append(a) + + if not args: + return -1 if sign == -1 else 1 + + # if everything is numeric → evaluate fully + if all(a.is_Number for a in args): + val = Mul(*args) + return -val if sign == -1 else val + + if len(args) == 1: + result = args[0] + else: + result = Mul(*args, evaluate=False) + + if sign == -1: + return Mul(-1, result, evaluate=False) + + return result if expr.is_Add: return expr.func(*[safe_format(a) for a in expr.args], evaluate=False) diff --git a/main.py b/main.py index 6e1a396..830aa96 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ from sympy import init_printing #define the entry point to the programs def main(): - init_printing(order='none') + init_printing(order='lex') problem = generate_problem() steps = generate_steps(problem); @@ -15,7 +15,22 @@ def main(): print(problem) print("Steps:") - print(steps) + pretty_print_steps(steps) + +def pretty_print_steps(steps): + print("\n" + "=" * 50) + + for i, step in enumerate(steps, start=1): + print(f"\nStep {i}") + print("-" * 50) + print(f"Before: {step.get('before', '')}") + print(f"After: {step.get('after', '')}") + + for key in step: + if key not in ("before", "after"): + print(f"{key.capitalize()}: {step[key]}") + + print("\n" + "=" * 50) #Starts the program if __name__ == "__main__": diff --git a/problem_generator.py b/problem_generator.py index 50fdc25..c6a23f7 100644 --- a/problem_generator.py +++ b/problem_generator.py @@ -120,7 +120,6 @@ def generate_quadratic (): x = symbols('x') expr = n *(s * x - r1) * (x - r2) - print(f"n:{n}, s:{s}") expr = expand(expr) root1 = Rational(r1, s) root2 = Integer(r2) @@ -235,9 +234,12 @@ def generate_binomial (): @register_problem_generator("tricky") def generate_tricky (): #(x² - x - a) / (x + b) = c + r1 = 0 + r2 = 0 + while r1 == r2 or r1 == 0 and r2 == 0: + r1 = random.choice(range(-10, 13)) + r2 = random.choice(range(-10, 13)) 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]) x = symbols('x') expr = (x - r1) * (x - r2) @@ -263,6 +265,6 @@ def generate_problem(): problem_type = random.choices(types, weights=weights)[0] template = TEMPLATES[problem_type] - return generate_binomial() + return generate_tricky() #return template() \ No newline at end of file diff --git a/steps_generator.py b/steps_generator.py index c1c3ac7..c152267 100644 --- a/steps_generator.py +++ b/steps_generator.py @@ -242,6 +242,10 @@ def generate_quadratic_steps (problem): ##Solve the Roots steps.extend(algebraic_steps.solve_roots(current)) + current = steps[-1]["after"] + + # Check for incorrect answers + steps.extend(algebraic_steps.check_roots(problem["problem"], current)) return steps @@ -270,7 +274,10 @@ def generate_difference_squares_steps (problem): ## Step 2 steps.append(algebraic_steps.square_root_both_sides(current)) + current = steps[-1]["after"] + # Check for incorrect answers + steps.extend(algebraic_steps.check_roots(problem["problem"], current)) return steps @@ -310,6 +317,11 @@ def generate_radical_steps (problem): 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"] + + # Check for incorrect answers + steps.extend(algebraic_steps.check_roots(problem["problem"], current)) return steps @@ -353,12 +365,14 @@ def generate_binomial_steps (problem): current = problem["problem"] ## Distribute Terms + init_printing(order='none') last_len = -1 while last_len != len(steps): last_len = len(steps) - steps.extend(algebraic_steps.distribute_step(current)) + steps.extend(algebraic_steps.distribute_left_step(current)) if len(steps): current = steps[-1]["after"] + init_printing(order='lex') ## Combine Like Terms steps.append(algebraic_steps.combine_like_terms(current)) @@ -376,6 +390,9 @@ def generate_binomial_steps (problem): 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"] ## Divide by coefficient a = left_expr.coeff(x) @@ -390,6 +407,38 @@ def generate_binomial_steps (problem): def generate_tricky_steps (problem): #(x² - x - a) / (x + b) = c steps = [] + x = symbols('x') + current = problem["problem"] + + # Multiply Denom + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + right_expr = parse_expr(right) + num, den = left_expr.as_numer_denom() + steps.append(algebraic_steps.multiply_both_sides(current, den)) + current = steps[-1]["after"] + + # Distribute out right side + init_printing(order='none') + steps.extend(algebraic_steps.distribute_right_step(current)) + current = steps[-1]["after"] + init_printing(order='lex') + + # Move Everything to Left + steps.append(algebraic_steps.move_all_to_one_side(current)) + current = steps[-1]["after"] + + # Combine Like Terms + steps.append(algebraic_steps.combine_like_terms(current)) + current = steps[-1]["after"] + + # Solve Quadratic + print(f"calling generate_quadratic_steps with: {current}") + steps.extend(generate_quadratic_steps({"problem" : current})) + current = steps[-1]["after"] + + # Check for incorrect answers + steps.extend(algebraic_steps.check_roots(problem["problem"], current)) return steps