Files
Youtube_Math/algebraic_steps.py
2026-05-05 15:43:42 -04:00

743 lines
25 KiB
Python

#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