#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Classe des polynômes.

fichiers : polynomes.py
@author: bob
"""

# Import ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import numpy as np # pour la fonction arange
import matplotlib.pyplot as plt # module permettant de générer le graphique

# Définition de classe ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Polynome:
 
    def __init__(self, liste_coefs=[0], domaine=None): # méthode constructeur de la classe
 
        self.coefs = liste_coefs # on définit la liste des coefficients du polynôme [a0, a1, ..., an]
        self.reduire() # suppression si nécessaire des zéros en queue de liste de coefficients. Exemple : [2, 3, 1, 0, 0] -> [2, 3, 1]
 
        self.domaine = domaine # on définit l'intervalle [x0, xn] représentant le domaine de définition de la fonction
 
    def __str__(self): # permet d'afficher le polynôme sous la forme 1 + 2x + 3x^2
        s="" # initialisation de la chaîne de caractères
        # on vérifie d’abord si le degré du polynôme est nul
        if (len(self.coefs)-1==0):
            return str(self.coefs[0])
        else: # sinon
            if self.coefs[0]!=0:
                s=str(self.coefs[0]) + " + "
            for i in range(1, len(self.coefs)): # parcours des indices des coefficients du polynôme : [a1, a2, ..., an]
                if self.coefs[i]!=0: # si le coefficient de degré i n'est pas nul
                    if self.coefs[i]!=1: # si le coefficient de degré i est différent de 1
                        s+="{}.X^{} + ".format(self.coefs[i],i)
                    else: s+="X^{} + ".format(i)      
            return s[:-3] # on retourne l'expression du polynôme
 
    def eval_degre(self): # retourne le degré du polynôme
        return (len(self.coefs)-1)
 
    def __add__(self, other): # méthode permettant de redéfinir l'opérateur « + » pour 2 polynômes : (1 + 2x + x^2) + (1 + x) = 2 + 3x + x^2
        # p1 = self, p2 = other     
        if len(other.coefs) >len(self.coefs): # si degré de p2 > degré de p1 
            poly = other; n = len(self.coefs) # on repère le polynôme de degré le plus élevé et la longueur de la liste de coefs la plus petite. 
        else: poly = self; n = len(other.coefs) # sinon, ...
 
        for i in range(n): # parcours des indices de poly.coefs
            poly.coefs[i] = self.coefs[i] + other.coefs[i] # addition des coefficients de degré i
 
        poly.reduire() # suppression si nécessaire des zéros en queue de liste. Exemple : [2, 3, 1, 0, 0] -> [2, 3, 1]
 
        return poly # renvoie le polynôme résultat de l'addition
 
    def reduire(self):
        # tant que le dernier élément de la liste est nul
       while self.coefs[-1] == 0 and len(self.coefs)>1:
            self.coefs.pop() # supprimer le dernier élément
 
    def __mul__(self, other): # méthode permettant de redéfinir l'opérateur « * » pour 2 polynômes : (1 + x) * (1 + 2x) =  1 + 3x + 2x^2
        # initialisation de la liste des coefficients qu'avec des zéros
        liste_coefs=[0]*(len(self.coefs)+len(other.coefs)-1) # exemple : [0, 0, 0]
        for i1 in range(len(self.coefs)): # parcours des indices des coefs du polynôme n°1
            for i2 in range(len(other.coefs)): # parcours des indices des coefs du polynôme n°2
                # multiplication des coefficients d'indices i1 et i2
                liste_coefs[i1+i2] = liste_coefs[i1+i2] + self.coefs[i1]*other.coefs[i2]
 
        poly=Polynome(liste_coefs, self.domaine) # création de l'objet Polynome basé sur la liste
 
        return poly # renvoie le polynôme résultat de la multiplication
 
    def __pow__(self, n): # méthode permettant de redéfinir l'opérateur de puissance : self ** n
        # on crée l'objet poly à partir d'une liste contenant un seul élément 1, élément neutre pour la multiplication des polynômes
        poly = Polynome([1],self.domaine) 
 
        for i in range(n): # on multiplie n fois poly par self à l'aide de l'opérateur *            
            poly = poly*self # équivalent à : poly = poly.__mul__(self)
 
        return poly # renvoie le polynôme résultat de l'opération (self ** n)
 
    def __eq__(poly1, other): # méthode permettant de redéfinir l'opérateur « == » pour 2 polynômes
 
        return (poly1.coefs==other.coefs) # renvoie True si les 2 liste de coefficients sont égales
 
    def eval(self,x): # méthode permettant d'évaluer le polynôme en fonction de x
        # intialisation des variables
        valeur_polynome = self.coefs[0] # valeur_polynome = a0
        power=1    
 
        for coef in self.coefs[1:]: # parcours des coefficients du polynôme à partir de a1 : a1, a2, ..., an
            power = power*x # calcul de la puissance de x pour ai : power = x^i
            valeur_polynome += coef*power # valeur_polynome = valeur_polynome + ai*x^i
 
        return valeur_polynome # renvoie la valeur du polynôme
 
    def eval_horner(self,x): # méthode permettant d'évaluer le polynôme en fonction de x
        # intialisation de la variable
        valeur_polynome = self.coefs[-1] # valeur_polynome = an
 
        for coef in reversed(self.coefs[:-1]): # parcours des coefficients du polynôme à partir de an-1 : an-1, ..., a1, a0
            valeur_polynome = valeur_polynome*x + coef  # valeur_polynome = valeur_polynome*x + ai
 
        return valeur_polynome # renvoie la valeur du polynôme
 
    # Fonction polynomial
    def expr_fonc(self): # affiche le polynôme sous la forme 1 + 2x + 3x^2
        s="" # initialisation de la chaîne de caractères
        # on vérifie d’abord si le degré du polynôme est nul
        if (len(self.coefs)-1==0):
            return str(self.coefs[0])
        else: # sinon
            for i in range(len(self.coefs)-1,0,-1): # parcours des indices des coefficients du polynôme : [a1, a2, ..., an]
                if self.coefs[i]!=0: # si le coefficient de degré i n'est pas nul
                    if self.coefs[i]!=1: # si le coefficient de degré i est différent de 1
                        s+="{}.x^{} + ".format(self.coefs[i],i)
                    else: s+="x^{} + ".format(i)
 
            if self.coefs[0]!=0:
                s+=str(self.coefs[0]) + " + "
 
            return s[:-3] # on retourne l'expression du polynôme
 
    def __call__ (self, x): # permet d'appeler la fonction eval() en faisant f(2) au lieu de f.eval(2)
        return self.eval(x)
 
    def tracer(self, x0=None, xn=None, pas=0.1): # trace la courbe représentant la fonction polynomiale sur l'intervalle [x0, xn]
 
        if x0==None and xn==None: # si les arguments x0 et xn sont ommis, on trace la courbe sur l'intervalle correspondant au domaine de définition
            if self.domaine!=None: # si le domaine de la fonction est défini
                x0 = self.domaine[0]; xn = self.domaine[1]
            else: print("Pas de domaine de définition pour la fonction !"); return 
 
        # on crée la liste des valeurs d'abscisse entre x0 et xn 
        x = np.arange(x0, xn, pas) # appel de la fonction arange de la librairie numpy 
 
        # on crée la liste des yi en évaluant la fonction polynomiale pour chaque xi : self(xi) est équivalent à self.eval(xi)
        y = [self(xi) for xi in x]
 
        # trace la courbe qui relie les points dont les abscisses x et les ordonnées y sont fournies en arguments. 
        plt.plot(x, y)
        plt.title('f(x) = '  + self.expr_fonc()) # copie du titre du graphique
        plt.show() # affiche la figure à l'écran
 
    def deriver(self): # renvoie la dérivée de la fonction polynomiale f(x)
        # initialisation de la liste des coefficients qu'avec des zéros
        liste_coefs=[0]*len(self.coefs) # exemple : [0, 0, 0, 0, 0]
        for i in range(len(self.coefs)-1):
            liste_coefs[i] = self.coefs[i+1]*(i+1)
 
        return Polynome(liste_coefs, self.domaine)
 
# Programme principal ==========================================================
## addition de 2 polynômes
p1 = Polynome([1, 2, 1]) # création du 1er objet de la classe Polynome : 1 + 2x + x^2
p2 = Polynome([1, 1]) # création du 2e objet Polynome : 1 + x
 
print(p1+p2) # affiche le résultat de l'addition de p1 et p2
 
# Multiplication de 2 polynômes
p1 = Polynome([1, 1]) # création du 1er objet de la classe Polynome : 1 + x
p2 = Polynome([1, 2]) # création du 2e objet de la classe Polynome : 1 + 2x
 
print(p1*p2) # affiche le résultat de la multiplication de p1 par p2
 
# Exponnetiation d'un polynôme
print(p1**3) # affiche le résultat de p1 à la puissance 3
 
# Création d'un objet fonction polynomiale
domaine_definition = [-5, 5] # intervalle représentant le domaine de définition de la fonction
 
f = Polynome([2, 1, 2], domaine_definition) # création de l'objet Polynome à partir de la liste de coefficients [2, 1, 2] et du domaine de définition de la fonction
 
print(f(2)) # affiche la valeur de la fonction polynomiale pour x=2
 
# trace la courbe représentant la fonction polynomiale sur l'intervalle [-5, 5] avec un pas de 0.1
f.tracer()
