'''
Skrevet av: Vegard G. Jervell
Hensikt: Hjelpe folk med å lære alt de ikke lærte i ITGK
Spesifikt: SymPy
'''
import sympy as sp
import scipy.constants as const
import numpy as np

# Poenget med sympy er å gjøre algebra. SymPy forstår at sqrt(2) er et irrasjonelt tall, og regner derfor
# med sqrt(2) som sqrt(2) og ikke 1.4... sånn som python vanligvis gjør
# det samme gjelder for logaritmer, pi osv.
# Dette gjør at vi kan gjøre symbolsk, eksakt matte helt frem til evalueringstidspunktet.

V_array = np.array([0.1, 0.7, 0.9, 1.5, 2.3, 2.9]) #Lager litt arrays som vi skal bruke senere til demo
T_array = np.array([20, 19, 22, 21, 23, 20])
n_array = np.array([1, 2, 2.5, 3, 3.25, 3.45])

V, n, T = sp.symbols('V, n, T') #definere symboler, disse er variabler i ordets matematiske forstand
R = const.gas_constant #gasskonstanten

p = n * R * T / V #Dette er en "sympy.Expr' altså et matematisk, algebraisk uttrykk, ikke en python funksjon

dp_dV = sp.diff(p, V) #partiellderivere p mhp. V
dp_dT = sp.diff(p, T) #partiellderivere p mhp. T

w = - sp.integrate(p, V) #w = -pdV, ubestemt integral
w1 = - sp.integrate(p, (V, 2, 5)) # Bestemt integral. Dette blir eksakt, altså ikke et flyttall.

w1_val = w1.subs({T:298, n:1}) #Sett inn verdier for T og n, returnerer: 2477.7090204 * ( log(2) - log(5) )
w1_val = w1_val.evalf() #Evaluer logaritmene for å få et tall

vars = p.atoms(sp.Symbol) #returnerer et sett med alle variablene til p. I dette tilfellet {n V T}
                            # NB: merk at et sett ikke har en bestemt rekkefølge, den er tilfeldig!

p_func = sp.lambdify((n, T, V), p, 'numpy') #p_func blir en funsjon du kan sende in numpy arrays eller vanlige python variabler til
# lambdify er det vi bruker for å gjøre et eksakt sympy uttrykk om til en funksjon vi kan bruke som en vanlig python funksjon.

trykk_i_en_tilstand = p_func(1,2,3) #gir et tall
trykk_i_flere_tilstander = p_func(n_array, T_array, V_array) #Gir en array med tall

def sigma_p(n_vals, T_vals, V_vals): #gausse usikkerhet i p
    sigma_V = 1
    sigma_T = 0.1
    sigma_n = 0.05

    dp_dV = sp.diff(p, V) #Partiellderiver symbolsk
    dp_dV = sp.lambdify((n, T, V), dp_dV, 'numpy') #Gjør uttrykket til en kallbar funskjon

    dp_dn = sp.diff(p, n)
    dp_dn = sp.lambdify((n, T, V), dp_dn, 'numpy')

    dp_dT = sp.diff(p, T)
    dp_dT = sp.lambdify((n, T, V), dp_dT, 'numpy')

    return np.sqrt((dp_dV(n_vals, T_vals, V_vals) * sigma_V) ** 2 +
                   (dp_dT(n_vals, T_vals, V_vals) * sigma_T) ** 2 +
                   (dp_dn(n_vals, T_vals, V_vals) * sigma_n) ** 2) #Gauss feilforplantningslov

def sigma_generell(func, vals, sigmas): #gausser alt :D
    #vals og sigmas er dicts på formen
    # vals = {'x1' : [0, 1, 2, 3 ...], 'x2' : [5, 6, 3, ...] osv.}
    # sigmas = {'x1' : 0.1, 'x2 : 0.3, 'x3' : 0.15 osv.}
    # 'key'-strengene må ha samme navn som variablene til 'func'

    df_dx = [sp.diff(func, var) for var in func.atoms(sp.Symbol)] #En liste med de symbolske partiellderiverte til f

    vars = np.array([x for x in func.atoms(sp.Symbol)]) #en liste, denne har en låst rekkefølge
    var_strs = np.array([str(x) for x in vars]) # en liste med navnet til hver variabel som streng, i samme rekkefølge som vars

    df_dx_lambdas = []
    for i in range(len(df_dx)):
        # konverterer de symbolske deriverte til kallbare funksjoner og legger dem i en liste
        df_dx_lambdas.append(sp.lambdify([vars], df_dx[i], 'numpy'))

    # Nå skal vi sørge for at 'vals' og 'sigmas' har samme rekkefølge som 'df_dx_lambdas'
    # sånn at vi ganger riktig partiellderivert med riktig usikkerhet
    vals = [vals[k] for k in var_strs] # vals[k] henter verdiene til variabelen 'k'
    sigmas = [sigmas[k] for k in var_strs] # sigmas[k] heter usikkerheten til variabelen 'k'

    #Gauss' feilforplantningslov
    return np.sqrt(np.sum([ (df_dx_lambdas[i](vals) * sigmas[i]) ** 2 for i in range(len(df_dx_lambdas))]))