'''
Clément de la Salle
Agrégation Physique
ENS de Lyon 2019-2021
'''

import numpy as np
import scipy.optimize as spo
import matplotlib.pyplot as plt
from sympy import *
import re

## Définition des fonctions


def residual(p, y, x, uy, ux, f, dx_f) :
    '''Fonction d'écart pondéré par les erreurs'''
    return (y - f(x, p)) / np.sqrt(uy ** 2 + (dx_f(x, p) * ux) ** 2)

def degrade(ini, fin, N) :
    r_ini, v_ini, b_ini = ini
    r_fin, v_fin, b_fin = fin
    R = [np.linspace(r_ini, r_fin, N)]
    V = [np.linspace(v_ini, v_fin, N)]
    B = [np.linspace(b_ini, b_fin, N)]
    return np.concatenate((R, V, B), axis = 0).transpose()

def super_index(txt, elt) :
    '''Donne tous les indices de chaque occurence de elt dans txt'''
    indices = []
    k = 0
    while elt in txt[k:] :
        k += txt[k:].index(elt)
        indices.append(k)
        k += len(elt)
    return indices

def index(txt, elt) :
    if elt in txt : return txt.index(elt)
    else : return np.inf

def remplacer(txt, avant, apres) :
    '''
    On indique dans les arguments quel élément on veut remplacer par quel autre.
    C'est mieux que la fonction déjà présente 'replace' puisque toutes les transformations se font en même temps.
    '''
    # k = len(args) // 2      # C'est le nombre d'éléments que l'on veut remplacer
    # avant = [args[2 * i] for i in range(k)]
    # apres = [args[2 * i + 1] for i in range(k)]
    i = 0
    nouveau = ''
    ind = 0
    while i < len(txt) :
        truc = np.array([[i + index(txt[i:], elt), j, len(elt)] for j, elt in enumerate(avant)])
        if truc[:, 0].min() < np.inf :
            idx = np.lexsort(truc.transpose())
            truc = np.array([truc[m, :] for m in idx])[::-1]
            ind = int(truc[:, 0].min())
            argind = np.abs(truc[:, 0] - ind).argmin()
            nouveau = nouveau + txt[i : ind] + apres[int(truc[argind, 1])]
            i = ind + int(truc[argind, 2])
        else :
            nouveau = nouveau + txt[i:]
            i = len(txt)
    return nouveau

def nouveau_tableau() :
    txt = input('Rentrer les noms des varibales (séparés par des virgules et osef des espaces\n')
    return txt.replace(' ', '').split(',')


def une_liste(liste) :
    '''Permet de mettre tous les éléments d'une liste de listes à la suite'''
    new = []
    for list in liste :
        for elt in list :
            new.append(elt)
    return new

def super_split(chaines, *args) :
    '''Permet de couper une (ou plusieurs !) chaines de caractère suivant plusieurs éléments en même temps.'''
    if type(chaines) == str :
        chaines = [chaines]
    if len(args) == 1 :
        return une_liste([[*txt.split(args[0])] for txt in chaines])
    else :
        chaines = une_liste([[*txt.split(args[0])] for txt in chaines])
        return super_split(chaines, *args[1:])



## Nouvelle version


alphabet = 'azertyuiopqsdfghjklmwxcvbnAZERTYUIOPQSDFGHJKLMWXCVBN'
chiffres = ['.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
symboles = [' ', '=', '+', '-', '*', '/', '**', '(', ')', '{', '}', '^', '.']
operateurs = ['cos', 'sin', 'tan', 'exp', 'ln', 'log', 'cosh', 'sinh', 'tanh', 'acos', 'asin', 'atan']
operateurs_numpy = ['np.' + op for op in operateurs]

class Graph :
    
    def __init__(self, adresse) :
        
        self.adresse = adresse
        self.nombre_sauvegarde = 0
        
        fichier = open(adresse)
        fichier = fichier.read()
        fichier = re.split('\n|\t', fichier)
        while '' in fichier :
            idx = fichier.index('')
            fichier = fichier[: idx] + fichier[idx + 1:]

        self.k = 0
        while self.k < len(fichier) and fichier[self.k][0] in alphabet :
            self.k += 1
        # Ici self.k est alors le nombre de variables
    
        self.Variables = [var for var in fichier[:self.k]]                  # Nom des variables
        self.Var_symboles = [Symbol(var) for var in self.Variables]         # Leurs symboles (au sens de sympy)
        
        # On cherche les paramètres
        debut_par = None
        l = self.k
        while l < len(fichier) and not fichier[l][0] in alphabet :
            l += 1
        if l < len(fichier) :   # Cas où il existe des paramètres
            debut_par = l       # debut_par stocke l'indice où commence la définition des paramètres dans le fichier source
        
        # On s'occupe des valeurs et incertitudes
        self.Donnees = np.array(fichier[self.k : debut_par]).reshape(len(fichier[self.k : debut_par]) // (self.k), self.k)
        self.Valeurs = np.zeros(np.shape(self.Donnees))             # Va contenir toutes les valeurs
        self.Incertitudes = np.zeros(np.shape(self.Donnees))        # Les incertitudes
        self.Spe = []                                               # Liste des mesures à mettre en avant (notée par !)
        Cache_ind = []                                              # Liste des mesures à ne pas prendre en compte (notée par ?)
        for i in range(len(self.Donnees)) :
            for j in range(self.Donnees.shape[1]) :
                if '!' in self.Donnees[i, j] :
                    if i not in self.Spe :
                        self.Spe.append(i)
                    self.Donnees[i, j] = self.Donnees[i, j][1:]
                elif '?' in self.Donnees[i, j] :
                    if i not in Cache_ind :
                        Cache_ind.append(i)
                    self.Donnees[i, j] = self.Donnees[i, j][1:]
                if '/' in self.Donnees[i, j] :
                    m = self.Donnees[i, j].index('/')
                    self.Valeurs[i, j] = float(self.Donnees[i, j][:m])
                    self.Incertitudes[i, j] = float(self.Donnees[i, j][m + 1:])
                else :
                    self.Valeurs[i, j] = float(self.Donnees[i, j])
        self.Spe = np.array(self.Spe)
        
        # On supprime les valeurs cachées
        Cache_ind = np.array(Cache_ind)
        self.Cache_valeurs = np.zeros((len(Cache_ind), self.k))
        self.Cache_incertitudes = np.zeros((len(Cache_ind), self.k))
        for k, i in enumerate(Cache_ind) :
            self.Cache_valeurs[k] = self.Valeurs[i]
            self.Cache_incertitudes[k] = self.Incertitudes[i]
            self.Valeurs = np.concatenate((self.Valeurs[: i], self.Valeurs[i + 1:]))
            self.Incertitudes = np.concatenate((self.Incertitudes[: i], self.Incertitudes[i + 1:]))
            Cache_ind -= 1
            self.Spe[self.Spe > i] -= 1
        
        # On revient sur les paramètres
        self.nombre_parametres = 0
        if debut_par != None :
            for exp in fichier[debut_par :] :
                self.ajout_parametre(exp)

        
    def ajout_parametre(self, Expression) :
        
        Expression = Expression.replace(' ', '')
        egal = Expression.index('=')
        Nom = Expression[: egal]
        Expression = Expression[egal + 1:]
        
        if '/' in Expression :
            slash = Expression.index('/')
            val = float(Expression[:slash])
            incert = float(Expression[slash + 1:])
        else :
            val = float(Expression)
            incert = .0
    
        self.Variables = [Nom] + self.Variables
        self.Var_symboles = [Symbol(Nom)] + self.Var_symboles
        self.Valeurs = np.concatenate((val * np.ones((len(self.Valeurs), 1)), self.Valeurs), axis = 1)
        self.Incertitudes = np.concatenate((incert * np.ones((len(self.Valeurs), 1)), self.Incertitudes), axis = 1)
        self.k += 1
        self.nombre_parametres += 1
    
    def sauvegarder(self) :
        
        self.nombre_sauvegarde += 1
        fichier = open(self.adresse[: -4] + '(' + str(self.nombre_sauvegarde) + ').txt', 'w')
        Code = '\t'.join(self.Variables[self.nombre_parametres:]) + '\n'
        for i in range(len(self.Valeurs)) :
            if i in self.Spe : Code = Code + '!'
            for j in range(self.nombre_parametres, self.Valeurs.shape[1]) :
                if self.Incertitudes[i, j] == 0 : Code = Code + str(self.Valeurs[i, j]) + '\t'
                else : Code = Code + str(self.Valeurs[i, j]) + '/' + str(self.Incertitudes[i, j]) + '\t'
            Code = Code + '\n'
        Code = Code + '\n'
        for par, val, incert in zip(self.Variables[: self.nombre_parametres + 1], self.Valeurs[0, : self.nombre_parametres + 1], self.Incertitudes[0, : self.nombre_parametres + 1]) :
            if incert == 0 : Code = Code + par + ' = ' + str(val)
            else : Code = Code + par + ' = ' + str(val) + ' / ' + str(incert) + '\n'
        fichier.write(Code)
        fichier.close()
    
    def supprimer_variable(self, nom_var) :
        
        if nom_var in self.Variables :
            ind_var = list(self.Variables).index(nom_var)
            self.Variables = np.concatenate((self.Variables[: ind_var], self.Variables[ind_var + 1 :])).tolist()
            self.Var_symboles = np.concatenate((self.Var_symboles[: ind_var], self.Var_symboles[ind_var + 1 :])).tolist()
            self.Valeurs = np.concatenate((self.Valeurs[:, : ind_var], self.Valeurs[:, ind_var + 1 :]), axis = 1)
            self.Incertitudes = np.concatenate((self.Incertitudes[:, : ind_var], self.Incertitudes[:, ind_var + 1 :]), axis = 1)
        else : return 'Cette variable n\'existe pas déso'
    
    def ajout_variable(self, Expression) :
        
        Expression = Expression.replace(' ', '')
        egal = Expression.index('=')
        Nom = Expression[: egal]
        Expression = Expression[egal + 1:]
        
        L = [truc for truc in self.Var_symboles]     # On est obligés de faire ça sinon le .self fait tout buguer !
        Code_variables = ['L[' + str(ind_var) + ']' for ind_var in range(len(self.Variables))]        
        Expression = remplacer(Expression, self.Variables, Code_variables)
        
        Expression_func = lambdify(L[: self.k], eval(Expression))
        valeurs = Expression_func(*self.Valeurs[:, : self.k].transpose())
        
        Expression_diff = [lambdify(L[: self.k], eval(Expression).diff(var)) for j, var in enumerate(self.Variables)]
        incert = np.sqrt(np.array([(df(*self.Valeurs[:, : self.k].transpose()) * self.Incertitudes[:, j]) ** 2 for j, df in enumerate(Expression_diff)]).sum(axis = 0))
        self.Variables.append(Nom)
        self.Var_symboles.append(eval(Expression))
        self.Valeurs = np.concatenate((self.Valeurs, np.array([valeurs]).transpose()), axis = 1)
        self.Incertitudes = np.concatenate((self.Incertitudes, np.array([incert]).transpose()), axis = 1)
    
    def ajout_mesure(self) :
        
        print('Rentrer une valeur pour chaque variable indépendante et lui associer une incertitude avec un / \n')
        mesure = [input(var + ' = ') for var in self.Variables[: self.k]]
        
        val = []
        inc = []
        for mes in mesure :
            if '/' in mes :
                val.append(float(mes.split('/')[0]))
                inc.append(float(mes.split('/')[1]))
            else :
                val.append(float(mes))
                inc.append(0.0)
        
        L = [truc for truc in self.Var_symboles]
        fonctions = [lambdify(L[:self.k], var) for var in L]
        var_diff = [[var1.diff(var2) for var2 in L[:self.k]] for var1 in L]
        fonctions_diff = [[lambdify(L[:self.k], var2) for var2 in var1] for var1 in var_diff]
        val_complet = [f(*val) for f in fonctions]
        inc_complet = [sqrt(sum([(df(*val) * incert) ** 2 for df, incert in zip(dfs, inc)])) for dfs in fonctions_diff]
        self.Valeurs = np.concatenate((self.Valeurs, [val_complet]), axis = 0)
        self.Incertitudes = np.concatenate((self.Incertitudes, [inc_complet]), axis = 0).astype(np.float)
    
    def tracer(self, *args, **kwargs) :
        
        '''
        On rentre les noms des variables auquelles on s'intéresse :
        - La première sera l'unique abscisse
        - Les suivantes sont les ordonnées à tracer
        Si on veut faire une régression linéaire, on écrit 'regression = True' dans les arguments et alors celle-ci ne se fera que sur la deuxième variable.
        '''
        
        trace = [self.Variables.index(args[i]) for i in range(len(args))]  # Contient les numéros correspondant aux variables à tracer (l'unique abscisse au début)
        
        if 'regression' in kwargs :   
                 
            # Premièrement, cherchons le nombre de parametres et leurs noms
            Caracteres_reconnus = une_liste([chiffres, symboles, operateurs, self.Variables])
            Exp_reg = kwargs['regression']
            if type(Exp_reg) == str :
                Exp_reg = [Exp_reg]
            Exp_reg1 = list(Exp_reg)
            Params = []
            for j, Exp in enumerate(Exp_reg) :
                Params.append([])
                Params[j].append(super_split(Exp, *Caracteres_reconnus))
                Params[j] = une_liste(Params[j])
                while '' in Params[j] :
                    del Params[j][Params[j].index('')]
            Ord_reg = [self.Variables.index(Exp.replace(' ', '').split('=')[0]) for Exp in Exp_reg]   # Contient les numéros des ordonnées à faire fiter
            Exp_reg = [Exp[Exp.index('=') + 1:].replace(' ', '') for Exp in Exp_reg]     # Contient les expressions sans le début ('y = '...)
                  
            L = [truc for truc in self.Var_symboles]
            Code_variables = ['L[' + str(ind_var) + ']' for ind_var in range(len(self.Variables))]
                    
        else : Ord_reg = []
    
        
        x = self.Valeurs[:, trace[0]]
        y = [self.Valeurs[:, trace[i]] for i in range(1, len(args))]
        ux = self.Incertitudes[:, trace[0]]
        uy = [self.Incertitudes[:, trace[i]] for i in range(1, len(args))]
        
        couleurs = degrade((1, .3, 0), (.5, 0, 0), len(y))
        
        yopt = []
        y_num = []
        Popt = []
        Upopt = []
        Maxi = []
        
        for j, numero in enumerate(Ord_reg) :
            
            Exp = Exp_reg[j]
            par = Params[j]
            ord = Ord_reg[j]
            m = trace.index(ord) - 1
            Code_par = ['par[' + str(ind_par) + ']' for ind_par in range(len(par))]
            Exp = remplacer(Exp, par + [self.Variables[trace[0]]], Code_par + ['ooo'])
            f = lambda ooo, par : eval(remplacer(Exp, operateurs, operateurs_numpy))
            ooo = Symbol('ooo')
            Par_symb = [Symbol(p) for p in par]
            truc = eval(remplacer(Exp, ['par'], ['Par_symb']))
            exp_df = eval(remplacer(Exp, ['par'], ['Par_symb'])).diff('ooo')
            exp_df = remplacer(str(exp_df), par, Code_par)
            df = lambda ooo, par : eval(remplacer(exp_df, operateurs, operateurs_numpy))
            
            # C'est l'estimation initiale des paramètres, généralement elle ne joue aucun rôle
            p0 = np.zeros(len(par))
            popt = np.zeros(len(par))
            
            chi2 = 10
            essais = 0
            if not popt.any() :
                while chi2 > 1 and essais < 100:
                # On utilise l'algorithme des moindres carrés non-linéaires (algorythme de Levenberg-Marquardt)
                    result = spo.leastsq(residual, p0, args = (y[m], x, uy[m], ux, f, df), full_output = True)
                    popt = result[0]                                           # Paramètres d'ajustement
                    chi2 = np.sum(np.square(residual(popt, y[m], x, uy[m], ux, f, df))) / (x.size-popt.size)
                    if not ux.all() :
                        ux += x.max() * 0.001
                    else : ux *= 1.1
                    if not uy[m].all() :
                        uy[m] += y[m].max() * 0.001
                    else : uy[m] *= 1.1
                    essais += 1
                    if essais == 10 : print('Échec de la modélisation')
            
            else :
                result = spo.leastsq(residual, p0, args = (y[m], x, uy[m], ux, f, df), full_output = True)
                popt = result[0]                                           # Paramètres d'ajustement
                chi2 = np.sum(np.square(residual(popt, y[m], x, uy[m], ux, f, df))) / (x.size-popt.size)
                
            pcov = result[1]                                               # Matrice de variance-covariance
            if type(pcov) == np.ndarray :
                upopt = np.sqrt(np.abs(np.diagonal(pcov)))                 # Incertitudes-types sur ces paramètres
                chi2 = np.sum(np.square(residual(popt, y[m], x, uy[m], ux, f, df))) / (x.size-popt.size) # On accède aussi au chi2
                
                # Pour arranger l'affichage des résultats, on ne garde qu'un chiffre significatif pour le incertitudes et on donne le résultat à autant de chiffres
                # qu'il en faut pour que l'incertitude soit de l'ordre d'une unité
                
                upopt_affichage = ['%.0e'%u for u in upopt]                 # Incertitude que l'on va afficher (ex: pour 0.0059, uaff = '5e-03')
                upopt_cs = [int(uaff[-3:]) for uaff in upopt_affichage]     # Ordre du premier chiffre significatif (ex : ucs = -3)
                puissance = [int(('%.0e'%r)[-3:]) for r in popt]
                popt_affichage = [str(round((r * 10 ** (- puissance[i])), puissance[i] - upopt_cs[i])) for i, r in enumerate(popt)]
                maxi = [max(puissance[i], upopt_cs[i]) for i in range(len(popt))]
                upopt_affichage = [str(float(uaff) * 10 ** (-maxi[i])) for i, uaff in enumerate(upopt_affichage)]
                
                # Moindres carrés : on divise chaque écart au carré par l'incertitude de ce point
                # Cf. Bup Bérouard
                
                xopt = np.linspace(x.min(), x.max(), 1000)
                reg = remplacer(Exp, ['par', 'ooo'], ['popt', 'xopt'])
                yopt.append(eval(reg))
                y_num.append(numero)
                Popt.append(popt_affichage)
                Upopt.append(upopt_affichage)
                Maxi.append(maxi)
        
        fig = plt.figure()
        ax = fig.add_subplot(111)
        ax.set_xlabel(self.Variables[trace[0]], size = '36')
        positions = list(ax.get_position().bounds)
        positions[1] += 0.04
        ax.set_position(positions)
        ax.tick_params(axis = 'both', labelsize = 20, pad = 5)
        
        incert = ux[0] != 0     # Booléen qui indique si on a donné les incertitudes (on par du principe que si la première valeur a une incertitude
                                # alors elles en ont toutes
                                
        # On trace les segments d'incertitudes
        if incert :
            for j in range(len(y)) :
                ax.plot([], [], color = couleurs[j], marker = '+', markersize = 25, linewidth = 0, label = self.Variables[trace[j + 1]] + ' (mes)')
                for i in range(len(x)) :
                    if i in self.Spe :      # Un point qu'on a voulu marquer en particulier
                        ax.plot([x[i] - 2 * ux[i], x[i] + 2 * ux[i]], [y[j][i], y[j][i]], color = 'blue')
                        ax.plot([x[i], x[i]], [y[j][i] - 2 * uy[j][i], y[j][i] + 2 * uy[j][i]], color = 'blue')
                    else :
                        ax.plot([x[i] - 2 * ux[i], x[i] + 2 * ux[i]], [y[j][i], y[j][i]], color = couleurs[j])
                        ax.plot([x[i], x[i]], [y[j][i] - 2 * uy[j][i], y[j][i] + 2 * uy[j][i]], color = couleurs[j])
        
        # On trace juste des croix si les incertitudes ne sont pas données
        else :
            for j in range(len(y)) :
                ax.plot([], [], color = couleurs[j], marker = 'x', markersize = 5, linewidth = 0, label = self.Variables[trace[j + 1]] + ' (mes)')
                ax.plot(x, y[j], 'o', marker = 'x', markersize = 5, color = couleurs[j])
                for i in self.Spe :
                    ax.plot(x[i], y[j][i], 'o', marker = 'x', markersize = 10, color = 'blue')
        
        for j, y in enumerate(yopt) :
            if type(y) != np.ndarray :
                y = eval(remplacer(str(y), operateurs, operateurs_numpy))
            numero = y_num[j]
            popt_affichage = Popt[j]
            upopt_affichage = Upopt[j]
            maxi = Maxi[j]
            print(Exp_reg1[Ord_reg.index(numero)] + ' : ')
            texte = ''
            idx = Ord_reg.index(numero)
            for p, par in enumerate(Params[idx]) :
                texte += '\t|' + par + ' = (' + popt_affichage[p] + r' +/- ' + upopt_affichage[p] + ')e' + str(maxi[p]) + '\t' + '\n'
            print(texte)
            ax.plot(xopt, y, linestyle = 'dashed', color = couleurs[trace.index(numero) - 1], alpha = 0.5, label = self.Variables[numero] + ' (fit)')
        
        if len(trace) == 2 :
            ax.set_ylabel(self.Variables[trace[1]], size = '36')
        
        ax.legend(prop = {'size' : 20})
        
        # mng = plt.get_current_fig_manager()
        # mng.window.state('zoomed')
        plt.show()


## Synthaxe

# En gros, faut écrire les valeurs dans un fichier texte à côté puis le charger avec la commande G = Graph(adresse)
#       Évidemment on met ce qu'on veut à la place de G, et adresse c'est une chaîne de caractère qui indique où trouver le .txt à ouvrir
#
# La syntaxe à suivre dans le .txt est la suivante :
#
# - La première ligne :
#       On écrit le nom des variables, espacés de tabulations.
# - Les lignes suivantes :
#       On donne les valeurs mesurées en séparant les colonnes par des tabulations et les lignes par des retour à la ligne.
#       Si on veut donner des incertitudes aux valeurs, on les écrit après un / juste après chaque valeur.
#       Si on veut marquer un point en particulier, pour qu'il ressorte sur l'affichage de la courbe, on insere un ! devant n'importe quel valeur de la ligne en question
#       Si on veut masquer un point pour que la modélisation ne le prenne pas en compte sans le supprimer, on fait de même que précédemment avec un ? à la place du !
#           ATTENTION les mesures cachées n'interviennent pas dans les calculs (ajout variables, régressions...) et disparaissent après sauvegarde.
#
# - Après les valeurs il est possible de définir des paramètres :
#       Les paramètres sont comme des constantes fixes, auxquelles on associe un nom, une valeur et une incertitude (prise nulle par défaut)
#
# Ex :  t	u
#       0/.01	0.907775973901153/.02
#       2/.01	0.912763755768538/.02
#       4/.01	0.907775973901153/.02
#       6/.01	0.912763755768538/.02
#       !8/.01	0.912763755768538/.02
#       ?10/.01	2.912763755768538/.02
#
#       c = 3e8
#       m = 550/1.5
#
#       Dans cet exemple, on a effectué 6 mesures pour deux variables. Les incertitudes sur t sont toutes de .01 et celles sur u de .02.
#       La dernière valeur semble s'écarter de la tendance, on décide de ne pas la prendre en compte avec la commande ?
#       L'avant-dernière est un point important (fait en live par exemple), il ressortira en rouge lors de l'affichage. La commande utilisée est !
#       De plus, pour simplifier les calculs, on a posé deux paramètres : c (vitesse de la lumière sans incertitude) et m (par exemple une masse mesurée avec son incertitude associée).


## Détail des méthodes

# Supposons que l'on ait créé un Graph appelé "G".
# G possède plusieurs méthodes (ici seulement celles qui peuvent être utiles) :
#
# - G.Valeurs :
#
#       Renvoie le tableau des valeurs tel que le nombre de lignes soit égal au nombre de mesures et le nombre de colonnes, égal au nombre de variables + paramètres.
#       Les colonnes associées aux paramètres sont donc des colonnes constantes (une seule valeur pour tout la colonne) et sont situés à gauche.
#
# - G.Incertitudes : Même chose mais pour les incertitudes.
#
# - G.Variables : Renvoie une liste qui contient le nom des paramètres et des variables.
#
# - G.adresse : Renvoie l'adresse du fichier utilisé. Ceci sera utile pour sauvegarder les modifications.
#
# - G.k : Renvoie le nombre de parametre + variables
#
# - G.nombre_parametre : Renvoie le nombre de parametres
#
# - G.Cache_valeurs et G.Cache_incertitudes : Comme G.Valeurs et G.Incetitudes mais pour les mesures cachées.
#
# - G.Spe : Indices des mesures importantes.


## Détail des fonctions

# G.supprimer_variable(nom) :
#
#   Cette fonction supprime la variable (ainsi que toutes les valeurs et incertitudes associées) dont le nom (chaîne de caractère) est donné en argument.
#   On peut également supprimer un paramètre avec exactement la même synthaxe.
#
# Ex : A.supprimer_variable('t')
#
#
# G.ajout_variable(expression) :
#
#   Ajoute une variable dont le nom et l'expression sont donnés en argument.
#   On écrit le nom que l'on souhaite donner, suivit d'un = et de l'expression, en faisant explicitement réference aux noms des variables et paramètres mis en jeu.
#
# Ex :  A.ajout_variable('z = 65 * t / (2 * m * exp(u))')
#
# NB : On peut utiliser les opérateurs cos, sin, tan, exp, ln, log, cosh, sinh, tanh, acos, asin, atan.
#
#
# G.ajout_parametre(expression) :
#
#   De même que pour ajouter une variable mais on ne peut entrer que des constantes
#
# Ex : G.ajout_parametre('x = 12/.1')
#
#
# G.ajout_mesure() :
#
#   Cette fonction ne prend pas d'argument. Il est demandé à l'utilisateur de donner les valeurs prises pour chacune des variables indépendantes...
#   Laissez-vous guider :)
#   Si on veut donner l'incertitude associé, on la note en la séparant de la valeur par un /
#   De même que dans le .txt, on peut marquer ce point en ajoutant un ! devant l'une des valeurs données
#
#
# G.tracer(abscisse, *ordonnées, **regression) :
#
#   Permet de tracer autant de variables voulue en fonction d'UNE SEULE abscisse.
#   On peut effectuer une regression (pas nécessairement linéaire) des graphs obtenus, grâce à la synthaxe regression = str / tuple.
#   La chaîne de caractère donnée donne la forme de la régression souhaitée (si on veut en faire plusieurs, on les mets dans un tuple).
#   Les valeurs trouvées pour les parametres (ainsi que leur sincertitudes) sont données dans la console.
#
# Ex :  A.tracer('t', 'z', 'u', regression = ('z = a * t + b', 'u = c * t ** 2 - d * log(t)'))
#
#
# G.sauvegarder() :
#
#   Sauvegarde les modifications dans un fichier texte situé dans le même dossier que le fichier parent.
#   Le nom de la sauvegarde est celui du fichier auquel on ajoute "(i)" où i est le numéro de la sauvegarde.
# 
# Ex : Si on ouvre le fichier "Test.txt" et que l'on est amené à le sauvegarder 3 fois, dans le dossier qui contient "Test.txt", on verra apparaître trois nouveau fichier "Test(1).txt", "Test(2).txt" et "Test(3).txt"
#
# NB : Les mesures cachées seront perdues (?), mais les mesures importantes (!) gardent cette caractéristique.

