diff --git a/__pycache__/algebraic_steps.cpython-313.pyc b/__pycache__/algebraic_steps.cpython-313.pyc index c77c17d..b9b1f14 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 48805c7..a9cd479 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 3cae915..2449d1e 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 3ce887f..da05906 100644 --- a/algebraic_steps.py +++ b/algebraic_steps.py @@ -56,8 +56,8 @@ def subtract_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 = 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"Subtract both sides by {sstr(value)}" @@ -101,6 +101,44 @@ def multiply_both_sides(equation, value): step["step"] = f"Multiply both sides by {sstr(value)}" step["rule"] = "Multiplication Property of Equality" return step + +def square_root_both_sides(equation): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + x_generic = symbols('x') + x_pos = symbols('x', positive=True) + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + new_left_expr = sqrt(left_expr.subs(x_generic, x_pos)) + new_right_expr = sqrt(right_expr) + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}, {sstr(new_left_expr)} = -{sstr(new_right_expr)}" + + step["step"] = f"Take the square root of both sides" + step["rule"] = "Square Root Property of Equality" + return step + +def square_both_sides(equation): + step = {} + + current = equation + step["before"] = current + left, right = current.split("=") + + left_expr = parse_expr(left, transformations=transformations, evaluate=False) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + + new_left_expr = clean(left_expr * left_expr) + new_right_expr = clean(right_expr * right_expr) + step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" + + step["step"] = f"Square both sides" + step["rule"] = "Multiplication property of equality" + return step def factor_collect(equation): step = {} @@ -122,6 +160,7 @@ def factor_collect(equation): return step def factor_form_collection(equation, factor): + # Collect factors of factor step = {} current = equation @@ -140,6 +179,180 @@ def factor_form_collection(equation, factor): step["rule"] = "Factor by grouping" return step +def factor_out(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 = clean(cancel(left_expr / factor)) + new_left_expr = Mul(factor, new_left_expr, evaluate = False) + step["after"] = f"{sstr(new_left_expr)} = {sstr(right_expr)}" + + step["step"] = f"Factor out the Greatest Common Factor, {sstr(factor)}" + step["rule"] = "Reverse Distributive Property" + return step + +def trinomial_by_grouping(equation, inner): + # expects n (ax**2+bx+c) = rhs : inner = (ax**2+bx+c), b != 0, c != 0 + + #4 steps + steps = [{}] + + current = equation + steps[-1]["before"] = current + left, right = current.split("=") + x = symbols('x') + + left_expr = parse_expr(left, transformations=transformations) + right_expr = parse_expr(right, transformations=transformations, evaluate=False) + n = simplify(left_expr / inner) + poly = inner.as_poly(x) + a = poly.coeff_monomial(x**2) + b = poly.coeff_monomial(x) + c = poly.coeff_monomial(1) + ac = Abs(a * c) + + ## Split Coeficients + factor1 = Integer(1) + factor2 = ac + while factor1 < factor2: + if ac % factor1 == 0: + factor2 = ac / factor1 + if c.is_negative: + if Abs(factor1 - factor2) == Abs(b): + break + else: + if factor1 + factor2 == Abs(b): + break + factor1 = factor1 + 1 + + if factor1 > factor2: + return [] + + if c.is_negative: + action = "differ by" + if b.is_negative: + left_expr = Add(a * x**2, factor1 * x, -factor2 * x, c, evaluate=False) + else: + left_expr = Add(a * x**2, -factor1 * x, factor2 * x, c, evaluate=False) + else: + action = "add up to" + if b.is_negative: + left_expr = Add(a * x**2, -factor1 * x, -factor2 * x, c, evaluate=False) + else: + left_expr = Add(a * x**2, factor1 * x, factor2 * x, c, evaluate=False) + if n != 1: + new_left_expr = Mul(n, left_expr, evaluate=False) + else: + 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" + + ## Factor Out X on left term + steps.append({}) + steps[-1]["before"] = steps[-2]["after"] + + terms = left_expr.as_ordered_terms() + t1, t2, t3, t4 = terms[0], terms[1], terms[2], terms[3] + factored_part1 = factor(t1 + t2) + factored_part2 = factor(t3 + t4) + rest = sum(terms[2:]) + new_left_expr = Add(factored_part1, rest, evaluate=False) + new_left_expr = Mul(n, new_left_expr, evaluate=False) + + steps[-1]["after"] = f"{sstr(new_left_expr)} = {sstr(right_expr)}" + steps[-1]["step"] = f"Factor out the x from the left two terms of the inner polynomial" + steps[-1]["rule"] = "Reverse Distributive Property" + + ## Factor Out GCD on right term + steps.append({}) + steps[-1]["before"] = steps[-2]["after"] + + left_expr = Add(factored_part1, factored_part2, evaluate=False) + 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]["rule"] = "Reverse Distributive Property" + + ## Add Like Terms + if steps[-1]["before"] != steps[-1]["after"]: + steps.append({}) + steps[-1]["before"] = steps[-2]["after"] + terms = left_expr.as_ordered_terms() + factors = [set(t.as_ordered_factors()) for t in terms] + common = set.intersection(*factors) + base = list(common)[0] + coeffs = [t.coeff(base) for t in terms] + new_expr = sum(coeffs) * base + new_left_expr = flatten_mul(Mul(n, new_expr, evaluate=False)) + + steps[-1]["after"] = f"{sstr(new_left_expr)} = {sstr(right_expr)}" + steps[-1]["step"] = f"Collect the like terms" + steps[-1]["rule"] = "Combine the like terms" + + return steps + +def solve_roots(equation): + # expects n(ax+b)(x+c) = 0 + + #4 steps + steps = [] + + left, right = equation.split("=") + 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)] + + solutions = "" + for factor in x_factors: + clean_factor = clean(factor) + steps.append({}) + if solutions: + solutions = solutions + ", " + steps[-1]["before"] = equation + steps[-1]["after"] = f"{sstr(clean_factor)} = 0" + steps[-1]["step"] = f"Focus on a root" + steps[-1]["rule"] = "Root of a polynomial" + + current = steps[-1]["after"] + a = clean_factor.coeff(x) + b = clean_factor.subs(x, 0) + if b.is_zero == False: + if b.is_negative: + steps.append(add_both_sides(current, -b)) + elif b.is_positive: + steps.append(subtract_both_sides(current, b)) + 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"] + if a != 1 and a != -1: + steps.append(divide_both_sides(current, a)) + elif a == -1: + steps.append(multiply_both_sides(current, a)) + solutions += steps[-1]["after"] + + steps.append({}) + steps[-1]["before"] = equation + steps[-1]["after"] = solutions + steps[-1]["step"] = f"Solved The Roots" + steps[-1]["rule"] = "Root of a polynomial" + + return steps + def combine_like_terms(equation): step = {} @@ -186,6 +399,110 @@ def combine_like_terms(equation): step["rule"] = "Combine the like terms" return step +def distribute_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_left_expr, distributed = distribute_once(left_expr) + + if distributed != None: + steps.append({}) + steps[-1]["before"] = current + steps[-1]["after"] = f"{sstr(safe_format(new_left_expr))} = {sstr(right_expr)}" + steps[-1]["step"] = f"Distribute out {sstr(distributed)}" + steps[-1]["rule"] = "Distributive Law of Multiplication" + + return steps + +def build_ordered_add(args): + expr = args[0] + for a in args[1:]: + expr = Add(expr, a, evaluate=False) + return expr + +def distribute_once(expr): + expr = flatten_mul(expr) + + # ------------------------------------------------------------ + # STEP 1: ONLY HANDLE DIRECT DISTRIBUTION CASES + # (i.e. Mul where one factor is Add) + # ------------------------------------------------------------ + if expr.is_Mul: + + print(f"expr: {sstr(expr)}") + + add_part = None + other_parts = [] + + # extract Add factor + everything else + for arg in expr.args: + print(f"arg: {sstr(arg)}") + + if arg.is_Add and add_part is None: + add_part = arg + else: + other_parts.append(arg) + + # -------------------------------------------------------- + # DISTRIBUTION RULE + # -------------------------------------------------------- + if add_part is not None: + print(f"expr used: {sstr(expr)}, add used: {sstr(add_part)}") + + distributed_value = Mul(*other_parts) + + distributed_terms = [ + Mul(*other_parts, term) + for term in add_part.args + ] + + new_expr = build_ordered_add(distributed_terms) + + return new_expr, distributed_value + + # ------------------------------------------------------------ + # STEP 2: PRIORITY-BASED RECURSION (IMPORTANT FIX) + # ------------------------------------------------------------ + if expr.args: + print(f"step2 args:{expr.args}") + # PASS 1: ONLY distributable Mul(Add(...)) + for i, arg in enumerate(expr.args): + if arg.is_Mul and arg.has(Add): + + new_arg, distributed = distribute_once(arg) + + if new_arg != arg: + new_args = list(expr.args) + new_args[i] = new_arg + return build_ordered_add(new_args), distributed + + # PASS 2: ONLY recurse into Add or structured nodes + for i, arg in enumerate(expr.args): + + # IMPORTANT FILTER: skip pure Mul like -4*x + if arg.is_Mul and not any(a.is_Add for a in arg.args): + continue + + new_arg, distributed = distribute_once(arg) + + if new_arg != arg: + new_args = list(expr.args) + new_args[i] = new_arg + return build_ordered_add(new_args), distributed + + # ------------------------------------------------------------ + # STEP 3: NO CHANGE + # ------------------------------------------------------------ + return expr, None + def clean(expr): # remove explicit 1 multipliers @@ -194,4 +511,30 @@ def clean(expr): lambda e: Mul(*[arg for arg in e.args if arg != 1]) ) + return expr + +def safe_format(expr): + if expr.is_Mul: + args = [] + for a in expr.args: + a = safe_format(a) + if a == 1: + continue + args.append(a) + return Mul(*args, evaluate=False) + + if expr.is_Add: + return expr.func(*[safe_format(a) for a in expr.args], evaluate=False) + + return expr + +def flatten_mul(expr): + if expr.is_Mul: + args = [] + for arg in expr.args: + if arg.is_Mul: + args.extend(arg.args) + else: + args.append(arg) + return Mul(*args, evaluate=False) return expr \ No newline at end of file diff --git a/main.py b/main.py index b2d253d..6e1a396 100644 --- a/main.py +++ b/main.py @@ -3,9 +3,11 @@ from problem_generator import generate_problem from steps_generator import generate_steps +from sympy import init_printing #define the entry point to the programs def main(): + init_printing(order='none') problem = generate_problem() steps = generate_steps(problem); diff --git a/problem_generator.py b/problem_generator.py index 1e2af0a..50fdc25 100644 --- a/problem_generator.py +++ b/problem_generator.py @@ -110,18 +110,24 @@ def generate_like_terms (): @register_problem_generator("quadratic") def generate_quadratic (): #ax² + bx + c = 0 - r1 = random.choice([i for i in range(-10, 16)]) - r2 = random.choice([i for i in range(-10, 16)]) + r1 = 0 + r2 = 0 + while 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]) s = random.choice([i for i in range(-5, 6) if i != 0]) x = symbols('x') - expr = n *(x - r1) * (x - r2) + expr = n *(s * x - r1) * (x - r2) + print(f"n:{n}, s:{s}") expr = expand(expr) - if r1 == r2: - solution = r1 + root1 = Rational(r1, s) + root2 = Integer(r2) + if root1 == root2: + solution = sstr(root1) else: - solution = [r1, r2] + solution = [sstr(root1), sstr(root2)] return { "type": "quadratic", @@ -210,8 +216,14 @@ def generate_binomial (): e = a * (ans + b) + c * (ans + d) x = symbols('x') - left_expr = Mul(a, x + b, evaluate=False) - right_expr = Mul(c, x + d, evaluate=False) + if a != 1: + left_expr = Mul(a, x + b, evaluate=False) + else: + left_expr = x+b + if c != 1: + right_expr = Mul(c, x + d, evaluate=False) + else: + right_expr = x+d expr = Add(left_expr, right_expr, evaluate=False) return { @@ -251,6 +263,6 @@ def generate_problem(): problem_type = random.choices(types, weights=weights)[0] template = TEMPLATES[problem_type] - return generate_like_terms() + return generate_binomial() #return template() \ No newline at end of file diff --git a/steps_generator.py b/steps_generator.py index 8472c2a..2ac1647 100644 --- a/steps_generator.py +++ b/steps_generator.py @@ -37,9 +37,11 @@ def generate_linear_steps(problem): elif b.is_positive: steps.append(algebraic_steps.subtract_both_sides(current, b)) current = steps[-1]["after"] - ## Second Step - if a != 1: + ## Second Step + if a != 1 and a != -1: steps.append(algebraic_steps.divide_both_sides(current, a)) + elif a == -1: + steps.append(algebraic_steps.multiply_both_sides(current, a)) return steps @@ -173,6 +175,7 @@ def generate_like_terms_steps (problem): #ax + bx + c = d steps = [] + x = symbols('x') current = problem["problem"] ## First Step @@ -183,6 +186,23 @@ def generate_like_terms_steps (problem): left, right = current.split("=") left_expr = parse_expr(left) b = left_expr.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)}" + current = steps[-1]["after"] + + ## Third Step + 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 @@ -191,6 +211,38 @@ def generate_quadratic_steps (problem): #ax² + bx + c = 0 steps = [] + x = symbols('x') + current = problem["problem"] + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + right_expr = parse_expr(right) + a = left_expr.coeff(x**2) + b = left_expr.coeff(x) + c = left_expr.subs(x, 0) + div = gcd(a, b, c) + if a.is_negative: + div = -div + + ## First Step + if div != 1 and c.is_zero == False: + steps.append(algebraic_steps.factor_out(current, div)) + current = steps[-1]["after"] + elif c.is_zero: + div = gcd(a, b) + steps.append(algebraic_steps.factor_out(current, div*x)) + current = steps[-1]["after"] + + if c.is_zero == False: + ## Second Steps + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + inner = left_expr / div + steps.extend(algebraic_steps.trinomial_by_grouping(current,inner)) + current = steps[-1]["after"] + + ##Solve the Roots + steps.extend(algebraic_steps.solve_roots(current)) + return steps @register_steps_generator("difference_squares") @@ -198,12 +250,37 @@ def generate_difference_squares_steps (problem): #x² - a² = 0 steps = [] + x = symbols('x') + current = problem["problem"] + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + b = left_expr.subs(x, 0) + + ## Step 1 + 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"] + + ## Step 2 + steps.append(algebraic_steps.square_root_both_sides(current)) + + return steps @register_steps_generator("zero_product") def generate_zero_product_steps (problem): #(x + a)(x + b) = 0 steps = [] + current = problem["problem"] + + steps.extend(algebraic_steps.solve_roots(current)) return steps @@ -211,6 +288,28 @@ def generate_zero_product_steps (problem): def generate_radical_steps (problem): #√(x + a) = b steps = [] + x = symbols('x') + current = problem["problem"] + + + ## First Step + steps.append(algebraic_steps.square_both_sides(current)) + + ## Second Step + current = steps[-1]["after"] + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + b = left_expr.subs(x, 0) + 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"] + 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 @@ -218,6 +317,31 @@ def generate_radical_steps (problem): def generate_fraction_steps (problem): #(x/a) + b = c steps = [] + x = symbols('x') + current = problem["problem"] + + ## First Step + left, right = current.split("=") + left_expr = parse_expr(left, transformations=transformations) + b = left_expr.subs(x, 0) + 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"] + 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"] + + ## Second step + num, den = fraction(left_expr) + if left_expr.subs(x,1).is_negative: + steps.append(algebraic_steps.multiply_both_sides(current, -den)) + else: + steps.append(algebraic_steps.multiply_both_sides(current, den)) return steps @@ -225,6 +349,17 @@ def generate_fraction_steps (problem): def generate_binomial_steps (problem): #a(x + b) + c(x + d) = e steps = [] + current = problem["problem"] + + last_len = -1 + while last_len != len(steps): + last_len = len(steps) + steps.extend(algebraic_steps.distribute_step(current)) + if len(steps): + current = steps[-1]["after"] + + steps.append(algebraic_steps.combine_like_terms(current)) + current = steps[-1]["after"] return steps