import numpy as np

# We create two rank 1 ndarrays
x = np.array([1,2,3,4])
y = np.array([5.5,6.5,7.5,8.5])

# We print x
print()
print('x = ', x)

# We print y
print()
print('y = ', y)
print()

# We perfrom basic element-wise operations
#  using arithmetic symbols and functions

print('x + y = ', x + y)
print('add(x,y) = ', np.add(x,y))
print()

print('x - y = ', x - y)
print('x - y = ', np.subtract(x,y))
print()

print('x * y = ', x * y)
print('x * y = ', np.multiply(x,y))
print()

print('x / y = ', x / y)
print('x / y = ', np.divide(x,y))
print()

# We can also perform the same element-wise arithmetic
# operations on rank 2 ndarrays. Again, remember that in
# order to do these operations the shapes of the ndarrays
#  being operated on, must have the same shape or be
# broadcastable.

# We create two rank 2 ndarrays
X = np.array([1,2,3,4]).reshape(2,2)
Y = np.array([5.5,6.5,7.5,8.5]).reshape(2,2)

# We print X
print()
print('X = \n', X)

# We print Y
print()
print('Y = \n', Y)
print()

# We perform basic element-wise operations using arithmetic symbols and functions
print('X + Y = \n', X + Y)
print()
print('add(X,Y) = \n', np.add(X,Y))
print()
print('X - Y = \n', X - Y)
print()
print('subtract(X,Y) = \n', np.subtract(X,Y))
print()
print('X * Y = \n', X * Y)
print()
print('multiply(X,Y) = \n', np.multiply(X,Y))
print()
print('X / Y = \n', X / Y)
print()
print('divide(X,Y) = \n', np.divide(X,Y))

# We can also apply mathematical functions,
# such as sqrt(x), to all elements of an ndarray at once.

# We create a rank 1 ndarray
x = np.array([1,2,3,4])

# We print x
print()
print('x = ', x)

# We apply different mathematical functions to all elements of x
print()
print('EXP(x) =', np.exp(x))
print()
print('SQRT(x) =',np.sqrt(x))
print()
print('POW(x,2) =',np.power(x,2)) # We raise all elements to the power of 2

# Another great feature of NumPy is that it has a wide variety of
#  statistical functions. Statistical functions provide us
#  with statistical information about the elements in an
#  ndarray.

# We create a 2 x 2 ndarray
X = np.array([[1,2], [3,4]])

# We print x
print()
print('X = \n', X)
print()

print('Average of all elements in X:', X.mean())
print('Average of all elements in the columns of X:', X.mean(axis=0))
print('Average of all elements in the rows of X:', X.mean(axis=1))
print()
print('Sum of all elements in X:', X.sum())
print('Sum of all elements in the columns of X:', X.sum(axis=0))
print('Sum of all elements in the rows of X:', X.sum(axis=1))
print()
print('Standard Deviation of all elements in X:', X.std())
print('Standard Deviation of all elements in the columns of X:', X.std(axis=0))
print('Standard Deviation of all elements in the rows of X:', X.std(axis=1))
print()
print('Median of all elements in X:', np.median(X))
print('Median of all elements in the columns of X:', np.median(X,axis=0))
print('Median of all elements in the rows of X:', np.median(X,axis=1))
print()
print('Maximum value of all elements in X:', X.max())
print('Maximum value of all elements in the columns of X:', X.max(axis=0))
print('Maximum value of all elements in the rows of X:', X.max(axis=1))
print()
print('Minimum value of all elements in X:', X.min())
print('Minimum value of all elements in the columns of X:', X.min(axis=0))
print('Minimum value of all elements in the rows of X:', X.min(axis=1))

# Finally, let's see how NumPy can add single numbers to all the elements of an ndarray without the use of complicated loops.

# We create a 2 x 2 ndarray
X = np.array([[1,2], [3,4]])

# We print x
print()
print('X = \n', X)
print()

print('3 * X = \n', 3 * X)
print()
print('3 + X = \n', 3 + X)
print()
print('X - 3 = \n', X - 3)
print()
print('X / 3 = \n', X / 3)

# n the examples above, NumPy is working behind the scenes
#  to broadcast 3 along the ndarray so that they have the
#  same shape. This allows us to add 3 to each element
#  of X with just one line of code.

# Subject to certain constraints, Numpy can do the same for
# two ndarrays of different shapes, as we can see below.

# We create a rank 1 ndarray
x = np.array([1,2,3])

# We create a 3 x 3 ndarray
Y = np.array([[1,2,3],[4,5,6],[7,8,9]])

# We create a 3 x 1 ndarray
Z = np.array([1,2,3]).reshape(3,1)

# We print x
print()
print('x = ', x)
print()

# We print Y
print()
print('Y = \n', Y)
print()

# We print Z
print()
print('Z = \n', Z)
print()

print('x + Y = \n', x + Y)
print()
print('Z + Y = \n',Z + Y)

## Quiz Questions


from typing import List, Optional
from dataclasses import dataclass

import numpy as np

from IPython import display
from d2l import torch as d2l

@dataclass
class PlotConfig:
    X: float
    Y: Optional[float]
    legend: Optional[List]

def function_(x):
    """
    example for this topic caculus
    f(x) = 3 * x^2 - 4 * x
    """
    return 3 * x ** 2 - 4 * x

def numerical_lim(f, x, h):
    """
    numerical result ( do Differentiation )
    f is what function used
    x is coordinate of x
    h is like dx
    """
    return (f(x + h) - f(x)) / h

def use_svg_display():
    """
    Use the svg format to display a plot in Jupyter.
    """
    display.set_matplotlib_formats("svg")

def set_figsize(figsize=(3.5, 2.5)):
    """
    Set the figure size for matplotlib.
    """
    use_svg_display()
    d2l.plt.rcParams["figure.figsize"] = figsize

def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
    """
    Set the axes for matplotlib.
    """
    axes.set_xlabel(xlabel)
    axes.set_ylabel(ylabel)
    axes.set_xscale(xscale)
    axes.set_yscale(yscale)
    axes.set_xlim(xlim)
    axes.set_ylim(ylim)
    if legend:
        axes.legend(legend)
    axes.grid()

def plot(
    config: PlotConfig,
    fmts=("-", "m--", "g-.", "r:"),
    figsize=(3.5, 2.5),
    axes=None,
):
    """
    Plot data points.
    return axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend for set_axes()
    """
    if config.legend is None:
        config.legend = []

    set_figsize(figsize)
    axes = axes if axes else d2l.plt.gca()

    # Return True if `X` (tensor or list) has 1 axis
    def has_one_axis(X):
        return (
            hasattr(X, "ndim")
            and X.ndim == 1
            or isinstance(X, list)
            and not hasattr(X[0], "__len__")
        )

    if has_one_axis(config.X):
        X = [config.X]
    if config.Y is None:
        X, Y = [[]] * len(X), X
    elif has_one_axis(config.Y):
        Y = [config.Y]
    if len(X) != len(Y):
        X = X * len(Y)

    axes.cla()

    for x, y, fmt in zip(X, Y, fmts):
        if len(x):
            axes.plot(x, y, fmt)
        else:
            axes.plot(y, fmt)

if __name__ == "__main__":
    x = np.arange(0, 3, 0.1)
    h = 0.1  # h is like dx

    plot_config = PlotConfig(
        X=x,
        Y=[function_(x), 2 * x - 3],
        legend=["function_(x)", "Tangent line (x=1)"],
    )

    plot(config=plot_config)

    for _ in range(5):
        print(
            f"h={h:.5f}, numerical limit={numerical_lim(function_, 1, h):.5f}"
        )
        h *= 0.1


# XGBoost & LightGBM

## XGBoost

mathematically, we can write our ensemble tree model in the form

$$\hat{y}_{i} = \sum_{k=1}^{K}f_{k}(x_{i}), f_{k}\in\mathcal{F}$$

where $K$ is the number of trees, $f$ is a function in the function space $\mathcal{F}$, and $\mathcal{F}$ is the set of all possible CARTs.

the objective function to be optimized is given by

$$\mbox{obj}(\theta) = \sum_{i=1}^{n}l(y_{i}, \hat{y}_{i}) + \sum_{k=1}^{K}\Omega(f_{k})$$

where $l$ is the loss term, $\Omega$ is the regularization term.

### Additive Learning

what are the parameters of trees? you can find that what we need to learn are those functions $f_{i}$, each containing the structure of the tree and the leaf scores.

learning tree strcture is much harder than traditional optimization problem where you can simply take the gradient, instead, we use an additive strategy: fix what we learned, and add one new tree at a time. we write the prediction value at step $t$ as $\hat{y}_{i}^{(t)}$, then

$$
\begin{equation}
\begin{split}
\mbox{obj}^{(t)} =& \sum_{i=1}^{n}l(y_{i}, \hat{y}_{i}^{(t)}) + \sum_{k=1}^{t}\Omega(f_{k}) \\
=& \sum_{i=1}^{n}l(y_{i}, \hat{y}_{i}^{(t - 1)} + f_{t}(x_{i})) + \Omega(f_{t}) + \mbox{constant}
\end{split}
\end{equation}
$$

$$\hat{y}_{i}^{(0)} = 0$$

in general case($l$ arbitrary), we take the taylor expansion of the loss function up to the second order

$$\mbox{obj}^{(t)} \approx \sum_{i=1}^{n}[l(y_{i}, \hat{y}_{i}^{(t - 1)}) + g_{i}f_{t}(x_{i}) + \frac{1}{2}h_{i}f_{t}(x_{i})^{2}] + \Omega(f_{t}) + \mbox{constant}$$

where $g_{i}$ and $h_{i}$ are defined as

$$g_{i} = \frac{\partial l(y_{i}, \hat{y}_{i}^{(t - 1)})}{\partial \hat{y}_{i}^{(t-1)}}, h_{i} = \frac{\partial^{2} l(y_{i}, \hat{y}_{i}^{(t - 1)})}{{\partial \hat{y}_{i}^{(t-1)}}^2}$$

after removing all the constants, the specific objective at step $t$ becomes

$$\sum_{i=1}^{n}[g_{i}f_{t}(x_{i}) + \frac{1}{2}h_{i}f_{t}(x_{i})^{2}] + \Omega(f_{t})$$

this becomes our optimization goal for the new tree.

### Model Complexity

we need to define the complexity of the tree $\Omega(f)$, in order to do so, let us first refine the definition of the tree $f(x)$ as

$$f(x) = w_{q(x)}, w \in \mathbb{R}^{T}, q: \mathbb{R}^{d} \to \{1,2,...,T\}$$

where $w$ is the vector of scores on leaves, $q$ is a function assigning each data point to the corresponding leaf, and $T$ is the number of leaves. in XGBoost, we define the complexity as

$$\Omega(f) = \gamma{T} + \frac{1}{2}\lambda\sum_{j=1}^{T}w_{j}^{2}$$

this works well in practice.

### The Structure Score

now we can write the objective value with the $t$-th tree as:

$$
\begin{equation}
\begin{split}
\mbox{obj}^{(t)} = & \sum_{i=1}^{n}[g_{i}f_{t}(x_{i}) + \frac{1}{2}h_{i}f_{t}(x_{i})^{2}] + \gamma{T} + \frac{1}{2}\lambda\sum_{j=1}^{T}w_{j}^{2} \\
=& \sum_{j=1}^{T}[(\sum_{i\in{I_{j}}}g_{i})w_{j} + \frac{1}{2}(\sum_{i\in{I_j}}h_{i} + \lambda)w_{j}^2] + \gamma{T}
\end{split}
\end{equation}
$$

where $I_{j} = \{i|q_{i}=j\}$ is the set of indices of data-points assign to the $j$-th leaf.

we could further compress the expression by defining $G_{j} = \sum_{i\in{I_{j}}}g_{i}, H_{j} = \sum_{i\in{I_j}}h_{i}$:

$$\mbox{obj}^{(t)} = \sum_{j=1}^{T}[G_{j}w_{j} + \frac{1}{2}(H_{j} + \lambda)w_{j}^2] + \gamma{T}$$

in this equation, $w_{j}$ are independent with respect to each other, the form $G_{j}w_{j} + \frac{1}{2}(H_{j} + \lambda)w_{j}^2$ is quadratic and the best $w_{j}$ for a given $q(x)$ and the best objective reduction we can get is:

$$w_{j}^{\ast} = -\frac{G_{j}}{H_{j} + \lambda}$$

$$\mbox{obj}^{\ast} = -\frac{1}{2}\sum_{i=1}^{T}\frac{G_{j}^2}{H_{j} + \lambda} + \gamma{T}$$

the last equation measures how good a tree structure $q(x)$ is.

### Learn the Tree Structure

now we have a way to measure how good a tree is, ideally we would enumerate all possible trees and pick the best one, but not practical.

instead we will try to optimize **one level** of the tree at a time, specifically we try to split a leaf into two leaves, and the score gain is:

$$Gain = \frac{1}{2}\left [\frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} - \frac{(G_L + G_R)^2}{H_L + H_R + \lambda}\right ] - \gamma$$

if the first part of $Gain$ is smaller than $\gamma$, we would do better not add that branch, this is exactly pruning!

for real valued data, we places all instances in sorted order(by the split feature), then a left to right scan is sufficient to calculate the structure score of all possible split solutions, and we can find the best split efficiently.

in practice, since it is intractable to enumerate all possible tree structures, we add one split at a time, this approach works well at most of the time.

### XGBoost Practice

"""quadratic dataset"""
import numpy as np
from sklearn.model_selection import train_test_split

np.random.seed(42)
X = np.random.rand(100, 1) - 0.5
y = 3*X[:, 0]**2 + 0.05 * np.random.randn(100)
X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=42)

"""basic xgboost"""
import xgboost
from sklearn.metrics import mean_squared_error

xgb_reg = xgboost.XGBRegressor()
xgb_reg.fit(X_train, y_train)
y_pred = xgb_reg.predict(X_val)
mean_squared_error(y_pred, y_val)

"""xgboost automatically taking care of early stopping"""
xgb_reg.fit(X_train, y_train, eval_set=[(X_val, y_val)], early_stopping_rounds=2)
y_pred = xgb_reg.predict(X_val)
mean_squared_error(y_pred, y_val)

## LightGBM

XGBoost uses level-wise tree growth:

![jupyter](images/level.png)

while LightGBM uses leaf-wise tree growth:

![jupyter](images/leaf.png)

we can formalize leaf-wise tree growth as:

$$(p_m, f_{m}, v_{m}) = \underset{(p,f,v)}{argmin}\ L(T_{m-1}(X).split(p, f, v), Y)$$

$$T_{m}(X) = T_{m-1}(X).split(p_m, f_{m}, v_{m})$$

finding the best split is costly, in XGBoost, we enumerate all features and all thresholds to find the best split.

LightGBM optimize that by using the histogram algorithm:


"""
Author : Achintya Gupta
Date Created : 22-09-2020
"""

"""
Problem Statement
------------------------------------------------
Q) A permutation is an ordered arrangement of objects. For example, 3124 is one possible permutation of the digits 1, 2, 3 and 4.
   If all of the permutations are listed numerically or alphabetically, we call it lexicographic order.
   The lexicographic permutations of 0, 1 and 2 are:
    012   021   102   120   201   210
   What is the millionth lexicographic permutation of the digits 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9?
"""
from utils import timing_decorator, get_factorial
import numpy as np

def recurr_loop(nums, nth, selected_nums = [], totald = 10):
    nums = sorted([k for k in nums if k not in selected_nums])
    total_digits = len(nums)
    blank_spaces = total_digits-1
    blank_combin = get_factorial(blank_spaces)
    if len(selected_nums) == totald-1:
        selected_nums = [str(k) for k in selected_nums+nums]
        return int("".join(selected_nums))
    numidx = int(np.floor(nth/blank_combin)) if nth%blank_combin != 0 else int(np.floor(nth/blank_combin))-1
    selected_digit = nums[numidx]
    selected_nums.append(selected_digit)
    return recurr_loop(nums, nth-blank_combin*numidx, selected_nums)

@timing_decorator
def nth_lexiperm(num_list=[0,1,2,4,3,5,6,7,8,9],N=1_000_000):
    perm_comb = recurr_loop(nums=num_list, nth=N)
    print(f'{N}`th Lexicographic permutation : ', perm_comb)

nth_lexiperm()


import numpy as np

def translate(point, vector, length=None):
    vector = np.array(vector)
    if length is not None:
        vector = length * vector / np.linalg.norm(vector)
    return (np.array(point) + vector).tolist()

def circle(center, perp_vect, radius, element_number=10):
    """
    Function computed the circle points. No drawing.
    perp_vect is vector perpendicular to plane of circle
    """
    # tl = [0, 0.2, 0.4, 0.6, 0.8]
    tl = np.linspace(0, 1, element_number)

    # vector form center to edge of circle
    # u is a unit vector from the centre of the circle to any point on the
    # circumference

    # normalized perpendicular vector
    n = perp_vect / np.linalg.norm(perp_vect)

    # normalized vector from the centre to point on the circumference
    u = perpendicular_vector(n)
    u = u / np.linalg.norm(u)

    pts = []

    for t in tl:
        # u = np.array([0, 1, 0])
        # n = np.array([1, 0, 0])
        pt = radius * np.cos(t * 2 * np.pi) * u +\
            radius * np.sin(t * 2 * np.pi) * np.cross(u, n) +\
            center

        pt = pt.tolist()
        pts.append(pt)

    return pts

def perpendicular_vector(v):
    r""" Finds an arbitrary perpendicular vector to *v*."""
    if v[1] == 0 and v[2] == 0:
        if v[0] == 0:
            raise ValueError('zero vector')
        else:
            return np.cross(v, [0, 1, 0])
    return np.cross(v, [1, 0, 0])

def cylinder_circles(nodeA, nodeB, radius, element_number=10):
    """
    Return list of two circles with defined parameters.
    """

    vector = (np.array(nodeA) - np.array(nodeB)).tolist()
    ptsA = circle(nodeA, vector, radius, element_number)
    ptsB = circle(nodeB, vector, radius, element_number)

    return ptsA, ptsB

def plane_fit(points):
    """
    p, n = plane_fit(points)

    Given an array, points, of shape (d,...)
    representing points in d-dimensional space,
    fit an d-dimensional plane to the points.
    Return a point, p, on the plane (the point-cloud centroid),
    and the normal, n.
    """
    import numpy as np
    from numpy.linalg import svd
    points = np.reshape(points, (np.shape(points)[0], -1)) # Collapse trialing dimensions
    assert points.shape[0] <= points.shape[1], "There are only {} points in {} dimensions.".format(points.shape[1], points.shape[0])
    ctr = points.mean(axis=1)
    x = points - ctr[:,np.newaxis]
    M = np.dot(x, x.T) # Could also use np.cov(x) here.
    return ctr, svd(M)[0][:,-1]


# %%
import numpy as np
import os
import matplotlib.pyplot as plt

plt.style.use("seaborn-whitegrid")

import tensorflow as tf

import tensorflow_probability as tfp

tfd = tfp.distributions
tfb = tfp.bijectors

# %%
# Coin flip example
# Use bernoulli samples
# Build beta distributions from conjugate priors

# Build Graph
rv_coin_flip_prior = tfp.distributions.Bernoulli(probs=0.5, dtype=tf.int32)

num_trials = tf.constant([0, 1, 2, 3, 4, 5, 8, 15, 50, 500, 1000, 2000])

coin_flip_data = rv_coin_flip_prior.sample(num_trials[-1])

# prepend a 0 onto tally of heads and tails, for zeroth flip
coin_flip_data = tf.pad(coin_flip_data, tf.constant([[1, 0]]), "CONSTANT")

# compute cumulative headcounts from 0 to 2000 flips, and then grab them at each of num_trials intervals
cumulative_headcounts = tf.gather(tf.cumsum(coin_flip_data), num_trials)

# Creates a series of beta distributions at each num_trials
rv_observed_heads = tfp.distributions.Beta(
    concentration1=tf.cast(1 + cumulative_headcounts, tf.float32),
    concentration0=tf.cast(1 + num_trials - cumulative_headcounts, tf.float32),
)

probs_of_heads = tf.linspace(start=0.0, stop=1.0, num=100, name="linspace")
observed_probs_heads = tf.transpose(
    rv_observed_heads.prob(probs_of_heads[:, tf.newaxis])
)

# %%
# For the already prepared, I'm using Binomial's conj. prior.
plt.figure(figsize=(16, 9))
for i in range(len(num_trials)):
    sx = plt.subplot(len(num_trials) / 2, 2, i + 1)
    plt.xlabel("$p$, probability of heads") if i in [
        0,
        len(num_trials) - 1,
    ] else None
    plt.setp(sx.get_yticklabels(), visible=False)
    plt.plot(
        probs_of_heads,
        observed_probs_heads[i],
        label="observe %d tosses,\n %d heads"
        % (num_trials[i], cumulative_headcounts[i]),
    )
    plt.fill_between(probs_of_heads, 0, observed_probs_heads[i], alpha=0.4)
    plt.vlines(0.5, 0, 4, color="k", linestyles="--", lw=1)
    leg = plt.legend()
    leg.get_frame().set_alpha(0.4)
    plt.autoscale(tight=True)

plt.suptitle(
    "Bayesian updating of posterior probabilities", y=1.02, fontsize=14
)
plt.tight_layout()

# %%

x = tf.range(start=0.0, limit=16.0, dtype=tf.float32)
lambdas = tf.constant([1.5, 4.25])

poi_pmf = tfd.Poisson(rate=lambdas[:, tf.newaxis]).prob(x)

# %%
temp = tfd.Poisson(rate=2.5)
tf.reduce_mean(temp.sample(1000))

temp = tfd.Exponential(rate=2.5)
print(tf.reduce_mean(temp.sample(1000)))
print(1 / temp.rate)

# %%
a = tf.range(start=0.0, limit=4.0, delta=0.04)
a = a[..., tf.newaxis]
lambdas = tf.constant([0.5, 1.0])

# Now we use TFP to compute probabilities in a vectorized manner.
expo_pdf = tfd.Exponential(rate=lambdas).prob(a)

# %%

# Defining our Data and assumptions
# fmt: off
count_data = tf.constant([
    13,  24,   8,  24,   7,  35,  14,  11,  15,  11,  22,  22,  11,  57,
    11,  19,  29,   6,  19,  12,  22,  12,  18,  72,  32,   9,   7,  13,
    19,  23,  27,  20,   6,  17,  13,  10,  14,   6,  16,  15,   7,   2,
    15,  15,  19,  70,  49,   7,  53,  22,  21,  31,  19,  11,  18,  20,
    12,  35,  17,  23,  17,   4,   2,  31,  30,  13,  27,   0,  39,  37,
    5,  14,  13,  22,
], dtype=tf.float32)
# fmt: on
n_count_data = tf.shape(count_data)
days = tf.range(n_count_data[0], dtype=tf.int32)

# %%
def joint_log_prob(count_data, lambda_1, lambda_2, tau):
    """
    Takes in data samples for count data and lambdas + tau
    Returns joint prob of all of those occurances, logged
    """
    # Estimate prior alpha as 1/average count
    alpha = 1.0 / tf.reduce_mean(count_data)
    rv_lambda_1 = tfd.Exponential(rate=alpha)
    rv_lambda_2 = tfd.Exponential(rate=alpha)

    rv_tau = tfd.Uniform()

    # Split days into different lambdas
    # Select lambda_1 for 0:tau-1, lambda_2 for tau:-1
    lambda_ = tf.gather(
        [lambda_1, lambda_2],
        indices=tf.cast(
            tau * tf.cast(tf.size(count_data), dtype=tf.float32)
            <= tf.cast(tf.range(tf.size(count_data)), dtype=tf.float32),
            dtype=tf.int32,
        ),
    )
    rv_observation = tfd.Poisson(rate=lambda_)

    return (
        rv_lambda_1.log_prob(lambda_1)
        + rv_lambda_2.log_prob(lambda_2)
        + rv_tau.log_prob(tau)
        + tf.reduce_sum(rv_observation.log_prob(count_data))
    )

# Define a closure over our joint_log_prob. - removes count_data as optimised parameter
def unnormalized_log_posterior(lambda1, lambda2, tau):
    return joint_log_prob(count_data, lambda1, lambda2, tau)

# %%
# wrap the mcmc sampling call in a @tf.function to speed it up
@tf.function(autograph=False)
def graph_sample_chain(*args, **kwargs):
    return tfp.mcmc.sample_chain(*args, **kwargs)

num_burnin_steps = 5000
num_results = 20000

# Set the chain's start state.
initial_chain_state = [
    tf.cast(tf.reduce_mean(count_data), tf.float32)
    * tf.ones([], dtype=tf.float32, name="init_lambda1"),
    tf.cast(tf.reduce_mean(count_data), tf.float32)
    * tf.ones([], dtype=tf.float32, name="init_lambda2"),
    0.5 * tf.ones([], dtype=tf.float32, name="init_tau"),
]

# Since HMC operates over unconstrained space, we need to transform the
# samples so they live in real-space.
unconstraining_bijectors = [
    tfp.bijectors.Exp(),  # Maps a positive real to R.
    tfp.bijectors.Exp(),  # Maps a positive real to R.
    tfp.bijectors.Sigmoid(),  # Maps [0,1] to R.
]

step_size = 0.2

kernel = tfp.mcmc.TransformedTransitionKernel(
    inner_kernel=tfp.mcmc.HamiltonianMonteCarlo(
        target_log_prob_fn=unnormalized_log_posterior,
        num_leapfrog_steps=2,
        step_size=step_size,
        state_gradients_are_stopped=True,
    ),
    bijector=unconstraining_bijectors,
)

kernel = tfp.mcmc.SimpleStepSizeAdaptation(
    inner_kernel=kernel, num_adaptation_steps=int(num_burnin_steps * 0.8)
)

# Sample from the chain.
(
    [lambda_1_samples, lambda_2_samples, posterior_tau],
    kernel_results,
) = graph_sample_chain(
    num_results=num_results,
    num_burnin_steps=num_burnin_steps,
    current_state=initial_chain_state,
    kernel=kernel,
)

tau_samples = tf.floor(
    posterior_tau * tf.cast(tf.size(count_data), dtype=tf.float32)
)

plt.hist(lambda_1_samples, bins=100)
plt.hist(posterior_tau)

# %%
w = 1.0 / tau_samples.shape[0] * np.ones_like(tau_samples)
plt.hist(
    tau_samples,
    bins=n_count_data[0],
    alpha=1,
    label=r"posterior of $\tau$",
    weights=w,
    rwidth=2.0,
)
plt.xticks(np.arange(n_count_data[0]))

plt.legend(loc="upper left")
plt.ylim([0, 0.75])
plt.xlim([35, len(count_data) - 20])
plt.xlabel(r"$\tau$ (in days)")
plt.ylabel(r"probability")

# %%
# Get samples of lambda1/2, tau for each day. Repeat 20k times. Use the posterior chains as samples
# Average lambda distribution per day and plot

# tau_samples, lambda_1_samples, lambda_2_samples contain
# N samples from the corresponding posterior distribution

N_ = tau_samples.shape[0]
expected_texts_per_day = tf.zeros(N_, n_count_data.shape[0])  # (10000,74)

plt.figure(figsize=(12.5, 9))

# Create matrix of days, copy over 20k samples
day_range = tf.range(0, n_count_data[0], delta=1, dtype=tf.int32)

# expand from shape of 74 to (10000,74)
day_range = tf.expand_dims(day_range, 0)
day_range = tf.tile(day_range, tf.constant([N_, 1]))

# Create multiple series of samples from tau, each day has 20k samples of tau
# expand from shape of 10000 to 10000,74
tau_samples_per_day = tf.expand_dims(tau_samples, 0)
tau_samples_per_day = tf.transpose(
    tf.tile(tau_samples_per_day, tf.constant([day_range.shape[1], 1]))
)

tau_samples_per_day = tf.cast(tau_samples_per_day, dtype=tf.int32)
# ix_day is (10000,74) tensor where axis=0 is number of samples, axis=1 is day. each value is true iff sampleXday value is < tau_sample value
ix_day = day_range < tau_samples_per_day

# Expand lambda samples over 20k samples, one sample pair per day
lambda_1_samples_per_day = tf.expand_dims(lambda_1_samples, 0)
lambda_1_samples_per_day = tf.transpose(
    tf.tile(lambda_1_samples_per_day, tf.constant([day_range.shape[1], 1]))
)
lambda_2_samples_per_day = tf.expand_dims(lambda_2_samples, 0)
lambda_2_samples_per_day = tf.transpose(
    tf.tile(lambda_2_samples_per_day, tf.constant([day_range.shape[1], 1]))
)

# Sum lambdas with binary on/off from tau
expected_texts_per_day = (
    tf.reduce_sum(
        lambda_1_samples_per_day * tf.cast(ix_day, dtype=tf.float32), axis=0
    )
    + tf.reduce_sum(
        lambda_2_samples_per_day * tf.cast(~ix_day, dtype=tf.float32), axis=0
    )
) / N_

# %% Add stats not just expectation
# My way
expected_texts_per_day_samples = tf.concat(
    [
        lambda_1_samples_per_day * tf.cast(ix_day, dtype=tf.float32),
        lambda_2_samples_per_day * tf.cast(~ix_day, dtype=tf.float32),
    ],
    axis=0,
)
# Removes 0s
expected_texts_per_day_samples
mask = tf.not_equal(expected_texts_per_day_samples, 0)
# different number of 0s per row
# need to transpose the mask and data and it iterates across rows
# reshape works along rows, so need long rows and then transpose back to full size
expected_texts_per_day_samples_mask = tf.transpose(
    tf.reshape(
        tf.boolean_mask(
            tf.transpose(expected_texts_per_day_samples), tf.transpose(mask)
        ),
        [-1, N_],
    )
)

# Find stats
expected_texts_per_day_mu = tf.reduce_mean(
    expected_texts_per_day_samples_mask, axis=0
)
expected_texts_per_day_std = tf.math.reduce_std(
    expected_texts_per_day_samples_mask, axis=0
)
expected_texts_per_day_std = tf.math.reduce
import numpy as np
expected_texts_per_day_quantiles = np.quantile(expected_texts_per_day_samples_mask.numpy(), [0.05, 0.95], axis=0)

assert (
    np.testing.assert_allclose(
        expected_texts_per_day.numpy(),
        expected_texts_per_day_mu.numpy(),
        rtol=1e-5,
    )
    is None
)

# %%
plt.figure(figsize=(12, 9))
plt.plot(
    range(n_count_data[0]),
    expected_texts_per_day_mu,
    lw=4,
    color="#E24A33",
    label="expected number of text-messages received",
)
if 0:
    plt.fill_between(
        range(n_count_data[0]),
        expected_texts_per_day_mu - expected_texts_per_day_std,
        expected_texts_per_day_mu + expected_texts_per_day_std,
        alpha=0.4,
    )
else:
    plt.fill_between(
        range(n_count_data[0]),
        expected_texts_per_day_quantiles[0],
        expected_texts_per_day_quantiles[1],
        alpha=0.4,
        color="#0000DA",
        label='ConfidenceInterval [0.05, 0.95]'
    )

plt.xlim(0, n_count_data.numpy()[0])
plt.xlabel("Day")
plt.ylabel("Expected # text-messages")
plt.title("Expected number of text-messages received")
plt.ylim(0, 60)
plt.bar(
    np.arange(len(count_data)),
    count_data,
    color="#5DA5DA",
    alpha=0.65,
    label="observed texts per day",
)

plt.legend(loc="upper left")

# %%


"""

Simple probability distribution model
==============================================

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

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

#%%

import numpy as np
import matplotlib.pyplot as plt
from UQpy.inference import DistributionModel, MLE
from UQpy.distributions import Normal
from UQpy.inference import MinimizeOptimizer

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

#%%

mu, sigma = 0, 0.1  # true mean and standard deviation
data_1 = np.random.normal(mu, sigma, 1000).reshape((-1, 1))
print('Shape of data vector: {}'.format(data_1.shape))

count, bins, ignored = plt.hist(data_1, 30, density=True)
plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)),
         linewidth=2, color='r')
plt.title('Histogram of the data')
plt.show()

#%% md
#
# Create an instance of the class Model. The user must define the number of parameters to be estimated, in this case 2
# (mean and standard deviation), and set those parameters to be learnt as None when instantiating the Distribution
# object. For maximum likelihood estimation, no prior pdf is required.

#%%

# set parameters to be learnt as None
dist = Normal(loc=None, scale=None)
candidate_model = DistributionModel(n_parameters=2, distributions=dist)

ml_estimator = MLE(inference_model=candidate_model, data=data_1, n_optimizations=3)
print('ML estimates of the mean={0:.3f} (true=0.) and std. dev={1:.3f} (true=0.1)'.format(
    ml_estimator.mle[0], ml_estimator.mle[1]))

#%% md
#
# We can also fix one of the parameters and learn the remaining one

#%%

d = Normal(loc=0., scale=None)
candidate_model = DistributionModel(n_parameters=1, distributions=d)

optimizer = MinimizeOptimizer(bounds=[[0.0001, 2.]])
ml_estimator = MLE(inference_model=candidate_model, data=data_1,
                   n_optimizations=1)
print('ML estimates of the std. dev={0:.3f} (true=0.1)'.format(ml_estimator.mle[0]))


'''
File: simplex_method.py
Author: Yutong Dai (rothdyt@gmail.com)
File Created: Saturday, 2018-12-15 15:17
Last Modified: Saturday, 2018-12-15 15:35
--------------------------------------------
Desscription:
'''
import numpy as np
from simplex_step import simplex_step
from simplex_init import simplex_init

def simplex_method(A, b, c, irule):
    """
    Find a basic optimal solution for the linear program

     min:    c*x
     ST:     Ax=b
             x>=0,

    where A is an (m,n) matrix.

    Input Parameters:

    A - (n,m) constraint matrix
    b - (m,1) POSITIVE vector appearing in the constraint equation above
    c - (1,n) vector giving the coefficients of the objective function

    irule - integer parameter speciying which pivot rule to use:
        irule = 0 indicates that the smallest coefficient rule should be
            used
        irule = 1 indicates that Bland's rule should be used

    Output Parameters:

    istatus - integer parameter reporting the condition of the
        istatus = 0  indicates normal completeion (i.e., a solution
        has been found and reported)
        istatus = 4  indicates the program is infeasible
        istatus = 16 indicates the program is feasible but our initialization
        procedure has failed
        istatus = 32 indicates that the program is unbounded

    X  - vector of length n specifying the solution
    eta - the minimum value of the objective function
    iB - integer vector specifying the m indices of the basic variables
        after the simplex step
    iN - integer vector specifying the n-m indices of the nonbasic
        variables after the simplex step
    xB - vector of length m specifying the values of the basic
        variables after the simplex step

    """
    #init_status, iB, iN, xB, tableau = simplex_init_modified(A, b, c)
    init_status, iB, iN, xB = simplex_init(A, b, c)
    if init_status == 4:
        istatus, X, eta, iB, iN, xB = 16, None, None, None, None, None
        return istatus, X, eta, iB, iN, xB
    elif init_status == 16:
        istatus, X, eta, iB, iN, xB = 4, None, None, None, None, None
        return istatus, X, eta, iB, iN, xB
    else:
        istatus_step = 1000
        count = 0
        while istatus_step != -1:
            istatus_step, iB, iN, xB, _ = simplex_step(A, b, c, iB, iN, xB, irule=0, Binv="")
            count += 1
            if istatus_step == 16 or count > 100:
                istatus, X, eta, iB, iN, xB = 32, None, None, None, None, None
                return istatus, X, eta, iB, iN, xB

        istatus = 0
        iB = iB - 1
        eta = np.matmul(c[0, iB].reshape(1, -2), xB)
        X = np.zeros((A.shape[1], 1))
        X[iB] = xB
        return istatus, X, eta, iB+1, iN, xB

if __name__ == "__main__":
    import numpy as np
    import random
    from simplex_method import simplex_method
    from simplex_test import simplex_test
    print("===============================")
    print("Test9.py")
    # first form an invertible matrix
    R = np.matrix([[4, 1,  1],
                   [-1, 2,  1],
                   [1, 1, -1]], dtype=np.float64)

    # form a vector b which is in the span of R
    b = R*np.matrix([[1],
                     [2],
                     [1]], dtype=np.float64)

    B = np.matrix([[1, 1, 1],
                   [1, 1, 0],
                   [1, 0, 0]], dtype=np.float64)
    A = np.hstack((R, B))

    # form a random permutation
    p = list(range(0, 6))
    random.shuffle(p)
    A = A[:, p]

    c = np.matrix([[-2, 1, 1, -1, -1, -1]], dtype=np.float64)
    c = c[:, p]

    # test
    irule = 0
    [istatus, X, eta, iB, iN, xB] = simplex_method(A, b, c, irule)
    print(X, eta, iB, iN, xB)
    # return

    if (istatus != 0):
        print('istatus is wrong\n')

    [X, eta, isfeasible, isoptimal, zN] = simplex_test(A, b, c, iB, xB)

    if (isfeasible != 1):
        print('your solution is not feasible!!!\n')

    if (isoptimal != 1):
        print('your solution is not optimal!!!\n')

    print("===============================")
    print("Test10.py")
    R = np.matrix([[4, 1,  1],
                   [1, 2,  1],
                   [1, 1,  1]], dtype=np.float64)

    # form a vector b which is in the span of R
    b = R*np.matrix([[1],
                     [-4],
                     [-1]], dtype=np.float64)

    B = np.matrix([[1, 1, 1],
                   [1, 1, 0],
                   [1, 0, 0]], dtype=np.float64)

    A = np.hstack((R, B))

    c = np.matrix([[-2, 1, 1, -1, -1, -1]], dtype=np.float64)

    irule = 1
    [istatus, X, eta, iB, iN, xB] = simplex_method(A, b, c, irule)

    if (istatus != 4):
        print('istatus is wrong\n')
    print("===============================")
    print("Test11.py")
    A1 = np.matrix([[-1,    1,     2],
                    [-1,    1,     1],
                    [0,    1,     1]], dtype=np.float64)

    A = np.hstack((np.eye(3), A1))

    b = np.matrix([[1],
                   [2],
                   [3]], dtype=np.float64)

    iB = [1, 2, 3]
    iN = [4, 5, 6]
    xB = np.matrix(np.copy(b))
    c = np.matrix([[0, 0, 0, -1, 2, 1]], dtype=np.float64)

    # form an invertible matrix B and modify the problem
    B = np.matrix([[4, 1, 0],
                   [1, -2, -1],
                   [1, 2, 4]], dtype=np.float64)
    A = B*A
    b = B*b

    # modify c
    N = A[:, [index_N-1 for index_N in iN]]
    c1 = np.matrix([[1, 1, 0]], dtype=np.float64)
    c2 = c[:, (4-1):6]+c1*B.I*N
    c = np.hstack((c1, c2))

    irule = 0
    [istatus, X, eta, iB, iN, xB] = simplex_method(A, b, c, irule)

    if (istatus != 32):
        print('istatus is wrong\n')


import numpy as np
import scipy
import scipy.linalg
import time

# Set grid sizes
N1 = 30
N2 = 12
N3 = 10
N4 = 4
N = N1*N2*N3*N4

# Fix random seed
#np.random.seed(seed=0)

col1 = np.linspace(0.0,1.0,N1)
A = scipy.linalg.toeplitz(col1)

col2 = np.linspace(0.0,1.0,N2)
B = scipy.linalg.toeplitz(col2)

col3 = np.linspace(0.0,1.0,N3)
C = scipy.linalg.toeplitz(col3)

col4 = np.linspace(0.0,1.0,N4)
D = scipy.linalg.toeplitz(col4)

start_time = time.time()
T = np.kron(np.kron(np.kron(A,B),C),D)
end_time = time.time()
print('\nDirect Formation Time:  {:.5f} s'.format(end_time-start_time))

def kron_product(A,B,y):
    A = A.astype(np.float64)
    B = B.astype(np.float64)
    y = y.astype(np.float64)
    input_dim = y.size
    output_dim = A.shape[0]*B.shape[0]
    vals = np.zeros([output_dim])

    A_dim_out = A.shape[0]
    A_dim_in = A.shape[1]
    B_dim_out = B.shape[0]
    B_dim_in = B.shape[1]

    y = np.reshape(y, [B_dim_in, -1], order='F')
    B_times_y = np.matmul(B,y)

    for i in range(0, A_dim_out):
        vals[i*B_dim_out : (i+1)*B_dim_out] = np.sum(A[i,:] * B_times_y[:,:], axis=1)
        #for j in range(0,A_dim_in):
        #    vals[i*B_dim_out : (i+1)*B_dim_out] += A[i,j] * B_times_y[:,j]
    return vals

###
###  REFERENCE: https://arxiv.org/pdf/1601.01507.pdf
###
def kron_vec_trick_2(A,B,y):
    A_dim_out = A.shape[0]
    B_dim_in = B.shape[1]
    y = np.reshape(y, [B_dim_in, A_dim_out], order='F')
    vals = np.matmul(B, np.matmul(y, A.transpose()))
    vals = np.reshape(vals, [-1], order='F')
    return vals

### Three Matrix Implementation
def kron_vec_trick_3(A,B,C,y):

    y = np.reshape(y, [-1, A.shape[0]], order='F')

    intermediate_list = []
    for i in range(0, y.shape[1]):
        intermediate_list.append(kron_vec_trick_2(B,C,y[:,i]))
    intermediate_list = np.array(intermediate_list).transpose()

    vals = np.matmul(intermediate_list, A.transpose())
    vals = np.reshape(vals, [-1], order='F')
    return vals

### Four Matrix Implementation
def kron_vec_trick_4(A,B,C,D,y):

    y = np.reshape(y, [-1, A.shape[0]], order='F')

    intermediate_list = []
    for i in range(0, y.shape[1]):
        intermediate_list.append(kron_vec_trick_3(B,C,D,y[:,i]))
    intermediate_list = np.array(intermediate_list).transpose()

    vals = np.matmul(intermediate_list, A.transpose())
    vals = np.reshape(vals, [-1], order='F')
    return vals

### General Implementation
def kron_vec_trick(matrix_list,y):

    # Remove first matrix from copy of original list
    matrix_list = matrix_list.copy()
    A = matrix_list.pop(0)

    if len(matrix_list) == 1:
        B = matrix_list.pop(0)
        y = np.reshape(y, [B.shape[1], A.shape[0]], order='F')
        vals = np.matmul(B, np.matmul(y, A.transpose()))
        vals = np.reshape(vals, [-1], order='F')
        return vals
    else:
        y = np.reshape(y, [-1, A.shape[0]], order='F')
        intermediate_list = []
        for i in range(0, y.shape[1]):
            intermediate_list.append(kron_vec_trick(matrix_list,y[:,i]))
        intermediate_list = np.array(intermediate_list).transpose()
        vals = np.matmul(intermediate_list, A.transpose())
        vals = np.reshape(vals, [-1], order='F')
        return vals

y = np.random.uniform(0.0,1.0,N)

start_time = time.time()
Ty = np.matmul(T,y)
end_time = time.time()
print('\nDirect Time:  {:.5f} s'.format(end_time-start_time))

start_time = time.time()
#Ty_approx = kron_vec_trick_3(A,B,C,y)
#Ty_approx = kron_vec_trick_4(A,B,C,D,y)
Ty_approx = kron_vec_trick([A,B,C,D],y)
end_time = time.time()
print('\nKernel Trick Time:  {:.5f} s \n'.format(end_time-start_time))

print(np.linalg.norm(Ty_approx-Ty))


import numpy as np
import numpy.linalg as npla

def pol2cart(pts: np.ndarray) -> np.ndarray:
  """Converts points from polar to cartesian.
  Args:
    pts (np.ndarray): points in polar coordinate [rho, theta, phi]
  Returns:
    np.ndarray: points in cartesian coordinate [x, y, z]
  """
  assert pts.shape[-2:] == (3, 1)
  x = np.cos(pts[..., 2:3, :]) * np.sin(pts[..., 1:2, :]) * pts[..., 0:1, :]
  y = np.sin(pts[..., 2:3, :]) * np.sin(pts[..., 1:2, :]) * pts[..., 0:1, :]
  z = np.cos(pts[..., 1:2, :]) * pts[..., 0:1, :]
  return np.concatenate((x, y, z), axis=-2)

def cart2pol(pts: np.ndarray) -> np.ndarray:
  """Converts points from cartesian to polar.
  Args:
    pts (np.ndarray): points in cartesian coordinate [x, y, z]
  Returns:
    np.ndarray: points in polar coordinate [rho, theta, phi]
  """
  assert pts.shape[-2:] == (3, 1)
  rho = npla.norm(pts, axis=-2, keepdims=True)
  phi = np.arctan2(pts[..., 1:2, :], pts[..., 0:1, :])
  theta = np.arctan2(np.sqrt(pts[..., 0:1, :]**2 + pts[..., 1:2, :]**2), pts[..., 2:3, :])
  return np.concatenate((rho, theta, phi), axis=-2)


from sympy import integer_nthroot
from math import log2, floor
from itertools import combinations

def subset_of(l):
    return sum(map(lambda r: list(combinations(l, r)), range(1, len(l)+1)), [])

class Pippenger:
    def __init__(self, group):
        self.G = group
        self.order = group.order
        self.lamb = group.order.bit_length()

    # Returns g^(2^j)
    def _pow2powof2(self, g, j):
        tmp = g
        for _ in range(j):
            tmp = self.G.square(tmp)
        return tmp

    # Returns Prod g_i ^ e_i
    def multiexp(self, gs, es):
        if len(gs) != len(es):
            raise Exception('Different number of group elements and exponents')

        es = [ei%self.G.order for ei in es]

        if len(gs) == 0:
            return self.G.unit

        lamb = self.lamb
        N = len(gs)
        s = integer_nthroot(lamb//N, 2)[0]+1
        t = integer_nthroot(lamb*N,2)[0]+1
        gs_bin = []
        for i in range(N):
            tmp = [gs[i]]
            for j in range(1,s):
                tmp.append(self.G.square(tmp[-1]))
            gs_bin.append(tmp)
        es_bin = []
        for i in range(N):
            tmp1 = []
            for j in range(s):
                tmp2 = []
                for k in range(t):
                    tmp2.append(int( bin(es[i])[2:].zfill(s*t)[-(j+s*k+1)]) )
                tmp1.append(tmp2)
            es_bin.append(tmp1)

        Gs = self._multiexp_bin(
                [gs_bin[i][j] for i in range(N) for j in range(s)],
                [es_bin[i][j] for i in range(N) for j in range(s)]
                )

        ans2 = Gs[-1]
        for k in range(len(Gs)-2,-1,-1):
            ans2 = self._pow2powof2(ans2, s)
            ans2 = self.G.mult(ans2, Gs[k])

        return ans2

    def _multiexp_bin(self, gs, es):
        assert len(gs) == len(es)
        M = len(gs)
        b = floor( log2(M) - log2(log2(M)) )
        b = b if b else 1
        subsets = [list(range(i,min(i+b,M))) for i in range(0,M,b)]
        Ts = [{sub: None for sub in subset_of(S)} for S in subsets]

        for T,S in zip(Ts, subsets):
            for i in S:
                T[(i,)] = gs[i]
            # Recursively set the subproducts in T
            def set_sub(sub):
                if T[sub] is None:
                    if T[sub[:-1]] is None:
                        set_sub(sub[:-1])
                    T[sub] = self.G.mult(T[sub[:-1]], gs[sub[-1]])
            for sub in T:
                set_sub(sub)

        Gs = []
        for k in range(len(es[0])):
            tmp = self.G.unit
            for T,S in zip(Ts, subsets):
                sub_es = [j for j in S if es[j][k]]
                sub_es = tuple(sub_es)
                if not sub_es:
                    continue
                tmp = self.G.mult(tmp, T[sub_es])
            Gs.append(tmp)

        return Gs


"""
Linear Regression
-----------------
Probabilistic Linear Regression with toy data in 1D.
"""
import numpy as np
import scipy.stats as st
import matplotlib.pyplot as plt

# Generate data
M = 20

X = np.linspace(0, 2, num=M)
# X = np.array([5, 14, 19], dtype=np.float)
t_real = np.sin(X)
t = t_real + np.random.randn(M) * 0.25

plt.scatter(X, t, label='Data points')

# Infer p(t|W,X,alpha) = N(t|XW+b,alpha); the predictive distribution
# MLE for W, b, and beta
W_ml = X.T @ t / (X.T @ X)  # Normal eq.
b_ml = np.mean(t) - W_ml * np.mean(X)

y = X * W_ml + b_ml

alpha_ml = np.mean((t - y)**2)

plt.plot(X, y, color='red', alpha=0.75, label='Regression line')

# Sample from predictive dist.
ys = np.random.normal(y, alpha_ml)

plt.scatter(X, ys, alpha=0.15, label='Posterior samples')
plt.legend(loc='best')
plt.show()


#!/usr/bin/env python

import numpy as np
import random

from q1_softmax import softmax
from q2_sigmoid import sigmoid, sigmoid_grad
from q2_gradcheck import gradcheck_naive

def forward_backward_prop(data, labels, params, dimensions):
    """
    Forward and backward propagation for a two-layer sigmoidal network

    Compute the forward propagation and for the cross entropy cost,
    and backward propagation for the gradients for all parameters.

    Arguments:
    data -- M x Dx matrix, where each row is a training example.
    labels -- M x Dy matrix, where each row is a one-hot vector.
    params -- Model parameters, these are unpacked for you.
    dimensions -- A tuple of input dimension, number of hidden units
                  and output dimension
    """

    ### Unpack network parameters (do not modify)
    ofs = 0
    Dx, H, Dy = (dimensions[0], dimensions[1], dimensions[2])

    W1 = np.reshape(params[ofs:ofs+ Dx * H], (Dx, H))
    ofs += Dx * H
    b1 = np.reshape(params[ofs:ofs + H], (1, H))
    ofs += H
    W2 = np.reshape(params[ofs:ofs + H * Dy], (H, Dy))
    ofs += H * Dy
    b2 = np.reshape(params[ofs:ofs + Dy], (1, Dy))

    ### YOUR CODE HERE: forward propagation
    l1 = sigmoid(np.dot(data, W1) + b1)
    l2 = softmax(np.dot(l1, W2) + b2)
    M, _ = data.shape
    cost = -np.sum(np.log(l2[labels == 1])) / M
    ### END YOUR CODE

    ### YOUR CODE HERE: backward propagation
    # gradient = y - y_head
    gradl2 = (l2 - labels) / M

    # input = gradl2, wight = l1.T
    gradW2 = np.dot(l1.T, gradl2)
    # bias is caused by input from the neuron by fixed activation 1
    gradb2 = np.sum(gradl2, axis=0, keepdims=True)

    # gradient = d(sigmoid(x)) / dx
    gradl1 = np.dot(gradl2, W2.T) * sigmoid_grad(l1)
    gradW1 = np.dot(data.T, gradl1)
    gradb1 = np.sum(gradl1, axis=0, keepdims=True)
    ### END YOUR CODE

    ### Stack gradients (do not modify)
    grad = np.concatenate((gradW1.flatten(), gradb1.flatten(),
        gradW2.flatten(), gradb2.flatten()))

    return cost, grad

def sanity_check():
    """
    Set up fake data and parameters for the neural network, and test using
    gradcheck.
    """
    print "Running sanity check..."

    N = 20
    dimensions = [10, 5, 10]
    data = np.random.randn(N, dimensions[0])   # each row will be a datum
    labels = np.zeros((N, dimensions[2]))
    for i in xrange(N):
        labels[i, random.randint(0,dimensions[2]-1)] = 1

    params = np.random.randn((dimensions[0] + 1) * dimensions[1] + (
        dimensions[1] + 1) * dimensions[2], )

    gradcheck_naive(lambda params:
        forward_backward_prop(data, labels, params, dimensions), params)

def your_sanity_checks():
    """
    Use this space add any additional sanity checks by running:
        python q2_neural.py
    This function will not be called by the autograder, nor will
    your additional tests be graded.
    """
    print "Running your sanity checks..."
    ### YOUR CODE HERE
    # raise NotImplementedError
    ### END YOUR CODE

if __name__ == "__main__":
    sanity_check()
    your_sanity_checks()


import casadi as cas
from ..casadi_helpers import sind, cosd

def solar_flux_outside_atmosphere_normal(day_of_year):
    """
    Normal solar flux at the top of the atmosphere (variation due to orbital eccentricity)
    :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31)
    :return: Solar flux [W/m^2]
    """
    # Space effects
    # # Source: https://www.itacanet.org/the-sun-as-a-source-of-energy/part-2-solar-energy-reaching-the-earths-surface/#2.1.-The-Solar-Constant
    # solar_flux_outside_atmosphere_normal = 1367 * (1 + 0.034 * cas.cos(2 * cas.pi * (day_of_year / 365.25)))  # W/m^2; variation due to orbital eccentricity
    # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-radiation-outside-the-earths-atmosphere
    return 1366 * (
            1 + 0.033 * cosd(360 * (day_of_year - 2) / 365))  # W/m^2; variation due to orbital eccentricity

def declination_angle(day_of_year):
    """
    Declination angle, in degrees, as a func. of day of year. (Seasonality)
    :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31)
    :return: Declination angle [deg]
    """
    # Declination (seasonality)
    # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/declination-angle
    return -23.45 * cosd(360 / 365 * (day_of_year + 10))  # in degrees

def solar_elevation_angle(latitude, day_of_year, time):
    """
    Elevation angle of the sun [degrees] for a local observer.
    :param latitude: Latitude [degrees]
    :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31)
    :param time: Time after local solar noon [seconds]
    :return: Solar elevation angle [degrees] (angle between horizon and sun). Returns 0 if the sun is below the horizon.
    """

    # Solar elevation angle (including seasonality, latitude, and time of day)
    # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/elevation-angle
    declination = declination_angle(day_of_year)

    solar_elevation_angle = cas.asin(
        sind(declination) * sind(latitude) +
        cosd(declination) * cosd(latitude) * cosd(time / 86400 * 360)
    ) * 180 / cas.pi  # in degrees
    solar_elevation_angle = cas.fmax(solar_elevation_angle, 0)
    return solar_elevation_angle

def incidence_angle_function(latitude, day_of_year, time, scattering=False):
    """
    What is the fraction of insolation that a horizontal surface will receive as a function of sun position in the sky?
    :param latitude: Latitude [degrees]
    :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31)
    :param time: Time since (local) solar noon [seconds]
    :param scattering: Boolean: include scattering effects at very low angles?
    """
    # Old description:
    # To first-order, this is true. In class, Kevin Uleck claimed that you have higher-than-cosine losses at extreme angles,
    # since you get reflection losses. However, an experiment by Sharma appears to not reproduce this finding, showing only a
    # 0.4-percentage-point drop in cell efficiency from 0 to 60 degrees. So, for now, we'll just say it's a cosine loss.
    # Sharma: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6611928/

    elevation_angle = solar_elevation_angle(latitude, day_of_year, time)

    theta = 90 - elevation_angle  # Angle between panel normal and the sun, in degrees
    cosine_factor = cosd(theta)

    if not scattering:
        return cosine_factor
    else:
        # Calculate scattering knockdown (See C:\Users\User\Google Drive\School\Grad School\2020 Spring\16-885\Solar Panel Scattering Rough Fit)
        theta_rad = theta * cas.pi / 180
        # Model 1
        c = (
            0.27891510500505767300438719757949,
            -0.015994330894744987481281839336589,
            -19.707332432605799255043166340329,
            -0.66260979582573353852126274432521
        )
        scattering_factor = c[0] + c[3] * theta_rad + cas.exp(
            c[1] * (
                    cas.tan(theta_rad - 1e-8) + c[2] * theta_rad
            )
        )
        # Model 2
        # c = (
        #     -0.04636,
        #     -0.3171
        # )
        # scattering_factor = cas.exp(
        #     c[0] * (
        #         cas.tan(theta_rad-1e-8) + c[1] * theta_rad
        #     )
        # )
        # Model 3
        # p1 = -21.74
        # p2 = 282.6
        # p3 = -1538
        # p4 = 1786
        # q1 = -923.2
        # q2 = 1456
        # x = theta_rad
        # scattering_factor = ((p1*x**3 + p2*x**2 + p3*x + p4) /
        #            (x**2 + q1*x + q2))

        # scattering_factor = cas.fmin(cas.fmax(scattering_factor, 0), 1)

        return cosine_factor * scattering_factor

def solar_flux_on_horizontal(latitude, day_of_year, time, scattering=False):
    """
    What is the solar flux on a horizontal surface for some given conditions?
    :param latitude: Latitude [degrees]
    :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31)
    :param time: Time since (local) solar noon [seconds]
    :param scattering: Boolean: include scattering effects at very low angles?
    :return:
    """
    return (
            solar_flux_outside_atmosphere_normal(day_of_year) *
            incidence_angle_function(latitude, day_of_year, time, scattering)
    )

if __name__ == "__main__":
    # Run some checks
    import plotly.graph_objects as go
    import numpy  as np

    latitudes = np.linspace(26, 49, 200)
    day_of_years = np.arange(0, 365) + 1
    times = np.linspace(0, 86400, 400)

    # Times, Latitudes = np.meshgrid(times, latitudes, indexing="ij")
    # fluxes = np.array(solar_flux_on_horizontal(Latitudes, 244, Times))
    # fig = go.Figure(
    #     data=[
    #         go.Surface(
    #             x=Times / 3600,
    #             y=Latitudes,
    #             z=fluxes,
    #         )
    #     ],
    # )
    # fig.update_layout(
    #     scene=dict(
    #         xaxis=dict(title="Time after Solar Noon [hours]"),
    #         yaxis=dict(title="Latitude [deg]"),
    #         zaxis=dict(title="Solar Flux [W/m^2]"),
    #         camera=dict(
    #             eye=dict(x=-1, y=-1, z=1)
    #         )
    #     ),
    #     title="Solar Flux on Horizontal",
    # )
    # fig.show()
    #
    # fig = go.Figure(
    #     data=[
    #         go.Contour(
    #             z=fluxes.T,
    #             x=times/3600,
    #             y=latitudes,
    #             colorbar=dict(
    #                 title="Solar Flux [W/m^2]"
    #             ),
    #             colorscale="Viridis",
    #         )
    #     ]
    # )
    # fig.update_layout(
    #     scene=dict(
    #         xaxis=dict(title="Hours after Solar Noon [hours]"),
    #         yaxis=dict(title="Latitude [deg]"),
    #     ),
    #     title="Solar Flux on Horizontal",
    #     xaxis_title="Time after Solar Noon [hours]",
    #     yaxis_title="Latitude [deg]",
    # )
    # fig.show()

    import matplotlib.pyplot as plt
    import matplotlib.style as style
    style.use("fivethirtyeight")
    # style.use("seaborn")
    # style.use("ggplot")

    plt.figure()
    lats_to_plot = [26, 49]
    lats_to_plot = np.linspace(0, 90, 7)
    colors = plt.cm.rainbow(np.linspace(0,1,len(lats_to_plot)))[::-1]
    [
        plt.plot(
            times/3600,
            solar_flux_on_horizontal(lats_to_plot[i], 244, times),
            label="%iN Latitude" % lats_to_plot[i],
            color=colors[i]
        ) for i in range(len(lats_to_plot))
    ]
    plt.grid(True)
    plt.legend()
    plt.title("Solar Flux on a Horizontal Surface (Aug. 31)")
    plt.xlabel("Time after Solar Noon [hours]")
    plt.ylabel(r"Solar Flux [W/m$^2$]")
    plt.tight_layout()
    plt.show()


# -*- coding: utf-8 -*-

import os, sys
import numpy as np

def build_cof_mat(num, poly_num=2):
    assert num > 2, "num should bigger than 2"
    cof_mat = np.zeros((num, num), dtype=np.float32)
    for i in range(0, num):
        for j in range(0, num):
            cof_mat[i, j] = np.power(np.absolute(i-j), poly_num)

    return cof_mat

def ordinal_mse(confusion_matrix, poly_num=2):
    category_num = confusion_matrix.shape[0]
    cof_mat = build_cof_mat(category_num, poly_num=poly_num)

    err_mat = np.multiply(confusion_matrix, cof_mat)
    mse_val = np.sum(err_mat) * 1.0 / np.sum(confusion_matrix)

    return mse_val


# tutorial on how to generate Poisson random numbers using the "direct method"
# if we have a sequence of random numbers E following an exp. distribution lam * exp(-x/lam)
# than the integer values where the cumulative sum \sum_{k=1}^n E_k > 1 follows a Poisson
# distribution (homogeneous Poisson point process)

import numpy as np
import matplotlib.pyplot as plt

from scipy.special import factorial

def poisson_generator(lam):
  exp_rand_sum     = 0
  poisson_rand_num = 0

  while True:
    # draw exponential random number with exponent 1/lambda
    exp_rand_num = np.random.exponential(scale = 1./lam, size = 1)[0]
    # add to the sum
    exp_rand_sum += exp_rand_num

    if exp_rand_sum <= 1:
      poisson_rand_num += 1
    else:
      break

  return poisson_rand_num

def poisson_distribution(lam, n):
  return np.exp(-lam)*(lam**n) / factorial(n)

#--------------------------------------------------------------------------------------
#--------------------------------------------------------------------------------------

np.random.seed(1)

# Poisson rate
lam  = 10.5
# number of poisson random variables to simulate
n = 100000

# generatate poisson random variables
poisson_random_variables = np.zeros(n, dtype = np.uint64)

for i in range(n):
  print(i, end = '\r')
  poisson_random_variables[i] = poisson_generator(lam)

#--------------------------------------------------------------------------------------

# plot the histogram
bin_edges   = np.arange(poisson_random_variables.max() + 2) - 0.5
bin_centers = 0.5*(bin_edges[:-1] + bin_edges[1:])

fig, ax = plt.subplots()
# plot the histogram of our poisson random variables
ax.hist(poisson_random_variables, bins = bin_edges, density = True)
# add a plot of the true Poisson distribution
ax.plot(bin_centers, poisson_distribution(lam, bin_centers), 'ro')

fig.tight_layout()
fig.show()


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

def train(X, T):
    # Standardize input data (X)

    # Calculate mean and std.
    means = X.mean(axis = 0)
    std = X.std(axis = 0)

    Xs = (X - means) / std

    # Tack a column of 1s
    Xs = np.insert(Xs, 0, 1, 1)

    # Use Xs to generate model (w)
    w = np.linalg.lstsq(Xs.T @ Xs, Xs.T @ T, rcond = None)[0]

    # Return as a dictionary
    dict = {'means': means, 'stds': std, 'w': w}
    return dict

def use(model, X):
    # Use model and input X to predict.
    means = model['means']
    std = model['stds']
    w = model['w']

    # Standardize X
    Xs = (X - means) / std

    # Tack column of 1s
    Xs = np.insert(Xs, 0, 1, 1)

    # Predict
    predict = Xs @ w
    #print(Xs.shape)
    #print(w.shape)
    return predict

def rmse(predict, T):
    rmerr = np.sqrt(np.mean((T - predict) ** 2))
    return rmerr

def trainSGD(X, T, learningRate, numberOfIterations):
    # Standardize inputs X.
    means = X.mean(axis = 0)
    std = X.std(axis = 0)
    Xs = (X - means) / std

    nSamples = Xs.shape[0]
    ncolsT = T.shape[1]

    # Tack a column of 1s
    Xs1 = np.insert(Xs, 0, 1, 1)
    ncolsX = Xs1.shape[1]
    # Initialize weights to zero.
    w = np.zeros((ncolsX, ncolsT))

    for i in range(numberOfIterations):
        for n in range(nSamples):
            predicted = Xs1[n:n+1, :] @ w
            w += learningRate * Xs1[n:n+1, :].T * (T[n:n+1, :] - predicted)
            #print(w)
    dict = {'means': means, 'stds': std, 'w': w}
    return dict

# Load the csv data.
dframe=pd.read_csv('Data\\energydata_complete.csv', sep=',',header=None)
# Filter out required columns.
dframe = dframe.drop(dframe.columns[[0, -2, -1]], axis=1)
# Input data columns
Xlabels = []
Xl = dframe.iloc[0, 2:]
for i in Xl:
	Xlabels.append(i)
# Target data columns
Tlabels = []
Tl = dframe.iloc[0, :2]
for i in Tl:
	Tlabels.append(i)

# Get target.
Td = dframe.iloc[1:, [0,1]]
Td = Td.as_matrix()
Tenergy = Td.astype(float)

# Get input.
Xd = dframe.iloc[1:, 2:]
Xd = Xd.as_matrix()
Xenergy = Xd.astype(float)

# Split into training (80 %) and testing data (20 %).
nRows = Xenergy.shape[0]
nTrain = int(round(0.8*nRows))
nTest = nRows - nTrain

# Shuffle row numbers
rows = np.arange(nRows)
np.random.shuffle(rows)

trainIndices = rows[:nTrain]
testIndices = rows[nTrain:]

Xtrain = Xenergy[trainIndices, :]
Ttrain = Tenergy[trainIndices, :]
Xtest = Xenergy[testIndices, :]
Ttest = Tenergy[testIndices, :]
'''
# Use functions to predict.
# 1. Using first function.
# Train
model = train(Xtrain, Ttrain)
# Use on test data.
predict = use(model, Xtest)
# Calculate RMSE
rmerr = rmse(predict, Ttest)

print("The RMSE with first function: ")
print(rmerr)
'''
# 2. Using the second function.
model2 = trainSGD(Xtrain, Ttrain, 0.01, 10)
predict2 = use(model2, Xtest)
rmerr2 = rmse(predict2, Ttest)

#print(model2)
#print("The RMSE with the second function: ")
print(predict2[:,0])


import numpy as np
from typing import Optional

class LRScheduler:

    def __init__(self, initial_lr: float, total_steps: int):
        self.initial_lr = initial_lr
        self.total_steps = total_steps

    def anneal_lr(self, cur_step: int):
        """Implemented by subclass"""
        pass

    def cur_lr(self, cur_step):
        lr = self.anneal_lr(cur_step)
        return lr

class CosineLRScheduler(LRScheduler):

    def __init__(self, initial_lr: float, total_steps: int):
        super().__init__(initial_lr, total_steps)

    def anneal_lr(self, cur_step: int, min_to_decay_to: float = 0.0):
        lr = 0.5 * self.initial_lr * (1 + np.cos(np.pi * cur_step / self.total_steps))
        lr = np.max([lr, min_to_decay_to])
        return lr

class StepDecay(LRScheduler):

    def __init__(self, initial_lr: float, total_steps: Optional[int] = None, decay_rate: float = 0.5, decay_after_n_steps: int = 5, min_lr: float = 1e-7):
        super().__init__(initial_lr, total_steps)
        assert decay_rate is not None and decay_after_n_steps is not None
        self.decay_rate = decay_rate
        self.decay_after_n_steps = decay_after_n_steps
        self.min_lr = min_lr

    def anneal_lr(self, cur_step: int, decay_rate: Optional[float] = None, decay_after_n_steps: Optional[int] = None):
        if decay_after_n_steps is None:
            decay_after_n_steps = self.decay_after_n_steps
        if decay_rate is None:
            decay_rate = self.decay_rate
        m = cur_step // decay_after_n_steps
        lr = self.initial_lr * (decay_rate ** m)
        lr = self.min_lr if lr < self.min_lr else lr
        return lr

supported_learning_rate_schedulers = {"cosine_anneal": CosineLRScheduler, "fixed": None, "constant": None,
                                      "step": StepDecay, "step_decay": StepDecay}


import argparse
import contextlib
import time

import cupy
import numpy

# This sample computes call and put prices for Europian options with
# Black-Scholes equation. It was based on a sample of the financial package
# in CUDA toolkit. For details, please see the corresponding whitepaper.
#
# The following code shows that CuPy enables us to write algorithms for GPUs
# without significantly modifying the existing NumPy's code.
# It also briefly describes how to create your own kernel with CuPy.
# If you want to speed up the existing code, please define the kernel
# with cupy.ElementwiseKernel.

# Naive implementation of the pricing of options with NumPy and CuPy.
def black_scholes(xp, s, x, t, r, v):
    sqrt_t = xp.sqrt(t)
    d1 = (xp.log(s / x) + (r + v * v / 2) * t) / (v * sqrt_t)
    d2 = d1 - v * sqrt_t

    def get_cumulative_normal_distribution(x):
        A1 = 0.31938153
        A2 = -0.356563782
        A3 = 1.781477937
        A4 = -1.821255978
        A5 = 1.330274429
        RSQRT2PI = 0.39894228040143267793994605993438
        W = 0.2316419

        k = 1 / (1 + W * xp.abs(x))
        cnd = RSQRT2PI * xp.exp(-x * x / 2) * \
            (k * (A1 + k * (A2 + k * (A3 + k * (A4 + k * A5)))))
        cnd = xp.where(x > 0, 1 - cnd, cnd)
        return cnd

    cnd_d1 = get_cumulative_normal_distribution(d1)
    cnd_d2 = get_cumulative_normal_distribution(d2)

    exp_rt = xp.exp(- r * t)
    call = s * cnd_d1 - x * exp_rt * cnd_d2
    put = x * exp_rt * (1 - cnd_d2) - s * (1 - cnd_d1)
    return call, put

# An example of calling the kernel via cupy.ElementwiseKernel.
# When executing __call__ method of the instance, it automatically compiles
# the code depending on the types of the given arrays, and calls the kernel.
# Other functions used inside the kernel can be defined by 'preamble' option.
black_scholes_kernel = cupy.ElementwiseKernel(
    'T s, T x, T t, T r, T v',  # Inputs
    'T call, T put',  # Outputs
    '''
    const T sqrt_t = sqrt(t);
    const T d1 = (log(s / x) + (r + v * v / 2) * t) / (v * sqrt_t);
    const T d2 = d1 - v * sqrt_t;

    const T cnd_d1 = get_cumulative_normal_distribution(d1);
    const T cnd_d2 = get_cumulative_normal_distribution(d2);

    const T exp_rt = exp(- r * t);
    call = s * cnd_d1 - x * exp_rt * cnd_d2;
    put = x * exp_rt * (1 - cnd_d2) - s * (1 - cnd_d1);
    ''',
    'black_scholes_kernel',
    preamble='''
    __device__
    inline T get_cumulative_normal_distribution(T x) {
        const T A1 = 0.31938153;
        const T A2 = -0.356563782;
        const T A3 = 1.781477937;
        const T A4 = -1.821255978;
        const T A5 = 1.330274429;
        const T RSQRT2PI = 0.39894228040143267793994605993438;
        const T W = 0.2316419;

        const T k = 1 / (1 + W * abs(x));
        T cnd = RSQRT2PI * exp(- x * x / 2) *
            (k * (A1 + k * (A2 + k * (A3 + k * (A4 + k * A5)))));
        if (x > 0) {
            cnd = 1 - cnd;
        }
        return cnd;
    }
    ''',
)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--gpu-id', '-g', default=0, type=int, help='GPU ID')
    parser.add_argument('--n-options', '-n', default=10000000, type=int)
    args = parser.parse_args()

    cupy.cuda.Device(args.gpu_id).use()

    def rand_range(m, M):
        samples = cupy.random.rand(args.n_options)
        return (m + (M - m) * samples).astype(numpy.float64)

    print('initializing...')
    stock_price_gpu = rand_range(5, 30)
    option_strike_gpu = rand_range(1, 100)
    option_years_gpu = rand_range(0.25, 10)

    stock_price_cpu = stock_price_gpu.get()
    option_strike_cpu = option_strike_gpu.get()
    option_years_cpu = option_years_gpu.get()

    @contextlib.contextmanager
    def timer(message):
        cupy.cuda.Stream.null.synchronize()
        start = time.time()
        yield
        cupy.cuda.Stream.null.synchronize()
        end = time.time()
        print('%s:\t%f sec' % (message, end - start))

    print('start computation')
    risk_free = 0.02
    volatility = 0.3
    with timer(' CPU (NumPy, Naive implementation)'):
        call_cpu, put_cpu = black_scholes(
            numpy, stock_price_cpu, option_strike_cpu, option_years_cpu,
            risk_free, volatility)

    with timer(' GPU (CuPy, Naive implementation)'):
        call_gpu1, put_gpu1 = black_scholes(
            cupy, stock_price_gpu, option_strike_gpu, option_years_gpu,
            risk_free, volatility)

    with timer(' GPU (CuPy, Elementwise kernel)'):
        call_gpu2, put_gpu2 = black_scholes_kernel(
            stock_price_gpu, option_strike_gpu, option_years_gpu,
            risk_free, volatility)

    # Check whether all elements in gpu arrays are equal to those of cpus
    cupy.testing.assert_allclose(call_cpu, call_gpu1)
    cupy.testing.assert_allclose(call_cpu, call_gpu2)
    cupy.testing.assert_allclose(put_cpu, put_gpu1)
    cupy.testing.assert_allclose(put_cpu, put_gpu2)


from math import sin, pi
import numpy as np
from sympy import Symbol, integrate
# function for the trapezoidal rule
def Simpson(a,b,f,n):
   h = (b-a)/float(n)
   sum = f(a)/float(2);
   for i in range(1,n):
       sum = sum + f(a+i*h)*(3+(-1)**(i+1))
   sum = sum + f(b)/float(2)
   return sum*h/3.0
# function to integrate
def function(x):
    return sin(2*pi*x)
# define integration limits and integration points
a = 0.0; b = 0.5;
n = 100
Exact = 1./pi
print("Relative error= ", abs( (Simpson(a,b,function,n)-Exact)/Exact))


from collections import Counter
import numpy as np

def entropy(predictions, normalize=False, n_categories=None):
    counts = Counter(list(predictions)).values()

    if len(counts) <= 1:
        return 0.0

    total_count = float(len(predictions))

    if not total_count:
        return 0.0

    probabilities = []
    for count in counts:
        probabilities.append(count / total_count)

    probabilities = np.array(probabilities)
    value = - np.sum(probabilities * np.log(probabilities))
    if normalize is True:
        if n_categories is not None:
            value /= float(np.log(n_categories))
        else:
            value /= float(np.log(len(counts)))
    return value


from sympy import *

def rotX(a):
    return Matrix([[1, 0, 0, 0],
		[0, cos(a), -sin(a), 0],
		[0, sin(a), cos(a), 0],
		[0, 0, 0, 1]])
def rotY(a):
    return Matrix([[cos(a), 0, sin(a), 0],
		[0, 1, 0, 0],
		[-sin(a), 0, cos(a), 0],
		[0, 0, 0, 1]])
def rotZ(a):
    return Matrix([[cos(a), -sin(a), 0, 0],
		[sin(a), cos(a), 0, 0],
		[0, 0, 1, 0],
		[0, 0, 0, 1]])
def transXYZ(x, y, z):
    return Matrix([[1, 0, 0, x],
		[0, 1, 0, y],
		[0, 0, 1, z],
		[0, 0, 0, 1]])

def matrix44FromTaitBryan(px, py, pz, om, fi, ka):
    return transXYZ(px, py, pz) * rotX(om) * rotY(fi) * rotZ(ka)

def matrix44FromTaitBryanZYX(px, py, pz, om, fi, ka):
    return transXYZ(px, py, pz) * rotZ(ka) * rotY(fi) * rotX(om)

def taitBryanFromMatrix44Case1(m):
    omfika = Matrix([0, 0, 0])
    omfika[0] = atan2(-m[1,2], m[2,2])#om
    omfika[1] = asin(m[0,2])          #fi
    omfika[2] = atan2(-m[0,1], m[0,0])#ka
    return omfika

def taitBryanFromMatrix44Case2(m):
    omfika = Matrix([0, 0, 0])
    omfika[0] = -atan2(m[1,0], m[1,1])#om
    omfika[1] = -pi / 2.0             #fi
    omfika[2] = 0                     #ka
    return omfika

def taitBryanFromMatrix44Case3(m):
    omfika = Matrix([0, 0, 0])
    omfika[0] = atan2(m[1,0], m[1,1]) #om
    omfika[1] = pi / 2.0              #fi
    omfika[2] = 0                     #ka
    return omfika


# !usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def identity_function(x):
    return x

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T

    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))

def mean_squared_error(y, t):
    return 0.5 * np.sum((y - t) ** 2)

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    if t.size == y.size:
        t = t.argmax(axis=1)

    batch_size = y.shape[0]

    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size


def hpd(data, level = .95) :
  """ The Highest Posterior Density (credible) interval of data at level level.

  :param data: sequence of real values
  :param level: (0 < level < 1)
  """

  d = list(data)
  d.sort()

  nData = len(data)
  nIn = int(round(level * nData))
  if nIn < 2 :
    raise RuntimeError("not enough data")

  i = 0
  r = d[i+nIn-1] - d[i]
  for k in range(len(d) - (nIn - 1)) :
    rk = d[k+nIn-1] - d[k]
    if rk < r :
      r = rk
      i = k

  assert 0 <= i <= i+nIn-1 < len(d)

  return (d[i], d[i+(nIn/2)-1],d[i+nIn-1])

import numpy as np
def HDI_from_MCMC(posterior_samples, credible_mass):
  # Computes highest density interval from a sample of representative values,
  # estimated as the shortest credible interval
  # Takes Arguments posterior_samples (samples from posterior) and credible mass (normally .95)
  sorted_points = sorted(posterior_samples)
  ciIdxInc = np.ceil(credible_mass * len(sorted_points)).astype('int')
  nCIs = len(sorted_points) - ciIdxInc
  ciWidth = [0]*nCIs
  for i in range(0, nCIs): ciWidth[i] = sorted_points[i + ciIdxInc] - sorted_points[i]
  HDImin = sorted_points[ciWidth.index(min(ciWidth))]
  HDImax = sorted_points[ciWidth.index(min(ciWidth))+ciIdxInc]
  return(HDImin, HDImax)


# Lab 08, Question 04
# AB Satyaprakash, 180123062

# imports
import numpy as np
import pandas as pd
from math import exp
import matplotlib.pyplot as plt

# functions

def P(x):
    return -10

def Q(x):
    return 0

def R(x):
    return 0

def y(x):
    return (exp(10*x)-1)/(exp(10)-1)

def genCentralMatrix():
    L = []
    xi = a+h
    n = int((b-a)/h) - 1
    for i in range(1, n+1):
        L1 = [0]*n
        if i-2 >= 0:
            L1[i-2] = 1 - h*P(xi)/2
        L1[i-1] = -2 + h*h*Q(xi)
        if i < n:
            L1[i] = 1+h*P(xi)/2
        L.append(L1)
        xi += h
    return np.array(L)

def genCentralVector():
    F = []
    xi = a+h
    n = int((b-a)/h) - 1
    for i in range(1, n+1):
        f = h*h*R(xi)
        if i == 1:
            f -= alpha*(1 - h*P(xi)/2)
        if i == n:
            f -= beta*(1 + h*P(xi)/2)
        F.append(f)
        xi += h
    return np.array(F)

def genBackwardMatrix():
    L = []
    xi = a+h
    n = int((b-a)/h) - 1
    for i in range(1, n+1):
        L1 = [0]*n
        if i-2 >= 0:
            L1[i-2] = 1 - h*P(xi)
        L1[i-1] = -2 + h*h*Q(xi) + h*P(xi)
        if i < n:
            L1[i] = 1
        L.append(L1)
        xi += h
    return np.array(L)

def genBackwardVector():
    F = []
    xi = a+h
    n = int((b-a)/h) - 1
    for i in range(1, n+1):
        f = h*h*R(xi)
        if i == 1:
            f -= alpha*(1 - h*P(xi))
        if i == n:
            f -= beta
        F.append(f)
        xi += h
    return np.array(F)

def genForwardMatrix():
    L = []
    xi = a+h
    n = int((b-a)/h) - 1
    for i in range(1, n+1):
        L1 = [0]*n
        if i-2 >= 0:
            L1[i-2] = 1
        L1[i-1] = -2 + h*h*Q(xi) - h*P(xi)
        if i < n:
            L1[i] = 1 + h*P(xi)
        L.append(L1)
        xi += h
    return np.array(L)

def genForwardVector():
    F = []
    xi = a+h
    n = int((b-a)/h) - 1
    for i in range(1, n+1):
        f = h*h*R(xi)
        if i == 1:
            f -= alpha
        if i == n:
            f -= beta*(1 + h*P(xi))
        F.append(f)
        xi += h
    return np.array(F)

# program body
# BVP : y" - 10y' = 0. Thus P = -10, Q = 0, R = 0
a, b = 0, 1
alpha, beta = 0, 1
h = 1/4

L = genCentralMatrix()
F = genCentralVector()
U = np.linalg.inv(L).dot(F)
Y_c = [alpha]
for u in U:
    Y_c.append(u)
Y_c.append(beta)
Y1 = [y(a+i*h) for i in range(len(Y_c))]
df = pd.DataFrame()
df['Nodal points'] = pd.Series(['0', '1/4', '2/4', '3/4', '1'])
df['Actual'] = pd.Series(Y1)
df['Central Approximation'] = pd.Series(Y_c)
df['Central Error'] = pd.Series(abs(np.array(Y1)-np.array(Y_c)))

L = genBackwardMatrix()
F = genBackwardVector()
U = np.linalg.inv(L).dot(F)
Y_b = [alpha]
for u in U:
    Y_b.append(u)
Y_b.append(beta)
df['Backward Approximation'] = pd.Series(Y_b)
df['Backward Error'] = pd.Series(abs(np.array(Y1)-np.array(Y_b)))

L = genForwardMatrix()
F = genForwardVector()
U = np.linalg.inv(L).dot(F)
Y_f = [alpha]
for u in U:
    Y_f.append(u)
Y_f.append(beta)
df['Forward Approximation'] = pd.Series(Y_f)
df['Forward Error'] = pd.Series(abs(np.array(Y1)-np.array(Y_f)))
print()
print(df)
print()

NodalPoints = [0, 1/4, 2/4, 3/4, 1]
plt.plot(NodalPoints, df['Central Error'], label='Central Scheme')
plt.plot(NodalPoints, df['Backward Error'], label='Backward Scheme')
plt.plot(NodalPoints, df['Forward Error'], label='Forward Scheme')
plt.xlabel('Nodal Points')
plt.ylabel('Error Magnitude')
plt.title('Errors using different schemes for dy/dx')
plt.legend()
plt.show()

Semester 6/MA 322 (Scientific Computing Theory)/Labs/Lab 8/Code/q4.py


from numpy import random,zeros_like

def multinomial(N,probabilities):
    '''
    works like  numpy.random.multinomial!

    You can draw from a multinomial using the following:
    1) get the probabilities for each state.
    That is a list with probabilities, [p1,p2,...,pn], each coresponding to an outcome

    2) calculate a list containing the cumulative probabilities (c=[p1, p1+p2, p1+p2+p3, ...,1])

    3) draw a random number, r, form 0 to 1

    4) if r<=c[i], return i.
    '''

    cumulatives=[]
    _cm=0
    for i in probabilities:
        _cm+=i
        cumulatives.append(_cm)

    state=zeros_like(probabilities)

    for i in range(N):
        _r=random.rand()
        for i,_p in enumerate(cumulatives):
            if _r<=_p:
                state[i]+=1
                break

    return state

if __name__=='__main__':
    '''Test run of the function'''
    _N=500
    _initial_p=[0.1,0.1,0.5,0.2,0.01,0.09]

    _m= multinomial(_N,_initial_p)

    print _initial_p,sum(_initial_p)

    _produced_p=[i/float(_N) for i in _m]
    print _produced_p, sum(_produced_p)

    print _m


#!/usr/bin/env python

# From: https://stackoverflow.com/a/13224592/1558655
# by doug

def PCA(data, dims_rescaled_data=2):
    """
    returns: data transformed in 2 dims/columns + regenerated original data
    pass in: data as 2D NumPy array
    """
    import numpy as NP
    from scipy import linalg as LA
    m, n = data.shape
    # mean center the data
    data -= data.mean(axis=0)
    # calculate the covariance matrix
    R = NP.cov(data, rowvar=False)
    # calculate eigenvectors & eigenvalues of the covariance matrix
    # use 'eigh' rather than 'eig' since R is symmetric,
    # the performance gain is substantial
    evals, evecs = LA.eigh(R)
    # sort eigenvalue in decreasing order
    idx = NP.argsort(evals)[::-1]
    evecs = evecs[:,idx]
    # sort eigenvectors according to same index
    evals = evals[idx]
    # select the first n eigenvectors (n is desired dimension
    # of rescaled data array, or dims_rescaled_data)
    evecs = evecs[:, :dims_rescaled_data]
    # carry out the transformation on the data using eigenvectors
    # and return the re-scaled data, eigenvalues, and eigenvectors
    return NP.dot(evecs.T, data.T).T, evals, evecs

def test_PCA(data, dims_rescaled_data=2):
    '''
    test by attempting to recover original data array from
    the eigenvectors of its covariance matrix & comparing that
    'recovered' array with the original data
    '''
    _ , _ , eigenvectors = PCA(data, dim_rescaled_data=2)
    data_recovered = NP.dot(eigenvectors, m).T
    data_recovered += data_recovered.mean(axis=0)
    assert NP.allclose(data, data_recovered)

def plot_pca(data):
    from matplotlib import pyplot as MPL
    clr1 =  '#2026B2'
    fig = MPL.figure()
    ax1 = fig.add_subplot(111)
    data_resc, evals, evecs = PCA(data)
    ax1.plot(data_resc[:, 0], data_resc[:, 1], '.', mfc=clr1, mec=clr1)
    MPL.show()


import numpy as np
# from math import gamma
import matplotlib.pyplot as plt
from scipy.special import gamma
from scipy import stats
import numpy as np
from scipy.optimize import minimize
import matplotlib.pyplot as plt
from scipy.stats import beta

x = np.arange (0.01, 1, 0.01)
y = beta.pdf(x,1,1)
plt.plot(x,y, label="alpha=1, beta=1")
y = beta.pdf(x,5,5)
plt.plot(x,y, label="alpha=5, beta=5")
y = beta.pdf(x,10,10)
plt.plot(x,y, label="alpha=10, beta=10")
plt.title("Beta Distributions")
plt.legend()

from scipy.stats import beta
x = np.arange (0.01, 1, 0.01)
y = beta.pdf(x,1,2)
plt.plot(x,y, label="alpha=1, beta=2")
y = beta.pdf(x,5,5)
plt.plot(x,y, label="alpha=5, beta=5")
y = beta.pdf(x,10,11)
plt.plot(x,y, label="alpha=10, beta=11")
plt.title("Beta Distributions")
plt.legend()


import numpy as np

def rosenbrock(X):
	x = X[0]
	y = X[1]
	f = ((1-x)**2 + 100*(y-x**2)**2)
	return f

def kenny(X):
	x=X[0]
	y=X[1]
	F = np.sin(x+y) +(x-y)**2-(1.5*x)+(2.5*y)+1
	return F


import jax.numpy as jnp
import numpy as np

class KalmanFilter:
    def __init__(self, A, B, Q, C, R):
        self.params = dict()
        self._init_params(A, B, Q, C, R)

    def _init_params(self, A, B, Q, C, R):
        self.params["A"] = A # process matrix
        self.params["B"] = B # control matrix
        self.params["Q"] = Q # symetric postiive definite process noise covariance
        self.params["C"] = C # measurement matrix
        self.params["R"] = R # symetric postiive definite measurement noise covariance

    def dynamic(self, x, u):
        return jnp.add(jnp.add(jnp.dot(self.params["A"], x), jnp.dot(self.params["B"], u)), jnp.dot(jnp.linalg.cholesky(self.params["Q"]),np.random.randn(len(x))))

    def observation(self, x):
        return jnp.add(jnp.dot(self.params["C"],x), jnp.dot(jnp.linalg.cholesky(self.params["R"]),np.random.randn(len(x))))

    def prediction(self, s, u):
        mu, sigma = s # unroll states
        mu_hat = jnp.add(jnp.dot(self.params["A"], mu), jnp.dot(self.params["B"], u))
        sigma_hat = jnp.add(jnp.dot(jnp.dot(self.params["A"], sigma), jnp.transpose(self.params["A"])), self.params["Q"])
        return (mu_hat, sigma_hat)

    def correction(self, s, y):
        mu, sigma = s # unroll states
        v = jnp.subtract(y, jnp.dot(self.params["C"], mu)) # innovation
        S = jnp.add(jnp.dot(jnp.dot(self.params["C"], sigma), jnp.transpose(self.params["C"])), self.params["R"]) # innovation covariance
        K = jnp.dot(jnp.dot(sigma, jnp.transpose(self.params["C"])), jnp.linalg.inv(S)) # Kalman gain
        new_mu = jnp.add(mu, jnp.dot(K, v)) # update state estimation
        new_sigma = jnp.dot(jnp.subtract(jnp.eye(len(mu)), jnp.dot(K, self.params["C"])), sigma) # update state covariance estimation
        return (new_mu, new_sigma)

    def update(self, s, u, y):
        mu, sigma = s
        s = self.prediction(s, u)
        s = self.correction(s, y)
        return s

    def mse_loss(self, s_hat, s_post):
        mu_hat, sigma_hat = s_hat
        mu_post, sigma_post = s_post
        # ex = jnp.linalg.norm(jnp.subtract(mu, jnp.add(jnp.dot(self.params["A"], mu), jnp.dot(self.params["B"], u))))
        # ey = jnp.linalg.norm(jnp.subtract(y, jnp.dot(self.params["C"], mu)))
        return jnp.linalg.norm(mu_hat - mu_post)


import numpy as np

def mean_squared_error(y_hat, y):
    return np.mean(np.power(y_hat - y, 2))

def d_mean_squared_error(y_hat, y):
    return 2 * (y - y_hat) / np.size(y_hat)


import numpy as np
import matplotlib.pyplot as plt

plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = (12, 8)

# 特征分解
# https://robot.czxy.com/docs/pcl/chapter03/point_cloud_math/
# 特征分解(旋转矩阵和特征向量矩阵相关，缩放矩阵与特征值矩阵相关)
sigma = np.array([
    [19 / 16, -15 * np.sqrt(3) / 16,],
    [-15 * np.sqrt(3) / 16, 49 / 16]
])

eVa, eVe = np.linalg.eig(sigma)
print("eigen value:\n", eVa)
print("eigen vector:\n", eVe)

# plt.scatter(Y[:, 0], Y[:, 1])
for value, eigen in zip(eVa, eVe.T):
    plt.plot(
        [0, 3 * np.sqrt(value) * eigen[0]],
        [0, 3 * np.sqrt(value) * eigen[1]],
        lw=5)

plt.title('Transformed Data')
plt.axis('equal')
plt.show()

"""
一个多元正态分布的概率密度函数中，其协方差矩阵 Σ 包含了分布的旋转角度和缩放信息，其均值向量
→
μ
 包含了密度中心的位置信息。
"""


import numpy as np

class Perceptron:
    def __init__(self, N, alpha=0.1):
        # Initialise the weight matrix and store the learning rate
        self.W = np.random.randn(N + 1) / np.sqrt(N)
        self.alpha = alpha

    def step(self, x):
        # Apply the step function
        return 1 if x > 0 else 0

    def fit(self, X, y, epochs=10):
        # Insert a column of 1's as the last entry of the feature matrix
        # This allows us the treat the bias as a trainable parameter with the weight matrix
        X = np.c_[X, np.ones((X.shape[0]))]

        # Loop over the desired number of epochs
        for epoch in np.arange(0, epochs):
            # Loop over each individual data point
            for (x, target) in zip(X, y):
                # Pass the dot product of the input features and weight matrix through the step function
                pred = self.step(np.dot(x, self.W))

                # Only performs a weight update if our prediction does not match the target
                if pred != target:
                    # Calculate the error
                    error = pred - target

                    # Update the weight matrix
                    self.W += -self.alpha * error * x

    def predict(self, X, add_bias=True):
        # Ensure our input is a matrix
        X = np.atleast_2d(X)

        # Check to see if the bias column should be added
        if add_bias:
            # Insert a column of 1's as the last entry in the feature matrix
            X = np.c_[X, np.ones((X.shape[0]))]

        # Pass the dot product of the input features and weight matrix through the step function
        return self.step(np.dot(X, self.W))


import tensorflow as tf
import numpy as np

#100 phony y, x data points are created in NumPy, y = 0.3 + x * 0.1
x_data = np.random.rand(100).astype(np.float32)
y_data = x_data * 0.1 + 0.3

# calculate y_data = b + W * x_data, by finding the values for b and W
# Knowing that b should be 0.3 and W 0.1, but it will be figured out by  the tensorflow
W = tf.Variable(tf.random_uniform([1], -1.0, 1.0))
b = tf.Variable(tf.zeros([1]))
y = W * x_data + b

# Minimize the mean squared errors.
loss = tf.reduce_mean(tf.square(y - y_data))
optimizer = tf.train.GradientDescentOptimizer(0.5)
train = optimizer.minimize(loss)

# Before starting, initialize the variables.  We will 'run' this first.
init = tf.initialize_all_variables()

# Launch the graph.
sess = tf.Session()
sess.run(init)

# Fit the line.
for step in xrange(201):
    sess.run(train)
    if step % 20 == 0:
        print(step, sess.run(W), sess.run(b))

# Learns best fit is W: [0.1], b: [0.


from sklearn.metrics import pairwise_distances
import numpy as np
from scipy.linalg import eigh
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import warnings
import networkx as nx

class LE:

    def __init__(self, X:np.ndarray, dim:int, k:int = 2, eps = None, graph:str = 'k-nearest', weights:str = 'heat kernel',
                 sigma:float = 0.1, laplacian:str = 'unnormalized', opt_eps_jumps:float = 1.5):
        """
        LE object
        Parameters
        ----------

        X: nxd matrix

        dim: number of coordinates

        k: number of neighbours. Only used if graph = 'k-nearest'

        eps: epsilon hyperparameter. Only used if graph = 'eps'.
        If is set to None, then epsilon is computed to be the
        minimum one which guarantees G to be connected

        graph: if set to 'k-nearest', two points are neighbours
        if one is the k nearest point of the other.
        If set to 'eps', two points are neighbours if their
        distance is less than epsilon

        weights: if set to 'heat kernel', the similarity between
        two points is computed using the heat kernel approach.
        If set to 'simple', the weight between two points is 1
        if they are connected and 0 otherwise. If set to 'rbf'
        the similarity between two points is computed using the
        gaussian kernel approach.

        sigma: coefficient for gaussian kernel or heat kernel

        laplacian: if set to 'unnormalized', eigenvectors are
        obtained by solving the generalized eigenvalue problem
        Ly = λDy where L is the unnormalized laplacian matrix.
        If set to 'random', eigenvectors are obtained by decomposing
        the Random Walk Normalized Laplacian matrix. If set to
        'symmetrized', eigenvectors are obtained by decomposing
        the Symmetrized Normalized Laplacian

        opt_eps_jumps: increasing factor for epsilon
        """

        self.X = X
        self.dim = dim
        self.k = k
        self.eps = eps
        if graph not in ['k-nearest', 'eps']:
            raise ValueError("graph is expected to be a graph name; 'eps' or 'k-nearest', got {} instead".format(graph))
        self.graph = graph
        if weights not in ['simple', 'heat kernel', 'rbf']:
            raise ValueError("weights is expected to be a weight name; 'simple' or 'heat kernel', got {} instead".format(weights))
        self.weights = weights
        self.sigma = sigma
        self.n = self.X.shape[0]
        if laplacian not in ['unnormalized', 'random', 'symmetrized']:
            raise ValueError("laplacian is expected to be a laplacian name; 'unnormalized', 'random' or 'symmetrized', got {} instead".format(laplacian))
        self.laplacian = laplacian
        self.opt_eps_jumps = opt_eps_jumps
        if self.eps is None and self.graph == 'eps':
            self.__optimum_epsilon()

    def __optimum_epsilon(self):
        """
        Compute epsilon

        To chose the minimum epsilon which guarantees G to be
        connected, first, epsilon is set to be equal to the distance
        from observation i = 0 to its nearest neighbour. Then
        we check if the Graph is connected, if it's not, epsilon
        is increased and the process is repeated until the Graph
        is connected
        """
        dist_matrix = pairwise_distances(self.X)
        self.eps = min(dist_matrix[0,1:])
        con = False
        while not con:
            self.eps = self.opt_eps_jumps * self.eps
            self.__construct_nearest_graph()
            con = self.cc == 1
            print('[INFO] Epsilon: {}'.format(self.eps))
        self.eps = np.round(self.eps, 3)

    def __heat_kernel(self, dist):
        """
        k(x, y) = exp(- ||x-y|| / sigma )
        """
        return np.exp(- (dist*dist)/self.sigma)

    def __rbf(self, dist):
        """
        k(x, y) = exp(- (1/2*sigma^2) * ||x-y||^2)
        """
        return np.exp(- dist**2/ (2* (self.sigma**2) ) )

    def __simple(self, *args):
        return 1

    def __construct_nearest_graph(self):
        """
        Compute weighted graph G
        """
        similarities_dic = {'heat kernel': self.__heat_kernel,
                            'simple':self.__simple,
                            'rbf':self.__rbf}

        dist_matrix = pairwise_distances(self.X)
        if self.graph == 'k-nearest':
            nn_matrix = np.argsort(dist_matrix, axis = 1)[:, 1 : self.k + 1]
        elif self.graph == 'eps':
            nn_matrix = np.array([ [index for index, d in enumerate(dist_matrix[i,:]) if d < self.eps and index != i] for i in range(self.n) ])
        # Weight matrix
        self._W = []
        for i in range(self.n):
            w_aux = np.zeros((1, self.n))
            similarities = np.array([ similarities_dic[self.weights](dist_matrix[i,v]) for v in nn_matrix[i]] )
            np.put(w_aux, nn_matrix[i], similarities)
            self._W.append(w_aux[0])
        self._W = np.array(self._W)
        # D matrix
        self._D = np.diag(self._W.sum(axis=1))
        # Check for connectivity
        self._G = self._W.copy() # Adjacency matrix
        self._G[self._G > 0] = 1
        G = nx.from_numpy_matrix(self._G)
        self.cc = nx.number_connected_components(G) # Multiplicity of lambda = 0
        if self.cc != 1:
            warnings.warn("Graph is not fully connected, Laplacian Eigenmaps may not work as expected")

    def __compute_unnormalized_laplacian(self):
        self.__construct_nearest_graph()
        self._L = self._D - self._W
        return self._L

    def __compute_normalized_random_laplacian(self):
        self.__construct_nearest_graph()
        self._Lr = np.eye(*self._W.shape) - (np.diag(1/self._D.diagonal())@self._W)
        return self._Lr

    def __compute_normalized_symmetrized_laplacian(self):
        self.__construct_nearest_graph()
        self.__compute_unnormalized_laplacian()
        d_tilde = np.diag(1/np.sqrt(self._D.diagonal()))
        self._Ls = d_tilde @ ( self._L @ d_tilde )
        return self._Ls

    def transform(self):
        """
        Compute embedding
        """

        m_options = {
            'unnormalized':self.__compute_unnormalized_laplacian,
            'random':self.__compute_normalized_random_laplacian,
            'symmetrized':self.__compute_normalized_symmetrized_laplacian
        }

        L = m_options[self.laplacian]()

        if self.laplacian == 'unnormalized':
            eigval, eigvec = eigh(L, self._D) # Generalized eigenvalue problem
        else:
            eigval, eigvec = np.linalg.eig(L)

        order = np.argsort(eigval)
        self.Y = eigvec[:, order[self.cc:self.cc+self.dim + 1]]

        return self.Y

    def plot_embedding_2d(self, colors, grid = True, dim_1 = 1, dim_2 = 2, cmap = None, size = (15, 10)):
        if self.dim < 2 and dim_2 <= self.dim and dim_1 <= self.dim:
            raise ValueError("There's not enough coordinates")

        plt.style.use('seaborn-whitegrid')
        fig = plt.figure(figsize=size)
        plt.axhline(c = 'black', alpha = 0.2)
        plt.axvline(c = 'black', alpha = 0.2)
        if cmap is None:
            plt.scatter(self.Y[:, dim_1 - 1], self.Y[:, dim_2 - 1], c = colors)

        plt.scatter(self.Y[:, dim_1 - 1], self.Y[:, dim_2 - 1], c = colors, cmap=cmap)
        plt.grid(grid)
        if self.graph == 'k-nearest':
            title = 'LE with k = {} and weights = {}'.format(self.k, self.weights)
        else:
            title = 'LE with $\epsilon$ = {} and weights = {}'.format(self.eps, self.weights)
        plt.title(title)
        plt.xlabel('Coordinate {}'.format(dim_1))
        plt.ylabel('Coordinate {}'.format(dim_2))
        plt.show()

    def plot_embedding_3d(self, colors, grid = True, dim_1 = 1, dim_2 = 2, dim_3 = 3, cmap = None, size = (15, 10)):
        if self.dim < 3 and dim_2 <= self.dim and dim_1 <= self.dim and dim_3 <= self.dim:
            raise ValueError("There's not enough coordinates")

        plt.style.use('seaborn-whitegrid')
        fig = plt.figure(figsize=size)
        ax = fig.add_subplot(111, projection="3d")
        if cmap is None:
            ax.scatter(self.Y[:, dim_1 - 1], self.Y[:, dim_2 - 1], self.Y[:, dim_3 - 1], c = colors)
        ax.scatter(self.Y[:, dim_1 - 1], self.Y[:, dim_2 - 1], self.Y[:, dim_3 - 1], c = colors, cmap = cmap)
        plt.grid(grid)
        ax.axis('on')
        if self.graph == 'k-nearest':
            title = 'LE with k = {} and weights = {}'.format(self.k, self.weights)
        else:
            title = 'LE with $\epsilon$ = {} and weights = {}'.format(self.eps, self.weights)
        plt.title(title)
        ax.set_xlabel('Coordinate {}'.format(dim_1))
        ax.set_ylabel('Coordinate {}'.format(dim_2))
        ax.set_zlabel('Coordinate {}'.format(dim_3))
        plt.show()


# -*- coding: utf-8 -*-

import scipy as sp
import numpy as np
from numpy.polynomial import polynomial as P
import math
import scipy.integrate as integrate
import scipy.special as special

def rk0( f, a, b, n ):
     if(n==1):
          resultado = (f(b) + f(a))*(b-a)/2
     else:
     	  # Calculamos la separación entre nodos
          h = (b-a)/n
          # Calculamos los nodos para interpolar
          x=[ a+h*i for i in range(n+1)]
          fx = 0
          for i in range(n-1):
          	fx = fx + f(x[i+1])
          resultado = ( h *( ( f(x[0]) + f(x[n]) )/2 + fx ) )
     return resultado

def intPuntoMedio(f,a,b):
    return f((a+b)/2)*(b-a)

def rk( Rk, Rk1, j ):
     return Rk + (1/(4**j-1))*(Rk - Rk1)

#Rk = R_{k,j-1} RK1 = R_{k-1,j-1}
def metodoRomberg(f, a, b, k):
		resultados=[]
		resultados.append( rk0( f, a, b, 1 ) )
		for i in range(k):
			resultados.append( rk0( f, a, b, 2**(i+1) ) )
		for i in range(k):
			for j in range(k-i):
		    		resultados[j]=rk( resultados[j+1], resultados[j], i+1 )
		return resultados[0]

#Apartado c)
f = lambda x: 3*x/(x**2 - 4)
a=1
b=1.8
n = 0
diferencia = 1
integral = integrate.quad(f,a,b)
trapecio = rk0(f,a,b,1)
simpson = rk0(f,a,b,2)
medio = intPuntoMedio(f,a,b)

print("Función 3x/(x^2-4)\n")
print ("Iteracion\t |\t R{n-1},{n-1}\t|\t R{n},{n} \t| \t Diferencia")
while diferencia >= 10**(-6):
    Rk = metodoRomberg(f, a, b, n)
    Rk1 = metodoRomberg(f, a, b, n+1)
    diferencia = abs(Rk-Rk1)
    print( format(n) + "\t\t | \t"+ str( Rk)+ "\t|\t"+ str(Rk1)+ "\t|\t" + str(diferencia))
    n = n+1
print("Su integral real es:",integral)
print("Su valor por el trapecio es:", trapecio)
print("Su valor por simpson es:", simpson)
print("Su valor por el punto medio es:", medio)

# Apartado d)
f = lambda x: math.cos(x)*math.exp(3*x)
a=0
b=math.pi/4
n = 0
diferencia = 1
integral = integrate.quad(f,a,b)
trapecio = rk0(f,a,b,1)
simpson = rk0(f,a,b,2)
medio = intPuntoMedio(f,a,b)

print("Función cos(x)*e^(3x)\n")
print ("Iteracion\t |\t R{n-1},{n-1}\t|\t R{n},{n} \t| \t Diferencia")
while diferencia >= 10**(-6):
    Rk = metodoRomberg(f, a, b, n)
    Rk1 = metodoRomberg(f, a, b, n+1)
    diferencia = abs(Rk-Rk1)
    print( format(n) + "\t\t | \t"+ str( Rk)+ "\t|\t"+ str(Rk1)+ "\t|\t" + str(diferencia))
    n = n+1
print("Su integral real es:",integral)
print("Su valor por el trapecio es:", trapecio)
print("Su valor por simpson es:", simpson)
print("Su valor por el punto medio es:",medio)


"""
RSI CALCULATION

The very first calculations for average gain and average loss are simple n-period averages:

    First Average Gain = Sum of Gains over the past n periods / n.
    First Average Loss = Sum of Losses over the past n periods / n

The second, and subsequent, calculations are based on the prior averages and the current gain loss:

    Average Gain = [(previous Average Gain) x (n-1) + current Gain] / n.
    Average Loss = [(previous Average Loss) x (n-1) + current Loss] / n.

RS = Average Gain / Average Loss
RSI = 100 - (100 / (1 + RS))

"""

import numpy as np

def rsi(ohlcv, period=14, ohlcv_series="close"):
    _ohlcv = ohlcv[[ohlcv_series]].copy(deep=True)
    _ohlcv["diff"] = _ohlcv[ohlcv_series].diff(periods=1)
    _ohlcv["diff_up"] = np.where(_ohlcv["diff"] >= 0, _ohlcv["diff"], 0)
    _ohlcv["diff_down"] = np.where(_ohlcv["diff"] < 0, _ohlcv["diff"], 0)
    # Calculate Average Gain and Average Loss
    _ohlcv[["rsi_u", "rsi_d"]] = _ohlcv[["diff_up", "diff_down"]].ewm(alpha=1 / period, min_periods=period).mean()
    _ohlcv["rs"] = abs(_ohlcv["rsi_u"]) / abs(_ohlcv["rsi_d"])
    indicator_values = 100 - (100 / (1 + _ohlcv["rs"]))

    return indicator_values

def ADD_RSIs(ohlcv, periods=[10,20,30], ohlcv_series="close"):
  for p in periods:
    indicator_name = f'rsi_{p}_{ohlcv_series[0]}'
    ohlcv[indicator_name] = rsi(ohlcv, p, ohlcv_series)
  return ohlcv


import random
import matplotlib.pyplot as plt
from scipy.stats import norm

n = 100
rho = .8
num_sims = 10000
beta = rho**0.5
alpha = (1 -rho)**0.5
prob_default = 0.1
tranche_cutoffs = [0, 0.2, 1]
# tranche_cutoffs = [0, 0.03, 0.07, 0.1, 0.15, 0.30, 1 ]
tranche_to_watch = 2 #(1 to num tranches)

# run simulation
z_default = norm.ppf(prob_default) # quantile function
max_defaults_protection = n * tranche_cutoffs[tranche_to_watch - 1]
wiped_out = n * tranche_cutoffs[tranche_to_watch ]
print (max_defaults_protection, wiped_out)

# histogram = [0] * (n+1)
trial_results = []
tranche_values = []
for _ in range(num_sims):
    M = random.gauss(0, 1)
    K = 0 # number of variables that are > 0
    for _ in range(n):
        R_i = beta * M + alpha * random.gauss(0, 1)
        if R_i < z_default:
            K += 1
    # histogram[K] += 1
    trial_results.append(K)
    tranch_value = wiped_out - min(wiped_out, max(0, K - max_defaults_protection))
    tranche_values.append(tranch_value)

# print(histogram)
print('Mean tranche value = ' + str(sum(tranche_values)/ num_sims))
plt.hist(tranche_values, bins=n+1, normed=0, align='mid')
plt.title('Distribution of Tranche Value in ' + str(num_sims) + ' trials')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.show()