Fattore di espressione quadratico e risolutore

Aug 22 2020

Ho deciso di creare un programma in grado di fattorizzare e risolvere espressioni quadratiche in micropython, dove la libreria standard è limitata, e non ho idea di come implementare moduli esterni su di essa, quindi ho dovuto creare questo programma da zero.

La semplice premessa è che può fattorizzare e risolvere la maggior parte delle espressioni quadratiche e visualizzare le soluzioni in modo da rendere più semplice il controllo della soluzione.

Ho documentato parte del programma per rendere più facile capire cosa fa ogni parte del programma. Vorrei qualche consiglio per ottimizzare e rendere il programma più efficiente e compatto, basato sull'idea di micropython.

# quadratic factorer, and solver
from math import sqrt


def is_integer(n):
    """
    checks if the float given is an integer
    True - float can be an integer
    False - float is not an integer
    """
    return int(n) == n
    

def gcd(*values):
    """
    finds the greatest common divisor of values
    and returns the absolute value of the divisor
    """
    x, *b = values
    for y in b:
        while y != 0:
            (x, y) = (y, x % y)
    return abs(x)


def isclose(a, b, tolerance):
    """
    checks whether the difference between the two values are smaller or equal to the tolerance
    return True - yes
    return False - no
    """
    return abs(a-b) <= tolerance


def fraction(a, factor=0, tolerance=0.01):
    """
    Uses brute force, to turn a float into a fraction
    if a is a whole number, then it is returned.
    if a is a float, then the closest possible fraction to tolerance level of difference
        and returns a fraction in string format.
    """
    while True:
        factor += 1
        a_rounded = int(round(a*factor))
        if isclose(a*factor, a_rounded, tolerance):
            break
    if factor == 1:
        return a_rounded
    else:
        return "{}/{}".format(a_rounded, factor)


def simplify_fraction(numer, denom):
    """
    simplifies a fraction, to a simpler form
    """
    if denom == 0:
        return None, None
    # Remove greatest common divisor:
    common_divisor = gcd(numer, denom)

    return numer // common_divisor, denom // common_divisor


def get_determinant(a, b, c):
    """
    returns the determinant of a polynomial ax^2 + bx + c
    """
    return b**2 - 4*a*c


def factors(n):
    """
    finds the factors of n, and returns a list of factors (unordered)
    """
    return list(set(x for tup in ([i, n//i] 
                for i in range(1, int(sqrt(n))+1) if n % i == 0) for x in tup))


def simplify_sqrt(n):
    """
    simplifies the n in sqrt(n)
    and turns it into a surd

    return values:
    (x, y) --> xsqrt(y)
    - x is the coefficient of the surd
    - y is the value remaining in the sqrt

    (0, y) --> sqrt(y)
    (y, 0) --> y
    """
    perfect_square = None
    float_to_int = lambda x: int(x) if is_integer(x) else x
    for factor in sorted(factors(n), reverse=True)[:-1]:
        if is_integer(sqrt(factor)):
            perfect_square = factor
            break

    if perfect_square == n:
        return (int(sqrt(perfect_square)), 0)

    elif perfect_square:
        factor1 = sqrt(perfect_square)
        factor2 = n / perfect_square
        return (float_to_int(factor1), float_to_int(factor2))

    else:
        return (0, n)


def format_tuple_to_sqrt(A, B): # Asqrt(B) 
    """
    turns a tuple from simplify_sqrt to an actual string representation.
    """
    if A == 0:
        A = ""
    elif B == 0:
        return str(A)
    return "{}sqrt({})".format(A, B)


def solve_completing_the_square(a, b, c): 
    """
    ( x +- ysqrt(B) )/z
    acquires the values of x, y, B, and z by reverse engineering the solutions
    and returns them
    """
    f = simplify_sqrt(get_determinant(a, b, c))
    g = gcd(f[0], 2*a, -b)
    # x, y, B, z
    return -b/g, [int(f[0]/g), f[1]], (2*a)/g # x, (h[0], h[1]), z


def format_complete_the_square_solutions(x, h, z):
    """
    h = (y, B) --> ysqrt(B)
    acquires the x, h, and z 
    and formats a proper string representation for the solution using complete the square

    if z is 1
    then no '/1' is shown.
    """
    # ( x +- h[0]sqrt(h[1]) )/z
    h[0] = 0 if h[0] == 1 else h[0]
    h = format_tuple_to_sqrt(*h)
    if z < 0:
        x, z = x*-1, z*-1

    sol1 = "( {} + {} )/{}".format(int(x), h, int(z))
    sol2 = "( {} - {} )/{}".format(int(x), h, int(z))
    if z == 1:
        return sol1[:-2], sol2[:-2]
    return sol1, sol2


def solve_quadratic_equation(a, b, c):
    """
    returns a tuple of solutions, if a polynomial abc, has atleast 1 solution, else returns None
    formula = (-b+-sqrt(b^2-4ac))/2a
    """
    # two solutions, or one solution
    if get_determinant(a, b, c) >= 0:
        return ( (-b+sqrt(get_determinant(a, b, c))) / (2*a), (-b-sqrt(get_determinant(a, b, c))) / (2*a)) # (x1, x2)
    # no solutions
    else:
        return None, None


def factor_quadratic_equation(a, b, c):
    """
    factors the quadratic polynomial a, b, c on multiple conditions
    support when
    1) c = 0
    2) b = 0 (if perfect square)
    3) a, b, c present
    4) complete the square is involved
    """
    get_sign = lambda x: "+" if x > 0 else "-" # set the sign based on x's value
    flip_sign_if_negative = lambda x, sign: -x if sign == '-' else x # switch the signs for formatting if sign == '-'
    float_to_int = lambda x: int(x) if is_integer(x) else x # only if the float is actually an integer like 3.0

    if a < 0:
        a, b, c = a/-1, b/-1, c/-1

    if c == 0: # factor by gcf 6x^2 - 2x
        gcf = gcd(a, b)
        a, b = a/gcf, b/gcf
        gcf = "" if gcf == 1 else gcf

        sign = get_sign(b)
        b = flip_sign_if_negative(b, sign)

        return "{}x({}x{}{})".format(float_to_int(gcf), fraction(a), sign, fraction(b))

    else: 
        denom = 2*a
        x1, x2 = solve_quadratic_equation(a, b, c)
        if x1 and x2:
            x1_numer, x2_numer = x1*denom, x2*denom
        else:
            x1_numer = x2_numer = None

        if (not x1 and not x2) or not (is_integer(x1_numer) and is_integer(x2_numer)) or not is_integer(denom): 
        # factor by completing the square 2(x+3) + 1
        # (x+p)^2 + q
            global completing_the_square
            completing_the_square = True

            if a != 1:
                a, b, c = a/a, b/a, c/a

            p = b/(2*a)
            q = c - (b**2)/(4*a)

            sign1 = get_sign(p)
            sign2 = get_sign(q)
            p = flip_sign_if_negative(p, sign1)
            q = flip_sign_if_negative(q, sign2)

            return "(x{}{})^2 {} {}".format(sign1, fraction(p), sign2, fraction(q))

        else: 
        # normal factoring (x+3)(x+3)
            x1_gcd, x2_gcd = gcd(x1_numer, denom), gcd(x2_numer, denom)
            x1_numer, x2_numer = -x1_numer/x1_gcd, -x2_numer/x2_gcd
            x1_denom, x2_denom = denom/x1_gcd, denom/x2_gcd
            gcf = gcd(a, b, c)*a/abs(a)

            sign1 = get_sign(x1_numer)
            sign2 = get_sign(x2_numer)
            x1_numer = flip_sign_if_negative(x1_numer, sign1)
            x2_numer = flip_sign_if_negative(x2_numer, sign2)

            return "{}({}x{}{})({}x{}{})".format(float_to_int(gcf) if gcf != 1 else "", fraction(x1_denom) if x1_denom != 1 else "", sign1, fraction(x1_numer), fraction(x2_denom) if x2_denom != 1 else "", sign2, fraction(x2_numer))



while True:
    completing_the_square = False
    a = float(input("insert a: "))
    b = float(input("insert b: "))
    c = float(input("insert c: "))
    factored_form = factor_quadratic_equation(a, b, c)
    solutions = solve_quadratic_equation(a, b, c)

    print(factored_form) if factored_form else print("No Factored Form")

    if solutions[0]:
        if completing_the_square:
            solution0_fraction, solution1_fraction \
                = format_complete_the_square_solutions(*solve_completing_the_square(a, b, c))

        else:
            solution0_fraction = "" if is_integer(solutions[0]) else fraction(solutions[0])
            solution1_fraction = "" if is_integer(solutions[1]) else fraction(solutions[1])

        solution1 = "x1 = {}".format(round(solutions[0], 5)) if solution0_fraction == "" else "x1 = {} or\n{}".format(round(solutions[0], 5), solution0_fraction)
        solution2 = "x2 = {}".format(round(solutions[1], 5)) if solution1_fraction == "" else "x2 = {} or\n{}".format(round(solutions[1], 5), solution1_fraction)

        print(solution1)
        print(solution2) if solutions[0] != solutions[1] else None

    else:
        print("No Solution")

    stop = input("'x' to stop: ")
    if stop == 'x':
        break

Risposte

4 DavidK Aug 23 2020 at 02:27

C'è un posto in factor_quadratic_equationcui il valore di ritorno di flip_sign_if_negative(x,sign)è qualcosa di diverso dal valore assoluto di x? In caso contrario, consiglierei di utilizzare il valore assoluto, poiché è già una funzione familiare.

Perché a/-1piuttosto che -a?

Le parti semplici sono ben documentate (sebbene la maggior parte di esse sarebbe facile da capire anche senza documentazione), ma poi ci sono parti complicate con poche o nessuna spiegazione. E non sono convinto che tu abbia pensato molto a quello che vuoi veramente factor_quadratic_equationfare.

Hai scritto un algoritmo abbastanza complicato qui. L'hai testato per vedere se i risultati sono quelli che ti aspettavi?

Ho copiato le tue funzioni in Python 3.8.3 e ho provato alcuni miei esempi.

factor_quadratic_equation(1,4,3)ha restituito '(x + 1) (x + 3)'. Quello è buono.

factor_quadratic_equation(0.5,2,1.5)ha restituito '0,5 (x + 1) (x + 3)'. Anche buono.

factor_quadratic_equation(0.125,0.5,0.375)ha restituito '(x + 2) ^ 2 - 1'. Che cosa? Perché la risposta non è "0,125 (x + 1) (x + 3)"? In che modo '(x + 2) ^ 2 - 1' è considerato lo stesso polinomio di (1/8) x ^ 2 + (1/2) x + (3/8), per non parlare di essere considerato una fattorizzazione di quel polinomio?

Posso capire che quando un vero quadratico non ha zeri, e quindi letteralmente non può essere scomposto in veri monomi, potresti ricorrere alla rappresentazione del vertice come spiegazione utile, ma questa funzione sembra fin troppo desiderosa di ricadere su quella rappresentazione per quadratiche con zeri.

factor_quadratic_equation(1.33,1.2,0)restituisce
"1.1102230246251565e-15x (1197957500880552x + 1080863910568919)".
Suppongo che questo abbia qualcosa a che fare con le rappresentazioni inesatte di 1.33 e 1.2 in IEEE 754, ma sembra bizzarro.

factor_quadratic_equation(133,120,0) ha prodotto un traceback, in fondo al quale era

ValueError: valore letterale non valido per int () con base 10: ''

E tuttavia factor_quadratic_equation(133/2,120/2,0)restituisce "0,5x (133x + 120)", come ci si potrebbe aspettare.

factor_quadratic_equation(6,5,0) ha anche prodotto un traceback.

Quali pensi dovrebbero essere i risultati in tutti questi casi? Ho ancora alcune domande sullo stile di codifica, ma penso che un comportamento corretto sia una priorità ancora più alta.