import numpy as np
import pandas as pd

def sound_speed(T, gamma=1.4, Rg=287):
    a = np.sqrt(T * gamma * Rg)
    return a

def gas_ideal_density(T, P, Rg=287):
    rho = P / (Rg * T)
    return rho

def total_entalphy(h, U):
    ho = h + U**2 / 2
    return ho

def total_temperature(Ma, T, gamma):
    To = (1 + (gamma - 1) * Ma**2 / 2) * T
    return To

def total_pressure(Ma, P, gamma):
    Po = ((1 + (gamma - 1) * Ma**2 / 2)**(gamma / (gamma - 1))) * P
    return Po

def total_density(Ma, rho, gamma):
    rho_o = ((1 + (gamma - 1) * Ma ** 2 / 2) ** (1 / (gamma - 1))) * rho
    return rho_o

def downstream_prop(ps_rel, **kwargs):  # ps_rel stands for post shock relations
    gas_dict = kwargs.copy()
    for k in gas_dict.keys():
        if k == 'Pressure': gas_dict[k].append(gas_dict[k][0] * ps_rel.p21)
        if k == 'Temperature': gas_dict[k].append(gas_dict[k][0] * ps_rel.T21)
        if k == 'Density': gas_dict[k].append(gas_dict[k][0] * ps_rel.rho21)
        if k == 'Total_Pressure': gas_dict[k].append(gas_dict[k][0] * ps_rel.Po21)
        if k == 'Speed': gas_dict[k].append(gas_dict[k][0] * ps_rel.U21)
        gas_df = pd.DataFrame.from_dict(gas_dict, orient='index')
    return gas_df


# -*- coding:utf-8 -*-
"""
Project   : numpy
File Name : 5_19_to_5_20
Author    : Focus
Date      : 8/21/2021 3:13 PM
Keywords  :
Abstract  :
Param     :
Usage     : py 5_19_to_5_20
Reference :
"""
import sys
import numpy as np
from matplotlib.pyplot import show
from matplotlib.pyplot import plot

t = np.linspace(-np.pi, np.pi, 201)
k = np.arange(1, float(sys.argv[1]))
f = np.zeros_like(t)

"""
Generate sawtooth wave.
"""
for i in range(len(t)):
    f[i] = np.sum(np.sin(2 * np.pi * k * t[i]) / k) * (-2 / np.pi)

plot(t, f, lw=1.0)

"""
Generate triangle wave.
"""
for i in range(len(t)):
    f[i] = np.sum(np.sin(2 * np.pi * k * t[i]) / k)
    f[i] = np.abs(f[i] * (-2 / np.pi))

plot(t, f, lw=2.0)
show()


import numpy as np

def horizontal_avg(field, yidx=slice(None), xidx=slice(None)):
    """
    Do a horizontal average

    Parameters
    ----------
    field : array,  2D or 3D
         the field that you want to average

    xidx, yidx : slice, optional
         ranging over the interior points, excluding the halo
         default is all points

    Returns
    -------
    res : float or 1D array
       the horizontal average

    """
    if len(field.shape) == 2:
        res = np.mean(field[yidx, xidx])
    elif len(field.shape) == 3:
        res = np.mean(np.mean(field[:, yidx, xidx], axis=2), axis=1)
    else:
        raise ValueError("field should be a 2D or 3D array")
    return res

def azimuthal_avg(field, x, y, dx, dy):

    # todo: make the function accepts 3D or 4D array for field
    assert len(field.shape) == 2
    assert len(x.shape) == 2
    assert len(y.shape) == 2

    d = np.sqrt(x**2+y**2)

    xmax = np.max(x)
    ymax = np.max(y)
    rmax = min([xmax, ymax])

    dr = min(dx, dy)
    nr = int(rmax/dr+0.5)

    r = np.arange(nr)*dr
    # mid ring radial distance
    rm = 0.5*(r[1:]+r[:-1])

    res = np.zeros(nr-1)
    # average over rings
    for k in range(nr-1):
        res[k] = np.mean(field[(d >= r[k]) & (d < r[k+1])])

    return rm, res

def test_azimuthal_avg():
    nx, ny = 201, 201
    x = np.linspace(-1,1,nx)
    y = np.linspace(-1,1,ny)

    xx, yy = np.meshgrid(x, y)

    r = np.sqrt(xx**2+yy**2)

    f = lambda x: np.sin(x*5)

    phi = f(r)
    dx = x[1]-x[0]
    dy = y[1]-y[0]
    rm, phiavg = azimuthal_avg(phi, xx, yy, dx, dy)

    plt.clf()
    plt.plot(rm, phiavg, '+', label='computed')
    plt.plot(rm, f(rm), label='exact')
    plt.legend()
    plt.xlabel('r')

if __name__ == '__main__':

    import matplotlib.pyplot as plt

    plt.ion()

    test_azimuthal_avg()


# So we can access the utility files:
import sys
sys.path.append('../')

import numpy as np

# The main library:
from least_squares import least_squares, chi2, rmse

# The test:
def test_least_squares():
    # First, we can test in the base case where it's a polynomial of the form
    x = np.linspace(0, 10, 1000)
    y = x**2

    # Due to all of the multiplication going on, the constants might not
    # properly zero out.  This is ok because we know that they should be zero.
    coefficients = np.round(least_squares(y, x), decimals = 10)

    # Coefficients are ordered by increasing power
    assert(coefficients[0] == 0)
    assert(coefficients[1] == 0)
    assert(coefficients[2] == 1)

    # Then, chi2.  This should be less than 1e-10 since we rounded by that decimal.

    f = coefficients[0] + coefficients[1]*x + coefficients[2]*x**2

    # We introduce neglible STD so that we know that our results are consistent.
    assert(chi2(y, f, 1) < 1e-10)

    # RMSE as well.  But this should be sqrt(1e-10/1000)
    assert(rmse(y, f, 1) < np.sqrt(1e-10/1000))

    # We can also do the linear case for the equation y = 5x + 10
    y = 5*x + 10

    coefficients = np.round(least_squares(y, x), decimals = 10)

    # This test is good because it shows that if you overshoot with the degree
    # least-squares will still return the lesser polynomial.
    # Coefficients are ordered by increasing power
    assert(coefficients[0] == 10)
    assert(coefficients[1] == 5)
    assert(coefficients[2] == 0)

    # And if we perturb the data a bit we can show that the error increases.
    x_p = x + np.random.choice(np.linspace(-0.1, 0.1, 1000))

    coefficients_p = np.round(least_squares(y, x_p), decimals = 10)

    f = coefficients[0] + coefficients[1]*x + coefficients[2]*x**2
    f_p = coefficients_p[0] + coefficients_p[1]*x + coefficients_p[2]*x**2

    assert(chi2(y, f, 1) < chi2(y, f_p, 1))

    # We don't have to worry about the case where
    # x_0 = x_1 = x_2 = ... x_P because
    # Vandermonde matrices force away singularity.


# Import libraries
import matplotlib.pyplot as plt
import numpy as np

# Set custom color codes
color_codes = ['#1f77b4','#ff7f0e','#2ca02c',
               '#d62728','#9467bd','#8c564b','#e377c2',
               '#7f7f7f','#bcbd22','#17becf']

# Createing vectors X and Y
x = np.linspace(-3, 3, 100)

#define functions to plot
def y1(x):
    return x

def y2(x):
    return x**2

def y3_1(x):
    return np.where(x <= 1.5, x**2, np.nan)

def y3_2(x):
    return np.where(x > 1.5, x, np.nan)

# Define the figure options
fig, (ax1, ax2, ax3) = plt.subplots(1, 3)
fig = plt.figure(figsize = (12, 7))

# Add a title
plt.title('Some functions that tends to a limit')

# first plot
# Create the plot
ax1.plot(x, y1(x), alpha = 0.8, label ='y = x',
         color = color_codes[1],
         linewidth = 2,
         markersize = 5)

# Setting plot limits
ax1.axis(xmin=-2,xmax=2)

# Add X and y Label
ax1.set_xlabel('x')
ax1.set_ylabel('y')

# Add a grid
ax1.grid(alpha =.6, linestyle ='--')

# Add hvlines
ax1.axhline(y=1.5, color="gray", linestyle ='--')
ax1.axvline(x=1.5, color="gray", linestyle ='--')

# Add a Legend
ax1.legend()

# second plot
# Create the plot
ax2.plot(x, y2(x), alpha = 0.8, label ='y = x**2',
         color = color_codes[1],
         linewidth = 2,
         markersize = 5)

# Add and arrow
# ax2.arrow(1, y2(1), 1.1, y2(1.1), head_width=0.2, head_length=0.15, color = color_codes[1])

# Setting plot limits
ax2.axis(xmin=-3,xmax=3)

# Add X and y Label
ax2.set_xlabel('x')
ax2.set_ylabel('y')

# Add a grid
ax2.grid(alpha =.6, linestyle ='--')

# Add hvlines
ax2.axhline(y=1.5**2, color="gray", linestyle ='--')
ax2.axvline(x=1.5, color="gray", linestyle ='--')

# Add a Legend
ax2.legend()

# third plot
# Create the plot
ax3.plot(x, y3_1(x), alpha = 0.8, label ='y = x**2 if x < 2 \n x if x >= 2',
         color = color_codes[1],
         linewidth = 2,
         markersize = 5)

ax3.plot(x, y3_2(x), alpha = 0.8,
         color = color_codes[1],
         linewidth = 2,
         markersize = 5)

# Add and arrow
# ax2.arrow(1, y2(1), 1.1, y2(1.1), head_width=0.2, head_length=0.15, color = color_codes[1])

# Setting plot limits
ax3.axis(xmin=-3,xmax=3)

# Add X and y Label
ax3.set_xlabel('x')
ax3.set_ylabel('y')

# Add a grid
ax3.grid(alpha =.6, linestyle ='--')

# Add hvlines
ax3.axhline(y=1.5**2, color="gray", linestyle ='--')
ax3.axhline(y=1.5, color="gray", linestyle ='--')
ax3.axvline(x=1.5, color="gray", linestyle ='--')

# Add a Legend
ax3.legend()

# Show the plot
plt.show()


# -----------------------------------------------------
# -*- coding: utf-8 -*-
# @Time    : 8/23/2018 3:07 PM
# @Author  : sunyonghai
# @Software: ZJ_AI
# -----------------------------------------------------
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import numpy as np

def intersection_rect(a, b):
    x1 = max(a[0], b[0])
    y1 = max(a[1], b[1])

    x2 = min(a[2], b[2])
    y2 = min(a[3], b[3])
    return [x1,y1,x2,y2]

def intersection(a, b):
    i_rect = intersection_rect(a,b)
    i_area = area(i_rect)
    return i_area

def area(a):
    w = a[2] - a[0]
    h = a[3] - a[1]
    # print('area:', w * h)
    return  w * h

def union(a, b):
    area_a = area(a)
    area_b = area(b)
    area_i = intersection(a, b)
    return   area_a + area_b - area_i

def calc_iou(a, b):
    try:
        u = union(a, b)
        i = intersection(a, b)
        if abs(u)  < 0.000000001:
            return 1

        iou =  i / u
    except Exception as ex:
        print("{}，{},{}".format(i, u, ex))
    return iou

def test_calc_iou():
    # curr_iou = calc_iou([10,10,100,100], [50, 50, 200, 200])
    curr_iou = calc_iou([10,10,100,100], [10,10,100,100])
    return curr_iou

if __name__ == '__main__':
   print(test_calc_iou())


import numpy as np

def softmax(predictions):
    '''
    Computes probabilities from scores

    Arguments:
      predictions, np array, shape is either (N) or (batch_size, N) -
        classifier output

    Returns:
      probs, np array of the same shape as predictions -
        probability for every class, 0..1
    '''
    # TODO implement softmax
    x = predictions.copy()
    if len(predictions.shape) == 1:
        x -= np.max(x)
        return np.exp(x) / np.sum(np.exp(x))
    else:
        x -= np.max(x, axis=1, keepdims=True)
        return np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True)

def cross_entropy_loss(probs, target_index):
    '''
    Computes cross-entropy loss

    Arguments:
      probs, np array, shape is either (N) or (batch_size, N) -
        probabilities for every class
      target_index: np array of int, shape is (1) or (batch_size) -
        index of the true class for given sample(s)

    Returns:
      loss: single value
    '''
    # TODO implement cross-entropy
    if len(probs.shape) == 1:
        return -np.log(probs[target_index])
    else:
        n_samples = probs.shape[0]
        return np.mean(-np.log(probs[np.arange(n_samples), target_index]))

def l2_regularization(W, reg_strength):
    '''
    Computes L2 regularization loss on weights and its gradient

    Arguments:
      W, np array - weights
      reg_strength - float value

    Returns:
      loss, single value - l2 regularization loss
      gradient, np.array same shape as W - gradient of weight by l2 loss
    '''
    # TODO: Copy from previous assignment
    loss = reg_strength * np.sum(W ** 2)
    grad = reg_strength * 2 * W

    return loss, grad

def softmax_with_cross_entropy(predictions, target_index):
    '''
    Computes softmax and cross-entropy loss for model predictions,
    including the gradient

    Arguments:
      predictions, np array, shape is either (N) or (batch_size, N) -
        classifier output
      target_index: np array of int, shape is (1) or (batch_size) -
        index of the true class for given sample(s)

    Returns:
      loss, single value - cross-entropy loss
      dprediction, np array same shape as predictions - gradient of predictions by loss value
    '''
    # TODO copy from the previous assignment
    probes = softmax(predictions)
    loss = cross_entropy_loss(probes, target_index)
    dprediction = probes.copy()

    if len(predictions.shape) == 1:
        dprediction[target_index] -= 1
    else:
        n_samples = probes.shape[0]
        dprediction[np.arange(n_samples), target_index] -= 1
        dprediction /= n_samples

    return loss, dprediction

class Param:
    '''
    Trainable parameter of the model
    Captures both parameter value and the gradient
    '''
    def __init__(self, value):
        self.value = value
        self.grad = np.zeros_like(value)

class ReLULayer:
    def __init__(self):
        self.mask_ = None

    def forward(self, X):
        # TODO copy from the previous assignment
        self.mask_ = X > 0
        return X * self.mask_

    def backward(self, d_out):
        # TODO copy from the previous assignment
        d_result = self.mask_ * d_out
        return d_result

    def params(self):
        return {}

class FullyConnectedLayer:
    def __init__(self, n_input, n_output):
        self.W = Param(0.001 * np.random.randn(n_input, n_output))
        self.B = Param(0.001 * np.random.randn(1, n_output))
        self.X = None

    def forward(self, X):
        # TODO copy from the previous assignment
        self.X = np.copy(X)
        return np.dot(X, self.W.value) + self.B.value

    def backward(self, d_out):
        # TODO copy from the previous assignment

        self.W.grad = np.dot(self.X.T, d_out)
        self.B.grad = d_out.sum(axis=0)[None, :]
        d_input = np.dot(d_out, self.W.value.T)

        return d_input

    def params(self):
        return {'W': self.W, 'B': self.B}

class ConvolutionalLayer:
    def __init__(self, in_channels, out_channels,
                 filter_size, padding, verbose=False):
        '''
        Initializes the layer

        Arguments:
        in_channels, int - number of input channels
        out_channels, int - number of output channels
        filter_size, int - size of the conv filter
        padding, int - number of 'pixels' to pad on each side
        '''

        self.filter_size = filter_size
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.W = Param(
            np.random.randn(filter_size, filter_size,
                            in_channels, out_channels)
        )
        self.B = Param(np.zeros(out_channels))
        self.padding = padding
        self.verbose = verbose
        self.X = None

    def forward(self, X):
        batch_size, height, width, channels = X.shape

        out_height = height - self.filter_size + 1 + self.padding * 2
        out_width = width - self.filter_size + 1 + self.padding * 2

        # TODO: Implement forward pass
        # Hint: setup variables that hold the result
        # and one x/y location at a time in the loop below
        out = np.zeros((batch_size, out_height, out_width, self.out_channels))
        if self.padding:
            X_padding = np.zeros((batch_size, height + self.padding * 2,
                                  width + self.padding * 2, channels))
            X_padding[:, self.padding:height + self.padding, self.padding:width + self.padding, :] = X
            self.X = X_padding.copy()
        else:
            self.X = X.copy()

        # It's ok to use loops for going over width and height
        # but try to avoid having any other loops
        for y in range(out_height):
            for x in range(out_width):
                # TODO: Implement forward pass for specific location
                current_X = self.X[:, y:y + self.filter_size, x:x + self.filter_size, :].reshape((batch_size, -1))
                current_W = self.W.value.reshape((-1, self.out_channels))
                if self.verbose:
                    print('X', current_X.shape)
                    print('W', current_W.shape)
                out[:, y, x, :] = np.dot(current_X, current_W) + self.B.value

        return out

    def backward(self, d_out):
        # Hint: Forward pass was reduced to matrix multiply
        # You already know how to backprop through that
        # when you implemented FullyConnectedLayer
        # Just do it the same number of times and accumulate gradients

        batch_size, height, width, channels = self.X.shape
        _, out_height, out_width, _ = d_out.shape

        # TODO: Implement backward pass
        # Same as forward, setup variables of the right shape that
        # aggregate input gradient and fill them for every location
        # of the output
        d_input = np.zeros_like(self.X)

        # Try to avoid having any other loops here too
        for y in range(out_height):
            for x in range(out_width):
                # TODO: Implement backward pass for specific location
                # Aggregate gradients for both the input and
                # the parameters (W and B)
                current_X = self.X[:, y:y + self.filter_size, x:x + self.filter_size, :].reshape((batch_size, -1))
                current_W = self.W.value.reshape((-1, self.out_channels))
                if self.verbose:
                    print('X', current_X.shape)
                    print('W', current_W.shape)

                self.W.grad += np.dot(current_X.T, d_out[:, y, x, :]).reshape(self.W.value.shape)
                self.B.grad += d_out[:, y, x, :].sum(axis=0)
                d_input[:, y:y + self.filter_size, x: x + self.filter_size, :] += \
                    np.dot(d_out[:, y, x, :], current_W.T).reshape((batch_size, self.filter_size,
                                                                    self.filter_size, channels))
        if self.padding:
            d_input = d_input[:, self.padding:height-self.padding, self.padding:width-self.padding, :]

        return d_input

    def params(self):
        return {'W': self.W, 'B': self.B}

class MaxPoolingLayer:
    def __init__(self, pool_size, stride):
        '''
        Initializes the max pool

        Arguments:
        pool_size, int - area to pool
        stride, int - step size between pooling windows
        '''
        self.pool_size = pool_size
        self.stride = stride
        self.X = None

    def forward(self, X):
        batch_size, height, width, channels = X.shape
        # TODO: Implement maxpool forward pass
        # Hint: Similarly to Conv layer, loop on
        # output x/y dimension
        out_height = (height - self.pool_size) // self.stride + 1
        out_width = (width - self.pool_size) // self.stride + 1

        out = np.zeros((batch_size, out_height, out_width, channels))
        self.X = X.copy()

        for y in range(out_height):
            for x in range(out_width):
                current_X = self.X[:,
                                   y * self.stride:y * self.stride + self.pool_size,
                                   x * self.stride:x * self.stride + self.pool_size,
                                   :]
                out[:, y, x, :] = np.max(current_X, axis=(1, 2))
        return out

    def backward(self, d_out):
        # TODO: Implement maxpool backward pass
        batch_size, height, width, channels = self.X.shape
        _, out_height, out_width, _ = d_out.shape

        d_input = np.zeros_like(self.X)

        for y in range(out_height):
            for x in range(out_width):
                current_X = self.X[:,
                                   y * self.stride:y * self.stride + self.pool_size,
                                   x * self.stride:x * self.stride + self.pool_size,
                                   :]
                d_input_part = np.equal(np.ones_like(current_X) *
                                        current_X.max(axis=(1, 2)).reshape(batch_size, 1, 1, channels),
                                        current_X) * d_out[:, y, x, :].reshape(batch_size, 1, 1, channels)
                d_input[:,
                        y * self.stride:y * self.stride + self.pool_size,
                        x * self.stride:x * self.stride + self.pool_size,
                        :] += d_input_part
        return d_input

    def params(self):
        return {}

class Flattener:
    def __init__(self):
        self.X_shape = None

    def forward(self, X):
        batch_size, height, width, channels = X.shape

        # TODO: Implement forward pass
        # Layer should return array with dimensions
        # [batch_size, hight*width*channels]
        self.X_shape = X.shape
        return X.reshape(batch_size, height * width * channels)

    def backward(self, d_out):
        # TODO: Implement backward pass
        return d_out.reshape(self.X_shape)

    def params(self):
        # No params!
        return {}


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

import numpy as np, pandas as pd, matplotlib.pyplot as plt, seaborn as sns, arviz as az, scipy
from cmdstanpy import CmdStanModel

#%% data

dfhogg = pd.DataFrame(dict(
	x       = [ 201, 244,  47,  287,  203,  58,  210,  202,  198,  158, 165,  201,  157, 131, 166,  160, 186, 125, 218,   146],
	y       = [ 592, 401, 583,  402,  495, 173,  479,  504,  510,  416, 393,  442,  317, 311, 400,  337, 423, 334, 533,   344],
	sigma_x = [   9,   4,  11,    7,    5,   9,    4,    4,   11,    7,   5,    5,    5,   6,   6,    5,   9,   8,   6,    5.],
	sigma_y = [  61,  25,  38,   15,   21,  15,   27,   14,   30,   16,  14,   25,   52,  16,  34,   31,  42,  26,  16,    22],
	rho_xy  = [-.84, .31, .64, -.27, -.33, .67, -.02, -.05, -.84, -.69,  .3, -.46, -.03,  .5, .73, -.52,  .9,  .4, -.78, -.56]
), dtype = "float32")
dfhogg.plot.scatter(x = "x", y = "y", xerr = "sigma_x", yerr = "sigma_y", c = "b")

X, Y, sigmaY = dfhogg['x'].values, dfhogg['y'].values, dfhogg['sigma_y'].values
model_data_dict = {"N": len(dfhogg), "X": X, "Y": Y, "sigmaY": sigmaY}
model_data_stan = """
	data {
		int<lower=0> N;
		vector[N] X;
		vector[N] Y;
		vector<lower=0>[N] sigmaY;
	}
"""
model_trans_params_stan = """
	transformed parameters {
		vector[N] Yhat = b0 + b1 * X;
	}
"""
var_name = ["b0", "b1"]

#%% models

# Simple Linear Model with no Outlier Correction
modelfile_ols = "mdl_ols.stan"
with open(modelfile_ols, "w") as file: file.write(model_data_stan + """
	parameters { // discrete parameters impossible
		real b0;
		real b1;
	}
""" + model_trans_params_stan + """
	model {
		Y ~ normal(Yhat, sigmaY);
	}
""")

sm_ols = CmdStanModel(stan_file = modelfile_ols)
fit_ols = sm_ols.sample(
	data = model_data_dict, show_progress = True, chains = 4,
	iter_sampling = 50000, iter_warmup = 10000, thin = 5
)

fit_ols.draws().shape # iterations, chains, parameters
fit_ols.summary().loc[var_name] # pandas DataFrame
print(fit_ols.diagnose())

posterior_ols = {k: fit_ols.stan_variable(k) for k in var_name}

az_trace_ols = az.from_cmdstanpy(fit_ols)
az.summary(az_trace_ols).loc[var_name] # pandas DataFrame
az.plot_trace(az_trace_ols, var_names = var_name)

gd = sns.jointplot(
	x = posterior_ols["b0"], y = posterior_ols["b1"],
	marginal_kws = {"kde": True, "kde_kws": {"cut": 1}},
)
gd.plot_joint(sns.kdeplot, zorder = 2, n_levels = 10, cmap = "gray_r")
gd.fig.suptitle("Posterior joint distribution (OLS)", y = 1.02)

# Simple Linear Model with Robust Student-T Likelihood: outliers to have a smaller influence in the likelihood estimation
modelfile_studentt = "mdl_studentt.stan"
with open(modelfile_studentt, "w") as file: file.write(model_data_stan + """
	parameters { // discrete parameters impossible
		real b0;
		real b1;
		real<lower=0> df;
	}
""" + model_trans_params_stan + """
	model {
		df ~ inv_gamma(1, 1);
		Y ~ student_t(df, Yhat, sigmaY);
	}
""")
var_name_studentt = var_name + ["df"]

sm_studentt = CmdStanModel(stan_file = modelfile_studentt)
fit_studentt = sm_studentt.sample(
	data = model_data_dict, show_progress = True, chains = 4,
	iter_sampling = 50000, iter_warmup = 10000, thin = 5
)

fit_studentt.draws().shape # iterations, chains, parameters
fit_studentt.summary().loc[var_name_studentt] # pandas DataFrame
print(fit_studentt.diagnose())

posterior_studentt = {k: fit_studentt.stan_variable(k) for k in var_name_studentt}

az_trace_studentt = az.from_cmdstanpy(fit_studentt)
az.summary(az_trace_studentt).loc[var_name_studentt] # pandas DataFrame
az.plot_trace(az_trace_studentt, var_names = var_name_studentt)

# Linear Model with Custom Likelihood to Distinguish Outliers: Hogg Method
# idea: mixture model whereby datapoints can be: normal linear model vs outlier (for convenience also be linear)
modelfile_hogg = "mdl_hogg.stan"
with open(modelfile_hogg, "w") as file: file.write(model_data_stan + """
	parameters { // discrete parameters impossible
		real b0;
		real b1;
		real Y_outlier; // mean for all outliers
		real<lower=0> sigmaY_outlier; // additional variance for outliers
		simplex[2] cluster_prob; // mixture ratio
	}
""" + model_trans_params_stan + """
	model {
		b0 ~ normal(0, 5); // weakly informative Normal priors (L2 ridge reg) for inliers
		b1 ~ normal(0, 5); // likewise

		Y_outlier ~ normal(0, 10);
		sigmaY_outlier ~ normal(0, 10); // half-normal because of above constraint

		for (n in 1:N) { // custom mixture model: cluster 1 = inlier, 2 = outlier
			real cluster1 = log(cluster_prob[1]) + normal_lpdf(Y[n] | Yhat[n], sigmaY[n]);
			real cluster2 = log(cluster_prob[2]) + normal_lpdf(Y[n] | Y_outlier, sigmaY[n] + sigmaY_outlier);
			target += log_sum_exp(cluster1, cluster2);
		}
	}
""")
var_name_hogg_array = var_name + ["Y_outlier", "sigmaY_outlier", "cluster_prob[1]", "cluster_prob[2]"]
var_name_hogg_combi = var_name + ["Y_outlier", "sigmaY_outlier", "cluster_prob"]

sm_hogg = CmdStanModel(stan_file = modelfile_hogg)
fit_hogg = sm_hogg.sample(
	data = model_data_dict, show_progress = True, chains = 4,
	iter_sampling = 50000, iter_warmup = 10000, thin = 5
)

fit_hogg.draws().shape # iterations, chains, parameters
fit_hogg.summary().loc[var_name_hogg_array] # pandas DataFrame
print(fit_hogg.diagnose())

posterior_hogg = {k: fit_hogg.stan_variable(k) for k in var_name_hogg_combi}

az_trace_hogg = az.from_cmdstanpy(fit_hogg)
az.summary(az_trace_hogg) # pandas DataFrame
az.plot_trace(az_trace_hogg, var_names = var_name_hogg_combi)

#%% some plots

Xrange = np.array([X.min() - 1, X.max()])
dfhogg.plot.scatter(x = "x", y = "y", xerr = "sigma_x", yerr = "sigma_y", c = "b", figsize = (8, 6))
for x, y, z in zip(
	[posterior_ols, posterior_studentt, posterior_hogg],
	["r", "g", "m"],
	["OLS", "Student-T", "Hogg method"]
):
	b0, b1 = x["b0"].mean(), x["b1"].mean()
	plt.plot(Xrange, b0 + b1 * Xrange, c = y, label = f"{z}: $ y = {b0:.1f} + {b1:.1f}x $")
plt.legend(loc = "lower right")

for var in var_name:
	a, b, c = posterior_ols[var], posterior_studentt[var], posterior_hogg[var]
	data = pd.DataFrame(data = {
		"y": np.concatenate((a, b, c)),
		"model": np.repeat([
			f"OLS: {a.mean():.1f}", f"Student-T: {b.mean():.1f}", f"Hogg method: {c.mean():.1f}"
		], 40000) # nb of data pts: 4chains × 50000 iter ÷ 5 thin
	})
	g = sns.displot(data, x = "y", hue = "model", bins = "sqrt", kde = True, palette = ["r", "g", "m"])
	g.fig.suptitle(f"Posterior distribution: {var}")
	g.fig.set_figwidth(8)
	g.fig.set_figheight(6)

#%% declare outliers

# Compute the un-normalized log probabilities for each cluster
cluster_0_log_prob = scipy.stats.norm.logpdf(
	np.expand_dims(Y, axis = 1),
	loc = posterior_hogg["b0"] + posterior_hogg["b1"] * np.expand_dims(X, axis = 1),
	scale = np.expand_dims(sigmaY, axis = 1)
) + np.log(posterior_hogg["cluster_prob"][:, 0])
cluster_1_log_prob = scipy.stats.norm.logpdf(
	np.expand_dims(Y, axis = 1),
	loc = posterior_hogg["Y_outlier"],
	scale = np.expand_dims(sigmaY, axis = 1) + posterior_hogg["sigmaY_outlier"]
) + np.log(posterior_hogg["cluster_prob"][:, 1])

# Bayes rule to compute the assignment probability: P(cluster = 1 | data) ∝ P(data | cluster = 1) P(cluster = 1)
log_p_assign_1 = cluster_1_log_prob - np.logaddexp(cluster_0_log_prob, cluster_1_log_prob)

# Average across the MCMC chain
log_p_assign_1bis = scipy.special.logsumexp(log_p_assign_1, axis=-1) - np.log(log_p_assign_1.shape[-1])

p_assign_1 = np.exp(log_p_assign_1bis)
dfhogg["is_outlier"] = [f"{100*i:.2f} %" for i in p_assign_1]
dfhogg["classed_as_outlier"] = p_assign_1 >= .95

#%% remove outliers + full model with all variances

no_outliers = dfhogg[~dfhogg.classed_as_outlier]

mdl_full_data = dict(
	N = len(no_outliers),
	X = no_outliers['x'].values,
	Y = no_outliers['y'].values,
	sigmaX = no_outliers['sigma_x'].values,
	sigmaY = no_outliers['sigma_y'].values,
	rhoXY = no_outliers['rho_xy'].values
)

modelfile_full = "mdl_full.stan"
with open(modelfile_full, "w") as file: file.write("""
	data {
		int<lower=0> N;
		vector[N] X;
		vector[N] Y;
		vector<lower=0>[N] sigmaX;
		vector<lower=0>[N] sigmaY;
		vector<lower=-1,upper=1>[N] rhoXY;
	}

	transformed data {
		real angle90 = pi()/2; // a cste
		vector[2] Z[N]; // data pt in vector form
		matrix[2,2] S[N]; // each data point’s covariance matrix
		for (i in 1:N) {
			Z[i] = [X[i], Y[i]]';
			real covXY = rhoXY[i]*sigmaX[i]*sigmaY[i];
			S[i] = [[sigmaX[i]^2, covXY], [covXY, sigmaY[i]^2]];
		}
	}

	parameters { // discrete parameters impossible
		real<lower=-angle90,upper=angle90> theta; // angle of the fitted line
		real b; // intercept
	}

	transformed parameters {
		vector[2] v = [-sin(theta), cos(theta)]'; // unit vector orthogonal to the line
		vector[N] lp; // log prob
		for (i in 1:N) {
			real delta = dot_product(v, Z[i]) - b*v[2]; // orthogonal displacement of each data point from the line
			real sigma2 = quad_form(S[i], v); // orthogonal variance of projection of each data point to the line
			lp[i] = delta^2/2/sigma2; // sum(lp) is faster than +=
		}
	}

	model {
		theta ~ uniform(-angle90, angle90);
		target += -sum(lp); // ATTENTION sign
	}

	generated quantities {
		real m = tan(theta); // slope
	}
""")
var_name_full = ["m", "b", "theta"]

sm_full = CmdStanModel(stan_file = modelfile_full)
fit_full = sm_full.sample(
	data = mdl_full_data, show_progress = True, chains = 4,
	iter_sampling = 50000, iter_warmup = 10000, thin = 5
)

fit_full.draws().shape # iterations, chains, parameters
fit_full.summary().loc[var_name_full] # pandas DataFrame
print(fit_full.diagnose())

posterior_full = {k: fit_full.stan_variable(k) for k in var_name_full}

az_trace_full = az.from_cmdstanpy(fit_full)
az.summary(az_trace_full) # pandas DataFrame
az.plot_trace(az_trace_full, var_names = var_name_full)

b0, b1 = posterior_full["b"].mean(), posterior_full["m"].mean() # name changed
no_outliers.plot.scatter(x = "x", y = "y", xerr = "sigma_x", yerr = "sigma_y", c = "b")
plt.plot(Xrange, b0 + b1 * Xrange, label = f"$ y = {b0:.1f} + {b1:.1f}x $")
plt.legend(loc = "lower right")

#%% full model with intrinsic scatter

modelfile_full_intrinsic = "mdl_full_intrinsic.stan"
with open(modelfile_full_intrinsic, "w") as file: file.write("""
	data {
		int<lower=0> N;
		vector[N] X;
		vector[N] Y;
		vector<lower=0>[N] sigmaX;
		vector<lower=0>[N] sigmaY;
		vector<lower=-1,upper=1>[N] rhoXY;
	}

	transformed data {
		real angle90 = pi()/2; // a cste
		vector[2] Z[N]; // data pt in vector form
		matrix[2,2] S[N]; // each data point’s covariance matrix
		for (i in 1:N) {
			Z[i] = [X[i], Y[i]]';
			real covXY = rhoXY[i]*sigmaX[i]*sigmaY[i];
			S[i] = [[sigmaX[i]^2, covXY], [covXY, sigmaY[i]^2]];
		}
	}

	parameters { // discrete parameters impossible
		real<lower=-angle90,upper=angle90> theta; // angle of the fitted line
		real b; // intercept
		real<lower=0> V; // intrinsic Gaussian variance orthogonal to the line
	}

	transformed parameters {
		vector[2] v = [-sin(theta), cos(theta)]'; // unit vector orthogonal to the line
		vector[N] lp; // log prob
		for (i in 1:N) {
			real delta = dot_product(v, Z[i]) - b*v[2]; // orthogonal displacement of each data point from the line
			real sigma2 = quad_form(S[i], v); // orthogonal variance of projection of each data point to the line
			real tmp = sigma2 + V; // intermediary result
			lp[i] = .5*(log(tmp) + delta^2/tmp); // sum(lp) is faster than +=
		}
	}

	model {
		theta ~ uniform(-angle90, angle90);
		target += -sum(lp); // ATTENTION sign
	}

	generated quantities {
		real m = tan(theta); // slope
		real move_up = sqrt(V) / v[2];
	}
""")
var_name_full_intrinsic = var_name_full + ["V", "move_up"]

sm_full_intrinsic = CmdStanModel(stan_file = modelfile_full_intrinsic)
fit_full_intrinsic = sm_full_intrinsic.sample(
	data = mdl_full_data, show_progress = True, chains = 4,
	iter_sampling = 50000, iter_warmup = 10000, thin = 5
)

fit_full_intrinsic.draws().shape # iterations, chains, parameters
fit_full_intrinsic.summary().loc[var_name_full_intrinsic] # pandas DataFrame
print(fit_full_intrinsic.diagnose())

posterior_full_intrinsic = {k: fit_full_intrinsic.stan_variable(k) for k in var_name_full_intrinsic}

az_trace_full_intrinsic = az.from_cmdstanpy(fit_full_intrinsic)
az.summary(az_trace_full_intrinsic) # pandas DataFrame
az.plot_trace(az_trace_full_intrinsic, var_names = var_name_full_intrinsic)

b0, b1 = posterior_full_intrinsic["b"].mean(), posterior_full_intrinsic["m"].mean()
move_up = posterior_full_intrinsic["move_up"].mean()
no_outliers.plot.scatter(x = "x", y = "y", xerr = "sigma_x", yerr = "sigma_y", c = "b")
plt.plot(Xrange, b0 + b1 * Xrange, label = f"$ y = {b0:.1f} + {b1:.1f}x $")
plt.plot(Xrange, b0 + b1 * Xrange + move_up, linestyle = "--")
plt.plot(Xrange, b0 + b1 * Xrange - move_up, linestyle = "--")
plt.legend(loc = "lower right")


import numpy as np
import matplotlib.pyplot as plt

# Plot 2D points
def displaypoints2d(points):
    plt.figure(0)
    plt.plot(points[0, :], points[1, :], '.b')
    plt.xlabel('Screen X')
    plt.ylabel('Screen Y')

# Plot 3D points
def displaypoints3d(points):
    fig = plt.figure(1)
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter(points[0, :], points[1, :], points[2, :], 'b')
    ax.set_xlabel("World X")
    ax.set_ylabel("World Y")
    ax.set_zlabel("World Z")

def cart2hom(points):
    """ Transforms from cartesian to homogeneous coordinates.

    Args:
      points: a np array of points in cartesian coordinates

    Returns:
      points_hom: a np array of points in homogeneous coordinates
    """

    #
    # You code here
    #
    ones = np.ones((1, np.shape(points)[1]))
    hom = np.vstack((points, ones))
    return hom

def hom2cart(points):
    """ Transforms from homogeneous to cartesian coordinates.

    Args:
      points: a np array of points in homogenous coordinates

    Returns:
      points_hom: a np array of points in cartesian coordinates
    """

    #
    # You code here
    #
    cart = points[0:-1, :] / points[-1, :]
    return cart

def gettranslation(v):
    """ Returns translation matrix T in homogeneous coordinates for translation by v.

    Args:
      v: 3d translation vector

    Returns:
      T: translation matrix in homogeneous coordinates
    """

    #
    # You code here
    #
    temp = np.reshape(v, (3, 1))
    hom = np.vstack((temp, 0))
    return hom

def getxrotation(d):
    """ Returns rotation matrix Rx in homogeneous coordinates for a rotation of d degrees around the x axis.

    Args:
      d: degrees of the rotation

    Returns:
      Rx: rotation matrix
    """

    #
    # You code here
    #
    return np.array([[1, 0, 0],
                     [0, np.cos(d), -np.sin(d)],
                     [0, np.sin(d), np.cos(d)]])

def getyrotation(d):
    """ Returns rotation matrix Ry in homogeneous coordinates for a rotation of d degrees around the y axis.

    Args:
      d: degrees of the rotation

    Returns:
      Ry: rotation matrix
    """

    #
    # You code here
    #
    return np.array([[np.cos(d), 0, np.sin(d)],
                     [0, 1, 0],
                     [-np.sin(d), 0, np.cos(d)]])

def getzrotation(d):
    """ Returns rotation matrix Rz in homogeneous coordinates for a rotation of d degrees around the z axis.

    Args:
      d: degrees of the rotation

    Returns:
      Rz: rotation matrix
    """

    #
    # You code here
    #
    return np.array([[np.cos(d), -np.sin(d), 0],
                     [np.sin(d), np.cos(d), 0],
                     [0, 0, 1]])

def getcentralprojection(principal, focal):
    """ Returns the (3 x 4) matrix L that projects homogeneous camera coordinates on homogeneous
    image coordinates depending on the principal point and focal length.

    Args:
      principal: the principal point, 2d vector
      focal: focal length

    Returns:
      L: central projection matrix
    """

    #
    # You code here
    #
    return np.array([[focal, 0, principal[0], 0],
                     [0, focal, principal[1], 0],
                     [0, 0, 1, 0]])

def getfullprojection(T, Rx, Ry, Rz, L):
    """ Returns full projection matrix P and full extrinsic transformation matrix M.

    Args:
      T: translation matrix
      Rx: rotation matrix for rotation around the x-axis
      Ry: rotation matrix for rotation around the y-axis
      Rz: rotation matrix for rotation around the z-axis
      L: central projection matrix

    Returns:
      P: projection matrix
      M: matrix that summarizes extrinsic transformations
    """

    #
    # You code here
    #
    R = Rz @ (Rx @ Ry)
    t = -R @ T[0:-1]
    temp = np.hstack((R, t))
    M = np.vstack((temp, [0, 0, 0, 1]))
    P = L @ M
    return P, M

def projectpoints(P, X):
    """ Apply full projection matrix P to 3D points X in cartesian coordinates.

    Args:
      P: projection matrix
      X: 3d points in cartesian coordinates

    Returns:
      x: 2d points in cartesian coordinates
    """

    #
    # You code here
    #
    temp_x = cart2hom(X)
    temp = P @ temp_x
    return hom2cart(temp)

def loadpoints():
    """ Load 2D points from obj2d.npy.

    Returns:
      x: np array of points loaded from obj2d.npy
    """

    #
    # You code here
    #
    points = np.load('data/obj2d.npy')

    return points

def loadz():
    """ Load z-coordinates from zs.npy.

    Returns:
      z: np array containing the z-coordinates
    """

    #
    # You code here
    #
    return np.load('data/zs.npy')

def invertprojection(L, P2d, z):
    """
    Invert just the projection L of cartesian image coordinates P2d with z-coordinates z.

    Args:
      L: central projection matrix
      P2d: 2d image coordinates of the projected points
      z: z-components of the homogeneous image coordinates

    Returns:
      P3d: 3d cartesian camera coordinates of the points
    """

    #
    # You code here
    #
    size_points = np.shape(P2d)
    ones = np.ones((1, size_points[1]))
    hom_points = np.vstack((P2d, ones))
    # check
    z_points = hom_points * z  # check
    # p_xy=L[0:2,2] #slicing construct no array
    # ??? why get [8, -10]
    p_xy = L[np.arange(2), 2]  # Integer array indexing

    p_xy_reshape = np.reshape(np.append(p_xy, 0), (3, 1))
    z_p_xy = p_xy_reshape * z
    inv_principal = z_p_xy + z_points
    L_truncted = L[np.arange(3), 0:3]
    P3d = np.linalg.inv(L_truncted) @ inv_principal

    return P3d

def inverttransformation(M, P3d):
    """ Invert just the model transformation in homogeneous coordinates
    for the 3D points P3d in cartesian coordinates.

    Args:
      M: matrix summarizing the extrinsic transformations
      P3d: 3d points in cartesian coordinates

    Returns:
      X: 3d points after the extrinsic transformations have been reverted
    """

    #
    # You code here
    #
    ones = np.ones((1, np.shape(P3d)[1]))
    p4d_C = np.vstack((P3d, ones))
    p4d_W = np.linalg.inv(M) @ p4d_C

    # p3d_W=p4d_W[0:-1,:]
    return p4d_W

def p3multiplecoice():
    '''
    Change the order of the transformations (translation and rotation).
    Check if they are commutative. Make a comment in your code.
    Return 0, 1 or 2:
    0: The transformations do not commute.
    1: Only rotations commute with each other.
    2: All transformations commute.
    '''

    return 0


r"""
Solve Poisson equation on (0, 2pi)x(0, 2pi) with periodic bcs

    \nabla^2 u = f, u(2pi, y) = u(0, y), u(x, 2pi) = u(x, 0)

Use Fourier basis and find u in VxV such that

    (v, div(grad(u))) = (v, f)    for all v in VxV

where V is the Fourier basis span{exp(1jkx)}_{k=-N/2}^{N/2-1} and
VxV is a tensorproductspace.

"""
import os
from sympy import symbols, cos, sin, lambdify
import numpy as np
from shenfun import inner, grad, TestFunction, TrialFunction, Array, FunctionSpace, \
    TensorProductSpace, Function, dx, comm, la

# Use sympy to compute a rhs, given an analytical solution
x, y = symbols("x,y", real=True)
ue = cos(4*x) + sin(8*y)
fe = ue.diff(x, 2) + ue.diff(y, 2)

# Size of discretization
N = (64, 64)

K0 = FunctionSpace(N[0], family='F', dtype='D', domain=(-2*np.pi, 2*np.pi))
K1 = FunctionSpace(N[1], family='F', dtype='d', domain=(-2*np.pi, 2*np.pi))
T = TensorProductSpace(comm, (K0, K1), axes=(0, 1))
u = TrialFunction(T)
v = TestFunction(T)

# Get f on quad points
fj = Array(T, buffer=fe)

# Compute right hand side
f_hat = Function(T)
f_hat = inner(v, fj, output_array=f_hat)

# Solve Poisson equation
u_hat = Function(T)
#A = inner(v, div(grad(u)))
A = inner(grad(v), grad(u))
sol = la.SolverDiagonal(A)
u_hat = sol(-f_hat, u_hat, constraints=((0, 0),))

uq = Array(T)
uq = T.backward(u_hat, uq, fast_transform=True)

uj = Array(T, buffer=ue)
assert np.allclose(uj, uq)

# Test eval at point
point = np.array([[0.1, 0.5], [0.5, 0.6]])
p = T.eval(point, u_hat)
ul = lambdify((x, y), ue)
assert np.allclose(p, ul(*point))
p2 = u_hat.eval(point)
assert np.allclose(p2, ul(*point))
print(np.sqrt(dx((uj-uq)**2)))

if 'pytest' not in os.environ:
    import matplotlib.pyplot as plt
    plt.figure()
    X = T.local_mesh(True)
    plt.contourf(X[0], X[1], uq)
    plt.colorbar()

    plt.figure()
    plt.contourf(X[0], X[1], uj)
    plt.colorbar()

    plt.figure()
    plt.contourf(X[0], X[1], uq-uj)
    plt.colorbar()
    plt.title('Error')
    plt.show()


import numpy as np

##################### Points #####################

def compute_distance(p1,p2=np.array([0,0])):
    """
        Euclidean distance between p1 and p2
    """
    p1 = np.array(p1)
    p2 = np.array(p2)
    return np.sqrt(np.sum((p1-p2)**2))

def random_displ(max_pix=5,min_pix=0,shape=None):
    """
        Get random displacements between min_pix and max_pix
    """
    disp = np.random.randint(min_pix,max_pix,shape)
    disp *= np.random.choice([-1,1],shape)
    return disp

##################### Lines #####################

def line_polar(p1,p2):
    """
        Compute distance coordinates and angle for the line connecting p1 and p2
    """
    dy = (p2[1]-p1[1])
    dx = (p2[0]-p1[0])
    alpha = np.arctan2(dy,dx)
    l = np.arange(compute_distance(p1,p2)+1)
    return l,alpha

def line_polar_to_cart(l,alpha,point=(0,0)):
    """
        Line points from polar to cartesian coordinates
    """
    return point[0]+l*np.cos(alpha),point[1]+l*np.sin(alpha)

def find_intersect(alpha1,p1,alpha2,p2,ret_xy=True):
    """
        Compute intersection between two lines in polar coordinates. Returns either x,y of the intersection or
        its distance from p1 along line1
    """
    if alpha1 == alpha2:
        if ret_xy:
            return np.inf,np.inf
        else:
            return np.inf

    p1 = np.array(p1)
    p2 = np.array(p2)

    p21 = p2-p1
    l_inter = (np.tan(alpha2)*p21[0]-p21[1])/(np.tan(alpha2)*np.cos(alpha1)-np.sin(alpha1))
    if ret_xy:
        return l_inter*np.cos(alpha1)+p1[0],l_inter*np.sin(alpha1)+p1[1]
    else:
        return l_inter

##################### Other #####################

def PolyArea(x,y):
    """
        Compute area of polygon defined by x,y points
    """
    return 0.5*np.abs(np.dot(x,np.roll(y,1))-np.dot(y,np.roll(x,1)))


import numpy as np
from fraction import Fraction as fr

def get_leading_one(row: np.array):
    for idx, val in enumerate(row):
        if val != fr(0,1):
            return idx
    return len(row)

def rref(matrix: np.array):
    n, m = matrix.shape

    # Iterate through the diagonal of the matrix
    for i in range(min(m, n)):
        leading_one_index = get_leading_one(matrix[i])

        if leading_one_index == m:
            continue

        leading_one = matrix[i][leading_one_index]
        if leading_one != fr(1, 1):
            matrix[i] /= leading_one

        current_row = matrix[i]

        for j in range(n):
            if j == i:
                continue

            factor = matrix[j][leading_one_index]  # /leading_one
            matrix[j] -= current_row * factor

    res = np.empty((n,m))
    for i in range(n):
        for j in range(m):
            res[i,j]=int(matrix[i,j])

    print(res)

    return matrix

if __name__ == "__main__":

    A = np.array([
        [2, 1, 7, -7,2],
        [-3, 4, -5, -6,3],
        [1, 1, 4, -5,2]
    ])

    n, m = A.shape

    B = np.empty((n,m), dtype="object")

    for i in range(n):
        for j in range(m):
            B[i, j] = fr(A[i, j], 1)

    rref(B)


import numpy as np
import pymc3 as pm
import pandas as pd
import arviz as az
from scipy import stats
from scipy.special import logsumexp
from bayesian_routines import standardize
import matplotlib.pyplot as plt

np.random.seed(20897234)
# 7H1
data = pd.read_csv('Laffer.csv', delimiter=';')

rate = standardize(np.array(data['tax_rate']))
revenue = standardize(np.array(data['tax_revenue']))

n_samples = 2000 // 4
n_tuning = 1000

with pm.Model() as linear:
	#a = pm.Normal('b0', mu=0., sigma=0.5)
	b = pm.Normal('b1', mu=0., sigma=0.5)

	s = pm.Exponential('s', lam=0.5)

	m = b * rate

	r = pm.Normal('r', mu=m, sigma=s, observed=revenue)

	traceLinear = pm.sample(n_samples, tune=n_tuning)

with pm.Model() as quadratic:
	a = pm.Normal('b0', mu=0., sigma=0.5)
	b = pm.Normal('b1', mu=0., sigma=0.5)
	c = pm.Normal('b2', mu=0., sigma=0.5)

	s = pm.Exponential('s', lam=0.5)

	m = a + b * rate + c * (rate ** 2)

	r = pm.Normal('r', mu=m, sigma=s, observed=revenue)

	traceQuadratic = pm.sample(n_samples, tune=n_tuning)

with pm.Model() as cubic:
	a = pm.Normal('b0', mu=0., sigma=0.5)
	b = pm.Normal('b1', mu=0., sigma=0.5)
	c = pm.Normal('b2', mu=0., sigma=0.5)
	d = pm.Normal('b3', mu=0., sigma=0.5)

	s = pm.Exponential('s', lam=0.5)

	m = a + b * rate + c * (rate ** 2) + d * (rate ** 3)

	r = pm.Normal('r', mu=m, sigma=s, observed=revenue)

	traceCubic = pm.sample(n_samples, tune=n_tuning)

r = az.compare({'L': traceLinear, 'Q':traceQuadratic, 'C':traceCubic}, 'WAIC')
print(r)

'''
#------------------------------------------------------------------------------------------------------------------
# 7H2
ww = az.waic(traceLinear, pointwise=True)

sInd = np.argmax(revenue)
sampleImportance = np.array(ww[6])

with pm.Model() as linearRobust:
	a = pm.Normal('b0', mu=0., sigma=0.5)
	b = pm.Normal('b1', mu=0., sigma=0.5)

	s = pm.Exponential('s', lam=0.5)

	m = a + b * rate

	r = pm.StudentT('r', nu=1., mu=m, sigma=s, observed=revenue)

	traceLinearRobust = pm.sample(n_samples, tune=n_tuning)

ww = az.waic(traceLinearRobust, pointwise=True)

sampleImportance2 = np.array(ww[6])

fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(sampleImportance, sampleImportance2)
ax.set_xlabel('Normal')
ax.set_ylabel('Student t')
ax.set_xlim(0, 25)
ax.set_ylim(0, 25)

# effect is reduce ~by half, but still it is pretty big, much bigger than the other points

#------------------------------------------------------------------------------------------------------------------
# 7H3
def entropy(x):
	return - np.sum(x * np.log2(x))

def kldivergence(p,q):
	return np.sum(p * np.log2(p / q))

d = np.array([[0.2, 0.2, 0.2, 0.2, 0.2], [0.8, 0.1, 0.05, 0.025, 0.025], [0.05, 0.15, 0.7, 0.05, 0.05]])

e = np.zeros(d.shape[0])
for i, island in enumerate(d):
	e[i] = entropy(island)

kls = np.zeros((3,3))

for i in range(3):
	for j in range(3):
		if i != j:
			kls[i,j] = kldivergence(d[i], d[j])

# entropy decreases from 1, 3, 2. 1 is the most 'random'. It is very much expecting species A very often
# more entropy means less KL divergence to other distributions, because it's harder to be surprised by what you see

plt.show()

#------------------------------------------------------------------------------------------------------------------
# 7H5

data = pd.read_csv('../chapter-6/foxes.csv', delimiter=';')

area = standardize(np.array(data['area']))
avgfood = standardize(np.array(data['avgfood']))
weight = standardize(np.array(data['weight']))
groupsize = standardize(np.array(data['groupsize']))

mp = 0.
sp = 0.4
lp = 0.5

n_samples = 2000 // 4
n_tuning = 1000

with pm.Model() as fox1:
	w_f = pm.Normal('wf', mu=mp, sigma=sp)
	w_g = pm.Normal('wg', mu=mp, sigma=sp)
	w_a = pm.Normal('wa', mu=mp, sigma=sp)
	w_0 = pm.Normal('w0', mu=mp, sigma=sp)

	s = pm.Exponential('s', lam=lp)

	mu = w_0 + w_f * avgfood + w_g * groupsize + w_a * area

	W = pm.Normal('w', mu=mu, sigma=s, observed=weight)

	trace1 = pm.sample(n_samples, tune=n_tuning)

with pm.Model() as fox2:
	w_f = pm.Normal('wf', mu=mp, sigma=sp)
	w_g = pm.Normal('wg', mu=mp, sigma=sp)
	w_0 = pm.Normal('w0', mu=mp, sigma=sp)

	s = pm.Exponential('s', lam=lp)

	mu = w_0 + w_f * avgfood + w_g * groupsize
	W = pm.Normal('w', mu=mu, sigma=s, observed=weight)

	trace2 = pm.sample(n_samples, tune=n_tuning)

with pm.Model() as fox3:
	w_g = pm.Normal('wg', mu=mp, sigma=sp)
	w_a = pm.Normal('wa', mu=mp, sigma=sp)
	w_0 = pm.Normal('w0', mu=mp, sigma=sp)

	s = pm.Exponential('s', lam=lp)

	mu = w_0 + w_g * groupsize + w_a * area

	W = pm.Normal('w', mu=mu, sigma=s, observed=weight)

	trace3 = pm.sample(n_samples, tune=n_tuning)

with pm.Model() as fox4:
	w_f = pm.Normal('wf', mu=mp, sigma=sp)
	w_0 = pm.Normal('w0', mu=mp, sigma=sp)

	s = pm.Exponential('s', lam=lp)

	mu = w_0 + w_f * avgfood
	W = pm.Normal('w', mu=mu, sigma=s, observed=weight)

	trace4 = pm.sample(n_samples, tune=n_tuning)

with pm.Model() as fox5:
	w_a = pm.Normal('wa', mu=mp, sigma=sp)
	w_0 = pm.Normal('w0', mu=mp, sigma=sp)

	s = pm.Exponential('s', lam=lp)

	mu = w_0 + w_a * area

	W = pm.Normal('w', mu=mu, sigma=s, observed=weight)

	trace5 = pm.sample(n_samples, tune=n_tuning)

r = az.compare({'m1': trace1, 'm2':trace2, 'm3':trace3, 'm4': trace4, 'm5': trace5}, 'WAIC')
'''
# model with all variables is the best
# ignoring group size is really bad, because it has a negative effect from the other two variables
# using area instead of avgfood is slightly worse and the difference is more consistent (smaller std error)
# probably because area is an indirect cause of weight, while food is direct?


import numpy as np
import matplotlib.pyplot as plt

def numerical_diff(f, x):
    h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)
def function_1(x):
    return 0.01*x**2 + 0.1*x

x = np.arange(0.0, 20.0, 0.1) # 0에서 ~ 20까지 0.1 단위
y = function_1(x)
#plt.plot(x, y)
#plt.show()

nd = numerical_diff(function_1, x)
plt.plot(x, nd)
plt.plot(x, y)
plt.show()

a = np.array([[1, 2, 3, 4],
          [4, 5, 6, 7],
          [8, 9, 3, 1]])

#print(np.linalg.pinv(a))

## 4.3.3 partial differential ##
def fuction_2(x):
    return x[0]**2 + x[1]**2

def function_tmp1(x):
    return x*x + 4.0**2.0

print(numerical_diff(function_tmp1, 3.0))

def numericalgradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    for idx in range(x.size):
        tmp_val = x[idx]
        x[idx] = tmp_val + h
        fxh1 = f(x)
        x[idx] = tmp_val - h
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val
    return grad


"""
Demonstrate Various Algorithms or prerequisites.

    - Householder reflections
    - Givens Rotations used in Jacobi-Transform and QR-Method.
"""

import numpy as np
import copy
from matplotlib import pyplot as plt

def hreflect1D(x):
    """
    Calculate Householder reflection: Q = I - 2*uu'.

    Parameters:
        X: numpy array.

    Returns:
        q: reflected vector.
        Q: Reflector (matrix).
    """
    # Construct v:
    v = copy.deepcopy(x)
    v[0] += np.linalg.norm(x)

    # Construct u: normalize v.
    vnorm = np.linalg.norm(v)
    if vnorm:
        u = v / np.linalg.norm(v)
    else:
        u = v

    # Construct Q:
    Q = np.eye(len(x)) - 2 * np.outer(u, u)

    return np.dot(Q, x), Q

def qr_factorize(X):
    """
    Compute QR factorization of X s.t. QR = X.

    Parameters:
        - X: square numpy ndarray.

    Returns:
        Q: square numpy ndarray, same shape as X. Rotation matrix.
        R: square numpy ndarray, same shape as X. upper triangular matrix.
    """
    assert type(X) == np.ndarray
    assert X.shape[0] == X.shape[1]

    A = copy.deepcopy(X)
    Q = np.eye(X.shape[0])

    for i in range(X.shape[0]):
        Pi = np.eye(A.shape[0])
        _, Qi = hreflect1D(A[i:, i])
        Pi[i:, i:] = Qi

        Q = Pi.dot(Q)
        A = Pi.dot(A)

    return Q, A

A = np.array([[3, -98/28, 1, 1, 1],
              [1, 122/28, 1, 3, 1],
              [2, -8/28, 89, 1, 1],
              [1, 66/28, 1, 3, 1],
              [1, 10/28, 1, 1, 2]])

Q, R = qr_factorize(A)

Q
R
np.round(R)

np.isclose(A, np.linalg.inv(Q).dot(R))

hreflect1D(np.array([5, 1, 3, 1]))

1/66 * np.array([[-55, -11, -33, -11],
                 [-11, 65, -3, -1],
                 [-33, -3, 57, -3],
                 [-11, -1, -3, 65]])


import numpy as np
import pypolyagamma as pypolyagamma

def calculate_D(S):
    N = S.shape[1]

    D = np.empty((N, N))

    for i in range(N):
        for j in range(N):
            D[i, j] = np.dot(S[1:, i].T, S[:-1, j])

    return D * 0.5

def calculate_C_w(S, w_i):
    w_mat = np.diag(w_i)

    return np.dot(S.T, np.dot(w_mat, S))

def sample_w_i(S, J_i):
    """

    :param S: observation matrix
    :param J_i: neuron i's couplings
    :return: samples for w_i from a polyagamma distribution
    """

    ppg = pypolyagamma.PyPolyaGamma(np.random.randint(2 ** 16))

    T = S.shape[0]
    A = np.ones(T)
    w_i = np.zeros(T)

    ppg.pgdrawv(A, np.dot(S, J_i), w_i)

    return w_i

def sample_J_i(S, C, D_i, w_i, sigma_J):
    N = S.shape[1]

    cov_mat = (1. / sigma_J) * np.identity(N)

    cov = np.linalg.inv(C + cov_mat)
    mean = np.dot(cov, D_i)

    J_i = np.random.multivariate_normal(mean, cov)

    return J_i

def sample_neuron(samp_num, burnin, sigma_J, S, D_i, ro, thin=0, save_all=True):
    """ This function uses the Gibbs sampler to sample from w, gamma and J

    :param samp_num: Number of samples to be drawn
    :param burnin: Number of samples to burn in
    :param sigma_J: variance of the J slab
    :param S: Neurons' activity matrix. Including S0. (T + 1) x N
    :param C: observation correlation matrix. N x N
    :param D_i: time delay correlations of neuron i. N
    :return: samp_num samples (each one of length K (time_steps)) from the posterior distribution for w,x,z.
    """

    T, N = S.shape

    # actual number of samples needed with thining and burin-in
    if (thin != 0):
        N_s = samp_num * thin + burnin
    else:
        N_s = samp_num + burnin

    samples_w_i = np.zeros((N_s, T), dtype=np.float32)
    samples_J_i = np.zeros((N_s, N), dtype=np.float32)

    J_i = np.random.normal(0, sigma_J, N)

    for i in xrange(N_s):
        w_i = sample_w_i(S, J_i)
        C_w_i = calculate_C_w(S, w_i)
        J_i = sample_J_i(S, C_w_i, D_i, w_i, sigma_J)
        samples_w_i[i, :] = w_i
        samples_J_i[i, :] = J_i

    if thin == 0:
        return samples_w_i[burnin:, :], samples_J_i[burnin:, :]
    else:
        return samples_w_i[burnin:N_s:thin, :], samples_J_i[burnin:N_s:thin, :]


# %% [447. Number of Boomerangs](https://leetcode.com/problems/number-of-boomerangs/)
import numpy as np

class Solution:
    def numberOfBoomerangs(self, points: List[List[int]]) -> int:
        n, pts = 0, np.array(points)
        for pt in pts:
            c = collections.Counter([np.linalg.norm(i) for i in pts - pt])
            n += sum(i * (i - 1) for i in c.values())
        return n


# Python program showing Graphical
# representation of tanh() function
import numpy as np
import matplotlib.pyplot as plt

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

in_array = np.linspace(-8, 8, 120)
out_array = sigmoid(in_array)

print("in_array : ", in_array)
print("\nout_array : ", out_array)

# red for numpy.tanh()
plt.figure(figsize=(5, 4))
plt.plot(in_array, out_array, color='blue')
plt.title("Funkcja logistyczna")
plt.xlabel("X")
plt.ylabel("Y")
plt.show()
#
# def ReLU(x):
#     return x * (x > 0)
#
# in_array = np.linspace(-8, 8, 120)
# out_array = ReLU(in_array)
#
# print("in_array : ", in_array)
# print("\nout_array : ", out_array)
#
# # red for numpy.tanh()
# plt.figure(figsize=(5, 4))
# plt.plot(in_array, out_array, color='blue')
# plt.title("Funkcja progowa unipolarna")
# plt.xlabel("X")
# plt.ylabel("Y")
# plt.show()

# in_array = np.linspace(-8, 8, 120)
# out_array = np.tanh(in_array)
#
# print("in_array : ", in_array)
# print("\nout_array : ", out_array)
#
# # red for numpy.tanh()
# plt.figure(figsize=(5, 4))
# plt.plot(in_array, out_array, color='blue')
# plt.title("Funkcja tangensa hiperbolicznego")
# plt.xlabel("X")
# plt.ylabel("Y")
# plt.show()

# in_array = np.linspace(-8, 8, 120)
# out_array = in_array
#
# print("in_array : ", in_array)
# print("\nout_array : ", out_array)

# # red for numpy.tanh()
# plt.figure(figsize=(5, 4))
# plt.plot(in_array, out_array, color='blue')
# plt.title("Funkcja liniowa")
# plt.xlabel("X")
# plt.ylabel("Y")
# plt.show()


from sympy import (Symbol, gamma, expand_func, beta, digamma, diff, conjugate)
from sympy.functions.special.gamma_functions import polygamma
from sympy.core.function import ArgumentIndexError
from sympy.utilities.pytest import raises

def test_beta():
    x, y = Symbol('x'), Symbol('y')

    assert isinstance(beta(x, y), beta)

    assert expand_func(beta(x, y)) == gamma(x)*gamma(y)/gamma(x + y)
    assert expand_func(beta(x, y) - beta(y, x)) == 0  # Symmetric
    assert expand_func(beta(x, y)) == expand_func(beta(x, y + 1) + beta(x + 1, y)).simplify()

    assert diff(beta(x, y), x) == beta(x, y)*(polygamma(0, x) - polygamma(0, x + y))
    assert diff(beta(x, y), y) == beta(x, y)*(polygamma(0, y) - polygamma(0, x + y))

    assert conjugate(beta(x, y)) == beta(conjugate(x), conjugate(y))

    raises(ArgumentIndexError, lambda: beta(x, y).fdiff(3))

    assert beta(x, y).rewrite(gamma) == gamma(x)*gamma(y)/gamma(x + y)


import numpy as np

# This file implements various activation functions that can be used in a layer in a well structured way
# In all of the activations, "forward" states the forward pass and "backward" states the backpropogation on that activation

def sigmoid(z, stage="forward"):
    forward = 1 / (1 + np.exp(-z))
    if stage == "forward":
        return forward
    elif stage == "backward":
        return forward * (1 - forward)
    else:
        raise ValueError("Invalid function stage, should be either \"forward\" or \"backward\"")

def relu(z, stage="forward"):
    if stage == "forward":
        return np.maximum(0.0, z)
    elif stage == "backward":
        return np.where(z >= 0, 1, 0)
    else:
        raise ValueError("Invalid function stage, should be either \"forward\" or \"backward\"")

def leaky_relu(z, stage="forward", negative_slope=0.01):
    if stage == "forward":
        return np.maximum(negative_slope * z, z)
    elif stage == "backward":
        return np.where(z >= 0, 1, negative_slope)
    else:
        raise ValueError("Invalid function stage, should be either \"forward\" or \"backward\"")

def tanh(z, stage="forward"):
    forward = (np.exp(z) - np.exp(-z)) / (np.exp(z) + np.exp(-z))
    if stage == "forward":
        return forward
    elif stage == "backward":
        return 1 - (forward ** 2)
    else:
        raise ValueError("Invalid function stage, should be either \"forward\" or \"backward\"")


from sympy import Symbol, sqrt, pi, sin, cos, cot, exp, I, diff, conjugate
from sympy.functions.special.spherical_harmonics import Ynm, Znm, Ynm_c

def test_Ynm():
    # https://en.wikipedia.org/wiki/Spherical_harmonics
    th, ph = Symbol("theta", real=True), Symbol("phi", real=True)
    from sympy.abc import n, m

    assert Ynm(0, 0, th, ph).expand(func=True) == 1 / (2 * sqrt(pi))
    assert Ynm(1, -1, th, ph) == -exp(-2 * I * ph) * Ynm(1, 1, th, ph)
    assert Ynm(1, -1, th, ph).expand(func=True) == sqrt(6) * sin(th) * exp(-I * ph) / (
        4 * sqrt(pi)
    )
    assert Ynm(1, 0, th, ph).expand(func=True) == sqrt(3) * cos(th) / (2 * sqrt(pi))
    assert Ynm(1, 1, th, ph).expand(func=True) == -sqrt(6) * sin(th) * exp(I * ph) / (
        4 * sqrt(pi)
    )
    assert Ynm(2, 0, th, ph).expand(func=True) == 3 * sqrt(5) * cos(th) ** 2 / (
        4 * sqrt(pi)
    ) - sqrt(5) / (4 * sqrt(pi))
    assert Ynm(2, 1, th, ph).expand(func=True) == -sqrt(30) * sin(th) * exp(
        I * ph
    ) * cos(th) / (4 * sqrt(pi))
    assert Ynm(2, -2, th, ph).expand(func=True) == (
        -sqrt(30) * exp(-2 * I * ph) * cos(th) ** 2 / (8 * sqrt(pi))
        + sqrt(30) * exp(-2 * I * ph) / (8 * sqrt(pi))
    )
    assert Ynm(2, 2, th, ph).expand(func=True) == (
        -sqrt(30) * exp(2 * I * ph) * cos(th) ** 2 / (8 * sqrt(pi))
        + sqrt(30) * exp(2 * I * ph) / (8 * sqrt(pi))
    )

    assert diff(Ynm(n, m, th, ph), th) == (
        m * cot(th) * Ynm(n, m, th, ph)
        + sqrt((-m + n) * (m + n + 1)) * exp(-I * ph) * Ynm(n, m + 1, th, ph)
    )
    assert diff(Ynm(n, m, th, ph), ph) == I * m * Ynm(n, m, th, ph)

    assert conjugate(Ynm(n, m, th, ph)) == (-1) ** (2 * m) * exp(-2 * I * m * ph) * Ynm(
        n, m, th, ph
    )

    assert Ynm(n, m, -th, ph) == Ynm(n, m, th, ph)
    assert Ynm(n, m, th, -ph) == exp(-2 * I * m * ph) * Ynm(n, m, th, ph)
    assert Ynm(n, -m, th, ph) == (-1) ** m * exp(-2 * I * m * ph) * Ynm(n, m, th, ph)

def test_Ynm_c():
    th, ph = Symbol("theta", real=True), Symbol("phi", real=True)
    from sympy.abc import n, m

    assert Ynm_c(n, m, th, ph) == (-1) ** (2 * m) * exp(-2 * I * m * ph) * Ynm(
        n, m, th, ph
    )

def test_Znm():
    # https://en.wikipedia.org/wiki/Solid_harmonics#List_of_lowest_functions
    th, ph = Symbol("theta", real=True), Symbol("phi", real=True)

    assert Znm(0, 0, th, ph) == Ynm(0, 0, th, ph)
    assert Znm(1, -1, th, ph) == (
        -sqrt(2) * I * (Ynm(1, 1, th, ph) - exp(-2 * I * ph) * Ynm(1, 1, th, ph)) / 2
    )
    assert Znm(1, 0, th, ph) == Ynm(1, 0, th, ph)
    assert Znm(1, 1, th, ph) == (
        sqrt(2) * (Ynm(1, 1, th, ph) + exp(-2 * I * ph) * Ynm(1, 1, th, ph)) / 2
    )
    assert Znm(0, 0, th, ph).expand(func=True) == 1 / (2 * sqrt(pi))
    assert Znm(1, -1, th, ph).expand(func=True) == (
        sqrt(3) * I * sin(th) * exp(I * ph) / (4 * sqrt(pi))
        - sqrt(3) * I * sin(th) * exp(-I * ph) / (4 * sqrt(pi))
    )
    assert Znm(1, 0, th, ph).expand(func=True) == sqrt(3) * cos(th) / (2 * sqrt(pi))
    assert Znm(1, 1, th, ph).expand(func=True) == (
        -sqrt(3) * sin(th) * exp(I * ph) / (4 * sqrt(pi))
        - sqrt(3) * sin(th) * exp(-I * ph) / (4 * sqrt(pi))
    )
    assert Znm(2, -1, th, ph).expand(func=True) == (
        sqrt(15) * I * sin(th) * exp(I * ph) * cos(th) / (4 * sqrt(pi))
        - sqrt(15) * I * sin(th) * exp(-I * ph) * cos(th) / (4 * sqrt(pi))
    )
    assert Znm(2, 0, th, ph).expand(func=True) == 3 * sqrt(5) * cos(th) ** 2 / (
        4 * sqrt(pi)
    ) - sqrt(5) / (4 * sqrt(pi))
    assert Znm(2, 1, th, ph).expand(func=True) == (
        -sqrt(15) * sin(th) * exp(I * ph) * cos(th) / (4 * sqrt(pi))
        - sqrt(15) * sin(th) * exp(-I * ph) * cos(th) / (4 * sqrt(pi))
    )


from statistics import mean
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import style
import random

style.use('fivethirtyeight')

#xs = np.array([1,2,3,4,5,6], dtype=np.float64)
#ys = np.array([5,4,6,5,6,7], dtype=np.float64)

def create_dataset(hm, variance, step=2, correlation=False):
    val = 1
    ys = []
    for i in range (hm):
        y = val + random.randrange(-variance, variance)
        ys.append(y)
        if correlation and correlation == 'pos':
            val += step
        elif correlation and correlation == 'neg':
            val -= step
    xs = [i for i in range(len(ys))]
    return np.array(xs, dtype=np.float64), np.array(ys, dtype=np.float64)

def best_fit_slope_and_intercept(xs,ys):
    m = ( ((mean(xs) * mean(ys)) - mean(xs*ys))/ (mean(xs)**2 - mean(xs**2)))
    b = mean(ys) - m*mean(xs)
    return m, b

def squared_error(ys_orig, ys_line):
    return sum((ys_line-ys_orig)**2)

def coefficient_of_determination(ys_orig,ys_line):
    y_mean_line = [mean(ys_orig) for y in ys_orig]
    squared_error_reg = squared_error(ys_orig, ys_line)
    squared_error_y_mean = squared_error(ys_orig, y_mean_line)
    return 1 - (squared_error_reg / squared_error_y_mean)

xs, ys = create_dataset (40,10,2, correlation='pos')
#this generates the random data in format (how many points, variance, step (can be pos or neg), and correlation
m, b = best_fit_slope_and_intercept(xs,ys)

regression_line = [(m*x)+b for x in xs]

predict_x = 8
predict_y = m*predict_x + b

r_squared = coefficient_of_determination(ys, regression_line)
print r_squared

plt.scatter(xs,ys)
#plt.scatter(predict_x, predict_y)
plt.plot(xs, regression_line)
plt.show()


"""
Compute the mean of a data set of 3D rotations.
Performs tangent PCA at the mean.
"""

import matplotlib.pyplot as plt
import numpy as np

import geomstats.visualization as visualization

from geomstats.special_orthogonal_group import SpecialOrthogonalGroup

SO3_GROUP = SpecialOrthogonalGroup(n=3)
METRIC = SO3_GROUP.bi_invariant_metric

N_SAMPLES = 20

def main():
    fig = plt.figure(figsize=(15, 5))

    data = SO3_GROUP.random_uniform(n_samples=N_SAMPLES)
    mean = METRIC.mean(data)

    eigenvalues, tangent_eigenvecs = METRIC.tangent_pca(data, base_point=mean)
    n_eigenvalues = min(N_SAMPLES, SO3_GROUP.dimension)

    explained_variances = np.cumsum(eigenvalues)
    explained_variances = explained_variances / explained_variances[-1]

    ax_eig = fig.add_subplot(131)
    xticks = np.arange(0, n_eigenvalues, 1)
    ax_eig.xaxis.set_ticks(xticks)
    ax_eig.set_title('Eigenvalues in decreasing order')
    ax_eig.plot(xticks, eigenvalues)

    ax_var = fig.add_subplot(132)
    xticks = np.arange(0, n_eigenvalues, 1)
    ax_var.xaxis.set_ticks(xticks)
    ax_var.set_title('Explained variance')
    ax_var.set_xlabel('Number of Principal Components')
    ax_var.plot(xticks, explained_variances)

    ax = fig.add_subplot(133, projection="3d")
    plt.setp(ax,
             xlim=(-1, 1), ylim=(-1, 1), zlim=(-1, 1),
             xlabel="X", ylabel="Y", zlabel="Z")

    visualization.plot(data, ax, space='SO3_GROUP', color='black')
    visualization.plot(mean, ax, space='SO3_GROUP', linewidth=3)
    plt.show()

if __name__ == "__main__":
    main()


'''
Generation of Sweep Signals
===========================

Routines to generate and save to wav linear and exponential sweep signals.

Author: 2018 (c) Robin Scheibler
License: MIT License
'''
# Experiment from 2016/08/31
from __future__ import division, print_function

import numpy as np
import sounddevice as sd
import time
from scipy.io import wavfile

def window(signal, n_win):
    ''' window the signal at beginning and end with window of size n_win/2 '''

    win = np.hanning(2*n_win)

    sig_copy = signal.copy()

    sig_copy[:n_win] *= win[:n_win]
    sig_copy[-n_win:] *= win[-n_win:]

    return sig_copy

def exponential_sweep(T, fs, f_low=50., f_high=22000.):
    '''
    Exponential Sweep

    Parameters
    ----------
    T: float
        Length of the sweep in seconds
    fs: float
        Sampling frequency
    f_low: float
        Lowest frequency in the sweep
    f_high: float
        Highest frequency in the sweep
    '''

    f1 = f_low     # Start frequency in [Hz]
    f2 = f_high  # End frequency in [Hz]
    Ts = 1./fs   # Sampling period in [s]
    N = np.floor(T/Ts)
    n  = np.arange(0, N, dtype='float64')  # Sample index

    om1 = 2*np.pi*f1
    om2 = 2*np.pi*f2

    x_exp = 0.95*np.sin(om1*N*Ts / np.log(om2/om1) * (np.exp(n/N*np.log(om2/om1)) - 1))
    x_exp = x_exp[::-1]

    return x_exp

def linear_sweep(T, fs, f_low=50., f_high=22000.):
    '''
    Linear Sweep

    Parameters
    ----------
    T: float
        Length of the sweep in seconds
    fs: float
        Sampling frequency
    f_low: float
        Lowest frequency in the sweep
    f_high: float
        Highest frequency in the sweep
    '''

    f1 = f_low     # Start frequency in [Hz]
    f2 = f_high  # End frequency in [Hz]
    Ts = 1./fs   # Sampling period in [s]
    N = np.floor(T/Ts)
    n  = np.arange(0, N, dtype='float64')  # Sample index

    om1 = 2*np.pi*f1
    om2 = 2*np.pi*f2

    x_exp = 0.95*np.sin(2 * np.pi * 0.5 * (f1 + (f2 - f1) * n / N) * n / fs)
    x_exp = x_exp[::-1]

    return x_exp

if __name__ == "__main__":

    # Signal parameters
    fs = 48000
    f_lo = 20
    f_hi = fs / 2 - 50.
    T = 3.

    # Setup device
    sd.default.device = 2
    sd.default.samplerate = fs
    sd.default.channels = (8,1)

    # create sweep signal
    sweep = np.zeros(int(T*fs))

    # save the sweep to deconvolve later
    sweep_exp = exponential_sweep(T, fs, f_low=f_lo, f_high=f_hi)
    sweep_exp_win = window(sweep_exp, int(fs*0.01))

    wavfile.write("sweep_exp.wav", fs, sweep_exp)
    wavfile.write("sweep_exp_win.wav", fs, sweep_exp_win)

    sweep_lin = linear_sweep(T, fs, f_low=f_lo, f_high=f_hi)
    sweep_lin_win = window(sweep_lin, int(fs*0.01))

    wavfile.write('sweep_lin.wav', fs, sweep_lin)
    wavfile.write('sweep_lin_win.wav', fs, sweep_lin_win)


#function HammingG
#input: a number r
#output: G, the generator matrix of the (2^r-1,2^r-r-1) Hamming code

def hammingGeneratorMatrix(r):
    n = 2**r-1

    #construct permutation pi
    pi = []
    for i in range(r):
        pi.append(2**(r-i-1))
    for j in range(1,r):
        for k in range(2**j+1,2**(j+1)):
            pi.append(k)

    #construct rho = pi^(-1)
    rho = []
    for i in range(n):
        rho.append(pi.index(i+1))

    #construct H'
    H = []
    for i in range(r,n):
        H.append(decimalToVector(pi[i],r))

    #construct G'
    GG = [list(i) for i in zip(*H)]
    for i in range(n-r):
        GG.append(decimalToVector(2**(n-r-i-1),n-r))

    #apply rho to get Gtranpose
    G = []
    for i in range(n):
        G.append(GG[rho[i]])

    #transpose
    G = [list(i) for i in zip(*G)]

    return G

#function decimalToVector
#input: numbers n and r (0 <= n<2**r)
#output: a string v of r bits representing n

def decimalToVector(n,r):
    v = []
    for s in range(r):
        v.insert(0,n%2)
        n //= 2
    return v

#Preliminary setup ########################################

import numpy as np

def int_to_bin(num,w):
    #input number to be converted into binary and width of list

    bin_lst = []

    #turns binary string into list of integers
    for j in np.binary_repr(num, width = w):
        bin_lst.append(int(j))
    return bin_lst

def bin_to_int(bin_lst):
    #input binary list to be converted into integer

    #reverse the list
    bin_lst = bin_lst[::-1]
    total = 0
    for i in range(len(bin_lst)):
        total += bin_lst[i]*2**i
    return total

#Q1 #######################################################

def message(a):
    l = len(a)
    r = 2
    k = 2**r - r - 1
    while l > k - r:
        r += 1
        k = 2**r - r -1
    m = int_to_bin(l,r)
    m.extend(a)
    m.extend([0]*(k-l-r))
    return m

#Q2 #######################################################

def hammingEncoder(m):
    len_m = len(m)
    r = 2
    len_vect = 2**r - r - 1
    while len_m != len_vect:
        r += 1
        len_vect = 2**r - r - 1
        if len_vect > len_m:
            return []
    G = hammingGeneratorMatrix(r)
    c = np.dot(m,G) % 2
    c = list(c)
    return c

#Q3 #######################################################

def hammingDecoder(v):
    len_v = len(v)
    r = 2
    len_vect = 2**r - 1
    while len_v != len_vect:
        r += 1
        len_vect = 2**r - 1
        if len_vect > len_v:
            return []

    #Apply Syndrome decoding with HT as the transpose of the matrix H
    HT = []
    for i in range(1,len_v+1):
	    lst = int_to_bin(i,r)
	    HT.append(lst)
    HT = np.array(HT)
    vHT = np.dot(v, HT) % 2
    if 1 not in vHT:
        return v
    i = bin_to_int(vHT)
    v[i-1] = (v[i-1] + 1) % 2
    return(v)

#Q4 #######################################################

def messageFromCodeword(c):
    len_c = len(c)
    r = 2
    len_vect = 2**r - 1
    while len_c != len_vect:
        r += 1
        len_vect = 2**r - 1
        if len_vect > len_c:
            return []
    c = list(c)
    for i in range(r):
        #replace data in indices of list to be deleted with a dummy variable, in this case the number "3"
        c[2**i-1] = 3
    while 3 in c:
        #remove all dummy variables from list to get message
        c.remove(3)
    m = c
    return m

#Q5 #######################################################

def dataFromMessage(m):
    len_m = len(m)
    r = 2
    len_vect = 2**r - r - 1
    while len_m != len_vect:
        r += 1
        len_vect = 2**r - r - 1
        if len_vect > len_m:
            return []
    bin_str = m[:r]
    l = bin_to_int(bin_str)
    if r+l > len_vect:
        return []
    a = m[r:r+l]
    return a

#Q6 #######################################################

def repetitionEncoder(m,n):
    c = m*n
    return c

#Q7 #######################################################

def repetitionDecoder(v):
    num_zeros = v.count(0)
    num_ones = v.count(1)
    if num_zeros == num_ones:
        return []
    elif num_zeros > num_ones:
        m = [0]
    else:
        m = [1]
    return m

#end ######################################################


from tqdm import tqdm
import numpy as np
import math

from AKDPRFramework.dl.activations import Sigmoid
from AKDPRFramework.utils.dataops import diag

class L1():
    def __init__(self, alpha):
        self.alpha = alpha

    def __call__(self, w):
        return self.alpha * np.linalg.norm(w)

    def grad(self, w):
        return self.alpha * np.sign(w)

class L2():
    def __init__(self, alpha):
        self.alpha = alpha

    def __call__(self, w):
        return self.alpha * 0.5 * w.T.dot(w)

    def grad(self, w):
        return self.alpha * w

class L1L2():
    def __init__(self, alpha, l1_ratio=0.5):
        self.alpha = alpha
        self.l1_ratio = l1_ratio

    def __call__(self, w):
        l1_contr = self.l1_ratio * np.linalg.norm(w)
        l2_contr = (1 - self.l1_ratio) * 0.5 * w.T.dot(w)
        return self.alpha * (l1_contr + l2_contr)

    def grad(self, w):
        l1_contr = self.l1_ratio * np.sign(w)
        l2_contr = (1 - self.l1_ratio) * w
        return self.alpha * (l1_contr + l2_contr)

class Regression(object):
    def __init__(self, iterations, learning_rate):
        self.iterations = iterations
        self.learning_rate = learning_rate
        self.regularization = lambda x: 0

    def random_initialization(self, number_of_features):
        '''
        Random initialization of weights.
        '''
        lim = 1 / math.sqrt(number_of_features)
        self.w = np.random.uniform(-lim, lim, (number_of_features,))

    def fit(self, X, y):
        # Insert constant ones for bias weights
        X = np.insert(X, 0, 1, axis=1)
        self.training_errors = []        # Record of MSE errors during training
        self.random_initialization(number_of_features=X.shape[1])

        for _ in tqdm(range(self.iterations)):
            y_pred = X.dot(self.w)
            # MSE
            mse = np.mean(0.5 * (y - y_pred) ** 2 + self.regularization(self.w))
            self.training_errors.append(mse)
            # Gradient Calculation
            grad_w = -(y - y_pred).dot(X) + self.regularization.grad(self.w)
            # Update the weights
            self.w -= self.learning_rate * grad_w

    def predict(self, X):
        # Insert constant ones for bias weights
        X = np.insert(X, 0, 1, axis=1)
        y_pred = X.dot(self.w)
        return y_pred

    def get_weights(self):
        """
        Returns weights at the end of training.
        Call like this
        >>> model = LinearRegression()
        >>> weights_before_training = model.get_weights()     # Random Initialization
        >>> model.fit(X, y)
        >>> weights = model.get_weights() # Updated weights after training
        """
        return self.w

class LinearRegression(Regression):
    """
    Linear Regression model

        Args:
            - ``iterations``: The number of training iterations the algorithm will tune the weights for.
            - ``learning_rate``: The step length that will be used when updating the weights.
            - ``gradient_descent``: True or False depending if gradient descent should be used when training. If False then we use batch optimization by least squares.
    """

    def __init__(self, iterations=1000, learning_rate=1e-3, gradient_descent=True):
        super(LinearRegression, self).__init__(iterations=iterations,
                                               learning_rate=learning_rate)
        self.gradient_descent = gradient_descent
        self.regularization = lambda x: 0
        self.regularization.grad = lambda x: 0

    def fit(self, X, y):
        # If not gradient descent => Least squares approximation of w
        if not self.gradient_descent:
            # Insert constant ones for bias weights
            X = np.insert(X, 0, 1, axis=1)
            # Calculate weights by least squares (using Moore-Penrose pseudoinverse)
            U, S, V = np.linalg.svd(X.T.dot(X))
            S = np.diag(S)
            X_sq_reg_inv = V.dot(np.linalg.pinv(S)).dot(U.T)
            self.w = X_sq_reg_inv.dot(X.T).dot(y)
        else:
            super(LinearRegression, self).fit(X, y)

class LogisticRegression:
    """
    Logistic Regression Class

        Args:
            - ``learning_rate``: Mention the learning Rate for the training. Defaults to 1e-3
            - ``gradient_descent``: True or False depending upon training with gradient or batch optimization by least squares.
    """

    def __init__(self, learning_rate=1e-3, gradient_descent=True):
        self.params = None
        self.learning_rate = learning_rate
        self.gradient_descent = gradient_descent
        self.sigmoid = Sigmoid()

    def _initialize_parameters(self, X):
        number_of_features = np.shape(X)[1]
        lim = 1 / math.sqrt(number_of_features)

        self.params = np.random.uniform(-lim, lim, (number_of_features,))

    def fit(self, X, y, n_iterations=1000):
        self._initialize_parameters(X)
        # Tune parameters for n iterations
        for _ in tqdm(range(n_iterations)):
            # Make a new prediction
            y_pred = self.sigmoid(X.dot(self.params))
            if self.gradient_descent:
                # Move against the gradient of the loss function with
                # respect to the parameters to minimize the loss
                self.params -= self.learning_rate * -(y - y_pred).dot(X)
            else:
                # Make a diagonal matrix of the sigmoid gradient column vector
                diag_gradient = diag(self.sigmoid.gradient(X.dot(self.params)))
                # Batch opt:
                self.param = np.linalg.pinv(X.T.dot(diag_gradient).dot(X)).dot(X.T).dot(diag_gradient.dot(X).dot(self.param) + y - y_pred)

    def predict(self, X):
        y_pred = np.round(self.sigmoid(X.dot(self.param))).astype(int)
        return y_pred


import math
import numpy as np
import random
import time
import pylab
import matplotlib.pyplot as plt

from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
from itertools import accumulate
from pprint import pprint

from task1 import RandomVariableSensor, RandomVariableTester

def timer(func: object):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        func()
        print(f'\n\nLead time: {time.time() - start_time}s')
    return wrapper

class GenerationRandomVariables:

    def __init__(self, number_amount: int) -> None:
        self.number_amount = number_amount

    def density(self, x: float, y: float) -> float:
        return math.exp(-x - y)

    def neumann_method(self, a: float, b: float) -> list:
        return [self.generate_xy(a, b) for i in range(self.number_amount)]

    def generate_xy(self, a: float, b: float) -> tuple:
        m, k = 2**31, 16807
        start = random.randint(10**8, 10**9)
        value = random.randint(10**8, 10**9)

        xy = RandomVariableSensor(2).mult_congruent_method(start, m, k)
        w = random.random() * 1
        xy = (a + xy[0]*(b - a), xy[1])

        return xy if w <= self.density(*xy) else self.generate_xy(a, b)

    def correlation(
            self, x: list, y: list,
            m_x: float, m_y: float,
            d_x: float, d_y: float):
        x, y = np.array(x), np.array(y)
        return (sum(x * y) / len(x) - m_x * m_y) / (d_x * d_y) ** (1/2)

    def generate_matrix(self, m: int, n: int) -> list:
        numbers = np.array([i for i in range(1, m * n + 1)])
        random.shuffle(numbers)
        return np.array_split(numbers / sum(numbers), m)

    def generate_vector(self, size: int) -> list:
        return [i for i in range(size)]

    def distribution_func(self, probabilities: list) -> list:
        return [0] + list(accumulate(probabilities))

    def generate_matrix_y(self, matrix: list, x_probabilities: list) -> list:
        return [row / x_probabilities[i] for i, row in enumerate(matrix)]

    def discrete_random_variables(
            self, v_x: list, v_y: list,
            f_x: list, f_y: list):
        x = [random.random() for i in range(self.number_amount)]
        y = [random.random() for i in range(self.number_amount)]

        new_x, new_y = [], []

        for i, j in zip(x, y):
            new_x.append(
                v_x[f_x.index(next(filter(lambda x: i < x, f_x))) - 1])

            z = new_x[0]

            new_y.append(
                v_y[f_y[z].index(next(filter(lambda y: j < y, f_y[z]))) - 1])

        drv = list(zip(new_x, new_y))

        matrix = np.zeros((len(v_x), len(v_y)))
        empir_matrix = np.copy(matrix)

        for pair in drv:
            matrix[pair[0]][pair[1]] += 1
            empir_matrix[pair[0]][pair[1]] += (1 / self.number_amount)

        return matrix, empir_matrix, drv

    def construction_distrib_func(self, vector: list, dist: list) -> tuple:
        x = sum([[i, j, np.nan] for i, j in zip(vector[:-1], vector[1:])], [])
        y = sum([[i, i, np.nan] for i in dist[1:]], [])
        return x + [x[-2], x[-2] + (x[-2] - x[-3]), np.nan], y

@timer
def main():
    start, quantity = 19941995, 100_000
    m, k = 2**31, 16807
    a, b = 0, 1
    intervals = 10
    s = 2
    m, n = 4, 4

    generator = GenerationRandomVariables(quantity)
    sensor = RandomVariableSensor(quantity)

    new_xy = generator.neumann_method(a, b)
    x, y = zip(*new_xy)

    hits_rate_x = RandomVariableTester(intervals, x).hits_rate()
    hits_rate_y = RandomVariableTester(intervals, y).hits_rate()

    m_x, d_x, h_x = RandomVariableTester(intervals, x).uniformity_testing()
    m_y, d_y, h_y = RandomVariableTester(intervals, y).uniformity_testing()

    print('\nRelative hit rates X:')
    pprint(hits_rate_x)
    print(f'Sum of probabilities: {round(sum(hits_rate_x.values()), 10)}')

    print('\nRelative hit rates Y:')
    pprint(hits_rate_y)
    print(f'Sum of probabilities: {round(sum(hits_rate_y.values()), 10)}')

    print('\nExpected value, dispersion, histogram border for X:')
    print(m_x, d_x, h_x)

    print('\nExpected value, dispersion, histogram border for Y:')
    print(m_y, d_y, h_y)

    m_t = -2 * math.exp(-1) + 1
    d_t = -5 * math.exp(-1) + 2 - m_t ** 2

    print('\nTheoretical expected value, dispersion for X:')
    print(f'M[x]={m_t}')
    print(f'D[x]={d_t}')

    print('\nTheoretical expected value, dispersion for Y:')
    print(f'M[y]={m_t}')
    print(f'D[y]={d_t}')

    print('\nCorrelation coefficient X:')
    print(RandomVariableTester(intervals, x).independence_testing(s))

    print('\nCorrelation coefficient Y:')
    print(RandomVariableTester(intervals, y).independence_testing(s))

    print('\nCorrelation coefficient XY:')
    print(generator.correlation(x, y, m_x, m_y, d_x, d_y))

    fig, axs = plt.subplots(1, 2, sharey=True, tight_layout=True)
    axs[0].set_yticks(np.arange(0, 1, 0.02), minor=False)

    axs[0].bar(
        sorted([i[0] for i in hits_rate_x.keys()]),
        hits_rate_x.values(),
        width=1/intervals, align='edge')

    axs[0].set_title('X')
    axs[1].bar(
        sorted([i[0] for i in hits_rate_y.keys()]),
        hits_rate_y.values(),
        width=1/intervals, align='edge')
    axs[1].set_title('Y')

    x, y = np.arange(0, 1, 0.1), np.arange(0, 1, 0.1)
    x, y = np.meshgrid(x, y)
    fig = pylab.figure()
    axes = Axes3D(fig)
    axes.plot_surface(x, y, np.exp(-x - y), rstride=1, cstride=1, cmap=cm.jet)

    matrix_xy = generator.generate_matrix(m, n)

    print('\nProbability matrix XY:')
    pprint(matrix_xy)
    print(f'Sum of probabilities XY: {sum(sum(matrix_xy))}')

    vector_x = generator.generate_vector(m)
    vector_y = generator.generate_vector(n)

    print(f'\nVector X:\n{vector_x}')
    print(f'\nVector Y:\n{vector_y}')

    x_probabilities = np.array([sum(i) for i in matrix_xy])
    print(f'\nProbability X:\n{x_probabilities}')

    distribution_x = generator.distribution_func(x_probabilities)
    print(f'\nDistribution function F(X):\n{distribution_x}')

    y_propabities = generator.generate_matrix_y(matrix_xy, x_probabilities)
    print('\nProbability matrix Y:')
    pprint(y_propabities)

    distribution_y = [generator.distribution_func(i) for i in y_propabities]
    print('\nDistribution function F(Y):')
    pprint(distribution_y)

    drv = generator.discrete_random_variables(
        vector_x, vector_y,
        distribution_x, distribution_y)

    print(f'\nDRV matrix:\n{drv[0]}')
    print(f'\nEmpirical matrix:\n{drv[1]}')

    xy = np.array(drv[2])
    x, y = xy[:, 0], xy[:, 1]
    m_x, d_x = np.mean(x), sum(x ** 2 - np.mean(x) ** 2) / len(x)
    m_y, d_y = np.mean(y), sum(y ** 2 - np.mean(y) ** 2) / len(y)

    print(f'\nExpected value, dispersion X:\n{m_x} {d_x}')
    print(f'\nExpected value, dispersion Y:\n{m_y} {d_y}')

    fig, axs = plt.subplots(1, 1, sharey=True, tight_layout=True)
    x, y = generator.construction_distrib_func(vector_x, distribution_x)
    axs.set_yticks(np.arange(0, 1.05, 0.05), minor=False)
    axs.plot(x, y, 'o-')
    axs.set_title('Distribution function F(X):')

    fig, axs = plt.subplots(
        1, len(distribution_y),
        sharey=True, tight_layout=True)

    for i, distribution in enumerate(distribution_y):
        x, y = generator.construction_distrib_func(vector_y, distribution)
        axs[i].set_yticks(np.arange(0, 1.05, 0.05), minor=False)
        axs[i].plot(x, y, 'o-')
        axs[i].set_title(f'Distribution function F(Y)[{i}]:')

    plt.show()
    pylab.show()

if __name__ == '__main__':
    main()


import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np

def linearoperation(P,L):
    J=[]
    for i in range(0,len(img)):
        temp=[]
        for j in range(0,len(img[i])):
            a=int(P*img[i][j]+L)
            if (a>255):
                a=255
            if (a<0):
                a=0
            temp.append(a)
        J.append(temp)
    return J

def histogram(img):
    arr=[]
    for i in range(0,len(img)):
        for j in range(0,len(img[i])):
            arr.append(img[i][j])
    plt.hist(arr, color = 'blue', edgecolor = 'black',bins = int(1800))
    plt.title("Histogram")
    plt.xlim(0, 255)

def max2d(img):
    maxarr=[]
    for i in range(0,len(img)):
        maxarr.append(max(img[i]))

    return max(maxarr)

def min2d(img):
    minarr=[]
    for i in range(0,len(img)):
        minarr.append(min(img[i]))

    return min(minarr)

img = mpimg.imread('space.jpg')
img = img[:,:,0]

A=min2d(img)
B=max2d(img)

res = linearoperation( 255/(B-A), (-255/(B-A))*A )

print(min2d(res),max2d(res))

plt.figure(1)
histogram(img)

plt.figure(2)
histogram(res)

plt.show()


#!/usr/bin/env python
# coding=utf-8
"""
Script to forecast pricing with geometric brownian motion
"""
from __future__ import division

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

def calc_price_geo_brown_motion(price_init, timespan, timestep, mu, sigma):
    """
    Forecast pricing with geometric brownian motion

    Parameters
    ----------
    price_init : float
        Initial price
    timespan : int
        Total timespan (e.g. in years)
    timestep : int
        Time discretization (e.g. in years)
    mu : float
        Drift / mean of price change per timestep
    sigma : float
        Shock / standard deviation of price change per timestep

    Returns
    -------
    brown_motion : array-like
        Array with new pricing data values
    """

    steps = int(round(timespan / timestep))
    time_arranged = np.linspace(0, timespan, steps)
    w = np.random.standard_normal(size=steps)
    w = np.cumsum(w) * np.sqrt(timestep)
    x = (mu - 0.5 * sigma ** 2) * time_arranged + sigma * w
    brown_motion = price_init * np.exp(x)

    return brown_motion

if __name__ == '__main__':

    timespan = 20

    mu_min = -0.05
    mu_max = 0.05

    sigma = 0.05
    price_init = 0.26
    timestep = 1

    nb_runs = 50

    for i in range(nb_runs):

        mu = random.uniform(mu_min, mu_max)

        prices = calc_price_geo_brown_motion(timestep=timestep,
                                             timespan=timespan,
                                             mu=mu, sigma=sigma,
                                             price_init=price_init)

        plt.plot(prices)

    plt.title('Brownian motion price change for ' + str(nb_runs) + ' runs')
    plt.xlabel('Time in years')
    plt.ylabel('Price in Euro')
    plt.show()


import scipy.integrate as spi
import numpy as np

print('Double integral computed by SciPy nquad')
print('Example 2-02 nquad')
print('Integral of 2xye^-xy from y=1 to x-->+inf and from x=1 to x-->+inf')

integrand = lambda x, y : 2 * x * y * np.exp(-x * y)
ya = 1.
yb = 5.

result, error = spi.nquad(integrand, [[1, np.inf],[1, np.inf]])
print('Result is ', result, 'with error ', error)


# -*- coding: utf-8 -*-
"""poisson-distribution-and-poisson-process-tutorial.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1u-t6oSxbMd2FrzaIMimXnPMR5dfV6Gel

# Diving Into the Poisson Distribution and Poisson Process

* Tutorial: https://news.towardsai.net/pd
* Github: https://github.com/towardsai/tutorials/tree/master/poisson-distribution-process

A TV Assembly unit performs a defects analysis task to understand the number of defects that could happen for a given defective TV. Using past quality and audit data is noted that 12 defects are marked on an average for a defective TV, calculate below:

* The probability that a defective laptop has exactly 5 defects.
* The probability that a defective laptop has less than 5 defects.
"""

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

n = np.arange(0,30)
n

rate = 12
poisson = stats.poisson.pmf(n,rate)

poisson

poisson[5]

poisson[0] + poisson[1] + poisson[2] + poisson[3] + poisson[4]

plt.plot(n,poisson, 'o-')
plt.show()


"""Softmax model."""

import numpy as np

class Softmax:
    def __init__(self, n_class: int, lr: float, epochs: int, reg_const: float):
        """Initialize a new classifier.

        Parameters:
            n_class: the number of classes
            lr: the learning rate
            epochs: the number of epochs to train for
            reg_const: the regularization constant
        """
        self.w = None
        self.lr = lr
        self.epochs = epochs
        self.reg_const = reg_const
        self.n_class = n_class

    def calc_gradient(self, X_train: np.ndarray, y_train: np.ndarray) -> np.ndarray:
        """Calculate gradient of the softmax loss.

        Inputs have dimension D, there are C classes, and we operate on
        mini-batches of N examples.

        Parameters:
            X_train: a numpy array of shape (N, D) containing a mini-batch
                of data
            y_train: a numpy array of shape (N,) containing training labels;
                y[i] = c means that X[i] has label c, where 0 <= c < C

        Returns:
            gradient with respect to weights w; an array of same shape as w
        """
        dw = np.zeros((np.shape(X_train)[1], self.n_class))
        for i in range(X_train.shape[0]):
            pred = np.dot(X_train[i], self.w)
            pred -= np.max(pred)
            prob = np.divide(np.exp(pred), np.sum(np.exp(pred), axis=0))
            for j in range(self.n_class):
                if j == y_train[i]:
                    dw[:, j] = np.add(dw[:, j], (prob[j] - 1) * X_train[i])
                else:
                    dw[:, j] = np.add(dw[:, j], prob[j] * X_train[i])
        dw /= X_train.shape[0]
        return dw + self.w * self.reg_const

    def train(self, X_train: np.ndarray, y_train: np.ndarray):
        """Train the classifier.

        Hint: operate on mini-batches of data for SGD.

        Parameters:
            X_train: a numpy array of shape (N, D) containing training data;
                N examples with D dimensions
            y_train: a numpy array of shape (N,) containing training labels
        """
        self.w = np.random.normal(0, .01, size=(np.shape(X_train)[1], self.n_class))
        batch_size = 200
        for _ in range(self.epochs):
            idx = np.sort(np.random.choice(X_train.shape[0],size=batch_size,replace=False))
            dw = self.calc_gradient(X_train[idx], y_train[idx])
            self.w -= self.lr * dw

    def predict(self, X_test: np.ndarray) -> np.ndarray:
        """Use the trained weights to predict labels for test data points.

        Parameters:
            X_test: a numpy array of shape (N, D) containing testing data;
                N examples with D dimensions

        Returns:
            predicted labels for the data in X_test; a 1-dimensional array of
                length N, where each element is an integer giving the predicted
                class.
        """
        pred = np.argmax(np.dot(X_test, self.w), axis = 1)
        return pred