#!/usr/bin/python3
# 8.3 - Perspective Projections
# http://learnwebgl.brown37.net/08_projections/projections_perspective.html
from math import *
from sympy import *
left,right,bottom,top,near,far = symbols('left right bottom top near far')

# near-far range
# OpenGL -1 to 1
# D3D, webGPU 0 to 1
# Solve system
# c1/z + c2
# When z = near, the equation must calculate 0.
# When z = far, the equation must calculate 1.
Z = Matrix([
    [1/near, 1,  0],
    [1/far,  1,  1]
]).rref()

z1 = simplify(Z[0][0,2])
z2 = simplify(Z[0][1,2])
#print(z1)
#print(z2)

# Scale the 2D (x’,y’) values
# in the viewing window to a
# 2-by-2 unit square; (-1,-1) to (+1,+1)
A = Matrix([
    [2/(right-left),             0, 0, 0],
    [             0,2/(top-bottom), 0, 0],
    [             0,             0, 1, 0],
    [             0,             0, 0, 1]
])
# Perform the perspective calculation
B = Matrix([
    [near,   0, 0, 0],
    [   0,near, 0, 0],
    [   0,   0, 1, 0],
    [   0,   0, 0, 1]
])
# Scale the depth values (z)
# into a normalized range (0,+1)
# (and setup for division by (z)).
# c1, c2, x, y, z = symbols('c1 c2 x y z')
# C = Matrix([
#     [1, 0,    0,    0],
#     [0, 1,    0,    0],
#     [0, 0,   c2,   c1],
#     [0, 0,    1,    0]
# ])
# print(C*Matrix([[x],[y],[z],[1]]))
C = Matrix([
    [1, 0,    0,    0],
    [0, 1,    0,    0],
    [0, 0,   z2,   z1],
    [0, 0,    1,    0]
])

# Translate the apex of the frustum to the origin.
D = Matrix([
    [1, 0, 0, -(left+right)/2],
    [0, 1, 0, -(bottom+top)/2],
    [0, 0, 1,               0],
    [0, 0, 0,               1]
])

R = A*B*C*D

P = R.evalf(subs={left:-1,right:1,bottom:-1,top:1,near:2,far:40})
M1 = P*Matrix([[ 1],[ 1],[ 2],[1]]); M1 = M1/M1[3]; #print(M1)
M2 = P*Matrix([[-1],[-1],[ 2],[1]]); M2 = M2/M2[3]; #print(M2)
M3 = P*Matrix([[ 1],[ 1],[40],[1]]); M3 = M3/M3[3]; #print(M3)
M4 = P*Matrix([[-1],[-1],[40],[1]]); M4 = M4/M4[3]; #print(M4)

assert (isclose(M1[0], 1)), "Must be at far right"
assert (isclose(M1[1], 1)), "Must be at up"
assert (isclose(M1[2], 0)), "Must be at zero"
assert (isclose(M2[0], -1)), "Must be at far left"
assert (isclose(M2[1], -1)), "Must be at bottom"
assert (isclose(M2[2],  0)), "Must be at ozerone"
# Perfect center in infinite and not the "far" point
assert (isclose(M3[0], 0.05)), "Must be near center, but +x+y quadrant"
assert (isclose(M3[1], 0.05)), "Must be near center, but +x+y quadrant"
assert (isclose(M3[2], 1)), "Must be at one"
assert (isclose(M4[0],-0.05)), "Must be near center, but -x-y quadrant"
assert (isclose(M4[1],-0.05)), "Must be near center, but -x-y quadrant"
assert (isclose(M4[2], 1)), "Must be at one"

[
    print("R[{0}] = {1};".format(
        j * 4 + i,
        ccode(simplify(R[i,j])))
    )
    for j in range(4)
    for i in range(4)
]


# script to show realation of binomial and poisson distribution in photon counting
import numpy as np
import math
import matplotlib.pyplot as plt

from scipy.stats import binom, poisson

n_exp    = 100000   # number of experiments to run
chunk_size = np.uint(1e3)
mu       = 7
seed     = 0

#---------------------------------------------------------------------------------------------------
np.random.seed(seed)

#for n_nuclei in [10,20,100,1000,10000]:    # number of nuclei to start with
for n_nuclei in [10,20,100,1000]:    # number of nuclei to start with

  p = mu/n_nuclei  # probability that an event (decay + detection) occurs

  if (n_exp) < chunk_size:
    r = np.random.rand(n_exp, n_nuclei)
    # number of events in every experiment
    n = (r<=p).sum(1)
  else:
    nchunks = math.ceil(n_exp / chunk_size)

    n = []
    for i in range(nchunks):
      print(f'{i+1} / {nchunks}')
      r = np.random.rand(chunk_size, n_nuclei)
      n.append((r<=p).sum(1))

    n = np.array(n).flatten()

  # histogram the number of detected events per experiement
  h = np.histogram(n, np.arange(n.max() + 2) - 0.5)

  # generate x and y for plots to make life easier
  x = (0.5*(h[1][:-1] + h[1][1:])).astype(np.uint) # the bin centers
  y = h[0]

  # calculate binom and poisson pmfs
  xx   = np.arange(n_nuclei+1)
  bpmf = binom.pmf(xx, n_nuclei, p)
  ppmf = poisson.pmf(xx, n_nuclei*p)

  # print max abs and relative error
  abs_error     = ppmf - bpmf
  rel_error     = abs_error / bpmf
  print(f'max abs error P/B {abs_error.max()}')
  print(f'max rel error P/B {rel_error.max()}')

  fig, ax = plt.subplots(1,1, figsize = (7,4))
  ax.bar(x, y/y.sum(), width = 1, color = 'silver')
  ax.plot(xx, bpmf, 'o-', color = 'tab:blue', label = f'Binomial pmf (N={n_nuclei:.1e}, p = {p:.1e})')
  ax.plot(xx, ppmf, '.:', color = 'tab:orange', label = f'Poisson pmf (mu = N*p = {mu})')
  ax.set_xlim(-0.2,19)
  ax.set_ylim(-0.002,0.3)
  ax.set_xlabel('n (number of events)')
  ax.set_title(f'histogram N = {n_nuclei}, p = {p}, {n_exp} exps.')
  ax.grid(ls = ':')
  ax.legend()
  fig.tight_layout()
  fig.savefig(f'mu_{mu}_p_{p:.1e}.png')
  fig.show()


# # Linear Regression using Gradient Descent

# ## Import Packages

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# ## Import Data set

train_data = pd.read_csv('apartment_data_train.csv')
train_data.head()

train_data.info()

train_data.describe()

test_data = pd.read_csv('apartment_data_test.csv')
test_data.head()

# ## Feature Separation and Normalization

X_train = train_data.iloc[:, :-1].values
X_test = test_data.iloc[:, :].values
y_train = train_data.iloc[:, -1:].values
m = y_train.shape[0]

train_data_mean = train_data.mean(0)
train_data_std = train_data.std(0)

X_train_mean = train_data_mean[0:2].values
X_train_std = train_data_std[0:2].values
X_train_norm = (X_train - X_train_mean) / X_train_std

X_test_norm = (X_test - X_train_mean) / X_train_std

# ## Add Intercept Term to Features

train_ones = np.ones((X_train_norm.shape[0], 1))
test_ones = np.ones((X_test_norm.shape[0], 1))
X_train_norm = np.column_stack((train_ones, X_train_norm))
X_test_norm = np.column_stack((test_ones, X_test_norm))

# ## Perform Gradient Descent

# ### Compute Cost

def compute_cost(X, y, theta):
    hx = np.matmul(X, theta)
    error_values = hx - y
    squared_error = np.square(error_values)
    cost = np.sum(squared_error, 0)
    return cost

# ### Gradient Descent

def gradient_descent(X, y, theta, alpha, iterations):
    iteration_array = np.array([itr for itr in range(iterations)])
    cost_history = []
    for iteration in range(iterations):
        hx = np.matmul(X, theta)
        error_value = hx - y
        error_value_multi = np.matmul(error_value.T, X)
        delta = np.multiply(error_value_multi.T, (alpha / m))
        theta = theta - delta
        cost_history.append(compute_cost(X, y, theta))
    return [theta, np.column_stack((iteration_array, np.asarray(cost_history)))]

# ### Use Gradient Descent

alpha = 0.01
num_iterations = 400
theta = np.zeros((X_train_norm.shape[1], 1))
theta, cost_history_result = gradient_descent(X_train_norm, y_train, theta, alpha, num_iterations)
result = np.matmul(X_test_norm, theta)
print(result)

# ### Cost Function

plt.plot(cost_history_result)
plt.xlabel("Iteration")
plt.ylabel("Cost")
plt.show()


from numpy.matlib import *
import numpy
import time
import scipy.sparse.linalg

def gausselim(A, B, pivoting = 0, ztol = 1.0e-8, debug = False):

    size = len(A)
    X    = [0.0] * size
    R    = range(size)
    C    = range(size)
    code = 0

    # Triangularization.
    for pivot in range(size-1):
        absm    = abs(A[pivot][pivot])
        exchrow = pivot
        exchcol = pivot

        if pivoting == 1: # partial pivoting.
            for row in range(pivot + 1, size):
                atestm  = abs(A[row][pivot])
                if atestm > absm:
                   absm    = atestm
                   exchrow = row

        elif pivoting == 2: #full pivoting.
            for row in range(pivot + 1, size):
                for col in range(pivot, size):
                   atestm  = abs(A[row][col])
                   if atestm > absm:
                      absm    = atestm
                      exchrow = row
                      exchcol = col

        elif pivoting == 3: # rook pivoting.

           absm  = 0.0
           Done = False
           while not Done:
               Done = True
               #Find row with maximum element in column exchcol.
               for row in range(exchrow, size):
                   atest = abs(A[row][exchcol])
                   if atest > absm:
                      absm  = atest
                      exchrow = row
                      Done = False

               #Find col with maximum element in row exchrow.
               for col in range(pivot, size):
                   atest = abs(A[exchrow][col])
                   if atest > absm:
                      absm = atest
                      exchcol = col
                      Done = False

        #Exchange columns ?
        if pivot != exchcol:
            if debug: print "exhanging columns", pivot, exchcol
            for row in range(pivot, size):
                A[row][pivot], A[row][exchcol] = A[row][exchcol], A[row][pivot]
            #B[pivot], B[exchcol] = B[exchcol],B[pivot]
            C[pivot] = exchcol

        #Exchange rows?
        if pivot != exchrow:
            if debug:
               print "Exchanging rows:",  pivot,  exchrow
            A[exchrow],A[pivot] = A[pivot], A[exchrow]
            B[exchrow],B[pivot] = B[pivot], B[exchrow]
            R[pivot] = exchrow

        if absm > ztol:
           if debug:
               print "pre reduction for row",pivot
               print "current augmented matrix:"
               # mataugprint(A,B)
           m = float(A[pivot][pivot])
           for row in range(pivot +1, size):
               kmul = float(A[row][pivot])/m
               # Apply rectangular rule.(Reduction)
               for col  in range(size -1, pivot, -1):
                    A[row][col] = float(A[row][col]) - kmul * A[pivot][col]
               B[row] = float(B[row]) - kmul * B[pivot]
               A[row][pivot] = 0.0

           if debug:
               print "post reduction:"
               print "current augmented matrix:"
               # mataugprint(A,B)
               print

        else:
           code = 1

    # Perform Back substitution.
    if debug:
       print "Back substitution: row, rhs sum, pivot"
    if code == 0:
        for row in range(size-1, -1, -1):
           sum = B[row]
           for k in range(row+1, size):
                sum -= (X[k] * A[row][k])
           #print row, sum, A[row][row]
           X[row] = sum/A[row][row]
        reorder(X,C)
        if debug: print "Computed value of X = ",  X
    return (code,R,C, A,X,B)

def reorder(X, C):
    for i, c in enumerate(C):
        if i != c:
           #exchange elements at positions i and c.
           X[i], X[c] =  X[c], X[i]
    return X

def Solve(A,b):

    (code, R,C, A, X, B) = gausselim(A, b)
    if code==0:
        return X
    else:
        return None


import numpy as np

##########################################################################
# Modeling: what we want to capture

# points = [(np.array([2]),4), (np.array([4]),2)]
# d = 1

# Generate artificial data
true_w = np.array([1,2,3,4,5])
d = len(true_w)
points = []
iterationCount = 500
for i in range(iterationCount):
    x = np.random.randn(d)
    y = true_w.dot(x) + np.random.randn()
    #  print(x, y)
    points.append((x,y))

def F(w):
    return sum((w.dot(x) - y)**2 for x, y in points) / len(points)

def dF(w):
    return sum(2*(w.dot(x) - y)*x  for x, y in points) / len(points)

def sF(w, i):
    x,y = points[i]
    return (w.dot(x) - y)**2

def sdF(w, i):
    x,y = points[i]
    return 2*(w.dot(x) - y)*x

##########################################################################
# Algorithms: how we compute it

def gradientDescent(F, dF, d):
    # gradient descent
    w = np.zeros(d)
    eta = 0.01
    for t in range(iterationCount):
        value = F(w)
        gradient = dF(w)
        w = w - eta * gradient
        print('iteration {}: w = {}, F(w) = {}'.format(t, w, value))

def stochasticGradientDescent(sF, sdF, d, n):
    # stochastic gradient descent
    w = np.zeros(d)
    eta = 1
    numUpdates = 0
    for t in range(iterationCount):
        for i in range(n):
            value = sF(w, i)
            gradient = sdF(w, i)
            numUpdates += 1
            eta = 1 / numUpdates
            w = w - eta * gradient
        print('iteration {}: w = {}, F(w) = {}'.format(t, w, value))

##########################################################################
# gradientDescent(F, dF, d)
stochasticGradientDescent(sF, sdF, d, len(points))


from sympy import diag, sqrt, symbols, tanh

from einsteinpy.symbolic import constants
from einsteinpy.symbolic.metric import MetricTensor

def AlcubierreWarp(
    x_s=symbols("x_s"), sigma=symbols("sigma"), R=symbols("R"), v=symbols("v")
):
    """
    Alcubierre Warp Drive Metric (G = c = 1) [1]_

    .. [1] Classical and Quantum Gravity,
    "The warp drive: hyper-fast travel within general relativity",
    Miguel Alcubierre, 1994, **11(5)**, pp. 73-77

    arXiv: `<https://arxiv.org/abs/gr-qc/0009013>`_)

    Journal: `<https://doi.org/10.1088%2F0264-9381%2F11%2F5%2F001>`_

    Parameters
    ----------
    x_s : ~sympy.core.basic.Basic or int or float
        Coordinate (a function of time, `t`)
    sigma : ~sympy.core.basic.Basic or int or float
        Arbitrary Parameter (See paper [1]_)
    R : ~sympy.core.basic.Basic or int or float
        Arbitrary Parameter (See paper [1]_)
    v : ~sympy.core.basic.Basic or int or float
        Coordinate Velocity

    Returns
    -------
    ~einsteinpy.symbolic.metric.MetricTensor
        Alcubierre Warp Drive Metric Tensor

    Alcubierre Warp Drive Metric (G = c = 1) [1]_

    """
    coords = symbols("t x y z")
    t, x, y, z = coords
    r_s = sqrt((x - x_s) ** 2 + y + z)
    f = (tanh(sigma * (r_s + R)) - tanh(sigma * (r_s - R))) / (2 * tanh(sigma * R))

    v2 = v ** 2
    f2 = f ** 2

    metric = diag(v2 * f2 - 1, 1, 1, 1).tolist()
    metric[0][1] = metric[1][0] = -v * f

    return MetricTensor(metric, coords, "ll", name="AlcubierreWarpMetric")


"""
Calculate eigenvalues and eigenvectors of a matrix using the power method.
"""

from linear_algebra import matrix_size, dot_product

import numpy as np

ZERO_NUMBER = 1e-20  # Values smaller than this numbers are considered to be zero

def dominant_eigen_system(matrix, iterations):
    """
    Calculates the dominant eigenvalue and a dominant unit eigenvector
    of a symmetric matrix using the power method.

    Parameters
    ----------
    matrix : numpy.ndarray
        A symmetric matrix.

    iterations : int
        The number of iterations of the power method.

    Returns
    -------
    tuple of (Int, numpy.ndarray)
        Dominant eigenvalue and a dominant unit eigenvector.
        The list is empty if the dominant eigenvalue is zero.
    """

    row_count, col_count = matrix_size(matrix)

    # Create an initial unit vector
    unit_vector = np.zeros([col_count, 1])
    unit_vector[0, 0] = 1

    # Calculate dominant unit eigenvector
    for _ in range(iterations):
        product = matrix @ unit_vector
        vec_length = np.linalg.norm(product)

        # Reached zero eigenvalue
        if vec_length < ZERO_NUMBER:
            return (0, [])

        unit_vector = product / vec_length

    # Calculate dominant eigenvalue
    eigenvalue = dot_product(matrix @ unit_vector, unit_vector)

    return (eigenvalue, unit_vector)


from scipy.optimize import linprog

# fx = [-3, -5]                             ;Funcion a optimizar igualada a cero
# restrictions = [[1, 0], [0, 2], [3, 2]]   ;Lado izquierdo de las restricciones
# [4, 12, 18]                               ;Lado derecho de las restricciones
def simplex(fx, restrictions, result_restrictions):
    x0_bounds = (0, None)
    x1_bounds = (0, None)
    # Solve the problem by Simplex method in Optimization (method='revised simplex', method='simplex', )
    res = linprog(fx, A_ub=restrictions, b_ub=result_restrictions,  bounds=(x0_bounds, x1_bounds), method='simplex', options={"disp": True})
    print(res)
    # X = Funcion objetivo
    # Fun = resultado de optimizacion
    return res

# Probando el metodo simplex
fx = [-3, -5]
restrictions = [[1, 0], [0, 2], [3, 2]]
result_restrictions = [4, 12, 18]
simplex(fx, restrictions, result_restrictions)


"""Collection of math modules."""

import numpy as np
import math

def distance(x1, y1, x2, y2):
    """Compute distnace between two points."""
    return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)

def point_distance(u, v):
    """Compute distance."""
    return distance(u[0], u[1], v[0], v[1])

def get_direction(x, y):
    """Compute direction between two points."""
    dx = x[0] - y[0]
    dy = x[1] - y[1]
    return np.arctan2(dy, dx)

def round_angles(direction):
    """Round angles such that direction is less that 2*np.pi"""
    return direction % (2 * np.pi)

"""
From Algorithm tutor
https://algorithmtutor.com/Computational-Geometry/Determining-if-two-consecutive-segments-turn-left-or-right/
"""

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def subtract(self, p):
    	return Point(self.x - p.x, self.y - p.y)

    def __str__(self):
        return '(' + str(self.x) + ', ' + str(self.y) + ')'

def cross_product(p1, p2):
	return p1.x * p2.y - p2.x * p1.y

def direction(p1, p2, p3):
	return  cross_product(p3.subtract(p1), p2.subtract(p1))

def collinear(p1, p2, p3):
	return direction(p1, p2, p3) == 0

def right(p1, p2, p3):
	return direction(p1, p2, p3) > 0

def left(p1, p2, p3):
	return direction(p1, p2, p3) < 0

# checks if p lies on the segment p1p2
def on_segment(p1, p2, p):
    return min(p1.x, p2.x) <= p.x <= max(p1.x, p2.x) and min(p1.y, p2.y) <= p.y <= max(p1.y, p2.y)

# checks if line segment p1p2 and p3p4 intersect
def check_intersect(p1, p2, p3, p4):
    p1 = Point(p1[0], p1[1])
    p2 = Point(p2[0], p2[1])
    p3 = Point(p3[0], p3[1])
    p4 = Point(p4[0], p4[1])
    d1 = direction(p3, p4, p1)
    d2 = direction(p3, p4, p2)
    d3 = direction(p1, p2, p3)
    d4 = direction(p1, p2, p4)

    if ((d1 > 0 and d2 < 0) or (d1 < 0 and d2 > 0)) and \
        ((d3 > 0 and d4 < 0) or (d3 < 0 and d4 > 0)):
        return True

    elif d1 == 0 and on_segment(p3, p4, p1):
        return True
    elif d2 == 0 and on_segment(p3, p4, p2):
        return True
    elif d3 == 0 and on_segment(p1, p2, p3):
        return True
    elif d4 == 0 and on_segment(p1, p2, p4):
        return True
    else:
        return False


import os
import pandas as pd
import matplotlib.ticker as ticker
from sklearn.metrics import mean_squared_error
import numpy as np
from scipy.stats import norm,normaltest,probplot
from scipy import optimize
import matplotlib.pyplot as plt
from matplotlib import style
from sklearn.linear_model import LinearRegression
from statistics import pstdev
import pylab

path = """path_to_folder"""

os.chdir(path)

data = pd.read_csv('temperature_data.csv')

x =np.array(list(data["Temp"].index))
y = np.array(list(data["Temp"]))

###

# TEMPERATURE DATA

plt.plot(x,y)
plt.show()

###
def form(x,A,B,C,phi):
    return A+B*x+C * np.sin(np.radians(x - phi))

param, covariance = optimize.curve_fit(form, x, y,[ 0, 0, 0 ,0])

fit_cosin = form(x, *param)

plt.scatter(x, y, label='data',alpha=0.5)
plt.plot(x, fit_cosin, 'r-')
plt.ticklabel_format(style='sci', axis='y', scilimits=(0,0))
plt.axes().xaxis.set_major_locator(ticker.MultipleLocator(45))
plt.show()

###

projection_mean = y-fit_cosin
plt.plot(projection_mean)
plt.show()

###

plt.hist(projection_mean,bins=500)
plt.show()

###

probplot(projection_mean, dist="norm", plot=pylab)
pylab.show()
###

absc = projection_mean[:-1].reshape((-1, 1))
ordo = projection_mean[1:]
model = LinearRegression(fit_intercept=True)
model.fit(absc, ordo)
b = model.coef_[0]
a = model.intercept_
x_test=np.linspace(min(absc[:-1]),max(absc[:-1]),num=len(absc)).reshape((-1, 1))
y_pred = model.predict(x_test)

se = mean_squared_error(ordo, y_pred)
print("b : ",b)
print("a : ", a)
print("se : ", se)

plt.scatter(absc,ordo,alpha=0.5)
plt.plot(x_test,y_pred,c="red")
plt.show()

###

# OU MODEL

K = -np.log(b)
theta = a/(1-b)
sigma = se*np.sqrt(-2*np.log(b)/(1-b**2))/10

print("K = ", K)
print("theta : ",theta)
print("sigma : ",sigma)

def OU(x,t,K,theta,sigma):
    res=[x]
    for i in range(t):
        ou_t = res[-1]*np.exp(-K)+theta*(1-np.exp(-K))+sigma*np.sqrt((1-np.exp(-2*K))/(2*K))*np.random.normal(0,1)
        res.append(ou_t)
    return np.array(res)

res= OU(projection_mean[0],len(projection_mean)-1,K,theta,sigma)
plt.plot(res,alpha=0.5,c="red")
plt.plot(projection_mean[:-1],alpha=0.5,c="blue")
plt.show()

###

#Autocorrelation

def autocorr(x):
    result = np.correlate(x, x, mode='full')
    return result[int(result.size/2):]

plt.plot(autocorr(res),c="red")
plt.plot(autocorr(projection_mean[:-1]),c="blue")
plt.show()

###

plt.hist(res,bins=500,color="red",alpha=0.3)
plt.hist(projection_mean[:-1],bins=500,color="blue",alpha=0.3)
plt.show()

###

#Prediction

def pred_T(n,x0,t0):
    new_x = np.array([x0+i for i in range(N)])
    fit_cosin_future = form(new_x, *param)
    ou_test= OU(projection_mean[-1],n-1,K,theta,sigma)
    temprerature_prediction = fit_cosin_future + ou_test
    return temprerature_prediction

N = 30*6
temprerature_prediction = pred_T(N,x[-1],y[-1])
plt.plot([i for i in range(len(x)+N)],list(y)+list(temprerature_prediction),c="red")
plt.plot(x,y,c="blue")
plt.show()

###

# MC Simulation
referential =y[-1]
def H(n,x0,t0):
    return (referential*n-np.sum(pred_T(n,x0,t0)))

n_sim=1000

# Average diff between our reference point and the our future prediction
sim = np.array([H(N,x[-1],y[-1]) for i in range(n_sim)])

mu_n = np.mean(sim)
sigma_n=pstdev(sim)
print("mu_n : ",mu_n)
print("sigma_n : ",sigma_n)

plt.plot(sim)
plt.show()

###

# PRICING

#risk free rate
r = 0.05/365

def call_hdd(t,strike,r):
    return( np.exp(-r*t)*((strike-mu_n)*(norm.cdf(alpha_n)-norm.cdf(-mu_n/sigma_n))+sigma_n/np.sqrt(2*np.pi)*(np.exp((-alpha_n**2)/2)-np.exp(-((mu_n/sigma_n)**2)/2))) )

strike_array = np.linspace(0,1000,num=10000)
prime_aray=[]
for strike in strike_array:
    alpha_n=(strike-mu_n)/sigma_n
    print("alpha_n : ",alpha_n)

    print("N : ", N)
    print("mu_n", mu_n)
    prime_aray.append(call_hdd(N,strike,r))
    print("CALL HDD Strike ", strike," : ",prime_aray[-1])

plt.plot(strike_array,prime_aray)
plt.axvline(mu_n,c="red")
plt.show()


# sympy_examples.py
# -------------------------------------------------------------------------
# Demonstrate some useful methods available in the SymPy module.
# Run each command separately at an IPython command line.
# -------------------------------------------------------------------------
from sympy import *
init_session()

expand( (x + y)**5 )
factor( x**6 - 1 )
pi.n(100)
plot(besselj(0, x), besselj(1, x), (x, 0, 10))
diff( x*sin(y), x, y )
integrate(cos(x)**2, x)
integrate(exp(-x**2), (x, -oo, oo))
Sum(k**3, (k, 0, m)).doit().factor()
dsolve(f(x).diff(x) + f(x) - x, f(x)).simplify()


##############################################################################################################
## This file contains functions to calculate derivative of the product spherical harmonics and derivative
## of the product Wigner D-functions.
##
##############################################################################################################
from matrix import SphericalHarmonics
from matrix import WignerDfunctions
from itertools import combinations
import numpy as np
import numpy.matlib as npmat

def DerivProdSH(theta,phi,B,update):
    #########################################################################################################
    ## Function to calculate derivative of product spherical harmonics
    ## input : m dimensions of theta and phi, bandlimited B, update "all" mean we derive the product spherical
    ## harmonics with respect to theta and phi (elevation and azimuth). Update "partial" only calculate
    ## derivative of the product spherical harmonics w.r.t phi and fix the theta with equispaced samples.
    ##
    ## Partial is used to verify the lower bound coherence of spherical harmonics matrix if we fix sampling on
    ## theta
    ##
    ##
    ## Since we want to optimize the coherence of the matrix (inner product of difference columns), we have
    ## to calculate the gradient w.r.t theta, or phi
    ##
    ## input : theta, phi m dimension samples on sphere, bandlimited B, and update
    ## output : gr_theta,gr_phi -- gradient with respect to (theta,phi) for update all,
    ##          gr_phi -- gradient with respect to (phi) for partial
    ##########################################################################################################
    m = len(theta)
    N=B**2
    phi_0 = np.zeros(m) #Generate only matrix associated Legendre by assigning phi = 0
    A = np.real(SphericalHarmonics(theta,phi_0,B)[1])
    lk = SphericalHarmonics(theta,phi_0,B)[2]
    Aderiv = np.real(SphericalHarmonics(theta,phi_0,B)[3])
    col_comb = np.array(list(combinations(range(N),2)))
    comb_lk = [lk[col_comb[:,0],:],lk[col_comb[:,1],:]]
    ## Product of combination of degree and order associated Legendre
    ProductasLeg = np.multiply(A[:,col_comb[:,0]],A[:,col_comb[:,1]])

    k = comb_lk[0][:,1] - comb_lk[1][:,1]
    ##########################################################################################################
    ## Derivative of q-norm
    ##########################################################################################################
    q = 8.0
    ##########################################################################################################
    ## Matrix-based derivative w.r.t phi
    ##########################################################################################################
    mat_cos = np.cos(np.outer(phi,k))
    mat_sin = np.sin(np.outer(phi,k))

    Qnorm = (q/2.0)*np.power(np.sqrt( np.power(abs(sum(np.multiply(ProductasLeg,mat_cos))),2)\
            + np.power( abs(sum(np.multiply(ProductasLeg,mat_sin))),2  )),(q-2))

    Qnorm1 = (1/q)*np.power(sum(np.power(np.sqrt( np.power(abs(sum(np.multiply(ProductasLeg,mat_cos))),2) \
            +  np.power(abs(sum(np.multiply(ProductasLeg,mat_sin))),2  )),q)),((1/q)-1))

    if update == 'all':
        ########################################################################################################
        ## Direct calculation of derivativei with respect to theta
        #######################################################################################################

        dPlktotal = np.multiply(A[:,col_comb[:,0]],Aderiv[:,col_comb[:,1]]) + np.multiply(Aderiv[:,col_comb[:,0]],A[:,col_comb[:,1]])

        ############################################################################################################
        ## Matrix-based derivative w.r.t theta
        ############################################################################################################
        gr_temp_theta1 = np.multiply((2.0*npmat.repmat(sum(np.multiply(ProductasLeg,mat_cos)),m,1)),(np.multiply(dPlktotal,mat_cos)))
        gr_temp_theta2 = np.multiply((2.0*npmat.repmat(sum(np.multiply(ProductasLeg,mat_sin)),m,1)),(np.multiply(dPlktotal,mat_sin)))
        gr_temp_theta = gr_temp_theta1 + gr_temp_theta2
        gr_theta = Qnorm1*np.sum(np.multiply(npmat.repmat(Qnorm,m,1),gr_temp_theta),axis = 1)

        ####################################################################################################
        ## Matrix-based derivative w.r.t phi
        ###################################################################################################
        Grad_temp1 =  2.0*npmat.repmat(sum(np.multiply(ProductasLeg,mat_cos)),m,1)
        Grad_temp2 = np.multiply(np.multiply(ProductasLeg,mat_sin),npmat.repmat(-k,m,1))
        Grad1 = np.multiply(Grad_temp1,Grad_temp2)

        Grad_temp3 = 2.0*npmat.repmat(sum(np.multiply(ProductasLeg,mat_sin)),m,1)
        Grad_temp4 = np.multiply(np.multiply(ProductasLeg,mat_cos),npmat.repmat(k,m,1))
        Grad2 = np.multiply(Grad_temp3,Grad_temp4)
        Grad_temp = Grad1 + Grad2

        gr_phi = Qnorm1*np.sum(np.multiply(npmat.repmat(Qnorm,m,1),Grad_temp),axis =1)

        return gr_theta, gr_phi

    else:

        Grad_temp1 =  2.0*npmat.repmat(sum(np.multiply(ProductasLeg,mat_cos)),m,1)
        Grad_temp2 = np.multiply(np.multiply(ProductasLeg,mat_sin),npmat.repmat(-k,m,1))
        Grad1 = np.multiply(Grad_temp1,Grad_temp2)

        Grad_temp3 = 2.0*npmat.repmat(sum(np.multiply(ProductasLeg,mat_sin)),m,1)
        Grad_temp4 = np.multiply(np.multiply(ProductasLeg,mat_cos),npmat.repmat(k,m,1))
        Grad2 = np.multiply(Grad_temp3,Grad_temp4)
        Grad_temp = Grad1 + Grad2

        gr_phi = Qnorm1*np.sum(np.multiply(npmat.repmat(Qnorm,m,1),Grad_temp),axis =1)

        return gr_phi

def DerivProdWigner(theta,phi,chi,B,update):
    #########################################################################################################
    ## Function to calculate derivative of product Wigner D-functions
    ## input : m dimensions of theta,phi, and chi, bandlimited B, update "all" mean we derive the product
    ## Wigner D-functions  with respect to theta, phi, chi (elevation, azimuth and polarization).
    ## Update "partial" only calculate derivative of the product Wigner D-functions  w.r.t phi and chi
    ##
    ## Partial is used to verify the lower bound coherence of Wigner D-functions if we fix sampling on theta
    ##
    ## Since we want to optimize the coherence of the matrix (inner product of difference columns), we have
    ## to calculate the gradient w.r.t theta, phi and chi
    ##
    ## input : theta, phi, chi in m-dimension samples on the rotation group, bandlimited B, and update
    ## output : gr_theta,gr_phi, gr_chi -- gradient with respect to (theta,phi,chi) for update all,
    ##          gr_phi,gr_chi -- gradient with respect to (phi,chi) for partial
    ##########################################################################################################

    m = len(theta)
    N = B*(2*B-1)*(2*B+1)//3

    phi_0 = np.zeros(m) #Generate only matrix Wigner d-functions by setting azimuth (phi) = 0
    chi_0 = np.zeros(m) #Generate only matrix Wigner d-functions by setting polarization (chi) = 0
    A = np.real(WignerDfunctions(theta,phi_0,chi_0,B)[1])
    lkn = WignerDfunctions(theta,phi_0,chi_0,B)[2]
    Aderiv = np.real(WignerDfunctions(theta,phi_0,chi_0,B)[3])
    col_comb = np.array(list(combinations(range(N),2)))
    comb_lkn = [lkn[col_comb[:,0],:],lkn[col_comb[:,1],:]]

    ## Product of combination degree and orders Wigner d-functions
    ProductWignerd = np.multiply(A[:,col_comb[:,0]],A[:,col_comb[:,1]])

    ########################################################################
    ## Matrix-based phi and chi
    #######################################################################
    k = comb_lkn[0][:,1] - comb_lkn[1][:,1]
    n = comb_lkn[0][:,2] - comb_lkn[1][:,2]

    ## Derivative of q norm
    q = 8.0

    mat_cos = np.cos(np.outer(phi,k) + np.outer(chi,n))
    mat_sin = np.sin(np.outer(phi,k) + np.outer(chi,n))

    Qnorm = (q/2.0)*np.power(np.sqrt( np.power(abs(sum(np.multiply(ProductWignerd,mat_cos))),2)\
                + np.power( abs(sum(np.multiply(ProductWignerd,mat_sin))),2  )),(q-2))
    Qnorm1 = (1/q)*np.power(sum(np.power(np.sqrt( np.power(abs(sum(np.multiply(ProductWignerd,mat_cos))),2) \
                 +  np.power(abs(sum(np.multiply(ProductWignerd,mat_sin))),2  )),q)),((1/q)-1))

    if update == 'all':

        ##################################################################################
        ## Direct derivative with respect to theta
        ##################################################################################
        dWignerd = np.multiply(A[:,col_comb[:,0]],Aderiv[:,col_comb[:,1]]) + np.multiply(Aderiv[:,col_comb[:,0]],A[:,col_comb[:,1]])

        ############################################################################################################
        ## Matrix-based derivative w.r.t to the q-norm
        ############################################################################################################
        gr_temp_theta1 = np.multiply((2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_cos)),m,1)),(np.multiply(dWignerd,mat_cos)))
        gr_temp_theta2 = np.multiply((2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_sin)),m,1)),(np.multiply(dWignerd,mat_sin)))
        gr_temp_theta = gr_temp_theta1 + gr_temp_theta2
        gr_theta = Qnorm1*np.sum(np.multiply(npmat.repmat(Qnorm,m,1),gr_temp_theta),axis = 1)

        ######################################################################
        ## Matrix-based phi
        #####################################################################
        Grad_temp1_phi =  2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_cos)),m,1)
        Grad_temp2_phi =  np.multiply(np.multiply(ProductWignerd,mat_sin),npmat.repmat(-k,m,1))
        Grad1_phi = np.multiply(Grad_temp1_phi,Grad_temp2_phi)

        Grad_temp3_phi =  2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_sin)),m,1)
        Grad_temp4_phi = np.multiply(np.multiply(ProductWignerd,mat_cos),npmat.repmat(k,m,1))
        Grad2_phi = np.multiply(Grad_temp3_phi,Grad_temp4_phi)
        Grad_temp_phi = Grad1_phi + Grad2_phi

        ######################################################################
        ## Matrix-based chi
        ######################################################################

        Grad_temp1_chi =  2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_cos)),m,1)
        Grad_temp2_chi =  np.multiply(np.multiply(ProductWignerd,mat_sin),npmat.repmat(-n,m,1))
        Grad1_chi = np.multiply(Grad_temp1_chi,Grad_temp2_chi)

        Grad_temp3_chi =  2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_sin)),m,1)
        Grad_temp4_chi = np.multiply(np.multiply(ProductWignerd,mat_cos),npmat.repmat(n,m,1))
        Grad2_chi = np.multiply(Grad_temp3_chi,Grad_temp4_chi)
        Grad_temp_chi = Grad1_chi + Grad2_chi

        gr_phi = Qnorm1*np.sum(np.multiply(npmat.repmat(Qnorm,m,1),Grad_temp_phi),axis = 1)
        gr_chi = Qnorm1*np.sum(np.multiply(npmat.repmat(Qnorm,m,1),Grad_temp_chi),axis = 1)

        return gr_theta, gr_phi, gr_chi
    else:

        ######################################################################
        ## Matrix-based phi
        #####################################################################
        Grad_temp1_phi =  2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_cos)),m,1)
        Grad_temp2_phi =  np.multiply(np.multiply(ProductWignerd,mat_sin),npmat.repmat(-k,m,1))
        Grad1_phi = np.multiply(Grad_temp1_phi,Grad_temp2_phi)

        Grad_temp3_phi =  2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_sin)),m,1)
        Grad_temp4_phi = np.multiply(np.multiply(ProductWignerd,mat_cos),npmat.repmat(k,m,1))
        Grad2_phi = np.multiply(Grad_temp3_phi,Grad_temp4_phi)
        Grad_temp_phi = Grad1_phi + Grad2_phi

        ######################################################################
        ## Matrix-based chi
        ######################################################################

        Grad_temp1_chi =  2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_cos)),m,1)
        Grad_temp2_chi =  np.multiply(np.multiply(ProductWignerd,mat_sin),npmat.repmat(-n,m,1))
        Grad1_chi = np.multiply(Grad_temp1_chi,Grad_temp2_chi)

        Grad_temp3_chi =  2.0*npmat.repmat(sum(np.multiply(ProductWignerd,mat_sin)),m,1)
        Grad_temp4_chi = np.multiply(np.multiply(ProductWignerd,mat_cos),npmat.repmat(n,m,1))
        Grad2_chi = np.multiply(Grad_temp3_chi,Grad_temp4_chi)
        Grad_temp_chi = Grad1_chi + Grad2_chi

        gr_phi = Qnorm1*np.sum(np.multiply(npmat.repmat(Qnorm,m,1),Grad_temp_phi),axis = 1)
        gr_chi = Qnorm1*np.sum(np.multiply(npmat.repmat(Qnorm,m,1),Grad_temp_chi),axis = 1)

        return gr_phi,gr_chi


##################################################################
### This file is used to generate Figures & Tables in Chapter7 ###
##################################################################

import os
import time
import numpy as np
import tensorflow as tf
from tensorflow.random import set_seed
from math import factorial
from scipy.stats import norm
from scipy.integrate import odeint
import numpy.polynomial.hermite_e as H
from sklearn.preprocessing import StandardScaler
import dolfin as fn
from numpy.polynomial.legendre import leggauss
import matplotlib.pyplot as plt

####### Plot Formatting ######
plt.rc('lines', linewidth = 4)
plt.rc('xtick', labelsize = 13)
plt.rc('ytick', labelsize = 13)
plt.rc('legend',fontsize=14)
plt.rcParams["font.family"] = "serif"
plt.rcParams['axes.labelsize'] = 18
plt.rcParams['axes.titlesize'] = 15
plt.rcParams['lines.markersize'] = 8
plt.rcParams['figure.figsize'] = (7.0, 5.0)

#### To make it cleaner, create Directory "images" to store all the figures ####
imagepath = os.path.join(os.getcwd(),"images")
os.makedirs(imagepath,exist_ok=True)

##################################################
##############        PCE vs MC     ##############
##################################################

############## PCE ###############
start_def = time.time()
def Phi(n):
    #define H_n
    coeffs = [0]*(n+1)
    coeffs[n] = 1
    return coeffs

def inner2_herm(n):       ###return the denominator when computing $k_i$
    return factorial(n)

def product3_herm(i,j,l):
    #compute \Phi_i*\Phi_j*\Phi_l
    return lambda x: H.hermeval(x, H.hermemul(H.hermemul(Phi(i),Phi(j)),Phi(l)))

def inner3_herm(P,i,j,l):
    #compute <\Phi_i\Phi_j\Phi_l>
    #Set up Gauss-Hermite quadrature, weighting function is exp^{-x^2}
    m=(P+1)**2
    x, w=H.hermegauss(m)
    inner=sum([product3_herm(i,j,l)(x[idx]) * w[idx] for idx in range(m)])               return inner/np.sqrt(2*np.pi)   #because of the weight
time_def = time.time() - start_def

start_prep = time.time()
P=4
ki_herm = [0,1]+[0]*(P-1)
Inner3_herm = np.zeros((P+1,P+1,P+1)) #store all inner3_herm values
Inner2_herm = np.zeros(P+1)
for i in range(P+1):
    for j in range(P+1):
        for l in range(P+1):
            Inner3_herm[i,j,l] = inner3_herm(P,i,j,l)
for i in range(P+1):
    Inner2_herm[i] = inner2_herm(i)
time_prep = time.time() - start_prep

start_ode = time.time()
def ode_system_herm(y, t, P):
    #P indicates the highest degree
    dydt = np.zeros(P+1)
    for l in range(len(dydt)):
        dydt[l] = -(sum(sum(Inner3_herm[i,j,l]*ki_herm[i]*y[j] for j in range(P+1)) for i in range(P+1)))/Inner2_herm[l]
    return dydt
time_ode = time.time() - start_ode

start_solveode = time.time()
sol_herm = odeint(ode_system_herm, [1.0]+[0.0]*P, np.linspace(0,1,101), args=(P, ))
time_solveode = time.time() - start_solveode

time_all = time_def + time_prep + time_ode + time_solveode

############## MC ###############
start_ode_mc = time.time()
def ode(y,t,nsample,k):
    '''
    Build the ode system
    '''
    dydt = np.zeros(nsample)
    for i in range(nsample):
        dydt[i] = -k[i]*y[i]
    return dydt
time_def_mc = time.time() - start_ode_mc

nsample = np.array([10, 100, 1000, 10000, 100000])
time_solveode_mc = np.zeros(len(nsample))
start_solveode_mc = np.zeros(len(nsample))
mean_mc_1 = np.zeros(len(nsample))
mean_mc_05 = np.zeros(len(nsample))
for i in range(len(nsample)):
    k = norm.rvs(loc=0, scale=1, size=nsample[i], random_state=12345)
    start_solveode_mc[i] = time.time()
    sol_mc = odeint(ode, [1.0]*nsample[i], np.linspace(0,1,101),args=(nsample[i],k))  #t:np.linspace(0,1,101)
    mean_mc_1[i] = np.mean(sol_mc[100,:])
    mean_mc_05[i] = np.mean(sol_mc[50,:])
    time_solveode_mc[i] = time.time() - start_solveode_mc[i]

time_all_mc = time_def_mc + time_solveode_mc

############## Comparison ###############
#### Table 7.2, row 1 ####
### PCE
print(time_solveode)
### MC
print(time_solveode_mc)

## t = 0.5
mean_pc_05 = sol_herm[:,0][50]   #mean value using pc at t=0.5
mean_exact_05 = np.e**(1/8)
## t = 1
mean_pc_1 = sol_herm[:,0][100]   #mean value using pc at t=1
mean_exact_1 = np.e**(1/2)

#### Table 7.2, row 2 ####
print(mean_pc_05)
print(mean_mc_05)
print(mean_exact_05)

print()

#### Table 7.2, row 3 ####
print(mean_pc_1)
print(mean_mc_1)
print(mean_exact_1)

##################################################
##############       NN vs Poly     ##############
##################################################

############## NN ###############
#Define the activation function
def rbf(x):
    return tf.math.exp(-x**2)
def d_rbf(x):
    return tf.gradients(rbf,x)
def rbf_grad(op, grad):
    x = op.inputs[0]
    n_gr = d_rbf(x)    #defining the gradient.
    return grad * n_gr
def py_func(func, inp, Tout, stateful=True, name=None, grad=None):
    # Need to generate a unique name to avoid duplicates:
    rnd_name = 'PyFuncGrad' + str(np.random.randint(0, 1E+2))
    tf.RegisterGradient(rnd_name)(grad)
    g = tf.get_default_graph()
    with g.gradient_override_map({"PyFunc": rnd_name, "PyFuncStateless": rnd_name}):
        return tf.py_func(func, inp, Tout, stateful=stateful, name=name)
def tf_rbf(x,name=None):
    with tf.name_scope(name, "rbf", [x]) as name:
        y = py_func(rbf,   #forward pass function
                    [x],
                    [tf.float32],
                    name=name,
                    grad= rbf_grad) #the function that overrides gradient
        y[0].set_shape(x.get_shape())     #when using with the code, it is used to specify the rank of the input.
    return y[0]

np.random.seed(12345)
size = 100
delta_t = 0.01
n = int(0.5/delta_t)
### Original data ###
lam_in = np.random.normal(0, 1, size)
y_exact = np.array([np.exp(-i*0.5) for i in lam_in])
y_out = np.array([(1-i*delta_t)**n for i in lam_in])
### After feature scaling ###
scaler = StandardScaler()
data_trans = scaler.fit_transform(lam_in.reshape(-1,1))

num_neuron = 5
tf.random.set_seed(12345)
model_ode = tf.keras.Sequential()
model_ode.add(tf.keras.layers.Dense(num_neuron,activation=rbf))
model_ode.add(tf.keras.layers.Dense(1))
model_ode.compile(loss='mean_squared_error', optimizer=tf.keras.optimizers.Adam(0.01))
model_ode.fit(data_trans[:,0],y_out, epochs=1500, verbose=0)

preds_ode = []
for j in data_trans[:,0]:
    preds_ode.append(model_ode.predict([j]))
preds_ode_shaped = tf.reshape(tf.constant(np.array(preds_ode)),len(preds_ode))
mse_fd_nn = tf.keras.losses.MSE(y_exact,preds_ode_shaped).numpy()

fig = plt.figure()
plt.xlabel("$\lambda$")
plt.ylabel("$q$")
plt.title("MSE=%.5f"%(mse_fd_nn))
plt.scatter(lam_in, y_out, label='Obs')
plt.scatter(lam_in, preds_ode_shaped, label='NN')
plt.legend()
plt.show();
# fig.savefig("images/comp_fd_nn.png")

############## Polynomial Regression ###############
mymodel3 = np.poly1d(np.polyfit(data_trans[:,0], y_out, 3))
preds_fd_pr3 = mymodel3(data_trans[:,0])
mse_fd_pr3 = tf.keras.losses.MSE(y_exact,preds_fd_pr3).numpy()

fig = plt.figure()
plt.xlabel("$\lambda$")
plt.ylabel("$q$")
plt.title("MSE=%.5f"%(mse_fd_pr3))
plt.scatter(lam_in, y_out, label='Obs')
plt.scatter(lam_in, preds_fd_pr3, label='PR (deg=3)')
plt.legend()
plt.show();
# fig.savefig("images/comp_fd_pr3.png")

####################################################
########  Stochastic collocation method   ##########
####################################################
def QoI_FEM(x0,y0,lam1,lam2,gridx,gridy,p):
    mesh = fn.UnitSquareMesh(gridx, gridy)
    V = fn.FunctionSpace(mesh, "Lagrange", p)

    # Define diffusion tensor (here, just a scalar function) and parameters
    A = fn.Expression((('exp(lam1)','a'),
                ('a','exp(lam2)')), a = fn.Constant(0.0), lam1 = lam1, lam2 = lam2, degree=3)

    u_exact = fn.Expression("sin(lam1*pi*x[0])*cos(lam2*pi*x[1])", lam1 = lam1, lam2 = lam2, degree=2+p)

    # Define the mix of Neumann and Dirichlet BCs
    class LeftBoundary(fn.SubDomain):
        def inside(self, x, on_boundary):
            return (x[0] < fn.DOLFIN_EPS)
    class RightBoundary(fn.SubDomain):
        def inside(self, x, on_boundary):
            return (x[0] > 1.0 - fn.DOLFIN_EPS)
    class TopBoundary(fn.SubDomain):
        def inside(self, x, on_boundary):
            return (x[1] > 1.0 - fn.DOLFIN_EPS)
    class BottomBoundary(fn.SubDomain):
        def inside(self, x, on_boundary):
            return (x[1] < fn.DOLFIN_EPS)

    # Create a mesh function (mf) assigning an unsigned integer ('uint')
    # to each edge (which is a "Facet" in 2D)
    mf = fn.MeshFunction('size_t', mesh, 1)
    mf.set_all(0) # initialize the function to be zero
    # Setup the boundary classes that use Neumann boundary conditions
    NTB = TopBoundary() # instatiate
    NTB.mark(mf, 1) # set all values of the mf to be 1 on this boundary
    NBB = BottomBoundary()
    NBB.mark(mf, 2) # set all values of the mf to be 2 on this boundary
    NRB = RightBoundary()
    NRB.mark(mf, 3)

    # Define Dirichlet boundary conditions
    Gamma_0 = fn.DirichletBC(V, u_exact, LeftBoundary())
    bcs = [Gamma_0]

    # Define data necessary to approximate exact solution
    f = ( fn.exp(lam1)*(lam1*fn.pi)**2 + fn.exp(lam2)*(lam2*fn.pi)**2 ) * u_exact
    g1 = fn.Expression("-exp(lam2)*lam2*pi*sin(lam1*pi*x[0])*sin(lam2*pi*x[1])", lam1=lam1, lam2=lam2, degree=2+p)    #pointing outward unit normal vector, pointing upaward (0,1)
    g2 = fn.Expression("exp(lam2)*lam2*pi*sin(lam1*pi*x[0])*sin(lam2*pi*x[1])", lam1=lam1, lam2=lam2, degree=2+p)     #pointing downward (0,1)
    g3 = fn.Expression("exp(lam1)*lam1*pi*cos(lam1*pi*x[0])*cos(lam2*pi*x[1])", lam1=lam1, lam2=lam2, degree=2+p)

    fn.ds = fn.ds(subdomain_data=mf)
    # Define variational problem
    u = fn.TrialFunction(V)
    v = fn.TestFunction(V)
    a = fn.inner(A*fn.grad(u), fn.grad(v))*fn.dx
    L = f*v*fn.dx + g1*v*fn.ds(1) + g2*v*fn.ds(2) + g3*v*fn.ds(3)  #note the 1, 2 and 3 correspond to the mf

    # Compute solution
    u = fn.Function(V)
    fn.solve(a == L, u, bcs)

    return u(x0,y0)

def exactQ(x,y):
    return (1-np.cos(np.pi*x))*np.sin(np.pi*y)/(np.pi**2*x*y)

x0 = [0.2, 0.2, 0.2, 0.3, 0.5, 0.5]
y0 = [0.3, 0.5, 0.8, 0.2, 0.2, 0.5]
M, N = 5, 5
x1,w1 = leggauss(M)
x2,w2 = leggauss(N)
tab = np.zeros((len(x0),2))
for k in range(len(x0)):
    #### Stochastic Collocation Mean at x0, y0 ####
    uij = np.zeros((M,N))
    for i in range(M):
        for j in range(N):
            uij[i,j] = QoI_FEM(x0[k],y0[k],(1+x1[i])/2,(1+x2[j])/2,10,10,2)

    sol = 0
    for i in range(M):
        for j in range(N):
            sol += w1[i]*w2[j]*uij[i,j]
    sol /= 4
    tab[k,0] = sol
    #### Exact Mean at x0, y0 ####
    tab[k,1] = exactQ(x0[k],y0[k])

print(tab)

##################################################
##############       Discussion     ##############
##################################################
def model(x):
    if x<=1:
        return 15*x+10
    elif x<=7:
        return x**3-12*x**2+36*x
    elif x<=10:
        return 15/np.pi*np.sin(np.pi*(x-7))+7
    else:
        return -30*np.sqrt(x-9)+37

np.random.seed(12345)
x_syn = np.random.uniform(0,15,100)
y_exact = np.array([model(i) for i in x_syn])
y_syn = y_exact+np.random.normal(0,1,len(x_syn))

scaler_syn = StandardScaler()
syndata_trans = scaler_syn.fit_transform(x_syn.reshape(-1,1))

num_neuron = 5
tf.random.set_seed(12345)
model_syn_all = tf.keras.Sequential()
model_syn_all.add(tf.keras.layers.Dense(num_neuron,activation=rbf))
model_syn_all.add(tf.keras.layers.Dense(num_neuron,activation=rbf))
# model_syn_all.add(tf.keras.layers.Dense(num_neuron,activation=rbf))
# model_syn_all.add(tf.keras.layers.Dense(num_neuron,activation=rbf))
model_syn_all.add(tf.keras.layers.Dense(1))
model_syn_all.compile(loss='mean_squared_error', optimizer=tf.keras.optimizers.Adam(0.01))
model_syn_all.fit(syndata_trans[:,0],y_syn, epochs=1000, verbose=0)

fig = plt.figure()
plt.xlabel("$x$")
plt.ylabel("$y$")
y_pred = []
for j in syndata_trans[:,0]:
    y_pred.append(model_syn_all.predict([j]))
y_pred_shaped = tf.reshape(tf.constant(np.array(y_pred)),len(y_pred))
error0 = tf.keras.losses.MSE(y_syn,y_pred_shaped).numpy()
plt.title("MSE=%.5f"%(error0))
plt.scatter(x_syn, y_syn, label='Obs')
plt.scatter(x_syn, y_pred_shaped, color='red',label="NN")
plt.legend()
plt.show();
# fig.savefig("images/comp_nn_1step.png")

## Step 1
num_neuron = 5
tf.random.set_seed(12345)
model_nn1 = tf.keras.Sequential()
model_nn1.add(tf.keras.layers.Dense(num_neuron,activation=rbf))
model_nn1.add(tf.keras.layers.Dense(1))
model_nn1.compile(loss='mean_squared_error', optimizer=tf.keras.optimizers.Adam(0.01))
model_nn1.fit(syndata_trans[:,0],y_syn, epochs=1000, verbose=0)

fig = plt.figure(figsize=(13,4))
plt.subplot(121)
plt.xlabel("$x$")
plt.ylabel("$y$")
y_pred1 = []
for j in syndata_trans[:,0]:
    y_pred1.append(model_nn1.predict([j]))
y_pred1_shaped = tf.reshape(tf.constant(np.array(y_pred1)),len(y_pred1))
plt.title("Overall Fit")
plt.scatter(x_syn, y_syn, label='Obs')
plt.scatter(x_syn, y_pred1_shaped, color='red',label="NN")
plt.legend();
plt.subplot(122)
plt.xlabel("$x$")
plt.ylabel("Residual")
mse1 = tf.keras.losses.MSE(y_syn,y_pred1_shaped).numpy()
plt.title("MSE=%.5f"%(mse1))
plt.scatter(x_syn, y_syn-y_pred1_shaped)
plt.show();
# fig.savefig("images/comp_nn_step1.png");

## Step 2
tf.random.set_seed(12345)
model_nn2 = tf.keras.Sequential()
model_nn2.add(tf.keras.layers.Dense(5,activation=rbf))
model_nn2.add(tf.keras.layers.Dense(1))
error1 = y_syn - y_pred1_shaped
scaler_syn2 = StandardScaler()
model_nn2.compile(loss='mean_squared_error', optimizer=tf.keras.optimizers.Adam(0.01))
model_nn2.fit(syndata_trans[:,0],error1, epochs=1000, verbose=0)

fig = plt.figure(figsize=(15,4))
plt.subplot(121)
plt.xlabel("$x$")
plt.ylabel("$y$")
y_pred2 = []
for j in syndata_trans[:,0]:
    y_pred2.append(model_nn2.predict([j]))
y_pred2_shaped = tf.reshape(tf.constant(np.array(y_pred2)),len(y_pred2))
plt.title("Overall Fit")
plt.scatter(x_syn, y_syn, label='Obs')
plt.scatter(x_syn, y_pred1_shaped+y_pred2_shaped, color='red',label="NN")
plt.legend();
plt.subplot(122)
plt.xlabel("$x$")
plt.ylabel("Residual")
mse2 = tf.keras.losses.MSE(y_syn,y_pred1_shaped+y_pred2_shaped).numpy()
plt.title("MSE=%.5f"%(mse2))
plt.scatter(x_syn, y_syn-y_pred1_shaped-y_pred2_shaped)
plt.show();
# fig.savefig("images/comp_nn_step2.png");

## Step 3
tf.random.set_seed(12345)
model_nn3 = tf.keras.Sequential()
model_nn3.add(tf.keras.layers.Dense(5,activation=rbf))
model_nn3.add(tf.keras.layers.Dense(1))
error2 = y_syn - y_pred1_shaped - y_pred2_shaped
scaler_syn3 = StandardScaler()
model_nn3.compile(loss='mean_squared_error', optimizer=tf.keras.optimizers.Adam(0.01))
model_nn3.fit(syndata_trans[:,0],error2, epochs=1000, verbose=0)

fig = plt.figure(figsize=(13,4))
plt.subplot(121)
plt.xlabel("$x$")
plt.ylabel("$y$")
y_pred3 = []
for j in syndata_trans[:,0]:
    y_pred3.append(model_nn3.predict([j]))
y_pred3_shaped = tf.reshape(tf.constant(np.array(y_pred3)),len(y_pred3))
plt.title("Overall Fit")
plt.scatter(x_syn, y_syn, label='Obs')
plt.scatter(x_syn, y_pred1_shaped+y_pred2_shaped+y_pred3_shaped, color='red',label="NN")
plt.legend();
plt.subplot(122)
plt.xlabel("$x$")
plt.ylabel("Residual")
mse3 = tf.keras.losses.MSE(y_syn,y_pred1_shaped+y_pred2_shaped+y_pred3_shaped).numpy()
plt.title("MSE=%.5f"%(mse3))
plt.scatter(x_syn, y_syn-y_pred1_shaped-y_pred2_shaped-y_pred3_shaped);
plt.show();
# fig.savefig("images/comp_nn_step3.png");

## all in one
num_neuron = 15
tf.random.set_seed(12345)
model_nn11 = tf.keras.Sequential()
model_nn11.add(tf.keras.layers.Dense(num_neuron,activation=rbf))
model_nn11.add(tf.keras.layers.Dense(1))
model_nn11.compile(loss='mean_squared_error', optimizer=tf.keras.optimizers.Adam(0.01))
model_nn11.fit(syndata_trans[:,0],y_syn, epochs=5000, verbose=0)

fig = plt.figure(figsize=(13,4))
plt.subplot(121)
plt.xlabel("$x$")
plt.ylabel("$y$")
y_pred11 = []
for j in syndata_trans[:,0]:
    y_pred11.append(model_nn11.predict([j]))
y_pred11_shaped = tf.reshape(tf.constant(np.array(y_pred11)),len(y_pred11))
plt.title("Overall Fit")
plt.scatter(x_syn, y_syn, label='Obs')
plt.scatter(x_syn, y_pred11_shaped, color='red', label="NN")
plt.legend();
plt.subplot(122)
plt.xlabel("$x$")
plt.ylabel("Residual")
mse11 = tf.keras.losses.MSE(y_syn,y_pred11_shaped).numpy()
plt.title("MSE=%.5f"%(mse11))
plt.scatter(x_syn, y_syn-y_pred11_shaped)
plt.show();
# fig.savefig("images/comp_nn_step11.png");


import numpy as np
import matplotlib.pyplot as plt
import pickle

##############################

data_file = 'data.pickle'

def data_load():
    with open(data_file, 'rb') as f:
        return pickle.load(f)

data = data_load()

def print_plots_names():
    for pl in data['plots']:
        print(pl['name'])

def plot_plot(p,lines=None,predict=None, save_as=None):
    fig = plt.figure()
    ax = fig.add_subplot(111)

    if p['ylim']:
        plt.ylim(0, p['ylim'])

    if not(lines):
        lines = list(range(p["no_lines"]))
    for line in lines:
        vals = p['lines'][line]
        if p['axis']=='progressive':
            x_vals = np.sqrt(np.prod(vals[:,0:6],axis=1)/10000)
        else:
            x_vals = vals[:,p['axis']]
        y_vals = vals[:, 6]
        ax.plot(x_vals, y_vals)
        if predict:
            p_vals = predict(vals)
            ax.plot(x_vals, p_vals,"k--")
    for (xs,ys,s) in p['text']:
        plt.text(xs,ys,s)
    ax.set_xlabel(p['x_axis_name'])
    ax.set_ylabel('Milliseconds')
    if save_as:
        plt.savefig(save_as, bbox_inches='tight')
    plt.show()

################## alpha flops ######################

# The function mypredict compute the alpha-flops correction accoridng to the formula given in the article
# "Dissecting FLOPs along input dimensions for GreenAI cost estimations" accepted for presentation at the
# the 7th International Online & Onsite Conference on Machine Learning, Optimization, and Data Science –
# October 4 – 8, 2021 – Grasmere, Lake District, England – UK

# we distinguish only two cases for K, namely K=1 and K>1.
# if K=1, beta is 0.02 and gamma is .99
# if K>1, then beta is 0.001 and gamma is .56
# This values have been obtained by regression over the dataset

def beta_gamma(K):
    Kis1 = K == 1
    beta = Kis1 * 0.02 + (1 - Kis1) * 0.001
    gamma = Kis1 * .99 + (1 - Kis1) * .56
    return beta,gamma

final = 0.0375

def mypredict(vals,print_prediction=False):
    D = np.prod(vals[:,0:2],axis=1)
    K = np.prod(vals[:,4:6],axis=1)
    C = np.prod(vals[:,2:4],axis=1)
    beta,gamma = beta_gamma(K)
    FLOPS = C * K * D / 1000000
    #logK = tf.math.log(K) + 1
    logK = np.log(K) +1
    x = beta * (D - logK)
    x = (logK + x) / D
    #x = tf.math.log(x)
    x = np.log(x)
    x = gamma * x
    #x = final * tf.math.exp(x)
    x = final * np.exp(x)
    Ypred = x * FLOPS
    if print_prediction:
        for i in range(0,Ypred.shape[0]):
            print("predicted = {}; actual = {}".format(Ypred[i],vals[i,6]))
    return(Ypred)

#################################################################
# Actual plotting.
# Predictions are plotted with dashed lines

plots = data["plots"]

plot_plot(plots[0],predict=mypredict) #av,se_as="PLOTS/plot1")
plot_plot(plots[1],predict=mypredict) #,save_as="PLOTS/plot2")
plot_plot(plots[2],predict=mypredict) #,save_as="PLOTS/plot3")
plot_plot(plots[3],predict=mypredict) #,save_as="PLOTS/plot4")  #K=1
plot_plot(plots[4],predict=mypredict) #,save_as="PLOTS/plot5")
plot_plot(plots[5],predict=mypredict) #,save_as="PLOTS/plot6_K1") #K=1
plot_plot(plots[6],predict=mypredict) #,save_as="PLOTS/plot11")
plot_plot(plots[7],predict=mypredict) #,save_as="PLOTS/dense_vs_batch") #K = 1


"""
author: Boris Shishov
"""

import numpy as np
from scipy.spatial import distance

EPSILON = 1e-10

def _error(actual: np.ndarray, predicted: np.ndarray):
    """ Simple error """
    return actual - predicted

def _percentage_error(actual: np.ndarray, predicted: np.ndarray):
    """
    Percentage error
    Note: result is NOT multiplied by 100
    """
    return _error(actual, predicted) / (actual + EPSILON)

def _naive_forecasting(actual: np.ndarray, seasonality: int = 1):
    """ Naive forecasting method which just repeats previous samples """
    return actual[:-seasonality]

def _relative_error(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
    """ Relative Error """
    if benchmark is None or isinstance(benchmark, int):
        # If no benchmark prediction provided - use naive forecasting
        if not isinstance(benchmark, int):
            seasonality = 1
        else:
            seasonality = benchmark
        return _error(actual[seasonality:], predicted[seasonality:]) /\
               (_error(actual[seasonality:], _naive_forecasting(actual, seasonality)) + EPSILON)

    return _error(actual, predicted) / (_error(actual, benchmark) + EPSILON)

def _bounded_relative_error(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
    """ Bounded Relative Error """
    if benchmark is None or isinstance(benchmark, int):
        # If no benchmark prediction provided - use naive forecasting
        if not isinstance(benchmark, int):
            seasonality = 1
        else:
            seasonality = benchmark

        abs_err = np.abs(_error(actual[seasonality:], predicted[seasonality:]))
        abs_err_bench = np.abs(_error(actual[seasonality:], _naive_forecasting(actual, seasonality)))
    else:
        abs_err = np.abs(_error(actual, predicted))
        abs_err_bench = np.abs(_error(actual, benchmark))

    return abs_err / (abs_err + abs_err_bench + EPSILON)

def _geometric_mean(a, axis=0, dtype=None):
    """ Geometric mean """
    if not isinstance(a, np.ndarray):  # if not an ndarray object attempt to convert it
        log_a = np.log(np.array(a, dtype=dtype))
    elif dtype:  # Must change the default dtype allowing array type
        if isinstance(a, np.ma.MaskedArray):
            log_a = np.log(np.ma.asarray(a, dtype=dtype))
        else:
            log_a = np.log(np.asarray(a, dtype=dtype))
    else:
        log_a = np.log(a)
    return np.exp(log_a.mean(axis=axis))

def mse(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Squared Error """
    return np.mean(np.square(_error(actual, predicted)))

def rmse(actual: np.ndarray, predicted: np.ndarray):
    """ Root Mean Squared Error """
    return np.sqrt(mse(actual, predicted))

def nrmse(actual: np.ndarray, predicted: np.ndarray):
    """ Normalized Root Mean Squared Error """
    return rmse(actual, predicted) / (actual.max() - actual.min())

def me(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Error """
    return np.mean(_error(actual, predicted))

def mae(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Absolute Error """
    return np.mean(np.abs(_error(actual, predicted)))

mad = mae  # Mean Absolute Deviation (it is the same as MAE)

def gmae(actual: np.ndarray, predicted: np.ndarray):
    """ Geometric Mean Absolute Error """
    return _geometric_mean(np.abs(_error(actual, predicted)))

def mdae(actual: np.ndarray, predicted: np.ndarray):
    """ Median Absolute Error """
    return np.median(np.abs(_error(actual, predicted)))

def mpe(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Percentage Error """
    return np.mean(_percentage_error(actual, predicted))

def mape(actual: np.ndarray, predicted: np.ndarray):
    """
    Mean Absolute Percentage Error
    Properties:
        + Easy to interpret
        + Scale independent
        - Biased, not symmetric
        - Undefined when actual[t] == 0
    Note: result is NOT multiplied by 100
    """
    return np.mean(np.abs(_percentage_error(actual, predicted)))

def mdape(actual: np.ndarray, predicted: np.ndarray):
    """
    Median Absolute Percentage Error
    Note: result is NOT multiplied by 100
    """
    return np.median(np.abs(_percentage_error(actual, predicted)))

def smape(actual: np.ndarray, predicted: np.ndarray):
    """
    Symmetric Mean Absolute Percentage Error
    Note: result is NOT multiplied by 100
    """
    return np.mean(2.0 * np.abs(actual - predicted) / ((np.abs(actual) + np.abs(predicted)) + EPSILON))

def smdape(actual: np.ndarray, predicted: np.ndarray):
    """
    Symmetric Median Absolute Percentage Error
    Note: result is NOT multiplied by 100
    """
    return np.median(2.0 * np.abs(actual - predicted) / ((np.abs(actual) + np.abs(predicted)) + EPSILON))

def maape(actual: np.ndarray, predicted: np.ndarray):
    """
    Mean Arctangent Absolute Percentage Error
    Note: result is NOT multiplied by 100
    """
    return np.mean(np.arctan(np.abs((actual - predicted) / (actual + EPSILON))))

def mase(actual: np.ndarray, predicted: np.ndarray, seasonality: int = 1):
    """
    Mean Absolute Scaled Error
    Baseline (benchmark) is computed with naive forecasting (shifted by @seasonality)
    """
    return mae(actual, predicted) / mae(actual[seasonality:], _naive_forecasting(actual, seasonality))

def std_ae(actual: np.ndarray, predicted: np.ndarray):
    """ Normalized Absolute Error """
    __mae = mae(actual, predicted)
    return np.sqrt(np.sum(np.square(_error(actual, predicted) - __mae))/(len(actual) - 1))

def std_ape(actual: np.ndarray, predicted: np.ndarray):
    """ Normalized Absolute Percentage Error """
    __mape = mape(actual, predicted)
    return np.sqrt(np.sum(np.square(_percentage_error(actual, predicted) - __mape))/(len(actual) - 1))

def rmspe(actual: np.ndarray, predicted: np.ndarray):
    """
    Root Mean Squared Percentage Error
    Note: result is NOT multiplied by 100
    """
    return np.sqrt(np.mean(np.square(_percentage_error(actual, predicted))))

def rmdspe(actual: np.ndarray, predicted: np.ndarray):
    """
    Root Median Squared Percentage Error
    Note: result is NOT multiplied by 100
    """
    return np.sqrt(np.median(np.square(_percentage_error(actual, predicted))))

def rmsse(actual: np.ndarray, predicted: np.ndarray, seasonality: int = 1):
    """ Root Mean Squared Scaled Error """
    q = np.abs(_error(actual, predicted)) / mae(actual[seasonality:], _naive_forecasting(actual, seasonality))
    return np.sqrt(np.mean(np.square(q)))

def inrse(actual: np.ndarray, predicted: np.ndarray):
    """ Integral Normalized Root Squared Error """
    return np.sqrt(np.sum(np.square(_error(actual, predicted))) / np.sum(np.square(actual - np.mean(actual))))

def rrse(actual: np.ndarray, predicted: np.ndarray):
    """ Root Relative Squared Error """
    return np.sqrt(np.sum(np.square(actual - predicted)) / np.sum(np.square(actual - np.mean(actual))))

def mre(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
    """ Mean Relative Error """
    return np.mean(_relative_error(actual, predicted, benchmark))

def rae(actual: np.ndarray, predicted: np.ndarray):
    """ Relative Absolute Error (aka Approximation Error) """
    return np.sum(np.abs(actual - predicted)) / (np.sum(np.abs(actual - np.mean(actual))) + EPSILON)

def mrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
    """ Mean Relative Absolute Error """
    return np.mean(np.abs(_relative_error(actual, predicted, benchmark)))

def mdrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
    """ Median Relative Absolute Error """
    return np.median(np.abs(_relative_error(actual, predicted, benchmark)))

def gmrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
    """ Geometric Mean Relative Absolute Error """
    return _geometric_mean(np.abs(_relative_error(actual, predicted, benchmark)))

def mbrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
    """ Mean Bounded Relative Absolute Error """
    return np.mean(_bounded_relative_error(actual, predicted, benchmark))

def umbrae(actual: np.ndarray, predicted: np.ndarray, benchmark: np.ndarray = None):
    """ Unscaled Mean Bounded Relative Absolute Error """
    __mbrae = mbrae(actual, predicted, benchmark)
    return __mbrae / (1 - __mbrae)

def mda(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Directional Accuracy """
    return np.mean((np.sign(actual[1:] - actual[:-1]) == np.sign(predicted[1:] - predicted[:-1])).astype(int))

def jensenshannon(actual: np.ndarray, predicted: np.ndarray):
    actual_dis, bin_edges_actual = np.histogram(actual, bins=100)
    predict_dis, bin_edges_predict = np.histogram(predicted, bins=bin_edges_actual)
    return distance.jensenshannon(actual_dis, predict_dis)

METRICS = {
    'mse': mse,
    'rmse': rmse,
    'nrmse': nrmse,
    'me': me,
    'mae': mae,
    'mad': mad,
    'gmae': gmae,
    'mdae': mdae,
    'mpe': mpe,
    'mape': mape,
    'mdape': mdape,
    'smape': smape,
    'smdape': smdape,
    'maape': maape,
    'mase': mase,
    'std_ae': std_ae,
    'std_ape': std_ape,
    'rmspe': rmspe,
    'rmdspe': rmdspe,
    'rmsse': rmsse,
    'inrse': inrse,
    'rrse': rrse,
    'mre': mre,
    'rae': rae,
    'mrae': mrae,
    'mdrae': mdrae,
    'gmrae': gmrae,
    'mbrae': mbrae,
    'umbrae': umbrae,
    'mda': mda,
    "jsd": jensenshannon,
}

def evaluate(actual: np.ndarray, predicted: np.ndarray, metrics=('mae', 'rmse', 'mse', 'smape')):
    results = {}
    for name in metrics:
        try:
            results[name] = round(METRICS[name](actual, predicted), 7)
        except Exception as err:
            results[name] = np.nan
            print('Unable to compute metric {0}: {1}'.format(name, err))
    return results

def evaluate_all(actual: np.ndarray, predicted: np.ndarray):
    return evaluate(actual, predicted, metrics=set(METRICS.keys()))


from sympy import *
x, y, z, k, n = symbols('x y z k n')
init_printing(use_unicode=True)

def verify_base_case(formula, valid_from_n, base_case):
    computed_base_case = (sympify(formula)).subs(n, valid_from_n)
    base_case_satisfied = computed_base_case == base_case
    return base_case_satisfied

def expand_induction_hypothesis_k_plus_1(formula, next_expression):
    # inductive hypothesis
    inductive_step = formula.replace('n', 'k') + '+' + next_expression.replace('n', 'k')
    return expand(inductive_step)

def expand_formula_k_plus_1(formula):
    return expand(formula.replace('n', '(k+1)'))

def prove_arithmetic_series_formula(formula, valid_from_n, base_case, next_expression):
    # verify base case
    base_case_satisfied = verify_base_case(formula, valid_from_n, base_case)

    # Inductive step
    inductive_hypothesis = expand_induction_hypothesis_k_plus_1(formula, next_expression)
    formula_next_term = expand_formula_k_plus_1(formula)

    # If the inductive hypothesis matches the formula for the next term
    # then we have proved the formula
    return {
        'base_case_satisfied': base_case_satisfied,
        'inductive_hypothesis': inductive_hypothesis,
        'formula_next_term': formula_next_term,
        'proved': base_case_satisfied and inductive_hypothesis == formula_next_term
    }

# Sum of all even numbers
INPUT_FORMULA = 'n*(n+1)'
INPUT_VALID_FROM_N = 1
INPUT_BASE_CASE = 2
INPUT_NEXT_STEP = '2*(n+1)'

print prove_arithmetic_series_formula(
    INPUT_FORMULA,
    INPUT_VALID_FROM_N,
    INPUT_BASE_CASE,
    INPUT_NEXT_STEP
    )


import numpy as np

def f(x):
    return x * x / 2, x

def optimize(f, a: float, b: float, eps: float = 1e-8):
    optimize.n_it = 0
    x, y, z = (a + b) / 2, (a + b) / 2, (a + b) / 2  # топ3 приближения минимума
    orac = f(x)
    fx, fy, fz = orac[0], orac[0], orac[0]
    dfx, dfy, dfz = orac[1], orac[1], orac[1]
    while b - a > eps:
        optimize.n_it += 1
        candidates = []
        if abs(x - y) >= eps and abs(dfx - dfy) > 0:
            u = (x * dfy - y * dfx) / (dfy - dfx)  # метод секущей поиска f'(x) = 0
            if (u > a) and (u < b):
                candidates.append(u)
        if abs(x - z) >= eps and abs(dfx - dfy) > 0:
            u = (x * dfz - z * dfx) / (dfz - dfx)
            if (u > a) and (u < b):
                candidates.append(u)
        if len(candidates):
            u = min(candidates, key=lambda v: abs(v - x))
        elif dfx > 0:  # бинпоиск нуля производной
            u = (a + x) / 2
        elif dfx < 0:
            u = (x + b) / 2
        else:
            return np.array(x)
        if (abs(a - x) < eps / 2) and (abs(x - u) < eps / 2):
            u = x + eps / 2
        elif (abs(b - x) < eps / 2) and (abs(x - u) < eps / 2):
            u = x - eps / 2
        fu, dfu = f(u)
        if fu <= fx:  # приближение u лучше, чем x
            if u > x:
                a = x
            else:
                b = x
            x, y, z = u, x, y
            fx, fy, fz = fu, fx, fy
            dfx, dfy, dfz = dfu, dfx, dfy
        else:
            if u > x:
                b = u
            else:
                a = u
            if fu <= fy or abs(y - x) < eps:  # u хуже x но лучше y
                y, z = u, y
                fy, fz = fu, fy
                dfy, dfz = dfu, dfy
            elif fu < fz or abs(z - x) < eps or abs(z - y) < eps:  # u хуже x, y, но лучше z
                z = u
                fz = fu
                dfz = dfu
    return np.array(u)


%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# Uniformly distrubited, equal chance of any value occurring
#
# Equal distribution between -10.0 and 10.0 as the variable
# Plotted with 10,000 data points
values = np.random.uniform(-10.0, 10.0, 100000)
plt.hist(values, 50)
plt.show()

# Visualizing a normal distribution
# The normal distribution is also known as a Gaussian distribution
x = np.arange(-3, 3, 0.001)
plt.plot(x, norm.pdf(x))

# Generate a custom normal distribution
# With certain parameters
#
# mu    -> mean
# sigma -> stddev
#
mu = 5.0
sigma = 2.0
num_datapoints = 10000
num_bins = 1000
values = np.random.normal(mu, sigma, num_datapoints)
plt.hist(values, num_bins)
plt.show()

# Exponential fall off probability density function
#
step = 0.001
l = 3
x = np.arange(0, l, step)
plt.plot(x, expon.pdf(x))

# Binomial
# Probability mass function
# i.e. for discrete data
#
from scipy.stats import binom

# n -> number of samples
# p -> probability of each outcome
n, p = 10, 0.5
step = 0.001
x = np.arange(0, 10, step)
plt.plot(x, binom.pmf(x, n, p))

# Poisson
# Can be skewed!
# Not the same as normal distribution
from scipy.stats import poisson
import matplotlib.pyplot as plt

# Mean
mu = 450
step = 0.5
end = 600
x = np.arange(400, end, step)
plt.plot(x, poisson.pmf(x, mu))

# The equivalent of a PDF for using discrete data is:
#
# A probability mass function


import numpy as np
import sympy as sy
from sympy import init_printing
from sympy import pprint
init_printing()

__author__='SatoshiTerasaki<terasakisatoshi.math@gmail.com>'
__version__='1.0.0'
__date__='2017/02/25'

def calc_with_sympy(A):
    print("calc with sympy")
    print("A="),pprint(A)
    for eigen_value,multi,eigen_vect in A.eigenvects():
        print("eigen_value",eigen_value,"its multiplocity is",multi)
        print("eigen_vect corresponds the eigen_value is")
        pprint(eigen_vect)

def calc_with_numpy(A):
    print("calc with numpy")
    eigen_vals,eigen_vects=np.linalg.eig(A)
    for idx,val in enumerate(eigen_vals):
        print("eigen vector corresponds {} is {}".format(val,eigen_vects[:,idx]))

def main():
    A=[[1,-1,2],
       [0,-1,3],
       [0,3,-1]]
    calc_with_sympy(sy.Matrix(A))
    calc_with_numpy(np.array(A))

if __name__=='__main__':
    main()


import numpy as np
import math

def mat2euler(M, cy_thresh=None):
    ''' Discover Euler angle vector from 3x3 matrix

    Uses the conventions above.

    Parameters
    ----------
    M : array-like, shape (3,3)
    cy_thresh : None or scalar, optional
       threshold below which to give up on straightforward arctan for
       estimating x rotation.  If None (default), estimate from
       precision of input.

    Returns
    -------
    z : scalar
    y : scalar
    x : scalar
       Rotations in radians around z, y, x axes, respectively

    Notes
    -----
    If there was no numerical error, the routine could be derived using
    Sympy expression for z then y then x rotation matrix, which is::

      [                       cos(y)*cos(z),                       -cos(y)*sin(z),         sin(y)],
      [cos(x)*sin(z) + cos(z)*sin(x)*sin(y), cos(x)*cos(z) - sin(x)*sin(y)*sin(z), -cos(y)*sin(x)],
      [sin(x)*sin(z) - cos(x)*cos(z)*sin(y), cos(z)*sin(x) + cos(x)*sin(y)*sin(z),  cos(x)*cos(y)]

    with the obvious derivations for z, y, and x

       z = atan2(-r12, r11)
       y = asin(r13)
       x = atan2(-r23, r33)

    Problems arise when cos(y) is close to zero, because both of::

       z = atan2(cos(y)*sin(z), cos(y)*cos(z))
       x = atan2(cos(y)*sin(x), cos(x)*cos(y))

    will be close to atan2(0, 0), and highly unstable.

    The ``cy`` fix for numerical instability below is from: *Graphics
    Gems IV*, Paul Heckbert (editor), Academic Press, 1994, ISBN:
    0123361559.  Specifically it comes from EulerAngles.c by Ken
    Shoemake, and deals with the case where cos(y) is close to zero:

    See: http://www.graphicsgems.org/

    The code appears to be licensed (from the website) as "can be used
    without restrictions".
    '''
    _FLOAT_EPS_4 = np.finfo(float).eps * 4.0
    M = np.asarray(M)
    if cy_thresh is None:
        try:
            cy_thresh = np.finfo(M.dtype).eps * 4
        except ValueError:
            cy_thresh = _FLOAT_EPS_4
    r11, r12, r13, r21, r22, r23, r31, r32, r33 = M.flat
    # cy: sqrt((cos(y)*cos(z))**2 + (cos(x)*cos(y))**2)
    cy = math.sqrt(r33*r33 + r23*r23)
    if cy > cy_thresh: # cos(y) not close to zero, standard form
        z = math.atan2(-r12,  r11) # atan2(cos(y)*sin(z), cos(y)*cos(z))
        y = math.atan2(r13,  cy) # atan2(sin(y), cy)
        x = math.atan2(-r23, r33) # atan2(cos(y)*sin(x), cos(x)*cos(y))
    else: # cos(y) (close to) zero, so x -> 0.0 (see above)
        # so r21 -> sin(z), r22 -> cos(z) and
        z = math.atan2(r21,  r22)
        y = math.atan2(r13,  cy) # atan2(sin(y), cy)
        x = 0.0
    return z, y, x


# This file is part of PWLQ repository.
# Copyright (c) Samsung Semiconductor, Inc.
# All rights reserved.

import torch
import numpy as np

##########################################################################################
####  Uniform quantization
##########################################################################################

# References:
#   https://arxiv.org/abs/1806.08342
#   https://arxiv.org/abs/1712.05877
def uniform_symmetric_quantizer(x, bits=8.0, minv=None, maxv=None, signed=True,
                                scale_bits=0.0, num_levels=None, scale=None, simulated=True):
    if minv is None:
        maxv = torch.max(torch.abs(x))
        minv = - maxv if signed else 0

    if signed:
        maxv = np.max([-float(minv), float(maxv)])
        minv = - maxv
    else:
        minv = 0

    if num_levels is None:
        num_levels = 2 ** bits

    if scale is None:
        scale = (maxv - minv) / (num_levels - 1)

    if scale_bits > 0:
        scale_levels = 2 ** scale_bits
        scale = torch.round(torch.mul(scale, scale_levels)) / scale_levels

    ## clamp
    x = torch.clamp(x, min=float(minv), max=float(maxv))

    x_int = torch.round(x / scale)

    if signed:
        x_quant = torch.clamp(x_int, min=-num_levels/2, max=num_levels/2 - 1)
        assert(minv == - maxv)
    else:
        x_quant = torch.clamp(x_int, min=0, max=num_levels - 1)
        assert(minv == 0 and maxv > 0)

    x_dequant = x_quant * scale

    return x_dequant if simulated else x_quant

def uniform_affine_quantizer(x, bits=8.0, minv=None, maxv=None, offset=None, include_zero=False,
                            scale_bits=0.0, num_levels=None, scale=None, simulated=True):
    if minv is None:
        maxv = torch.max(x)
        minv = torch.min(x)
        if include_zero:
            if minv > 0:
                minv = 0
            elif maxv < 0:
                maxv = 0

    if num_levels is None:
        num_levels = 2 ** bits

    if not scale:
        scale = (maxv - minv) / (num_levels - 1)

    if not offset:
        offset =  minv

    if scale_bits > 0:
        scale_levels = 2 ** scale_bits
        scale = torch.round(torch.mul(scale, scale_levels)) / scale_levels
        offset = torch.round(torch.mul(offset, scale_levels)) / scale_levels

    ## clamp
    x = torch.clamp(x, min=float(minv), max=float(maxv))

    x_int = torch.round((x - offset) / scale)

    x_quant = torch.clamp(x_int, min=0, max=num_levels - 1)

    x_dequant = x_quant * scale + offset

    return x_dequant if simulated else x_quant


"""
Demo gradient descent
"""
import numpy as np
import pylab as pl

x_min, x_max = -0.6, 1.1
y_min, y_max = -0.1, 1.1

x0, y0 = 3., 2.
epsilon = .1

for index, epsilon in enumerate((1, .01)):
    # s_x, s_y = 1.5, .01
    #def f(x, y):
    #    return -s_x*np.cos(x - x0) - s_y*np.cos(y - y0)

    #def f_prime(x, y):
    #    return s_x*np.sin(x - x0), s_y*np.sin(y - y0)

    def rosenbrock(x, y):
        return epsilon*(1 - x)**2 + (y - x**2)**2

    def rosenbrock_prime(x, y):
        return (-epsilon*2*(1 - x) - 4*x*(y - x**2),
                2*(y - x**2))

    f = rosenbrock
    f_prime = rosenbrock_prime

    x, y = np.mgrid[x_min:x_max:100j, y_min:y_max:100j]
    x = x.T
    y = y.T

    pl.figure(index, figsize=(3, 2.5))
    pl.clf()
    #pl.axes([0, 0, 1, 1])

    contours = pl.contour(f(x, y),
                        extent=[x_min, x_max, y_min, y_max],
                        cmap=pl.cm.gnuplot, origin='lower')
    pl.clabel(contours,
            inline=1,
            fmt='%1.1f',
            fontsize=14)

    x_i, y_i = 0, 0
    all_x_i = list()
    all_y_i = list()

    for i in range(10):
        all_x_i.append(x_i)
        all_y_i.append(y_i)
        dx_i, dy_i = f_prime(x_i, y_i)
        x_i += -dx_i
        y_i += -dy_i

    pl.plot(all_x_i, all_y_i, 'b-')
    pl.plot(all_x_i, all_y_i, 'k+')

    pl.plot([x0], [y0], 'rx')


# -*- coding: utf-8 -*-
"""
Created on Fri Jun  5 11:56:32 2020
Some functions to do Logistic Regression
@author: Ji-Ping Dai
"""
import numpy as np
import matplotlib.pyplot as plt
plt.rc('text',usetex=True)
plt.rc('font',family='Times New Roman')

def plotdata(X,y,ax):
    '''PLOTDATA Plots the data points x and y into a new figure'''
    plt.xticks(fontsize=20)
    plt.yticks(fontsize=20)
    ax.plot(X[(y==1).flatten(),0], X[(y==1).flatten(),1], 'kx')
    ax.plot(X[(y==0).flatten(),0], X[(y==0).flatten(),1], 'yo')
    return

def sigmoid(z):
    '''sigmoid function, output between 0-1'''
    sigmoid = 1/(1+np.exp(-z))
    return sigmoid

def costfunc(theta,x,y):
    '''Compute cost for Logistic Regression'''
    m = len(y)
    theta = theta.reshape(-1,1)
    z = x@theta
    J = -1/m*(y.T@np.log(sigmoid(z))+(1-y).T@np.log(1-sigmoid(z)))
    return J

def costfuncReg(theta,x,y,lam):
    '''Compute cost for Logistic Regression'''
    m = len(y)
    theta = theta.reshape(-1,1)
    J=np.zeros([len(theta),1])
    z = x@theta
    J = -1/m*(y.T@np.log(sigmoid(z))+(1-y).T@np.log(1-sigmoid(z)))+lam/2/m*theta.T[:,1:]@theta[1:,:]
    return J

def grid(theta,x,y):
    '''Compute gradient for Logistic Regression'''
    m = len(y)
    theta = theta.reshape(-1,1)
    z = x@theta
    grid = 1/m*(x.T@(sigmoid(z)-y))
    return grid.flatten()

def gridReg(theta,x,y,lam):
    '''Compute gradient for Logistic Regression'''
    m = len(y)
    theta = theta.reshape(-1,1)
    grid=np.zeros([len(theta),1])
    z = x@theta
    grid[0,:] = 1/m*(x.T@(sigmoid(z)-y))[0,:]
    grid[1:,:] = 1/m*(x.T@(sigmoid(z)-y))[1:,:] + lam/m*theta[1:,:]
    return grid.flatten()

def gradientDescent(x,y,theta,iterations,alpha):
    '''Performs gradient descent to learn theta'''
    m = len(y)
    cost = np.zeros(iterations)
    for i in range(iterations):
        z = x@theta
        theta = theta-1/m*alpha*x.T@(sigmoid(z)-y)
        cost[i] = costfunc(theta,x,y)
    return theta, cost

def plotboundary(X,y,theta,ax):
    '''Plots the Decision Boundary'''
    plotdata(X,y,ax)
    if len(theta)<=3:
        px=np.linspace(np.min(X[:,0])-2,np.max(X[:,0])+2,100)
        py=(-theta[0]-theta[1]*px)/theta[2]
        ax.plot(px,py,ls='--',color='b')
    else:
        px=np.linspace(-1,1.5,100)
        py=np.linspace(-1,1.5,100)
        cost=np.zeros([100,100])
        for i in range(len(px)):
            for j in range(len(py)):
                cost[i,j]=mapX(px[i],py[j])@theta
        cost=cost.T
        PX,PY=np.meshgrid(px,py)
        ax.contour(PX,PY,cost,[0],colors='k')
    return

def predict(x,theta):
    '''Predict whether the label is 0 or 1 using learned logistic '''
    p = sigmoid(x@theta.reshape(-1,1))
    p[p>=0.5]=1
    p[p<0.5]=0
    return p

def mapX(x1,x2):
    '''Feature mapping function to polynomial features'''
    order = 6
    X = np.ones([x1.size,1])
    order=6
    for i in range(1,order+1):
        for j in range(i+1):
            newcol = (x1**(i-j)*x2**(j)).reshape(-1,1)
            X = np.concatenate([X,newcol],axis=1)
    return X


#!/usr/bin/env python

''' Método de Newton: encontrar o mínimo dessa função '''

from sympy import lambdify, diff, cos, sin, exp
from sympy.abc import x

from Error import *
from Log import *

MAX = 50
PATH = 'log/newton-opt/'
TOLERANCE = 0.00000001 # 10**(-8)

def newton_optimization(fn, cx, tol, nmax) :
    l = Log()
    e = Error()
    previous = 0

    dx = fn.diff(x)
    dx2 = dx.diff(x)

    dx = lambdify(x, dx)
    dx2 = lambdify(x, dx2)

    for n in range(nmax) :
        if (dx2(cx) == 0) :
            return "Error 27: derivative less than zero"
            breakpoint
        cx = cx - (dx(cx) / dx2(cx) )
        e.absolute(cx, previous)
        l.append([cx, e._absolute, dx(cx), dx2(cx)])
        if (e._absolute < tol) :
            l.set_header(['x', 'absolute_error', 'function', 'derivative'])
            l.list2file((PATH+str(fn)))
            return cx
            breakpoint
        previous = cx
    return False

def run_test(function, a, TOLERANCE, MAX):
    m = newton_optimization(function, a, TOLERANCE, MAX)
    print('f(x) =',function, '-> newton optimization =', m)
    return m

if __name__ == "__main__":

    ''' Tests
        https://en.wikipedia.org/wiki/Newton%27s_method_in_optimization
    '''

    log = []
    a = .0000005
    fx = [(cos(x) - x**3), (x**2 - 612), ((x**3)-(2*x)+2), (x**6 - x - 1), (x**3-6*x**2+4*x+2)]

    euler = exp(-( ( ((x-1)**2)/(2*(0.75**2)) ) + ( ((x-2)**2)/(2*(0.5**2)) )  ))
    fx = [1 - euler + ( 0.04 * (((x-1)**2) + ((x-2)**2)) )]

    for f in fx:
        run_test(f, a, TOLERANCE, MAX)


"""
brownian() implements one dimensional Brownian motion (i.e. the Wiener process).
"""

# File: brownian.py
# From SciPy examples: http://scipy-cookbook.readthedocs.io/items/BrownianMotion.html

from math import sqrt
from scipy.stats import norm
import numpy as np

def brownian(x0, n:int, dt, delta, out=None):
    """
    Generate an instance of Brownian motion (i.e. the Wiener process):

        X(t) = X(0) + N(0, delta**2 * t; 0, t)

    where N(a,b; t0, t1) is a normally distributed random variable with mean a and
    variance b.  The parameters t0 and t1 make explicit the statistical
    independence of N on different time intervals; that is, if [t0, t1) and
    [t2, t3) are disjoint intervals, then N(a, b; t0, t1) and N(a, b; t2, t3)
    are independent.

    Written as an iteration scheme,

        X(t + dt) = X(t) + N(0, delta**2 * dt; t, t+dt)

    If `x0` is an array (or array-like), each value in `x0` is treated as
    an initial condition, and the value returned is a numpy array with one
    more dimension than `x0`.

    x0 : float or numpy array (or something that can be converted to a numpy array
         using numpy.asarray(x0)).
        The initial condition(s) (i.e. position(s)) of the Brownian motion.
    n : int
        The number of steps to take.
    dt : float
        The time step.
    delta : float
        delta determines the "speed" of the Brownian motion.  The random variable
        of the position at time t, X(t), has a normal distribution whose mean is
        the position at time t=0 and whose variance is delta**2*t.
    out : numpy array or None
        If `out` is not None, it specifies the array in which to put the
        result.  If `out` is None, a new numpy array is created and returned.

    A numpy array of floats with shape `x0.shape + (n,)`.

    Note that the initial value `x0` is not included in the returned array.
    """

    x0 = np.asarray(x0)

    # For each element of x0, generate a sample of n numbers from a
    # normal distribution.
    r = norm.rvs(size=x0.shape + (n,), scale=delta*sqrt(dt))

    # If `out` was not given, create an output array.
    if out is None:
        out = np.empty(r.shape)

    # This computes the Brownian motion by forming the cumulative sum of
    # the random samples.
    np.cumsum(r, axis=-1, out=out)

    # Add the initial condition.
    out += np.expand_dims(x0, axis=-1)

    return out

# Adapted from the NumPy Geometric Brownian Motion function by Yves Hilpisch:
# https://www.wakari.io/sharing/bundle/yves/Yves_Hilpisch_Python_in_Finance_Talk_NYC?has_login=False
def geometric_brownian(S0, mu, sigma, dt, num_paths: int, num_steps: int):
    """
    :param S0: the initial value
    :param mu: the percentage drift
    :param sigma: the percentage volatility
    :param dt: the time-step (fraction of a year)
    :param num_paths: the number of paths to generate
    :param num_steps: the number of steps to generate
    :return: an array with the paths
    """
    S = S0 * np.exp(np.cumsum((mu - 0.5 * sigma ** 2) * dt
                + sigma * np.sqrt(dt) * np.random.standard_normal((num_steps, num_paths)), axis=0))
    return S.transpose()


#
# Copyright (c) 2017 Intel Corporation
# SPDX-License-Identifier: BSD-2-Clause
#

# This code was ported from a Julia implementation as part of the
# ParallelAccelerator Julia package which in turn was ported from a
# MATLAB implementation available at
# https://www.piso.at/julius/programming/octave-2d-wave-equation
# with permission of the author.
# Original MATLAB implementation is copyright (c) 2014 Julius Piso.

from __future__ import print_function

import sys
import numpy as np
from numba import njit, stencil
try:
    import matplotlib.pyplot as plt
    from matplotlib.animation import FuncAnimation
except ImportError:
    raise RuntimeError("matplotlib is needed to run this example. Try 'conda install matplotlib'")

@stencil()
def rws_kernel(p, c, r):
    return 2 * c[0, 0] - p[0, 0] + r * r * (c[-1, 0] + c[1, 0] + c[0, -1] + c[0, 1] - 4*c[0, 0])

@njit(parallel=True)
def runWaveStencil(p, c, f, r, t, n, s2, s4, s):
    rws_kernel(p, c, r, out=f)

    # Dynamic source
    f[s2+s4-2:s2+s4+1, 0:2] = 1.5 * np.sin(t*n)
    f[s2-s4-2:s2-s4+1, 0:2] = 1.5 * np.sin(t*n)
    f[1:s-1, 0:2] = 1.0

    # Transparent boundary handling
    f[1:s-1, 0]   = (2.0 * c[1:s-1, 0]   + (r-1.0) * p[1:s-1, 0]   + 2.0*r*r*(c[1:s-1, 1]   + c[2:s, 0]   + c[0:s-2, 0]   - 3.0 * c[1:s-1, 0]))   / (1.0+r) # Y:1
    f[1:s-1, s-1] = (2.0 * c[1:s-1, s-1] + (r-1.0) * p[1:s-1, s-1] + 2.0*r*r*(c[1:s-1, s-2] + c[2:s, s-1] + c[0:s-2, s-1] - 3.0 * c[1:s-1, s-1])) / (1.0+r) # Y:s
    f[0, 1:s-1]   = (2.0 * c[0, 1:s-1]   + (r-1.0) * p[0, 1:s-1]   + 2.0*r*r*(c[1, 1:s-1]   + c[0, 2:s]   + c[0, 0:s-2]   - 3.0 * c[0, 1:s-1]))   / (1.0+r) # X:1
    f[s-1, 1:s-1] = (2.0 * c[s-1, 1:s-1] + (r-1.0) * p[s-1, 1:s-1] + 2.0*r*r*(c[s-2, 1:s-1] + c[s-1, 2:s] + c[s-1, 0:s-2] - 3.0 * c[s-1, 1:s-1])) / (1.0+r) # Y:s

class State(object):
    def __init__(self, speed, s):
        self.speed = speed
        self.s = s

        self.t = 0.0

        self.s = s
        self.s2  = self.s // 2
        self.s4  = self.s // 4
        self.s8  = self.s // 8
        self.s16 = self.s // 16

        self.p = np.zeros((self.s, self.s)) # past
        self.c = np.zeros((self.s, self.s)) # current
        self.f = np.zeros((self.s, self.s)) # future

        self.dt = 0.0001     # Time resolution of the simulation
        self.dx = 0.01       # Distance between elements
        self.r = self.speed * self.dt / self.dx

        self.n = 300
        self.last = -1

        for i in range(self.s2 - self.s16 - 1, self.s2 + self.s16):
            # Initial conditions
            self.c[i, self.s2 - self.s16 - 1 : self.s2 + self.s16 ] = - 2 * np.cos(0.5 * 2 * np.pi / (self.s8) * np.arange(self.s2 - self.s16 , self.s2 + self.s16 + 1)) * np.cos(0.5 * 2 * np.pi / (self.s8) * i)
            self.p[i, 0:self.s] = self.c[i, 0:self.s]

        self.im = plt.imshow(self.c, animated=True)

    def step(self):
        # Wave equation
        runWaveStencil(self.p, self.c, self.f, self.r, self.t, self.n, self.s2, self.s4, self.s)

        # Transparent corner handling
        self.f[0, 0] =     (2 * self.c[0, 0]     + (self.r-1) * self.p[0, 0]     + 2*self.r*self.r* (self.c[1, 0]       + self.c[0, 1]       - 2*self.c[0, 0]))     / (1+self.r) # X:1; Y:1
        self.f[self.s-1, self.s-1] = (2 * self.c[self.s-1, self.s-1] + (self.r-1) * self.p[self.s-1, self.s-1] + 2*self.r*self.r* (self.c[self.s-2, self.s-1] + self.c[self.s-1, self.s-2] - 2*self.c[self.s-1, self.s-1])) / (1+self.r) # X:s; Y:s
        self.f[0, self.s-1] =   (2 * self.c[0, self.s-1]   + (self.r-1) * self.p[0, self.s-1]   + 2*self.r*self.r* (self.c[1, self.s-1]     + self.c[0, self.s-2]   - 2*self.c[0, self.s-1]))   / (1+self.r) # X:1; Y:s
        self.f[self.s-1, 0] =   (2 * self.c[self.s-1, 0]   + (self.r-1) * self.p[self.s-1, 0]   + 2*self.r*self.r* (self.c[self.s-2, 0]   + self.c[self.s-1, 1]     - 2*self.c[self.s-1, 0]))   / (1+self.r) # X:s; Y:1

        # Rotate buffers for next iteration
        tmp = self.p
        self.p = self.c
        self.c = self.f
        self.f = tmp

        self.t += self.dt
        self.im.set_array(self.c)

    def plot_step(self, x):
        if x > self.last:
            print("plot_step", x)
            self.step()
            self.last = x

def wave2d():
    speed = 10      # Propagation speed
    s = 512         # Array size (spatial resolution of the simulation)

    fig = plt.figure()
    state = State(speed, s)
    ani = FuncAnimation(fig, state.plot_step)
    plt.show()

    return True

def main(*args):
    wave2d()

if __name__ == "__main__":
    main(*sys.argv[1:])


# Run the blending process
# Martha Grover, July 6, 2017 (MATLAB)
# John Hedengren, July 14, 2017 (Python)

import numpy as np
from scipy.integrate import odeint

# Here is the playlist for the accompanying screencasts:
# https://www.youtube.com/playlist?list=PL4xAk5aclnUhb0tM6nypIATyxPRk0fB3L

# Following on the first screencast (Blending Process: Dynamic Simulation),
# here you can simulate the nonlinear coupled set of differential equations.
# As in the second screencast (Blending Process: Steady States),
# observe here how the mass fraction in the tank
# approaches the steady state value of xbar as the system approaches long
# time.  The volume does not have a unique steady state.  Since filling a
# tank is a integrating process, it may increase or decrease without bound,
# based on the difference between the inlet and outlet mass flow rates.

# Parameters
rho = 1000   # Density [kg/m^3]

# Here we will consider x1 and x2 to remain as constants
x1 = 0.1
x2 = 0

#######################################################################
############### Define the Nonlinear Model  ###########################
#######################################################################
# Nominal inputs:
# x1: Mass fraction of Species A in Stream 1 (unitless)
# w1: Mass flow rate in Stream 1 [kg/s]
# x2: Mass fraction of Species A in Stream 2 (unitless)
# w2: Mass flow rate in Stream 2 [kg/s]
# w:  Outlet mass flow [kg/s]

def blending(z,t,u):
    # Dynamic model of the blending process
    # State variables passed in as vector z:
    V = z[0]   # Volume [m3]
    x = z[1]   # Mass fraction of Species A in the tank (unitless)
    # Inputs passed in as vector u:
    w1 = u[0]  # Mass flow rate in Stream 1 [kg/s]
    w2 = u[1]  # Mass flow rate in Stream 2 [kg/s]
    w = u[2]   # Here we set the outlet flow equal to the total inlet flow

    # Dynamic equations
    dVdt = (1.0/rho)*(w1+w2-w)                     # Change in volume
    dxdt = (1.0/(rho*V))*(w1*(x1-x)+w2*(x2-x))     # Change in mass fraction

    # Assemble the two time derivatives back into a vector to pass back
    dzdt = [dVdt,dxdt]
    return dzdt

#######################################################################
############# Dynamic Simulation of the Nonlinear System ##############
#######################################################################

# Initial conditions
V0 = 10        # Initial volume in the tank [m^3]
x0 = 0.025     # Initial mass fraction of Species A in the tank (unitless)
# Note: these are currently set at their nominal steady-state values...
# But you can try changing them to other values.
z0 = [V0,x0]  # Initial condition vector for the system

# The flow rates will be the variables in u that may change over time
w1 = 1         # w1: Mass flow rate in Stream 1 [kg/s]
w2 = 3         # w2: Mass flow rate in Stream 2 [kg/s]
w = w1 + w2    # w:  Outlet mass flow [kg/s]
u = [w1,w2,w]  # Assemble these together in a column vector u

tf = 3600      # Final simulation time [s]
t = np.linspace(0,tf,100) # points to report solution

# Simulate the blending process
z = odeint(blending,z0,t,args=(u,))

# plotting results
import matplotlib.pyplot as plt

# Plot the blending process under steady operation
plt.figure(1)
plt.subplot(2,1,1)
plt.plot(t,z[:,0])
plt.xlabel('time t [s]')
plt.ylabel('V [m^3]')
plt.title('Blending process under constant inputs')
plt.subplot(2,1,2)
plt.plot(t,z[:,1])
plt.xlabel('time t [s]')
plt.ylabel('x')

# Calculate the steady-state value for x
xbar = (w1*x1 + w2*x2)/(w1+w2)
# Check to see if this matches the long-term value of x on the plot
# Since V does not have a unique steady state, specify a desired operating value
# Here it is set equal to the initial volume
Vbar = V0  # volume [m^3]

# Define ubar as the set of inputs to achieve this steady state
ubar = u

# Next simulate the system for the higher value of w1 = 1.1 (0.1 higher)
dw1 = 0.1
u[0] = ubar[0] + dw1

zs = odeint(blending,z0,t,args=(u,))

# Plot the blending process under a step change in w1
plt.figure(2)
plt.subplot(2,1,1)
plt.plot(t,zs[:,0])
plt.xlabel('time t [s]')
plt.ylabel('V [m^3]')
plt.title('Blending process under a step input to w_1')
plt.subplot(2,1,2)
plt.plot(t,zs[:,1])
plt.xlabel('time t [s]')
plt.ylabel('x')

# show plots
plt.show()


######################################################################
#
# Functions to calculate integration points and weights for Gaussian
# quadrature
#
# x,w = gaussxw(N) returns integration points x and integration
#           weights w such that sum_i w[i]*f(x[i]) is the Nth-order
#           Gaussian approximation to the integral int_{-1}^1 f(x) dx
# x,w = gaussxwab(N,a,b) returns integration points and weights
#           mapped to the interval [a,b], so that sum_i w[i]*f(x[i])
#           is the Nth-order Gaussian approximation to the integral
#           int_a^b f(x) dx
#
# This code finds the zeros of the nth Legendre polynomial using
# Newton's method, starting from the approximation given in Abramowitz
# and Stegun 22.16.6.  The Legendre polynomial itself is evaluated
# using the recurrence relation given in Abramowitz and Stegun
# 22.7.10.  The function has been checked against other sources for
# values of N up to 1000.  It is compatible with version 2 and version
# 3 of Python.
#
# Written by Mark Newman <mejn@umich.edu>, June 4, 2011
# You may use, share, or modify this file freely
#
######################################################################

from numpy import ones,copy,cos,tan,pi,linspace

def gaussxw(N):

    # Initial approximation to roots of the Legendre polynomial
    a = linspace(3,4*N-1,N)/(4*N+2)
    x = cos(pi*a+1/(8*N*N*tan(a)))

    # Find roots using Newton's method
    epsilon = 1e-15
    delta = 1.0
    while delta>epsilon:
        p0 = ones(N,float)
        p1 = copy(x)
        for k in range(1,N):
            p0,p1 = p1,((2*k+1)*x*p1-k*p0)/(k+1)
        dp = (N+1)*(p0-x*p1)/(1-x*x)
        dx = p1/dp
        x -= dx
        delta = max(abs(dx))

    # Calculate the weights
    w = 2*(N+1)*(N+1)/(N*N*(1-x*x)*dp*dp)

    return x,w

def gaussxwab(N,a,b):
    x,w = gaussxw(N)
    return 0.5*(b-a)*x+0.5*(b+a),0.5*(b-a)*w


from __future__ import division
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
from problem_6a import construct_A
from problem_6b import gen_vectors
from matplotlib import rc

def generate_four_tuples(A, b_all):

	tuples = []

	for i in range(10):

		b = b_all[i]

		x = np.linalg.solve(A, b)

		for j in range(10):
			# print(i, j)
			b_db = b_all[(i+1) * 10 + j]
			x_dx = np.linalg.solve(A, b_db)

			db = b_db - b
			dx = x_dx - x

			tuples.append((b, db, x, dx))

	return np.array(tuples)

def generate_plot(cno_A, tuples):

	points_x = []
	points_y = []

	for i in range(len(tuples)):

		db_norm = np.linalg.norm(tuples[i][1], ord=2)
		b_norm = np.linalg.norm(tuples[i][0], ord=2)
		dx_norm = np.linalg.norm(tuples[i][3], ord=2)
		x_norm = np.linalg.norm(tuples[i][2], ord=2)

		x = cno_A * (db_norm / b_norm)
		y = dx_norm / x_norm

		points_x.append(x)
		points_y.append(y)

	points_x = np.array(points_x, dtype=np.float128)
	points_y = np.array(points_y, dtype=np.float128)

	plt.figure()
	plt.xlabel(u"cond(A) * (||\delta b|| / ||b||)")
	plt.ylabel(u"||\delta x|| / ||x||")
	plt.plot(points_x, points_y, "ko")
	plt.savefig("problem_6c.png")
	# Uncomment this line to see the plot
	# plt.show()

def main():

	A, cno_A = construct_A(512)
	b_all = gen_vectors()
	tuples = generate_four_tuples(A, b_all)
	generate_plot(cno_A, tuples)

if __name__ == '__main__':
	main()