from sympy.abc import _clash1 # So that sympify will be able to convert string to symbolic objects
from statistics import mean # Average method
from math import log10 # Log base 10
from sympy import * # Module to work with symbolic variables
from util import * # Auxiliar function in  util.py

# EXACT METHODS _______________________________________________________________________________________

# GAUSSIAN ELIMINATION METHOD
def gaussianElimination(coeffs: Matrix, result: Matrix, displayConsoleLog = True):
    # Checks if all coefficients are numeric
    try:
        for coef in coeffs: coef = float(coef)
    except:
        raise Exception("The coefficients must be numeric.")

    # Checks the system's order
    if coeffs.shape[0] == coeffs.shape[1] == result.shape[0] and result.shape[1] == 1:
        order = coeffs.shape[0] # System order (number of equations and variables)
        variables = Matrix([f"x{i}" for i in range(order)])
    else:
        raise Exception("The system is invalid.")

    # Checks if it is an independent linear system
    if coeffs.det() != 0 and displayConsoleLog:
        print("___________________________________________________________________________\n\n")
        print(f"GAUSSIAN ELIMINATION METHOD ::: The linear system is an independent one (has only 1 solution).")
    elif coeffs.det() == 0:
        if displayConsoleLog:
            print("___________________________________________________________________________\n\n")
            print(f"GAUSSIAN ELIMINATION METHOD ::: The linear system has either no or infinite solutions.\n")
            print("___________________________________________________________________________\n\n")

        return

    aug = coeffs.col_insert(order + 1, result) # Augmented Matrix

    if displayConsoleLog:
        print(f"GAUSSIAN ELIMINATION METHOD ::: Step no.{'0'.zfill(len(str(order)))}: augmented matrix [A|b] = ")
        pprint(aug)
        print()

    for j in range(order-1): # Columns
        while True:
            # Swaps elements if needed
            if aug.col(j)[j] == 0:
                i = j

                # Selects the first line below the current whose corresponding element isn't 0
                while aug.col(j)[i] == 0 and i < order: i += 1

                # If no line was found, there is no solution
                if aug.col(j)[i] == 0: raise Exception("No solution was found (main diagonal element equals to 0).")

                aug = swapMatrix(aug, "row", i, j)
                if displayConsoleLog:
                    print(f"GAUSSIAN ELIMINATION METHOD ::: Step no.{str(j+1).zfill(len(str(order)))}: swap rows {j+1} and {i+1}.")

            for i in range(j+1, order): # Rows
                multiplier = aug.row(i)[j] / aug.row(j)[j]
                aug = subMatrix(aug, "row", i, aug.row(i) - multiplier*aug.row(j))

                if displayConsoleLog:
                    print(f"GAUSSIAN ELIMINATION METHOD ::: Step no.{str(j+1).zfill(len(str(order)))}: row{i+1} = row{i+1} - ({multiplier}) * row{j+1}.")

            # If there's no need for more swaping, breaks out of the while loop
            if aug.col(j)[j] != 0: break

        if displayConsoleLog:
            print(f"GAUSSIAN ELIMINATION METHOD ::: Step no.{str(j+1).zfill(len(str(order)))}: augmented matrix [A|b] = ")
            pprint(aug)
            print()

    if displayConsoleLog: print("___________________________________________________________________________\n\n")

    result = aug.col(-1).copy()
    coeffs = aug.copy(); coeffs.col_del(-1)

    sol = solve(coeffs*variables - result, variables)
    sol = Matrix([sol.get(var) for var in variables])

    return coeffs, result, sol

# CHOLESKY'S METHOD
def cholesky(coeffs: Matrix, result = None, displayConsoleLog = True):
    # Checks if all coefficients are numeric
    try:
        for coef in coeffs: coef = float(coef)
    except:
        raise Exception("The coefficients must be numeric.")

    # Checks if the coefficient matrix is a square one
    if coeffs.shape[0] == coeffs.shape[1]:
        order = coeffs.shape[0] # System order
    else:
        raise Exception("The system is invalid.")

    # Checks if the result parameter is a matrix
    if result:
        if type(result) is not Matrix: raise Exception("The 'result' parameter must be a matrix.")
        elif result.shape[0] != order or result.shape[1] != 1: raise Exception("The system is invalid.")
        else:
            variables = Matrix([f"x{i}" for i in range(order)])

    # Checks if it is an independent linear system
    if coeffs.det() != 0 and displayConsoleLog:
        print("___________________________________________________________________________\n\n")
        print("CHOLESKY METHOD ::: The linear system is an independent one (has only 1 solution).")
    elif coeffs.det() == 0:
        if displayConsoleLog:
            print("___________________________________________________________________________\n\n")
            print("CHOLESKY METHOD ::: The linear system has either no or infinite solutions.\n")
            print("___________________________________________________________________________\n\n")

        return

    # Checks if the coefficient matrix is simetric
    if coeffs.is_symmetric() and displayConsoleLog:
        print("CHOLESKY METHOD ::: The coefficient matrix is symmetric.")
    elif not coeffs.is_symmetric():
        if displayConsoleLog:
            print("CHOLESKY METHOD ::: The coefficient matrix is not symmetric.")

        return

    G = []
    for i in range(order): # Rows
        row = []

        for j in range(order): # Columns
            if j < i:
                el = (coeffs.row(i)[j] - sum([(row[k])*(G[j][k]) for k in range(j)]))/G[j][j]

            elif j == i:
                el = sqrt(coeffs.row(j)[j] - sum([(row[k])**2 for k in range(j)]))

            else: # j > i
                el = 0

            if displayConsoleLog and j <= i:
                print(f"CHOLESKY METHOD ::: g_{i+1}{j+1} = % .4f." % el)

            row.append(el)

        G.append(row)

    G = Matrix(G)

    if displayConsoleLog:
        print(f"\nCHOLESKY METHOD ::: G = ")
        pprint(G)

        print(f"\nCHOLESKY METHOD ::: G^T = ")
        pprint(G.T)
        print()

        if not result: print("___________________________________________________________________________\n\n")

    # If a result matrix was passed, it is possible to calculate the system's solution
    if result:
        # Solution:
        # A*x = b --> G*G_T*x = b --> G*y = b
        # G_T*x = y

        auxSol = solve(G*variables - result, variables)
        auxSol = Matrix([auxSol.get(var) for var in variables])

        if displayConsoleLog:
            print("CHOLESKY METHOD ::: A*x = b --> G*G^T*x = b --> G*y = b and G^T*x = y.")
            print("CHOLESKY METHOD ::: G*y = b --> y = ")
            pprint(auxSol)
            print()
            print("___________________________________________________________________________\n\n")

        sol = solve(G.T*variables - auxSol, variables)
        sol = Matrix([sol.get(var) for var in variables])

        return G, sol

    else:
        return G, False

# ITERATIVE METHODS _______________________________________________________________________________________

# JACOBI-RICHARDSON'S (JR) METHOD
def jacobiRichardson(coeffs: Matrix, variables: Matrix, result: Matrix, var0: Matrix, epsilon = 10**-6, MAXITER = 100, displayConsoleLog = True, printTotalIterations = True):
    # Try and convert the variables to symbolic
    try:
        for var in variables: var = sympify(var)
    except:
        raise Exception("The entered variables are invalid.")

    # Checks if all coefficients are numeric
    try:
        for coef in coeffs: coef = float(coef)
    except:
        raise Exception("The coefficients must be numeric.")

    # Checks if there are as many variables as equations
    if coeffs.shape[0] == coeffs.shape[1] == variables.shape[0] == result.shape[0]  and variables.shape[1] == result.shape[1] == 1:
        order = coeffs.shape[0] # System order (number of equations and variables)
    else:
        raise Exception("The system is invalid.")

    # Checks if it is an independent linear system
    if coeffs.det() != 0 and displayConsoleLog:
        print("___________________________________________________________________________\n\n")
        print("JACOBI-RICHARDSON METHOD ::: The linear system is an independent one (has only 1 solution).")
    elif coeffs.det() == 0:
        if displayConsoleLog:
            print("___________________________________________________________________________\n\n")
            print("JACOBI-RICHARDSON METHOD ::: The linear system has either no or infinite solutions.\n")
            print("___________________________________________________________________________\n\n")

        return

    if displayConsoleLog: printTotalIterations = True

    L = []
    D = []
    R = []

    for row in range(order):
        rowL = []
        rowD = []
        rowR = []

        for col in range(order):

            if row > col:
                rowL.append(coeffs.row(row)[col])
                rowD.append(0)
                rowR.append(0)

            elif row == col:
                rowL.append(0)
                rowD.append(coeffs.row(row)[col])
                rowR.append(0)

            elif row < col:
                rowL.append(0)
                rowD.append(0)
                rowR.append(coeffs.row(row)[col])

        L.append(rowL)
        D.append(rowD)
        R.append(rowR)

    L = Matrix(L)
    D = Matrix(D)
    R = Matrix(R)

    if displayConsoleLog:
        print("JACOBI-RICHARDSON METHOD ::: Ax = b --> A = L + D + R.")
        print("JACOBI-RICHARDSON METHOD ::: L = ")
        pprint(L)
        print()

        print("JACOBI-RICHARDSON METHOD ::: D = ")
        pprint(D)
        print()

        print("JACOBI-RICHARDSON METHOD ::: R = ")
        pprint(R)
        print()

    if D.det() == 0:
        if displayConsoleLog:
            print("JACOBI-RICHARDSON METHOD ::: As |D| = 0, the method cannot be used.")

        return

    # A x = b --> x_k = B x_(k-1) + g
    B = - D**-1 * (L + R)
    g = D**-1 * result

    if displayConsoleLog:
        print("JACOBI-RICHARDSON METHOD ::: A x = b --> x_k = B x_(k-1) + g.")
        print("JACOBI-RICHARDSON METHOD ::: B = ")
        pprint(B)
        print()

        print("JACOBI-RICHARDSON METHOD ::: g = ")
        pprint(g)
        print()

    # Checks convergence
    if B.norm(oo) < 1:
        if displayConsoleLog:
            print(f"JACOBI-RICHARDSON METHOD ::: ||B||_oo = {B.norm(oo)} < 1, so there is guarantee of convergence.")
    elif B.norm(1) < 1:
        if displayConsoleLog:
            print(f"JACOBI-RICHARDSON METHOD ::: ||B||_1 = {B.norm(1)} < 1, so there is guarantee of convergence.")
    elif B.norm(2) < 1:
        if displayConsoleLog:
            print(f"JACOBI-RICHARDSON METHOD ::: ||B||_2 = {B.norm(2)} < 1, so there is guarantee of convergence.")
    else:
        if displayConsoleLog:
            print(f"JACOBI-RICHARDSON METHOD ::: ||B||_oo = {B.norm(oo)}, ||B||_1 = {B.norm(1)}, ||B||_2 = {B.norm(2)} are all > 1, so there is no guarantee of convergence.")
    if displayConsoleLog: print()

    # No. of decimal places for the results
    precision = int(abs(log10(epsilon))) if epsilon > 0 and log10(epsilon).is_integer else int(6)

    # Only 2 elements will be used, [0] for even iterations and [1] for odd ones
    x = [var0, 0]

    if displayConsoleLog:
        print("JACOBI-RICHARDSON METHOD ::: Iteration no.00: ", end="")
        for i in range(len(variables)): print(f"{variables[i]} = % .{precision}f" % x[0][i], end = ", ")
        print("||vars_k - vars_(k-1)||_oo =  " + "-"*(precision + 2))

    # Auxiliar index variable correspondent to the current element in the 'x' list
    j = 0

    # Loops through the algorithm until the maximum number of iterations is met
    for i in range(1, MAXITER):
        j += 1 if j < 1 else -j # j = 0 --> j = 1 --> j = 0 --> j = 1 ...

        x[j] = B*x[j-1] + g

        if displayConsoleLog:
            print(f"JACOBI-RICHARDSON METHOD ::: Iteration no.{str(i).zfill(2)}: ", end="")
            for k in range(len(variables)): print(f"{variables[k]} = % .{precision}f" % x[j][k], end = ", ")
            print(f"||vars_k - vars_(k-1)||_oo = % .{precision}f" % (x[j] - x[j-1]).norm(oo))

        if( (x[j] - x[j-1]).norm(oo) < epsilon * max(1, x[j].norm(oo)) ):
            if printTotalIterations:
                print("___________________________________________________________________________\n\n") if not displayConsoleLog else print("\n")
                print(f"JACOBI-RICHARDSON METHOD ::: The solution was successfully found after {i+1} iterations.\n")
                print("___________________________________________________________________________\n\n")

            return x[j], int(precision)

    # If it manages to get out of the loop, it means no root was found, so it prints an error message
    raise Exception("The maximum number of iterations was met and no root was found.")

# GAUSS-SEIDEL'S (GS) METHOD
def gaussSeidel(coeffs: Matrix, variables: Matrix, result: Matrix, var0: Matrix, epsilon = 10**-6, MAXITER = 100, displayConsoleLog = True, printTotalIterations = True):
    # Try and convert the variables to symbolic
    try:
        for var in variables: var = sympify(var)
    except:
        raise Exception("The entered variables are invalid.")

    # Checks if all coefficients are numeric
    try:
        for coef in coeffs: coef = float(coef)
    except:
        raise Exception("The coefficients must be numeric.")

    # Checks if there are as many variables as equations
    if coeffs.shape[0] == coeffs.shape[1] == variables.shape[0] == result.shape[0]  and variables.shape[1] == result.shape[1] == 1:
        order = coeffs.shape[0] # System order (number of equations and variables)
    else:
        raise Exception("The system is invalid.")

    # Checks if it is an independent linear system
    if coeffs.det() != 0 and displayConsoleLog:
        print("___________________________________________________________________________\n\n")
        print("GAUSS-SEIDEL METHOD ::: The linear system is an independent one (has only 1 solution).")
    elif coeffs.det() == 0:
        if displayConsoleLog:
            print("___________________________________________________________________________\n\n")
            print("GAUSS-SEIDEL METHOD ::: The linear system has either no or infinite solutions.\n")
            print("___________________________________________________________________________\n\n")

        return

    if displayConsoleLog: printTotalIterations = True

    L = []
    D = []
    R = []

    for row in range(order):
        rowL = []
        rowD = []
        rowR = []

        for col in range(order):

            if row > col:
                rowL.append(coeffs.row(row)[col])
                rowD.append(0)
                rowR.append(0)

            elif row == col:
                rowL.append(0)
                rowD.append(coeffs.row(row)[col])
                rowR.append(0)

            elif row < col:
                rowL.append(0)
                rowD.append(0)
                rowR.append(coeffs.row(row)[col])

        L.append(rowL)
        D.append(rowD)
        R.append(rowR)

    L = Matrix(L)
    D = Matrix(D)
    R = Matrix(R)

    if displayConsoleLog:
        print("GAUSS-SEIDEL METHOD ::: Ax = b --> A = L + D + R.")
        print("GAUSS-SEIDEL METHOD ::: L = ")
        pprint(L)
        print()

        print("GAUSS-SEIDEL METHOD ::: D = ")
        pprint(D)
        print()

        print("GAUSS-SEIDEL METHOD ::: R = ")
        pprint(R)
        print()

    if D.det() == 0:
        if displayConsoleLog:
            print("GAUSS-SEIDEL METHOD ::: As |D| = 0, the method cannot be used.")

        return

    # A x = b --> x_k = B x_(k-1) + g
    B = - (D**-1 * L + eye(order))**-1 * (D**-1 * R)
    g = (D**-1 * L + eye(order))**-1 * (D**-1 * result)

    if displayConsoleLog:
        print("GAUSS-SEIDEL METHOD ::: A x = b --> x_k = B x_(k-1) + g.")
        print("GAUSS-SEIDEL METHOD ::: B = ")
        pprint(B)
        print()

        print("GAUSS-SEIDEL METHOD ::: g = ")
        pprint(g)
        print()

    # Checks convergence
    if B.norm(oo) < 1:
        if displayConsoleLog:
            print(f"GAUSS-SEIDEL METHOD ::: ||B||_oo = {B.norm(oo)} < 1, so there is guarantee of convergence.")
    elif B.norm(1) < 1:
        if displayConsoleLog:
            print(f"GAUSS-SEIDEL METHOD ::: ||B||_1 = {B.norm(1)} < 1, so there is guarantee of convergence.")
    elif B.norm(2) < 1:
        if displayConsoleLog:
            print(f"GAUSS-SEIDEL METHOD ::: ||B||_2 = {B.norm(2)} < 1, so there is guarantee of convergence.")
    else:
        if displayConsoleLog:
            print(f"GAUSS-SEIDEL METHOD ::: ||B||_oo = {B.norm(oo)}, ||B||_1 = {B.norm(1)}, ||B||_2 = {B.norm(2)} are all > 1, so there is no guarantee of convergence.")
    if displayConsoleLog: print()

    # No. of decimal places for the results
    precision = int(abs(log10(epsilon))) if epsilon > 0 and log10(epsilon).is_integer else int(6)

    # Only 2 elements will be used, [0] for even iterations and [1] for odd ones
    x = [var0, 0]

    if displayConsoleLog:
        print("GAUSS-SEIDEL METHOD ::: Iteration no.00: ", end="")
        for i in range(len(variables)): print(f"{variables[i]} = % .{precision}f" % x[0][i], end = ", ")
        print("||vars_k - vars_(k-1)||_oo =  " + "-"*(precision + 2))

    # Auxiliar index variable correspondent to the current element in the 'x' list
    j = 0

    # Loops through the algorithm until the maximum number of iterations is met
    for i in range(1, MAXITER):
        j += 1 if j < 1 else -j # j = 0 --> j = 1 --> j = 0 --> j = 1 ...

        x[j] = B*x[j-1] + g

        if displayConsoleLog:
            print(f"GAUSS-SEIDEL METHOD ::: Iteration no.{str(i).zfill(2)}: ", end="")
            for k in range(len(variables)): print(f"{variables[k]} = % .{precision}f" % x[j][k], end = ", ")
            print(f"||vars_k - vars_(k-1)||_oo = % .{precision}f" % (x[j] - x[j-1]).norm(oo))

        if (x[j] - x[j-1]).norm(oo) < epsilon * max(1, x[j].norm(oo)) :
            if printTotalIterations:
                print("___________________________________________________________________________\n\n") if not displayConsoleLog else print("\n")
                print(f"GAUSS-SEIDEL METHOD ::: The solution was successfully found after {i+1} iterations.\n")
                print("___________________________________________________________________________\n\n")

            return x[j], int(precision)

    # If it manages to get out of the loop, it means no root was found, so it prints an error message
    raise Exception("The maximum number of iterations was met and no root was found.")

if __name__ == "__main__":
    # A = Matrix([ [2, 1, 2], [1, 2, 1], [1, 1, 2] ])
    # # A = Matrix([ [1,2,0], [1,2,1], [0,1,1] ])
    # var = Matrix([ 'x', 'y', 'z' ])
    # res = Matrix([ 1, 2, 3 ])
    # # res = Matrix([ 3, 1, -4 ])

    # coeffs, result, sol = gaussianElimination(A, var, res)

    # print("\n")
    # pprint(coeffs)
    # print()
    # pprint(result)
    # print()
    # pprint(sol)
    # print()

    # # A = Matrix([ [4,2,2], [2,2,1], [2,1,2] ])
    # A = Matrix([ [4, 12, -16], [12, 37, -43], [-16, -43, 98] ])
    # var = Matrix([ 'x', 'y', 'z' ])

    # G = cholesky(A, var)

    A = Matrix([ [10,2,1], [1,5,1], [2,3,10] ])
    var = Matrix([ 'x', 'y', 'z' ])
    res = Matrix([ 7, -8, 6 ])
    var0 = Matrix([7/10, -8/5, 8/5])

    sol, precision = jacobiRichardson(A, var, res, var0, epsilon=10**-5)

    for k in range(len(var)):
        print(f"{var[k]} = % .{precision}f" % sol[k], end = "")
        if k < len(var) - 1: print(", ", end="")
        else: print("\n")

    sol, precision = gaussSeidel(A, var, res, var0, epsilon=10**-5)

    for k in range(len(var)):
        print(f"{var[k]} = % .{precision}f" % sol[k], end = "")
        if k < len(var) - 1: print(", ", end="")
        else: print("\n")


# -*- coding: utf-8 -*-
"""
Created on Thu Dec  2 20:34:32 2021

@author: Oliver
"""
"""Multivariate Optimization"""
import matplotlib.pyplot as plt
import numpy as np
import math
import sys
print(f'sys executable {sys.executable}')
"""Steepest Ascent"""
def HOpt(F,dFx,dFy,x,y):
    import sympy as sym
    from sympy import symbols, solve
    import matplotlib.pyplot as plt

    hsym = symbols('hsym')

    xlist = []
    ylist = []
    flist = []
    dfxlist = []
    dfylist = []

    for i in range(0, 10, 1):
        xold = x
        yold = y

        dfx = dFx(x)
        dfy = dFy(y)

        #Create a function for the path to the top of the mountain.
        g = F(x+dfx*hsym, y+dfy*hsym)
        hexpr = sym.diff(g, hsym)

        hsolved = solve(hexpr)
        hopt = hsolved[0]

        x = xold + hopt*dfx
        y = yold + hopt*dfy

        Fxy = F(x, y)
        dfx = dFx(x)
        dfy = dFy(y)

        xlist.append(x)
        ylist.append(y)
        flist.append(Fxy)
        dfxlist.append(dfx)
        dfylist.append(dfy)

        if dfx <= 0.0001 and dfy <= 0.0001:
            break

    print(x, y, Fxy, dfx, dfy)

def F(x,y):
    return 2*x*y+2*x-x**2-2*y**2

def dFx(x):
    return 2*y+2-2*x

def dFy(y):
    return 2*x-4*y

x = 1.
y = 1.
print(HOpt(F, dFx, dFy, x, y))

# ADD MICROSOFT EXCEL HERE

"""Constrained Optimization using Laplace Multiplier"""

cost_social = 25
cost_tv = 250
budget = 2500

#lets get the minimum and maximum number of campaigns:
social_min = 0
social_max = budget / cost_social

tv_min = 0
tv_max = budget / cost_tv

# if we fix the number of tv campaings, we know the number of social campaigns left to buy by inverting the formula
def n_social(n_tv, budget):
    return (budget - 250 * n_tv) / 25

# if we fix the number of social campaings, we know the number of tv campaigns left to buy by inverting the formula
def n_tv(n_social, budget):
    return (budget - 25 * n_social) / 250

social_x = np.linspace(social_min, social_max, 100)
tv_y = n_tv(social_x, budget)

plt.figure(figsize=(10,5))
plt.plot(social_x, tv_y)
plt.xlabel('Number of social campaigns')
plt.ylabel('Number of tv campaigns')
plt.title('Possible ways of spending the budget')
plt.show()

def revenues(social, tv):
    return social**(3/4) * tv**(1/4) * 7

from mpl_toolkits.mplot3d import Axes3D
social_axis = np.linspace(social_min, social_max, 100)
tv_axis = np.linspace(tv_min, tv_max, 100)
social_grid, tv_grid = np.meshgrid(social_axis, tv_axis)

fig = plt.figure()
ax = fig.gca(projection='3d')

ax.plot_surface(tv_grid, social_grid, revenues(social_grid, tv_grid))

ax.plot(tv_y, social_x, linewidth = 5, color = 'r')

ax.set_xlabel('Number of hours bought')
ax.set_ylabel('Number of materials bought')
ax.set_title('Possible ways of spending the budget')
plt.show()

fig, (ax_l, ax_r) = plt.subplots(1, 2, figsize = (15, 5))

social_axis = np.linspace(social_min, social_max, 100)
tv_axis = np.linspace(tv_max, tv_min, 100)
social_grid, tv_grid = np.meshgrid(social_axis, tv_axis)

im = ax_l.imshow(revenues(social_grid, tv_grid), aspect = 'auto', extent=[social_min, social_max, tv_min, tv_max])
ax_l.plot(social_axis, n_tv(social_axis, 2500), 'r')
ax_l.set_xlabel('Number of social campaigns bought')
ax_l.set_ylabel('Number of tv campaigns bought')
ax_l.set_title('Possible ways of spending the budget')

# The contours are showing how the intersection looks like

social_axis = np.linspace(social_min, social_max)
tv_axis = np.linspace(tv_min, tv_max)
social_grid, tv_grid = np.meshgrid(social_axis, tv_axis)

im2 = ax_r.contour(revenues(social_grid,tv_grid), extent=[social_min, social_max, tv_min, tv_max])
ax_r.plot(social_axis, n_tv(social_axis, 2500), 'r')
ax_r.set_xlabel('Number of social campaings bought')
ax_r.set_ylabel('Number of tv campaigns bought')
ax_r.set_title('Possible ways of spending the budget')

plt.colorbar(im,ax=ax_l)
plt.colorbar(im2,ax=ax_r)

plt.show()

from sympy import *

s, t, l = symbols('s t l')

solve([Eq((21/4)*((t**(1/4))/s**(1/4)) - 25*l, 0),
   Eq((7/4)*(s**(3/4)/t**(3/4)) - 250*l, 0),
   Eq(25*s+250*t - 2500, 0)], [s,t,l], simplify=False)

revenues(75, 2.5)

"""DOES NOT WORK PAST THIS LINE"""
# Lagrange multiplier from the lecture slides:
import numpy as np
from scipy.optimize import minimize
def objective(X):
    x, y = X
    return (x-5)**2 + (y-8)**2
#This is the constraint function that has lambda as a coefficient.
def eq(X):
    x, y = X
    return x*y - 5

def F(L):
    'Augmented Lagrange function'
    x, y, _lambda = L
    return objective([x, y]) + _lambda * eq([x, y])

from autograd import grad
import autograd.numpy as np

# Gradients of the Lagrange function
dfdL = grad(F, 0)

# Find L that returns all zeros in this function.
def obj(L):
    x, y, _lambda = L
    dFdx, dFdy, dFdlam = dfdL(L)
    return [dFdx, dFdy, eq([x, y])]
from scipy.optimize import fsolve
x, y, _lam = fsolve(obj, [0.0, 0.0, 0.0])
print(f'The answer is at {x, y}')

# solving an economic problem
def objective(X):
    x, y = X
    return (160*x**0.66)*(y**0.33)
#This is the constraint function that has lambda as a coefficient.
def eq(X):
    x, y = X
    return 20*x + 0.15*y - 20000.

def G(L):
    'Augmented Lagrange function'
    x, y, _lambda = L
    return objective([x, y]) + _lambda * eq([x, y])

# Gradients of the Lagrange function
dfdL = grad(G, 0)
# Find L that returns all zeros in this function.
def obj(L):
    x, y, _lambda = L
    dFdx, dFdy, dFdlam = dfdL(L)
    return [dFdx, dFdy, eq([x, y])]
from scipy.optimize import fsolve
x, y, _lam = fsolve(obj, [1., 1., 1.0])
print(f'The answer is at {x, y}')

import numpy,from scipy,import sympy,from sympy


import numpy

def compute_weights(H, y):
    '''
    This function compute the regression coefficient using the Closed-form solution.

    Args:
        H: |NxD| Matrix of N observations with D features each
        y: |N| Vector of labels

    Returns:
        W: |D| Vector of features
    '''
    if not type(H) == numpy.matrix:
        H = numpy.matrix(H)
        if H.shape[0] == 1:  # In case there was one feature
            H = H.T
    if not type(y) == numpy.matrix:
        y = numpy.matrix(y).T
    elif y.shape[0] == 1:  # creating a column vector
        y = y.T
    H = add_bias_feature(H)
    first_part = (H.T * H)

    if not (first_part.shape[0] == first_part.shape[1] and numpy.linalg.matrix_rank(first_part) == first_part.shape[0]):
        raise Exception("'H.T x H' is not invertible!")

    W = numpy.linalg.inv(first_part) * H.T * y
    return W

def add_bias_feature(H):
    '''
    Adds an all 1 feature to serve as the bias feature for W[0]

    Args:
        H: |NxD| Matrix of N observations with D features each

    Returns:
        H: |Nx(D+1)| numpy matrix
    '''
    if not type(H) == numpy.matrix:
        H = numpy.matrix(H)
        if H.shape[0] == 1:  # In case there was one feature
            H = H.T
    return numpy.hstack((numpy.ones((H.shape[0], 1)), H))

def predict(H, W):
    '''
    Predicts for H the labels, based on W
    Args:
        H: |NxD| Matrix of N observations with D features each
        W: |D| Vector of features

    Returns:
        y: |N| Vector of predicted labels
    '''
    H = add_bias_feature(H)
    if not type(W) == numpy.matrix:
        W = numpy.matrix(W).T
    elif W.shape[0] == 1:  # creating a column vector
        W = W.T
    y = H * W
    return y

def RSS(y, H, W):
    '''
    Computes the residual sum of squares.
    Args:
        y: |N| vector of true labels
        H: |NxD| Matrix of N observations with D features each
        W: |D| Vector of features

    Returns:
        y: |N| Vector of labels
    '''
    if not type(y) == numpy.matrix:
        y = numpy.matrix(y).T
    elif y.shape[0] == 1:  # creating a column vector
        y = y.T

    E = y - predict(H, W)
    return (E * E.T).sum()


"""Simple implementation of Linear regression model
   with gradiant descent optimisation.
"""
import numpy as np

class LinearRegression():
    """Finds a line which best fits the training data
    of a single i/p variable.
    y = a + bX
    we find the best value of a and b applying gradiant
    descent on a cost function given by
    J(a, b) = (Y - a + bX)^2

    Gradiant Descent:
    a = a - alpha * delta(J(a, b))
    b = b - alpha * delta(J(a, b))
    while keeping b and a constant respectively.

    Parameters
    ==========

    alpha: float, learning rate
       Learning rate in float.
    stopping_condition: int, defaults to 5.
       int indicates the time to stop. 1<t<10
    """

    def __init__(self, alpha, stopping_condition = 5):
        self.alpha = alpha
        self.stopping_condition = stopping_condition

    def fit(self, X, y):
        """
        Fit the data on the linear regression model, i.e fit a line
        in the given data.

        Parameters
        ==========
        X : array of shape (n_samples)
          Independent input data.
        y : array of shape (n_samples)
          Output data, y = f(X)
        """
        X = np.asarray(X)
        if X.ndim != 1:
            raise ValueError("Only single feature input is supported")

        # initialise theta as zero.
        self.theta = [0, 0]
        if self.stopping_condition > 10:
            raise ValueError("Time should be less than 10 secs")

        self._gradiant_descent(X, y)

    def transform(self, X):
        """
        Transform X to f(x) given the linear regression parameters.

        Parameters
        ==========
        X : array of shape(n_samples)
          The input
        """
        X = np.asarray(X)
        if X.ndim != 1:
            raise ValueError("Only single feature input is supported")

        X *= self.theta[1]
        X += self.theta[0]
        return X

    def _gradiant_descent(self, X, y):
        """
        Gradiant descent optmization method to minimise cost function with
        parameters theta0 and theta1.

        Parameters
        ==========
        X : array of shape(n_samples)
          The input
        y : array of shape(n_samples)
          The output, y = f(x)
        """
        import time

        start = time.time()
        N = len(X)
        while True:
            delta_theta0 = sum(((2.0 / N) * (-y + (self.theta[0] + (self.theta[1] * X)))))
            delta_theta1 = sum(((2.0 / N) * X * (-y + (self.theta[0] + (self.theta[1] * X)))))
            self.theta[0] = self.theta[0] - self.alpha  * delta_theta0
            self.theta[1] = self.theta[1] - self.alpha  * delta_theta1
            if (time.time() - start >= self.stopping_condition):
                break

    def plot(self, X, y, y_pred):
        """
        Plot graph of X, real y and predicted y as a line

        Parameters
        ==========
        X : array, 1-D
         The input feature vector
        y : array, 1-D
         The output
        """

        from matplotlib import pyplot as plt

        plt.plot(X, y, 'rs', X, y_pred)
        plt.show()


from numpy import *
import numpy as np
# from numba import jit

# @jit

def detrending_coeff(win_len , order):

#win_len = 51
#order = 2
	n = (win_len-1)/2
	A = mat(ones((win_len,order+1)))
	x = np.arange(-n , n+1)
	for j in range(0 , order + 1):
		A[:,j] = mat(x ** j).T

	coeff_output = (A.T * A).I * A.T
	return coeff_output , A

# coeff_output,A = detrending_coeff(5,2)
# print(coeff_output)
# print(A)


"""Numeric routines

Author: Dr.-Ing. Mauricio Fernández

This module offers a collection of tensor routines using numpy.
"""

# %% Import

import numpy as np

import tensor_routines as tr

# %% Products

def td(a: np.ndarray, b: np.ndarray, n: int) -> np.ndarray:
    """Tensor dot.

    Shortcut for `np.tensordot(a, b, n)`.

    Parameters
    ----------
    a : np.ndarray
        Tensor
    b : np.ndarray
        Tensor
    n : int
        Number of neighbour axes to be contracted

    Returns
    -------
    np.ndarray
        Result

    Examples
    --------
    Contract 2 neighbour axes

    >>> a = np.random.rand(2, 3, 4, 5)
    ... b = np.random.rand(4, 5, 6, 7)
    ... c = td(a, b, 2) # c_ijkl = a_ijmn b_mnkl
    ... print(c.shape)
    (2, 3, 6, 7)
    """
    return np.tensordot(a, b, n)

def sp(a: np.ndarray, b: np.ndarray) -> float:
    """Scalar product.

    Compute the scalar product (full contraction) of
    tensors with equal shape.

    Parameters
    ----------
    a : np.ndarray
        Tensor
    b : np.ndarray
        Tensor with b.shape == a.shape

    Returns
    -------
    float
        a_ijkl... b_ijkl...

    Examples
    --------
    >>> a = np.random.rand(2, 3, 4)
    ... b = np.random.rand(2, 3, 4)
    ... c = sp(a, b) # c = a_ijk b_ijk
    """
    return td(a, b, a.ndim)

def nf(a: np.ndarray) -> float:
    """Frobenius norm.

    Frobenius norm of a real-valued tensor (square
    root of the sum of all squared tensor components)
    by calling `np.linalg.norm(a)`.

    Parameters
    ----------
    a : np.ndarray
        Tensor

    Returns
    -------
    float
        Frobenius norm

    Examples
    --------
    >>> a = np.random.rand(2, 3, 4)
    ... nf(a) - np.sqrt(np.sum(a**2))
    """
    return np.linalg.norm(a)

def tp(*args) -> np.ndarray:
    """Tensor product.

    Recursively calls `np.tensordot(a, b, 0)` for argument list
    `args = [a0, a1, a2, ...]`, yielding, e.g.,
        tp(a0, a1, a2) = tp(tp(a0, a1), a2)

    Parameters
    ----------
    args : sequence
        Sequence of tensors

    Returns
    -------
    np.ndarray
        Tensor product

    Examples
    --------

    >>> a = np.random.rand(2, 3, 4)
    ... b = np.random.rand(7, 8, 9)
    ... c = tp(a, b) # c_ijkmno = a_ijk b_mno
    ... c.shape == (2, 3, 4, 7, 8, 9)
    """
    temp = args[0]
    for i in range(1, len(args)):
        temp = np.tensordot(temp, args[i], 0)
    return temp

def lm(a: np.ndarray, b: np.ndarray) -> np.ndarray:
    """Linear map.

    Compute linear map by contracting in order
    all axes of b with the last axes of a, e.g.,
    a_ijklm and b_nop, c = lm(a, b) with
    c_ij = a_ijmno b_mno.

    Parameters
    ----------
    a : np.ndarray
        Tensor
    b : np.ndarray
        Tensor with a.ndim >= b.ndim and matching commong axes

    Returns
    -------
    np.ndarray
        Linear map

    Examples
    --------
    >>> a = np.random.rand(2, 3, 4, 5, 6)
    ... b = np.random.rand(4, 5, 6)
    ... c = lm(a, b)
    ... c.shape == (2, 3)
    True
    """
    return td(a, b, b.ndim)

def rp(q: np.ndarray, a: np.ndarray) -> np.ndarray:
    """Rayleight product.

    Compute Rayleight product `b = rp(q, a)` defined as
        b_{i_1 i_2 ...} = q_{i_1 j_1} q_{i_2 j_2} ... a_{j_1 j_2 ...}
    where the second order tensor q in R^{n_{out} x n_{in}}
    is used to map
        a in R^{n_{in} x n_{in} x ...}
    to
        b in R^{n_{out} x n_{out} x ...}

    Parameters
    ----------
    q : np.ndarray
        Second-order tensor
    a : np.ndarray
        Tensor with constant axis dimension, e.g., a.shape = (4, 4, 4, 4)

    Returns
    -------
    np.ndarray
        Rayleigh product
    """
    n = a.ndim
    p = [n-1] + list(range(n-1))  # index permutation per Q
    for _ in range(n):
        a = np.transpose(td(a, np.transpose(q), 1), p)
    return a

# %% Transpositions

def tt(a: np.ndarray, t: list) -> np.ndarray:
    """Tensor dimension transposition.

    Shortcut for np.transpose.

    Parameters
    ----------
    a : np.ndarray
        Any numpy array
    t : list
        List of dimension transpositions with a.ndim == len(t)

    Returns
    -------
    np.ndarray
        Transposed array

    Examples
    --------
    >>> a = np.random.rand(2, 3, 4, 5)
    ... tt(a, (2, 0, 3, 1)).shape == (4, 2, 5, 3)
    True

    Personal note
    -------------
    np transposes dimensions, NOT indices. It can be
    considered as the index transpose of the result `at`
    (see examples below).

    Pattern:
        a in R(d_0, d_1, ...)
        t = (t_0, t_1, ...)
        d_t = (d_{t_0}, d_{t_1}, ...)
        i_t = (i_{t_0}, i_{t_1}, ...)
        at = tr(a, t) in R(*d_t)
        at_{*i_t} = a_{i_0 i_1 ...}

    Example 1:
        a in R(d_0, d_1, d_2, d_3)
        at = tr(a, (3, 1, 0, 2)) in R(d_3, d_1, d_0, d_2)
        at_{i_0 i_1 i_2 i_3} = a_{i_2 i_1 i_3 i_0}
        at_{i_3 i_1 i_0 i_2} = a_{i_0 i_1 i_2 i_3} <- rename to
            get dimension transposition

    Example 2:
        a in R(d_0, d_1, d_2, d_3)
        at = tr(a, (1, 3, 2, 0)) in R(d_1, d_3, d_2, d_0)
        at_{i_0 i_1 i_2 i_3} = a_{i_3 i_0 i_2 i_1}
        at_{i_1 i_3 i_2 i_0} = a_{i_0 i_1 i_2 i_3} <-

    With respect to Mathematica (index transposition):
    a in R(d_0, d_1, d_2, d_3)
    tr(a, (3, 1, 0, 2)) == TensorTranspose(a, {2, 1, 3, 0} + 1)
    tr(a, (1, 3, 2, 0)) == TensorTranspose(a, {3, 0, 2, 1} + 1)
    TensorTranspose(tr(a, t), t+1) == a
    """
    return np.transpose(a, t)

def tt_r(a: np.ndarray) -> np.ndarray:
    """Right index pair transposition

    Transpose right index pair of fourth-order tensor a.

    Parameters
    ----------
    a : np.ndarray
        Fourth-order tensor, a_ijkl

    Returns
    -------
    np.ndarray
        Fourth-order tensor, a_jikl
    """
    return tt(a, (0, 1, 3, 2))

def tt_l(a: np.ndarray) -> np.ndarray:
    """Left index pair transposition.

    Transpose left index pair of fourth-order tensor a.

    Parameters
    ----------
    a : np.ndarray
        Fourth-order tensor, a_ijkl

    Returns
    -------
    np.ndarray
        Fourth-order tensor, a_ijlk
    """
    return tt(a, (1, 0, 2, 3))

def tt_m(a: np.ndarray) -> np.ndarray:
    """Major transposition

    Parameters
    ----------
    a : np.ndarray
        Fourth-order tensor, a_ijkl

    Returns
    -------
    np.ndarray
        Fourth-order tensor, a_klij
    """
    return tt(a, (2, 3, 0, 1))

# %% Symmetrizations

def sym_2(a: np.ndarray) -> np.ndarray:
    """Symmetric part of second-order tensor.

    Parameters
    ----------
    a : np.ndarray
        Second-order tensor

    Returns
    -------
    np.ndarray
        Symmetric part
    """
    return (a + a.T)/2

def sym_r(a: np.ndarray) -> np.ndarray:
    """Symmetrize right index pair.

    Symmetrize a fourth-order tensor a_ijkl with respect to
    its right index pair.

    Parameters
    ----------
    a : np.ndarray
        Fourth-order tensor, a_ijkl

    Returns
    -------
    np.ndarray
        Fourth-order tensor, symmetric with respect to kl
    """
    return (a + tt_r(a))/2

def sym_l(a: np.ndarray) -> np.ndarray:
    """Symmetrize left index pair.

    Symmetrize a fourth-order tensor a_ijkl with respect to
    its left index pair.

    Parameters
    ----------
    a : np.ndarray
        Fourth-order tensor, a_ijkl

    Returns
    -------
    np.ndarray
        Fourth-order tensor, symmetric with respect to ij
    """
    return (a + tt_l(a))/2

def sym_lr(a: np.ndarray) -> np.ndarray:
    """Symmetrize left and right index pair.

    Symmetrize a fourth-order tensor a_ijkl with respect to
    its left and right index pair.

    Parameters
    ----------
    a : np.ndarray
        Fourth-order tensor, a_ijkl

    Returns
    -------
    np.ndarray
        Fourth-order tensor, symmetric with respect to ij and kl
    """
    return sym_r(sym_l(a))

def sym_m(a: np.ndarray) -> np.ndarray:
    """Symmetrize major index pair.

    Parameters
    ----------
    a : np.ndarray
        Fourth-order tensor, a_ijkl

    Returns
    -------
    np.ndarray
        Fourth-order tensor fulfilling a_ijkl = a_klij.
    """
    return (a + tt_m(a))/2

def sym_mm(a: np.ndarray) -> np.ndarray:
    """Symmetrize minor and major index pairs of given fourth-order tensor.

    Parameters
    ----------
    a : np.ndarray
        Fourth-order tensor, a_ijkl.

    Returns
    -------
    np.ndarray
        Fourth-order tensor fulfilling a_ijkl = a_jikl = a_klij.
    """
    return sym_m(sym_lr(a))

# %% Isotropic

def iso_ev(a: np.ndarray) -> np.ndarray:
    """Project eigenvalues of isotropic tensor.

    Compute for a given second- or fourth-order tensor the
    eigenvalues of the isotropic part of the given tensor `a`
        * For second-order tensors for `a = lambda ID_2 + aniso_part`,
            `lambda` is computed.
        * For fourth-order tensors
            `a = sum_{i=1}^3 lambda_i P_ISOi + aniso_part`,
            the lambda_i are computed.

    Parameters
    ----------
    a : np.ndarray
        Isotropic tensor.
        * Second order: a.shape == (3, 3)
        * Fourth order: a.shape == (3, 3, 3, 3)

    Returns
    -------
    np.ndarray
        * Second order: lambda
        * Fourth order: 1D-array with [lambda_1,2,3]

    Examples
    --------
    >>> iso_ev(7*np.eye(3)) == 7
    True
    >>> (iso_ev(13*P_ISO_1 + 20*P_ISO_2) == np.array([13, 20])).all()
    True
    """
    if a.shape == (3, 3, 3, 3):
        # Fourth-order
        return np.array([sp(a, P)/sp(P, P) for P in P_ISO])
    else:
        # Second-order
        return sp(a, ID_2)/3

def iso_t(lam: np.ndarray) -> np.ndarray:
    """Isotropic tensor.

    Create an isotropic second- or fourth-order tensor from the
    given eigenvalue(s).

    Parameters
    ----------
    lam : float or np.ndarray
        * Second order: float corresponding to lambda for lambda ID_2
        * Fourth order: 1D-array = [lambda_1,2|3] for
            sum_{i=1}^3 lambda_i P_ISO_i
            * [lambda_1, lambda_2] for only two first isotropic projects
            * [lambda_1,2,3] for all isotropic projectors

    Returns
    -------
    np.ndarray
        Isotropic tensor of second or fourth order.

    Examples
    --------
    >>> (iso_t(10) == 10*np.eye(3)).all()
    True
    >>> (iso_t([3, 7]) == 3*P_ISO_1 + 7*P_ISO_2).all()
    True
    """
    if isinstance(lam, (list, np.ndarray)):
        # Fourth-order
        if len(lam) == 2:
            return lam[0]*P_ISO_1 + lam[1]*P_ISO_2
        else:
            return lam[0]*P_ISO_1 + lam[1]*P_ISO_2 + lam[2]*P_ISO_3
    else:
        # Second-order
        return lam*ID_2

def iso_proj(a: np.ndarray) -> np.ndarray:
    """Isotropic projection.

    Compute the isotropic projection/part of the second-
    or fourth-order tensor `a`.

    Parameters
    ----------
    a : np.ndarray
        Second- or fourth-order tensor

    Returns
    -------
    np.ndarray
        Isotropic projection/part of `a`
    """
    return iso_t(iso_ev(a))

def iso_inv(a: np.ndarray) -> np.ndarray:
    """Inverse of isotropic tensor.

    Compute the inverse or pseudo-inverse of the isotropic
    second- or fourth-order tensor `a`.

    Parameters
    ----------
    a : np.ndarray
        Isotropic second- or fourth-order tensor

    Returns
    -------
    np.ndarray
        Inverse or pseudo-inverse

    Examples
    --------
    >>> ls = np.array([11, 7, 0])
    ... a = tn.iso_t(ls)
    ... ai = tn.iso_inv(a)
    ... tol = 1e-13
    ... assert nf(td(a, ai, 2) - tn.ID_S)/nf(tn.ID_S) < tol
    True
    """
    if a.shape == (3, 3, 3, 3):
        # Fourth order
        return iso_t([1/la for la in iso_ev(a) if la != 0])
    else:
        # Second order
        return iso_t(1/iso_ev(a))

# %% Voigt notation (not normalized)

def vn(a: np.ndarray) -> np.ndarray:
    """Voigt notation (based on convention of tr.VN_CONVENTION)

    Parameters
    ----------
    a : np.ndarray
        Second- or fourth-order tensor

    Returns
    -------
    np.ndarray
        Corresponding first- or second-order output in Voigt notation
    """
    if a.shape == (3, 3):
        out = np.array([
            a[tr.VN_CONVENTION[0][0], tr.VN_CONVENTION[0][1]],
            a[tr.VN_CONVENTION[1][0], tr.VN_CONVENTION[1][1]],
            a[tr.VN_CONVENTION[2][0], tr.VN_CONVENTION[2][1]],
            a[tr.VN_CONVENTION[3][0], tr.VN_CONVENTION[3][1]],
            a[tr.VN_CONVENTION[4][0], tr.VN_CONVENTION[4][1]],
            a[tr.VN_CONVENTION[5][0], tr.VN_CONVENTION[5][1]]
        ])
    else:
        out = np.eye(6)
        for i_1 in range(6):
            for i_2 in range(6):
                out[i_1, i_2] = a[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1]
                ]
    return out

def vn_inv(a: np.ndarray) -> np.ndarray:
    """Inverse Voigt notation.

    Reconstruct symmetric second-order or minor symmetric fourth-order
    tensor.

    Parameters
    ----------
    a : np.ndarray
        6D vector or 6x6 matrix in Voigt notation

    Returns
    -------
    np.ndarray
        Symmetric second-order tensor or minor symmetric fourth-order
        tensor.
    """
    if a.shape == (6,):
        out = np.zeros(shape=[3, 3])
        for i in range(3):
            out[
                tr.VN_CONVENTION[i][0],
                tr.VN_CONVENTION[i][1]
            ] = a[i]
        for i in range(3, 6):
            out[
                tr.VN_CONVENTION[i][0],
                tr.VN_CONVENTION[i][1]
            ] = a[i]
            out[
                tr.VN_CONVENTION[i][1],
                tr.VN_CONVENTION[i][0]
            ] = a[i]
    else:
        out = np.zeros((3, 3, 3, 3))
        for i_1 in range(6):
            for i_2 in range(6):
                temp = a[i_1, i_2]
                out[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][1],
                    tr.VN_CONVENTION[i_2][0],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_2][1],
                    tr.VN_CONVENTION[i_2][0],
                ] = temp
    return out

# %% Normalized Voigt notation (NOT Voigt notation)

def nvn(a: np.ndarray) -> np.ndarray:
    """Normalized Voigt notation.

    Return the symmetric second- or minor symmetric fourth-order tensor
    `a` in normalized Voigt notation according to the convention defined
    in the module by the constant NVN_CONVENTION.

    The norm of the returned objects corresponds to the actual Frobenius
    norm of the original tensors. For fourth-order tensors, the inverse
    of the returned matrix corresponds to the `nvn` of the inverse of `a`
    (with respect to the linear map on symmetric second-order tensors).

    Parameters
    ----------
    a : np.ndarray
        * Second order: a.shape == (3, 3) and symmetric
        * Fourth order: a.shape == (3, 3, 3, 3) and minor symmetric,
            i.e., a == sym_lr(a)

    Returns
    -------
    np.ndarray
        * Second order: 6D vector representation
        * Fourth order: 6x6 matrix representation
    """
    if a.shape == (3, 3):
        out = np.array([
            a[tr.VN_CONVENTION[0][0], tr.VN_CONVENTION[0][1]],
            a[tr.VN_CONVENTION[1][0], tr.VN_CONVENTION[1][1]],
            a[tr.VN_CONVENTION[2][0], tr.VN_CONVENTION[2][1]],
            a[tr.VN_CONVENTION[3][0], tr.VN_CONVENTION[3][1]]*SR2,
            a[tr.VN_CONVENTION[4][0], tr.VN_CONVENTION[4][1]]*SR2,
            a[tr.VN_CONVENTION[5][0], tr.VN_CONVENTION[5][1]]*SR2
        ])
    else:
        out = np.eye(6)
        for i_1 in range(6):
            for i_2 in range(6):
                out[i_1, i_2] = a[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1]
                ]
                if i_1 > 2:
                    out[i_1, i_2] *= SR2
                if i_2 > 2:
                    out[i_1, i_2] *= SR2
    return out

def nvn_inv(a: np.ndarray) -> np.ndarray:
    """Inverse of normalized Voigt notation.

    Reconstruct symmetric second- or minor symmetric fourth-order
    tensor from `nvn` representation.

    Parameters
    ----------
    a : np.ndarray
        * 6D vector
        * 6x6 second-order tensor

    Returns
    -------
    np.ndarray
        * symmetric 3x3 second-order tensor
        * minor symmetric 3x3x3x3 fourth-order tensor
    """
    if a.shape == (6,):
        out = np.zeros(shape=[3, 3])
        for i in range(3):
            out[
                tr.VN_CONVENTION[i][0],
                tr.VN_CONVENTION[i][1]
            ] = a[i]
        for i in range(3, 6):
            out[
                tr.VN_CONVENTION[i][0],
                tr.VN_CONVENTION[i][1]
            ] = a[i]/SR2
            out[
                tr.VN_CONVENTION[i][1],
                tr.VN_CONVENTION[i][0]
            ] = a[i]/SR2
    else:
        out = np.zeros((3, 3, 3, 3))
        for i_1 in range(6):
            for i_2 in range(6):
                temp = a[i_1, i_2]
                if i_1 > 2:
                    temp /= SR2
                if i_2 > 2:
                    temp /= SR2
                out[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][1],
                    tr.VN_CONVENTION[i_2][0],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_2][1],
                    tr.VN_CONVENTION[i_2][0],
                ] = temp
    return out

def inv_nvn(a: np.ndarray) -> np.ndarray:
    """Compute inverse through nvn.

    Compute inverse of minor symmetric fourth-order tensor
    through `nvn`. The inverse applies only on symmetric
    second-order tensors.

    Parameters
    ----------
    a : np.ndarray
        Fourth-order minor symmetrc tensor with a.shape == (3, 3, 3, 3)

    Returns
    -------
    np.ndarray
        Inverse of `a` w.r.t. symmetric second-order tensors
    """
    return nvn_inv(np.linalg.inv(nvn(a)))

# %% T1

def nv_polar(phi: float) -> np.ndarray:
    """Normal vector based on polar angle

    Parameters
    ----------
    phi : float
        Polar angle

    Returns
    -------
    np.ndarray
        2D normal vector
    """
    x = np.cos(phi)
    y = np.sin(phi)
    return np.array([x, y])

def nv_spherical(theta: float, phi: float) -> np.ndarray:
    """Normal vector based on spherical angles

    Parameters
    ----------
    theta : float
        Angle in [0, pi], start from z axis [0, 0, 1]
    phi : float
        Angle in x-y-plance, in [0, 2*pi]

    Returns
    -------
    np.ndarray
        Normal vector in 3D
    """
    x = np.sin(theta)*np.cos(phi)
    y = np.sin(theta)*np.sin(phi)
    z = np.cos(theta)
    return np.array([x, y, z])

# %% T4: stiffness/compliance tensors of important symmetry groups

def stiffness_component_dict(components=None) -> dict:
    """Generate stiffness component dictionary

    Generate dictionary based on the 21 values passed
    and based on the active Voigt notation (see
    VN_CONSTANT in module)

    Parameters
    ----------
    components : float, optional
        The 21 components, by default None

    Returns
    -------
    dict
        Dictionary with index and component as key-value pairs.
    """
    out = {}
    counter = 0
    for i_1 in range(6):
        for i_2 in range(i_1, 6):
            k = tr.VN_CONVENTION[i_1] + tr.VN_CONVENTION[i_2]
            k = np.array(k) + 1
            k = "".join(str(k_) for k_ in k)
            if not isinstance(components, type(None)):
                out[k] = components[counter]
                counter += 1
            else:
                out[k] = 0
    return out

def stiffness_tric(
    components: np.ndarray = None,
    components_d: dict = None
) -> np.ndarray:
    """Generate triclinic fourth-order stiffness tensor.

    Parameters
    ----------
    components : np.ndarray
        21 components of triclinic tensor, see
        stiffness_component_dict
    components_d : dictionary
        dictionary with 21 components
        of triclinic tensor, see
        stiffness_component_dict

    Returns
    -------
    np.ndarray
        Fourth-order triclinic tensor with minor
        and major symmetries
    """
    out = np.zeros(shape=[3, 3, 3, 3])
    if not isinstance(components, type(None)):
        components_d = stiffness_component_dict(components)
    for k, v in components_d.items():
        i = [int(s)-1 for s in k]
        out[i[0], i[1], i[2], i[3]] = v
        # tt_l
        out[i[1], i[0], i[2], i[3]] = v
        # tt_r
        out[i[0], i[1], i[3], i[2]] = v
        out[i[1], i[0], i[3], i[2]] = v  # + tt_l
        # tt_m
        out[i[2], i[3], i[0], i[1]] = v
        out[i[3], i[2], i[0], i[1]] = v  # + tt_l
        out[i[2], i[3], i[1], i[0]] = v  # + tt_r
        out[i[3], i[2], i[1], i[0]] = v  # + tt_l + tt_r
    return out

def stiffness_hex(
    t_1111: float,
    t_1122: float,
    t_1133: float,
    t_3333: float,
    t_2323: float
) -> np.ndarray:
    """Generate hexagonal fourht-order tensor

    Generate hexagonal/transversaly isotropic fourth order
    tensor with symmetry axis e_3.

    Parameters
    ----------
    t_1111 : float
    t_1122 : float
    t_1133 : float
    t_3333 : float
    t_2323 : float

    Returns
    -------
    np.ndarray
        Hexagonal fourth-order tensor with shape (3, 3, 3, 3)
    """
    # Isotropy axis is e_3
    components_d = {
        "1111": t_1111,
        "1122": t_1122,
        "1133": t_1133,
        "1123": 0,
        "1113": 0,
        "1112": 0,
        "2222": t_1111,
        "2233": t_1133,
        "2223": 0,
        "2213": 0,
        "2212": 0,
        "3333": t_3333,
        "3323": 0,
        "3313": 0,
        "3312": 0,
        "2323": t_2323,
        "2313": 0,
        "2312": 0,
        "1313": t_2323,
        "1312": 0,
        "1212": (t_1111 - t_1122)/2
    }
    out = stiffness_tric(components_d=components_d)
    return out

def stiffness_cub_l(l_1: float, l_2: float, l_3: float) -> np.ndarray:
    """Generate cubic stiffness based on its 3 eigenvalues

    Parameters
    ----------
    l_1 : float
        First eigenvalue (1x)
    l_2 : float
        Second eigenvalue (2x)
    l_3 : float
        Third eigenvalue (3x)

    Returns
    -------
    np.ndarray
        Cubic stiffness, fourth-order tensor
    """
    return td(np.array([l_1, l_2, l_3]), P_CUB, 1)

def stiffness_cub_get_l(stiffness: np.ndarray) -> np.ndarray:
    """Get eigenvalues of cubic stiffness

    Parameters
    ----------
    stiffness : np.ndarray
        Cubic stiffness, fourth-order tensor

    Returns
    -------
    np.ndarray
        The 3 cubic eigenvalues
    """
    return np.array([sp(stiffness, P)/sp(P, P) for P in P_CUB])

def stiffness_cub(
    c_1111: float,
    c_1122: float,
    c_2323: float
) -> np.ndarray:
    """Generate cubic stiffness based on free components.

    Parameters
    ----------
    c_1111 : float
        Free component
    c_1122 : float
        Free component
    c_2323 : float
        Free component

    Returns
    -------
    np.ndarray
        Stiffness, fourth-order tensor
    """
    l_1 = c_1111 + 2*c_1122
    l_2 = c_1111 - c_1122
    l_3 = 2*c_2323
    return stiffness_cub_l(l_1, l_2, l_3)

def stiffness_iso_l(l_1: float, l_2: float) -> np.ndarray:
    """Generate an isotropic stiffness based on eigenvalues.

    Parameters
    ----------
    l_1 : float
        First eigenvalue (=3K)
    l_2 : float
        Second eigenvalue (=2G)

    Returns
    -------
    np.ndarray
        Stiffness, fourth-order tensor
    """
    return iso_t([l_1, l_2])

def stiffness_iso(E: float, nu: float) -> np.ndarray:
    """Generate an isotropic stiffness based on Young's modulus E
    and Poisson's ration nu.

    Parameters
    ----------
    E : float
        Young's modulus
    nu : float
        Poisson's ratio

    Returns
    -------
    np.ndarray
        Stiffness, fourth-order tensor
    """
    l_1 = E/(1-2*nu)
    l_2 = E/(1+nu)
    return iso_t([l_1, l_2])

# %% Rotations

def rotation_matrix(n: np.ndarray, phi: float) -> np.ndarray:
    """Rotation matrix.

    Generate rotation matrix with rotation axis `n` and
    rotation angle `phi`.

    Parameters
    ----------
    n : np.ndarray
        Direction of ration axis (does not have to be normalized)
    phi : float
        Rotation angle in [0, np.pi)

    Returns
    -------
    np.ndarray
        Rotation matrix
    """
    n = np.array(n)
    n = n/nf(n)
    n_0 = np.cos(phi)*ID_2
    n_1 = -np.sin(phi)*lm(PT, n)
    n_2 = (1 - np.cos(phi))*tp(n, n)
    return n_0 + n_1 + n_2

# %% Constants

# Scalars
SR2 = np.sqrt(2)

# Identity on vectors
ID_2 = np.eye(3)

# Permutation tensor
PT = np.zeros((3, 3, 3))
PT[0, 1, 2] = 1
PT[1, 2, 0] = 1
PT[2, 0, 1] = 1
PT[1, 0, 2] = -1
PT[2, 1, 0] = -1
PT[0, 2, 1] = -1

# Fourth-order
ITI = tp(ID_2, ID_2)
ID_4 = tt(ITI, (0, 2, 1, 3))
ID_S = sym_r(ID_4)
ID_A = ID_4 - ID_S
P_ISO_1 = ITI/3
P_ISO_2 = ID_S - P_ISO_1
P_ISO_3 = ID_A
P_ISO = np.array([P_ISO_1, P_ISO_2, P_ISO_3])
P_CUB_1 = P_ISO_1
D_CUB = np.zeros(shape=[3]*4)
for ii in range(3):
    D_CUB[ii, ii, ii, ii] = 1
P_CUB_2 = D_CUB - P_CUB_1
P_CUB_3 = ID_S - (P_CUB_1 + P_CUB_2)
P_CUB = np.array([P_CUB_1, P_CUB_2, P_CUB_3])

# nvn variants
ID_S_NVN = nvn(ID_S)


import math
import sympy as sy
from sympy.abc import t

# 构造非线性方程中的f(x)
def f():
    fx = t ** 6 - 5 * t ** 5 + 3 * t ** 4 + t ** 3 - 7 * t ** 2 + 7 * t - 20
    return fx

# 构造f(x)的导数
def f_diff():
    fx_diff = sy.diff(f(), t)
    return fx_diff

# 计算f(x)
def cal_fx(x):
    return f().evalf(subs={t: x})

# 计算f(x)的导数
def cal_fx_diff(x):
    return f_diff().evalf(subs={t: x})

# 计算φ(x)
def phi(x):
    return x - cal_fx(x)

# 计算φ(x)的导数
def phi_diff(x):
    return 1 - cal_fx_diff(x)

# 符号函数，返回输入的正负
def sgn(x):
    if x >= 0:
        return 1
    else:
        return -1

# 简单迭代法，采用松弛加速
def iter(x, epsilon, k):
    x0 = x
    for i in range(k):
        temp = x
        x = (phi(x) - phi_diff(x0) * x) / (1 - phi_diff(x0))
        error = math.fabs(x - temp)
        if error < epsilon:
            return [x, error, i + 1]

# 牛顿法
def Newton(x, epsilon, k):
    for i in range(k):
        temp = x
        x = x - cal_fx(x) / cal_fx_diff(x)
        error = math.fabs(x - temp)
        if error < epsilon:
            return [x, error, i + 1]

# 弦割法
def Secant(x0, x1, epsilon, k):
    for i in range(k):
        x2 = x1 - (x1 - x0) * (cal_fx(x1) / cal_fx(x0)) / (cal_fx(x1) / cal_fx(x0) - 1)
        error = math.fabs(x2 - x1)
        x1, x0 = x2, x1
        if error < epsilon:
            return [x2, error, i + 1]

if __name__ == "__main__":
    epsilon: float = 1e-8  # 误差
    k = 100  # 最大迭代次数
    a = -1  # 区间左端点
    b = 5  # 区间右端点

    # 二分法
    while b - a >= 1:
        mid = (a + b) / 2
        if sgn(cal_fx(mid)) * sgn(cal_fx(a)) == -1:
            b = mid
        else:
            a = mid

    x0 = (a + b) / 2

    print("经过二分法，区间为[%s, %s]\n" % (a, b))

    [x, error, times] = iter(x0, epsilon, k)
    print("采用简单迭代法求解非线性方程：")
    print("x = %s" % x)
    print("误差：%s" % error)
    print("迭代次数：%s" % times)

    [x, error, times] = Newton(x0, epsilon, k)
    print("\n采用牛顿法求解非线性方程：")
    print("x = %s" % x)
    print("误差：%s" % error)
    print("迭代次数：%s" % times)

    [x, error, times] = Secant(b, a, epsilon, k)
    print("\n采用弦割法求解非线性方程：")
    print("x = %s" % x)
    print("误差：%s" % error)
    print("迭代次数：%s" % times)


from Cryptoalpha import *
from sympy import *
from random import randint

def encrypt(E,p,a):
    return a.MtoS(matrix_mod(E*a.StoM(p),len(a.alphabet)))
    """Apply matrix E to string p mod 26 and return an encrypted string, relative to Cryptoalpha a """
def decrypt(D,c,a):
    return a.MtoS(matrix_mod(D*a.StoM(c),len(a.alphabet)))
    ####"""Apply matrix D to string c mod 26 and return a decrypted string, relative to Cryptoalpha a """
def get_decryption_matrix(P,C,a):
    """ Knowing two digraphs in string P are encoded as string C, determine a unique decryption matrix, relative to Cryptoalpha a """
    pdione = P[:2] #first plain digraph - letters string
    pditwo = P[2:] #second plain digraph - letters string
    cdione = C[:2] #first cipher digraph - letters string
    cditwo = C[2:] #second cipher digraph - letters string
    pdioneN = a.stoa(pdione) #make letters into numbers list
    pditwoN = a.stoa(pditwo) #make letters into numbers list
    cdioneN = a.stoa(cdione) #make letters into numbers list
    cditwoN = a.stoa(cditwo) #make letters into numbers list
    #ROUND TWO CODE
    plainmatrixnums = Matrix([[pdioneN[0],pditwoN[0]],[pdioneN[1],pditwoN[1]]]) #plaintext number matrix P
    #print("plainmatrix: ")
    #pprint(plainmatrixnums)
    ciphermatrixnums = Matrix([[cdioneN[0],cditwoN[0]],[cdioneN[1],cditwoN[1]]])#ciphertext number matrix C
    #print("ciphermatrix: ")
    #pprint(ciphermatrixnums)
    #find inverse of the cipher matrix
    cipherinverse = ciphermatrixnums.inv_mod(len(a.alphabet))
    #print("CipherInv: ")
    #pprint(cipherinverse)
    d = plainmatrixnums*cipherinverse #multiply plain with the cipher inverse to get d
    #print("D")
    #pprint(d)
    dModed = Matrix([
        [(d[0,0]%len(a.alphabet)), (d[0,1]%len(a.alphabet))],
        [(d[1,0]%len(a.alphabet)), (d[1,1]%len(a.alphabet))]
    ])
    #pprint(dModed)
    return dModed
def get_random_invertible_matrix(m):
    """ return a random 2x2 matrix M with gcd(det(M),m)= 1 """
    d = 2
    M = Matrix([[0,0],[0,0]])
    while d>1:
        #make a random 2x2 matrix, M, and let d = gcd(M, m)
        M = Matrix([[randint(0,m),randint(0,m)],[randint(0,m),randint(0,m)]])
        #range is 1 to m-1 -> your modulus value is the top
        d = gcd(det(M),m)
        #this must be last line; if the gcd is not 1 then you don't have an invertible matrix
    return M


"""Unvec operation."""
from typing import List, Optional

import numpy as np

def unvec(vector: np.ndarray, shape: Optional[List[int]] = None) -> np.ndarray:
    r"""
    Perform the unvec operation on a vector to obtain a matrix [Rigetti2020]_.

    Takes a column vector and transforms it into a :code:`shape[0]`-by-:code`shape[1]` matrix.
    This operation is the inverse of :code:`vec` operation in :code:`toqito`.

    For instance, for the following column vector

    .. math::
        u = \begin{pmatrix} 1 \\ 3 \\ 2 \\ 4 \end{pmatrix},

    it holds that

    .. math::
        \text{unvec}(u) =
        begin{pmatrix}
            1 & 2 \\
            3 & 4
        \end{pmatrix}

    More formally, the vec operation is defined by

    .. math::
        \text{unvec}(e_a \otimes e_b) = E_{a,b}

    for all :math:`a` and :math:`b` where

    .. math::
        E_{a,b}(c,d) = \begin{cases}
                          1 & \text{if} \ (c,d) = (a,b) \\
                          0 & \text{otherwise}
                        \end{cases}

    for all :math:`c` and :math:`d` and where

    .. math::
        e_a(b) = \begin{cases}
                     1 & \text{if} \ a = b \\
                     0 & \text{if} \ a \not= b
                 \end{cases}

    for all :math:`a` and :math:`b`.

    This function has been adapted from [Rigetti20]_.

    Examples
    ==========

    Consider the following vector

    .. math::
        u = \begin{pmatrix} 1 \\ 3 \\ 2 \\ 4 \end{pmatrix}

    Performing the :math:`\text{unvec}` operation on :math:`u` yields

    .. math::
        \text{unvec}(u) = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}

    >>> from toqito.matrix_ops import unvec
    >>> import numpy as np
    >>> u = np.array([1, 2, 3, 4])
    >>> unvec(u)
    [[1 2]
     [3 4]]

    See Also
    ========
    vec

    References
    ==========
    .. [Rigetti2020] Forest Benchmarking (Rigetti).
        https://github.com/rigetti/forest-benchmarking

    :param vector: A (:code:`shape[0] * shape[1]`)-by-1 numpy array.
    :param shape: The shape of the output matrix; by default, the matrix is assumed to be square.
    :return: Returns a :code:`shape[0]`-by-:code:`shape[1]` matrix.
    """
    vector = np.asarray(vector)
    if shape is None:
        dim = int(np.sqrt(vector.size))
        shape = dim, dim
    mat = vector.reshape(*shape).T
    return mat


import numpy
from numpy import array, absolute

t= [1, 2, 3]
print ('\nValue at first location:', t[0])
print ('Value at third location:', t[2])
print ('Value located using negative index:', t[-2])

print ('Comparison of a concatenated list with an array ')
r= [4, 5, 6]
print ('r=', r)
print ('t=', t )
result1= t + r
print ('Concatenation example using t and r lists.', result1)
result2=array([t,r])
print ('\nComparison of concatenation with array([t,r]).')
print (result2)

# Note the different results produced by indexing for a list and an array.
print ('\nThe second element of the preceding list and array')
print (result1[1])
print (result2[1])

print ('\nSlicing to produce an element, a column and a row of the array')
print (result2[0,1])
print (result2[:,1])  # Note the horizontal output of a column.
print (result2[1,:])

print ('\nExample 1 Section 2.3 Lial showing array addition from page 72.')
m= array([[10,12,5],[15, 20, 8]])
n= array([[45, 35, 20],[65, 40, 35]])
q= m + n
print ('\nArrays in order: m , n and m+n')
print (m)
print ('\n', n)   # Note that context determines the purpose of "n".
print ('\n', q)

print ('\nSubtraction of n from m equals q')
q= m - n
print (q)

s= 2.0
print ('\nMultiplication of q by a scalar', s)
q= s*q
print (q)

print ('\nFunctions can be applied to an array such as absolute value')
q= absolute(q)
print (q)


import numpy as np
from scipy.optimize import curve_fit
from typing import Callable, List, Tuple, TypeVar

def auto_correlation(data: List[float], lag: int) -> float:
    """Calculates auto-correlation of `data` with `lag` time delay

    Args:
        data (List[float]): data
        lag (int): lag of comparision

    Returns:
        float: auto-correlation of `data[lag:]` with `data[:-lag]
    """

    if lag == 0:
        return 1
    else:
        correlation = np.corrcoef(data[:len(data) - lag], data[lag:])[0, 1]
        return 0 if np.isnan(correlation) else correlation

def _exp_model(x: float, char_length: float) -> float:
    """Model of `f(x) = exp(- x / l)`

    Args:
        x (float): input
        char_length (float): characteristic length (l)

    Returns:
        float: output of function
    """

    return np.exp(-x / char_length)

def exp_characteristic_length(x_s: List[float], y_s: List[float]) -> Tuple[float, float]:
    """Finds characteristic length of `exp(-x / l)` function.

    Args:
        x_s (List[float]): inputs
        y_s (List[float]): outputs

    Returns:
        Tuple[float, float]: characteristic length and its error
    """

    try:
        x_length = x_s[-1] - x_s[0]
        x_length = 1 if x_length == 0 else x_length
        # should normalized x
        fit_para, fit_error = curve_fit(_exp_model, x_s / x_length, y_s, p0=(.5,))
        # because of normalization
        fit_para = fit_para[0] * x_length
        fit_error = np.sqrt(fit_error[0]) * x_length
    except:
        fit_para, fit_error = 0, 0

    return fit_para, fit_error

X = TypeVar('X')

def bootstrap_error(data: List[X], function: Callable[[List[X]], float], size: int = 100) -> float:
    """Calculates bootstrap error of `data`.

    Args:
        data (List[X]): list of any data
        function (Callable[[List[X]], float]): function gives the value that we want to calculate its error
        size (int, optional): number of batches. Defaults to 100.

    Returns:
        float: bootstrap error
    """

    ensemble_values = np.zeros(size)

    for i in range(size):
        # select `len(data)` values from data randomly
        random_numbers = np.random.randint(0, len(data), len(data))
        # get function output of this batch
        ensemble_values[i] = function(data[random_numbers])

    # return standard deviation of function output of batches
    return np.std(ensemble_values)


import numpy as np

def parse_input(lines):
    for l in lines:
        if 'increment' in l:
            yield ('increment', int(l.split()[-1]))
        elif 'stack' in l:
            yield ('stack',)
        elif 'cut' in l:
            yield ('cut', int(l.split()[-1]))
        else:
            raise Exception("Invalid line")

def modinv(a, m):
    def egcd(a, b):
        if a == 0:
            return (b, 0, 1)
        else:
            g, y, x = egcd(b % a, a)
            return (g, x - (b // a) * y, y)

    g, x, _ = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m

def reverse_deal(D, i):
    return D - 1 - i

def reverse_cut(D, i, N):
    return (i + N + D) % D

def reverse_increment(D, i, N):
    return modinv(N, D) * i % D

def reverse_shuffle(inputs, D, pos):
    for inp in inputs:
        if inp[0] == 'stack':
            pos = reverse_deal(D, pos)
        elif inp[0] == 'increment':
            pos = reverse_increment(D, pos, inp[1])
        elif inp[0] == 'cut':
            pos = reverse_cut(D, pos, inp[1])
        else:
            raise Exception("Invalid line")
    return pos

if __name__ == "__main__":
    with open('22.txt') as f:
        lines = f.readlines()
    N = 10007

    cards = np.arange(0, N)
    inputs = list(parse_input(lines))
    for i in inputs:
        if i[0] == 'stack':
            cards = np.flipud(cards)
        elif i[0] == 'increment':
            n = i[1]
            new = np.zeros(N, dtype=int)
            for idx in range(N):
                new[(idx * n) % len(cards)] = cards[idx]
            cards = new
        elif i[0] == 'cut':
            cards = np.roll(cards, -i[1])
        else:
            raise Exception("Invalid line")
    print("Part 1: {}".format(np.where(cards == 2019)[0][0]))

    # https://www.reddit.com/r/adventofcode/comments/ee0rqi/2019_day_22_solutions/fbnifwk?utm_source=share&utm_medium=web2x
    D = 119315717514047  # deck size
    X = 2020
    Y = reverse_shuffle(inputs[::-1], D, X)
    Z = reverse_shuffle(inputs[::-1], D, Y)
    A = (Y - Z) * modinv(X - Y + D, D) % D
    B = (Y - A * X) % D
    n = 101741582076661
    result = (pow(A, n, D) * X + (pow(A, n, D) - 1) * modinv(A - 1, D) * B) % D
    print("Part 2: {}".format(result))


############### THIS PROGRAM COMPUTES AN EXAMPLE OF LOGISTIC REGRESSSION ##################
# A dataset is read into X1,X2 which contains results for two student exams,
# and Y which contains the labels 1 and 0 for admission/rejection in a grad school.

import numpy as np
import os
import time
import pandas as pd
import matplotlib.pyplot as plt
from sklearn import linear_model
import scipy.optimize as opt
from matplotlib import rc

start=time.time()

path=os.getcwd() + '/Desktop/python_alb/ML/ng3/ex2data1.txt'
data=pd.read_csv(path, header=None, names=['Test1', 'Test2', 'Label'])
data.insert(0,'Ones',1) # add a column of ones for B

X0=np.array(data['Ones'])
X1=np.array(data['Test1'])
X2=np.array(data['Test2'])
Y=np.array(data['Label'])
Y=np.reshape(Y,(Y.shape[0],1))
X=np.stack((X0,X1,X2)) # stacks X0,X1,X2 features in rows of new array for matrix product

####### Visualizing the data ########
plt.close('all')
Tpass=data[data['Label'].isin([1])] # creates dataframe with values labeled by y=1
Tfail=data[data['Label'].isin([0])] # same with y=0

fig1, ax = plt.subplots(figsize=(12,8))
ax.scatter(Tpass['Test1'],Tpass['Test2'], c='b', label='Pass')
ax.scatter(Tfail['Test1'],Tfail['Test2'], c='r', label='Fail')
ax.axis([0, max(X1)+10,0, max(X2)+10])
plt.show()
plt.grid('on')
plt.legend()
plt.title('Students admitted')
plt.xlabel('Grade in Test 1')
plt.ylabel('Grade in Test 2')
end=time.time()

####### Define functions for Logistic Regression #######
def sigmoid(z):
    "sigmoid activation function for logistic regression"
    sigmoid=1/(1+np.exp(-z))
    return sigmoid

def Logistic_Loss(W,X,Y):
    "Calculates logistic loss from data, weights and labels"
    loss=-np.multiply(Y,np.log(sigmoid(np.dot(W,X))))-np.multiply((1-Y),np.log(1-sigmoid(np.dot(W,X))))
    return loss

def Logistic_Cost(W,X,Y):
    "Averages Logistic loss over samples"
    cost=np.mean(Logistic_Loss(W,X,Y))
    return cost

def Gradient(W,X,Y):
    "Computes gradient for a gradient descent step"
    gradient=1/Y.shape[0]*np.dot(sigmoid(np.dot(W,X))-Y.T,X.T)
    return gradient

####### Initialize learning rate and weight matrix, set number of iterations #######
alpha=0.001
W=np.zeros([1,X.shape[0]])
#W=np.array([-25,0,0])
iters=1000

def Gradient_Descent(W,X,Y,alpha,iters):
    Wnew=W.copy()
    Cost=np.zeros(iters)
    Witer=np.zeros([iters,3])
    for i in range(iters):
        Wnew=Wnew-alpha*Gradient(Wnew,X,Y)
        Cost[i]=Logistic_Cost(Wnew,X,Y)
        Witer[i]=Wnew
    return Wnew, Cost, Witer

plt.figure(2)

for k in range(4):
    Wnew, Cost, Witer = Gradient_Descent(W,X,Y,alpha*10**-k,iters)
    learn=u'\u03B1 = '+ str(alpha*10**-k)
    plt.plot(Cost, '.', label=learn, markersize=3)

plt.show()
plt.legend()
plt.grid('on')
plt.xlabel('N of iterations')
plt.ylabel('Cost Function')
plt.title('Cost function evolution')

plt.figure(3)
plt.plot(Witer)
plt.xlabel('iteration')
plt.ylabel('Weights')
plt.grid('on')
plt.title('Weight evolution')
plt.show()

Xnew=X.T
#Wnew=np.array([-25,0.222222222,0.222222222]) #why does setting this completely change accuracy?????
W_optimization = opt.fmin_tnc(func=Logistic_Cost, x0=Wnew, fprime=Gradient, args=(X, Y))
min_cost = Logistic_Cost(W_optimization[0], X, Y)
W_opt=np.reshape(W_optimization[0], (1,3))
def Predict_Admission(X,W_opt):
    probability=sigmoid(np.dot(W_opt,X))
    size=np.size(probability)
    Admission_result=np.zeros(size)
    print(probability)
    for l in range(size):
        if probability[0,l]>0.5:
            Admission_result[l]=1
        else:
            Admission_result[l]=0
    return Admission_result
    #return probability
Cand_grades=np.array([[1],[10],[10]])
Admission_result=Predict_Admission(X,W_opt)

theta_min = np.matrix(W_optimization[0])
correct = [1 if ((a == 1 and b == 1) or (a == 0 and b == 0)) else 0 for (a, b) in zip(Admission_result, Y)]
accuracy = (sum(map(int, correct)) % len(correct))
print('accuracy = {0}%'.format(accuracy))

plt.figure(4)
x_sigm=np.linspace(-100,100,1000)
y_sigm=sigmoid(x_sigm)
plt.plot(x_sigm,y_sigm,'b')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.grid('on')
plt.title('Sigmoid Function')
plt.show()

print("This process ran in " + str(end-start) + " seconds")


# Code:
import numpy as np

# Use index slicing and show how to get the subarray consisting of the last 2 rows, and 3rd and 4th columns
given_array = [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15], [16, 17, 18, 19, 20]
np_array = np.asarray(given_array).reshape(4, 5)
print("Full array:")
print(np_array)
print("Subarray:")
print(np_array[2:4, 2:4])

# Show how to create a one-dimensional array that sums the columns
col_sums_array = np.asarray(np_array.sum(axis=0)).reshape(1, 5)
print("One dimensional array that sums the columns:")
print(col_sums_array)

# Output:
# Full array:
# [[ 1  2  3  4  5]
#  [ 6  7  8  9 10]
#  [11 12 13 14 15]
#  [16 17 18 19 20]]
# Subarray:
# [[13 14]
#  [18 19]]
# One dimensional array that sums the columns:
# [[34 38 42 46 50]]


import numpy as np

def compute_gae(rewards, values, gamma: float = 0.99, lam: float = 0.95):
    """
    Calculates the unnormalized generalized advantage estimation function.
    Check out https://arxiv.org/abs/1506.02438.

    Arguments:
    rewards : ndarray of size (N,)
     - rewards earned during a trajectory
    values : ndarray of size(N+1,)
     - calculated values of states during a trajectory
    gamma / lam: float
     - discounting constants described in paper

    Returns:
    adv : ndarray of size
     - generalized advantage estimation
    """
    assert len(values) == len(rewards) + 1

    N = len(rewards)
    adv = np.zeros((N,))
    gae = 0

    for step in reversed(range(len(rewards))):
        delta = rewards[step] + gamma * values[step+1] - values[step]
        gae = delta + gamma * lam * gae
        adv[step] = gae

    return adv

def compute_rtg(rewards, gamma: float = 0.99):
    """
    Calculates the unnormalized sum of rewards after a point in a trajectory.
    Here we add a discounting factor that puts less emphasis on results that
    happen further in the future.

    Arguments:
    rewards : ndarray of size (N,)
     - rewards earned during a trajectory
    gamma : float
    - discounting factor (closer to one implies more weight for future events)

    Returns:
    returns : ndarray
    - returns of action, or the discounted future rewards
    """
    N = len(rewards)
    returns = np.zeros((N,))

    for step in reversed(range(N)):
        future_rewards = gamma * (returns[step+1] if step+1 < N else 0)
        returns[step] = rewards[step] + future_rewards
    return returns


#Learn how to apply L2 normalization to data
import numpy as np
import sklearn.preprocessing

#x' = x / sqrt(sigma_{i=1}^n((x_i)^2))

arr = np.array([[4, 1, 2, 2],
       [3, 4, 0, 0],
       [7, 5, 9, 2]])

#apply L2 normalization to each row
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!each row!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

normalizer = sklearn.preprocessing.Normalizer()

print(normalizer.fit_transform(arr))
# [[0.8        0.2        0.4        0.4       ]
#  [0.6        0.8        0.         0.        ]
#  [0.55513611 0.39652579 0.71374643 0.15861032]]


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Sep 11 14:22:08 2021
@author: jaimewalter
"""

## Visualize a cube that rotates in 3D, and its 2D orthographic projections. Made with linear transformations.

import numpy as np
import math
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection, Line3DCollection
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
from matplotlib.widgets import Slider, Button

# Assign angles of rotation
phi = 0
theta = 0
psi = 0

# Create function that generates coordinates of the vertices of the cube after rotating.
def rotate(phi, theta, psi):
    # Vertex coordinates of original cube
    vertices = np.array([[-1, -1, -1],
                      [1, -1, -1 ],
                      [1, 1, -1],
                      [-1, 1, -1],
                      [-1, -1, 1],
                      [1, -1, 1 ],
                      [1, 1, 1],
                      [-1, 1, 1]])

    # Rotation matrices
    XrollMatrix = np.mat([[1, 0, 0], \
                          [0, math.cos(phi), math.sin(phi)], \
                          [0, -math.sin(phi), math.cos(phi)]])

    YpitchMatrix = np.mat([[math.cos(theta), 0, -math.sin(theta)], \
                          [0, 1, 0], \
                          [math.sin(theta), 0, math.cos(theta)]])

    ZyawMatrix = np.mat([[math.cos(psi), math.sin(psi), 0], \
                          [-math.sin(psi), math.cos(psi), 0], \
                          [0, 0, 1]])

    # Generate vertex coords of rotated cube
    Z = np.zeros((8,3))
    for i in range(8): Z[i,:] = np.dot(vertices[i,:],XrollMatrix)
    for i in range(8): Z[i,:] = np.dot(Z[i,:],YpitchMatrix)
    for i in range(8): Z[i,:] = np.dot(Z[i,:],ZyawMatrix)

    # Vertices in each face of the cube
    faces = [[Z[0],Z[1],Z[2],Z[3]],
      [Z[4],Z[5],Z[6],Z[7]],
      [Z[0],Z[1],Z[5],Z[4]],
      [Z[2],Z[3],Z[7],Z[6]],
      [Z[1],Z[2],Z[6],Z[5]],
      [Z[4],Z[7],Z[3],Z[0]]]

    # Projection matrices
    ortXY = np.mat([[1, 0, 0], \
                   [0, 1, 0]])
    ortXZ = np.mat([[1, 0, 0], \
                    [0, 0, 1]])
    ortYZ = np.mat([[0, 1, 0], \
                    [0, 0, 1]])

    # Generate new coordinates and faces
    XY = np.zeros((8,2))
    for i in range(8): XY[i,:] = np.dot(ortXY, Z[i,:])
    #XY = np.delete(XY, 2, 1)

    XZ = np.zeros((8,2))
    for i in range(8): XZ[i,:] = np.dot(ortXZ, Z[i,:])
    #XZ = np.delete(XZ, 1, 1)

    YZ = np.zeros((8,2))
    for i in range(8): YZ[i,:] = np.dot(ortYZ, Z[i,:])
    #YZ = np.delete(YZ, 0, 1)

    facesXY = [[XY[7],XY[4],XY[5],XY[6]],
      [XY[1],XY[2],XY[3],XY[0]],
      [XY[4],XY[0],XY[3],XY[7]],
      [XY[5],XY[1],XY[2],XY[6]],
      [XY[7],XY[3],XY[2],XY[6]],
      [XY[0],XY[4],XY[5],XY[1]]]
    facesXZ = [[XZ[7],XZ[4],XZ[5],XZ[6]],
      [XZ[1],XZ[2],XZ[3],XZ[0]],
      [XZ[4],XZ[0],XZ[3],XZ[7]],
      [XZ[5],XZ[1],XZ[2],XZ[6]],
      [XZ[7],XZ[3],XZ[2],XZ[6]],
      [XZ[0],XZ[4],XZ[5],XZ[1]]]
    facesYZ = [[YZ[7],YZ[4],YZ[5],YZ[6]],
      [YZ[1],YZ[2],YZ[3],YZ[0]],
      [YZ[4],YZ[0],YZ[3],YZ[7]],
      [YZ[5],YZ[1],YZ[2],YZ[6]],
      [YZ[7],YZ[3],YZ[2],YZ[6]],
      [YZ[0],YZ[4],YZ[5],YZ[1]]]

    # Return all important values
    return Z, XY, XZ, YZ, facesXY, facesXZ, facesYZ, faces

# Get values from rotate()
results = rotate(phi, theta, psi)
Z = results[0]
XY = results[1]
XZ = results[2]
YZ = results[3]
facesXY = results[4]
facesXZ = results[5]
facesYZ = results[6]
faces = results[7]

 ## Make the plot
fig = plt.figure()

ax = fig.add_subplot(221, projection='3d', proj_type = "ortho", azim = -45, elev = 35, title="Isometric")
ax.set_box_aspect([1,1,1])

ax2 = fig.add_subplot(222, title="Top")
ax2.set_aspect('equal', adjustable='box')
ax3 = fig.add_subplot(224, title="Front")
ax3.set_aspect('equal', adjustable='box')
ax4 = fig.add_subplot(223, title="Side")
ax4.set_aspect('equal', adjustable='box')

plt.subplots_adjust(bottom=0.3)
plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

# Define the function for updating the plot
def plot(Z, XY, XZ, YZ, facesXY, facesXZ, facesYZ, faces):
    # Clear previous points
    ax.clear()
    ax2.clear()
    ax3.clear()
    ax4.clear()

    ax.set_box_aspect([1,1,1])

    ax.set_title("Isometric")

    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')

    ax.set_xlim([-1.5, 1.5])
    ax.set_ylim([-1.5, 1.5])
    ax.set_zlim([-1.5, 1.5])

    ax.locator_params(axis='x', nbins=3)
    ax.locator_params(axis='y', nbins=3)
    ax.locator_params(axis='z', nbins=3)

    ax2.set_xlim([-2, 2])
    ax2.set_ylim([-2, 2])
    ax3.set_xlim([-2, 2])
    ax3.set_ylim([-2, 2])
    ax4.set_xlim([-2, 2])
    ax4.set_ylim([-2, 2])

    ax2.set_title("Top")
    ax3.set_title("Front")
    ax4.set_title("Side")

    ax2.set_xlabel('X')
    ax2.set_ylabel('Y')
    ax3.set_xlabel('X')
    ax3.set_ylabel('Z')
    ax4.set_xlabel('Y')
    ax4.set_ylabel('Z')

    # Plot new graphs
    ax.scatter3D(Z[:, 0], Z[:, 1], Z[:, 2])

    ax.add_collection3d(Poly3DCollection(faces, facecolors='cyan', linewidths=1, edgecolors='blue', alpha=.25))

    ax2.scatter(XY[:, 0], XY[:, 1])
    ax3.scatter(XZ[:, 0], XZ[:, 1])
    ax4.scatter(YZ[:, 0], YZ[:, 1])

    for i in range(6):
        poly = Polygon(facesXY[i], facecolor='cyan', edgecolor='blue', alpha = 0.25)
        ax2.add_patch(poly)
    for i in range(6):
        poly = Polygon(facesXZ[i], facecolor='cyan', edgecolor='blue', alpha = 0.25)
        ax3.add_patch(poly)
    for i in range(6):
        poly = Polygon(facesYZ[i], facecolor='cyan', edgecolor='blue', alpha = 0.25)
        ax4.add_patch(poly)

# Make the first plot
plot(Z, XY, XZ, YZ, facesXY, facesXZ, facesYZ, faces)

#Create sliders
axphi = plt.axes([0.19, 0.15, 0.65, 0.03], facecolor='lightgoldenrodyellow')
phi_slider = Slider(
    ax=axphi,
    label='Phi [rad]',
    valmin=0,
    valmax=1.57,
    valinit=phi,
)

axtheta = plt.axes([0.19, 0.1, 0.65, 0.03], facecolor='lightgoldenrodyellow')
theta_slider = Slider(
    ax=axtheta,
    label='Theta [rad]',
    valmin=0,
    valmax=1.57,
    valinit=theta,
)

axpsi = plt.axes([0.19, 0.05, 0.65, 0.03], facecolor='lightgoldenrodyellow')
psi_slider = Slider(
    ax=axpsi,
    label='Psi [rad]',
    valmin=0,
    valmax=1.57,
    valinit=psi,
)

# The function to be called anytime a slider's value changes
def update(val):
    phi = phi_slider.val
    theta = theta_slider.val
    psi = psi_slider.val
    results = rotate(phi, theta, psi)
    Z = results[0]
    XY = results[1]
    XZ = results[2]
    YZ = results[3]
    facesXY = results[4]
    facesXZ = results[5]
    facesYZ = results[6]
    faces = results[7]
    plot(Z, XY, XZ, YZ, facesXY, facesXZ, facesYZ, faces)

# Register the update function with each slider
phi_slider.on_changed(update)
theta_slider.on_changed(update)
psi_slider.on_changed(update)


"""

Parameter estimation using MCMC - Probability Model
=============================================================================

In the following we learn the mean and covariance of a univariate gaussian distribution from data.

"""

#%% md
#
# Initially we have to import the necessary modules.

#%%

import numpy as np
import matplotlib.pyplot as plt

from UQpy.inference import DistributionModel, BayesParameterEstimation
from sklearn.neighbors import KernelDensity  # for the plots
from UQpy.distributions import JointIndependent, Uniform, Lognormal, Normal

def pdf_from_kde(domain, samples1d):
    bandwidth = 1.06 * np.std(samples1d) * samples1d.size ** (-1 / 5)
    kde = KernelDensity(bandwidth=bandwidth).fit(samples1d.reshape((-1, 1)))
    log_dens = kde.score_samples(domain)
    return np.exp(log_dens)

#%% md
#
# First, for the sake of this example, we generate fake data from a gaussian distribution with mean 10 and standard
# deviation 1.

#%%

np.random.seed(100)
mu, sigma = 10, 1  # true mean and standard deviation
data_1 = np.random.normal(mu, sigma, 100).reshape((-1, 1))
np.random.seed()

# plot the data and true distribution
count, bins, ignored = plt.hist(data_1, 30, density=True)
plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)),
         linewidth=2, color='r')
plt.title('data as histogram and true distribution to be estimated')
plt.show()

#%% md
#
# In a Bayesian setting, the definition of a prior pdf is a key point. The prior for the parameters must be defined in
# the model. Note that if no prior is given, an improper, uninformative, prior is chosen, :math:`p(\theta)=1` for all
# :math:`\theta`.

#%%

p0 = Uniform(loc=0., scale=15)
p1 = Lognormal(s=1., loc=0., scale=1.)
prior = JointIndependent(marginals=[p0, p1])

candidate_model = DistributionModel(distributions=Normal(loc=None, scale=None), n_parameters=2, prior=prior)

# Learn the unknown parameters using MCMC
from UQpy.sampling import MetropolisHastings

mh1 = MetropolisHastings(jump=10, burn_length=10, seed=[1.0, 0.2], random_state=123)

bayes_estimator = BayesParameterEstimation(inference_model=candidate_model,
                                           data=data_1,
                                           nsamples=500,
                                           sampling_class=mh1)

# print results
s = bayes_estimator.sampler.samples
plt.scatter(s[:, 0], s[:, 1])
plt.scatter(10, 1, marker='+', label='true parameter')
plt.title('MCMC samples')
plt.legend()
plt.show()

fig, ax = plt.subplots(1, 2, figsize=(10, 4))

domain = np.linspace(0, 15, 200)[:, np.newaxis]
pdf = pdf_from_kde(domain, s[:, 0])
ax[0].plot(domain, p0.pdf(domain), label='prior')
ax[0].plot(domain, pdf, label='posterior')
ax[0].set_title('posterior pdf of theta=mu')
ax[0].legend()

domain = np.linspace(0, 2, 200)[:, np.newaxis]
pdf = pdf_from_kde(domain, s[:, 1])
ax[1].plot(domain, p1.pdf(domain), label='prior')
ax[1].plot(domain, pdf, label='posterior')
ax[1].set_title('posterior pdf of theta=sigma')
ax[1].legend()

plt.show()


import numpy as np
np.set_printoptions(precision=4, suppress=True)

from BN import *

gra = [[],[0],[0],[1,2]]
ev = (1,1,1,1)

p1 = Node( np.array([.5]), gra[0] )# cloudy
print( "p1 false %.4e p1 true %.4e" % (p1.computeProb(ev)[0] , p1.computeProb(ev)[1]))

p2 = Node( np.array([.5,.1]), gra[1] )# sprinkler

p3 = Node( np.array([.2,.8]), gra[2] )# rain

p4 = Node( np.array([[.0,.9],[.9,.99]]), gra[3] )# wetgrass
print( "p2 = 1, p3 = 1, p4 false %.4e p4 true %.4e" % (p4.computeProb(ev)[0] , p4.computeProb(ev)[1]))

prob = [p1,p2,p3,p4]

bn = BN(gra, prob)

jp = []
for e1 in [0,1]:
	for e2 in [0,1]:
		for e3 in [0,1]:
			for e4 in [0,1]:
				jp.append(bn.computeJointProb((e1, e2, e3, e4)))

print("sum joint %.3f (1)" % sum(jp))

### Tests to joint Prob
ev = (0,0,0,0)
print( "joint %.4g (0.2)" % bn.computeJointProb(ev) )

ev = (1,1,1,1)
print( "joint %.4g (0.0396)" % bn.computeJointProb(ev) )

### Tests to post Prob
# P(e1|e4=1)
ev = (-1,[],[],1)
print("ev : ")
print(ev)
print( "post : %.4g (0.5758)" % bn.computePostProb(ev)  )

# P(e4|e1=1)
ev = (1,[],[],-1)
print("ev : ")
print(ev)
print( "post : %.4g (0.7452)" % bn.computePostProb(ev)  )

# P(e1|e2=0,e3=0)
ev = (-1,0,0,[])
print("ev : ")
print(ev)
print( "post : %.4g (0.3103)" % bn.computePostProb(ev)  )