Facteur et solveur d'expression quadratique

Aug 22 2020

J'ai décidé de créer un programme capable de factoriser et de résoudre des expressions quadratiques en micropython, où la bibliothèque standard est limitée, et je n'ai aucune idée de comment y implémenter des modules externes, j'ai donc dû créer ce programme à partir de zéro.

Le principe simple est qu'il peut factoriser et résoudre la plupart des expressions quadratiques et afficher les solutions de manière à faciliter la vérification de votre solution.

J'ai documenté une partie du programme pour qu'il soit plus facile de comprendre ce que fait chaque partie du programme. J'aimerais quelques conseils pour optimiser et rendre le programme plus efficace et compact, basé sur l'idée du 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

Réponses

4 DavidK Aug 23 2020 at 02:27

Y a-t-il un endroit factor_quadratic_equationoù la valeur de retour de flip_sign_if_negative(x,sign)est autre chose que la valeur absolue de x? Sinon, je recommanderais d'utiliser la valeur absolue, car c'est déjà une fonction familière.

Pourquoi a/-1plutôt que -a?

Les parties simples sont bien documentées (bien que la plupart d'entre elles soient faciles à comprendre même sans documentation), mais il y a ensuite des parties compliquées avec peu ou pas d'explications. Et je ne suis pas convaincu que vous ayez beaucoup réfléchi à ce que vous voulez vraiment factor_quadratic_equationfaire.

Vous avez écrit ici un algorithme assez compliqué. L'avez-vous testé pour voir si les résultats sont ceux que vous attendiez?

J'ai copié vos fonctions dans Python 3.8.3 et j'ai essayé quelques exemples de ma part.

factor_quadratic_equation(1,4,3)renvoyé '(x + 1) (x + 3)'. C'est bon.

factor_quadratic_equation(0.5,2,1.5)renvoyé '0,5 (x + 1) (x + 3)'. Bien aussi.

factor_quadratic_equation(0.125,0.5,0.375)renvoyé '(x + 2) ^ 2 - 1'. Quoi? Pourquoi la réponse n'est-elle pas «0,125 (x + 1) (x + 3)»? Comment '(x + 2) ^ 2 - 1' est-il même considéré comme le même polynôme que (1/8) x ^ 2 + (1/2) x + (3/8), et encore moins comme une factorisation de ce polynôme?

Je peux comprendre que lorsqu'un quadratique réel n'a pas de zéros et ne peut donc littéralement pas être pris en compte dans de vrais monômes, vous pouvez revenir à la représentation des sommets comme explication utile, mais cette fonction semble trop désireuse de revenir à cette représentation pour les quadratiques avec des zéros.

factor_quadratic_equation(1.33,1.2,0)renvoie
«1.1102230246251565e-15x (1197957500880552x + 1080863910568919)».
Je suppose que cela a quelque chose à voir avec les représentations inexactes de 1.33 et 1.2 dans IEEE 754, mais cela semble bizarre.

factor_quadratic_equation(133,120,0) a produit une trace, au bas de laquelle était

ValueError: littéral invalide pour int () avec base 10: ''

Et factor_quadratic_equation(133/2,120/2,0)renvoie pourtant «0,5x (133x + 120)», comme on pouvait s'y attendre.

factor_quadratic_equation(6,5,0) a également produit un retraçage.

Selon vous, quels devraient être les résultats dans tous ces cas? J'ai encore des questions sur le style de codage, mais je pense qu'un comportement correct est une priorité encore plus élevée.