#All Rights Reserved John Salguero #Steps that are generated from sympy import * import re 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 = clean(parse_expr(left, transformations=transformations, evaluate=False)) right_expr = clean(parse_expr(right, transformations=transformations, evaluate=False)) new_expr = f"({sstr(left_expr)}) - ({sstr(right_expr)}) = 0" step["after"] = new_expr step["step"] = f"Subtract {sstr(clean(right_expr))} from both sides" step["left"] = f"-{sstr(clean(right_expr))}" step["right"] = f"-{sstr(clean(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 = 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"Add {sstr(value)} to both sides" step["left"] = f"+{sstr(value)}" step["right"] = f"+{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 = 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 {sstr(value)} from both sides" step["left"] = f"-{sstr(value)}" step["right"] = f"-{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["left"] = f"÷{sstr(value)}" step["right] = f"÷{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) if left_expr != 1 and left_expr != -1 and value != 1 and value != -1: new_left_expr = cancel(left_expr * value) elif left_expr == 1: new_left_expr = value elif value == 1: new_left_expr = left_expr elif left_expr == -1: new_left_expr = -value else: new_left_expr = -left_expr if right_expr != 1 and right_expr != -1 and value != 1 and value != -1: new_right_expr = safe_format(Mul(right_expr, value, evaluate=False)) elif right_expr == 1: new_right_expr = value elif value == 1: new_right_expr = right_expr elif right_expr == -1: new_right_expr = -value else: new_right_expr = -right_expr step["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" step["step"] = f"Multiply both sides by {sstr(value)}" step["left"] = f"×{sstr(value)}" step["right"] = f"×{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 = {} 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): # Collect factors of 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 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"Split the x coefficient to two terms that multiply to the first coefficient({sstr(Abs(a))}) " f"times last coefficient({sstr(Abs(c))}) = ({sstr(ac)}) and {action} {sstr(Abs(b))}" ) steps[-1]["rule"] = "Factoring by grouping" ## 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) base = gcd(sum(terms[2:]), factored_part1) div = simplify(sum(terms[2:]) / base) factored_part2 = simplify((t3 + t4) / div) factored_part2 = Mul(div,factored_part2, evaluate=False) rest = sum(terms[2:]) new_left_expr = Add(factored_part1, rest, evaluate=False) if n != 1: 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 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 = left_expr if n != 1: 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 {sstr(div)} to match the common binomial" steps[-1]["rule"] = "Reverse Distributive Property" ## Factor out base 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] #print(f"coeffs:{sstr(sum(coeffs))}, base:{sstr(base)}") new_expr = Mul(sum(coeffs), base, evaluate=False) new_left_expr = new_expr if n != 1: new_left_expr = flatten_mul(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 common factor ({sstr(base)})" steps[-1]["rule"] = "Reverse Distributive Property" ## Flatten out identical roots optionally if sum(coeffs) == base: steps.append({}) steps[-1]["before"] = steps[-2]["after"] new_left_expr = flatten_mul(Mul(n, Mul(2, base, evaluate=False), evaluate=False)) steps[-1]["after"] = f"{sstr(new_left_expr)} = {sstr(right_expr)}" steps[-1]["step"] = f"Collect the factor ({sstr(base)})" steps[-1]["rule"] = "Collect Like Terms" ## multiply outer number steps.append({}) steps[-1]["before"] = steps[-2]["after"] new_left_expr = flatten_mul(Mul(2*n, base, evaluate=False)) steps[-1]["after"] = f"{sstr(new_left_expr)} = {sstr(right_expr)}" steps[-1]["step"] = f"Multiply Outer Numbers" steps[-1]["rule"] = "Simplify" 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) ## 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) 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"] = "Zero Product Property" current = steps[-1]["after"] a = clean_factor.coeff(x) b = clean_factor.subs(x, 0) if b.is_nonzero: 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"List the potential solutions" steps[-1]["rule"] = "Zero product property" return steps def combine_like_terms(equation): steps = [] steps.append({}) current = equation steps[-1]["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) steps[-1]["after"] = f"{sstr(new_left_expr)} = {sstr(new_right_expr)}" steps[-1]["step"] = "Collect Like Terms" steps[-1]["rule"] = "Combine the like terms" if steps[-1]["before"] == steps[-1]["after"]: steps = [] return steps def distribute_left_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(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 a Valid Solution" 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"List Valid Solutions" steps[-1]["rule"] = "Problem Solved" 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 = [] for arg in args: if arg.is_Add: flat_args.extend(arg.args) else: flat_args.append(arg) return Add(*flat_args, evaluate=False) 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 # remove explicit 1 multipliers def clean(expr): expr = expr.replace( lambda e: isinstance(e, Mul), lambda e: Mul(*[arg for arg in e.args if arg != 1]) ) return 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 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) 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