import numpy as np
import matplotlib.pyplot as plt
pi = np.pi

# Page 15 Figure 1.6
# Reflection coefficient at a planer boundary between two dielectric media with n_1=1.0 and n_2=1.5
n_1 = 1.0
n_2 = 1.5
phi_1 = np.linspace(0, 90, 10000) # incident angle (unit: degree)
# TE (s) wave
Gamma_perp = (n_1 * np.cos(phi_1 * pi / 180) - np.sqrt(n_2**2 - n_1**2 * np.sin(phi_1 * pi / 180)**2)) / (n_1 * np.cos(phi_1 * pi / 180) + np.sqrt(n_2**2 - n_1**2 * np.sin(phi_1 * pi / 180)**2)) # TE wave reflection coefficient
plt.plot(phi_1, Gamma_perp, 'k-')
# TM (p) wave
Gamma_parallel = (n_2**2 * np.cos(phi_1 * pi / 180) - n_1 * np.sqrt(n_2**2 - n_1**2 * np.sin(phi_1 * pi / 180)**2)) / (n_2**2 * np.cos(phi_1 * pi / 180) + n_1 * np.sqrt(n_2**2 - n_1**2 * np.sin(phi_1 * pi / 180)**2)) # TM wave reflection coefficient
plt.plot(phi_1, Gamma_parallel, 'k--')
plt.xlim(0, 90)
plt.ylim(-1, 1)
plt.xlabel('$\phi_1~/~^{\circ}$', {'size': 16})
plt.ylabel('$\Gamma$', {'size': 16})
plt.legend(['$\Gamma_{\perp}$', '$\Gamma_{\parallel}$'])
plt.title('$n_1=1.0$, $n_2=1.5$')
plt.grid()
plt.show()

# Page 15 Figure 1.7
# Amplitude of reflection coefficient at a planar boundary between two dielectric media with n_1=1.5 and n_2=1.0
n_1 = 1.5 + 0j
n_2 = 1.0 + 0j
# TE (s) wave
Gamma_perp_abs = np.abs((n_1 * np.cos(phi_1 * pi / 180) - np.sqrt(n_2**2 - n_1**2 * np.sin(phi_1 * pi / 180)**2)) / (n_1 * np.cos(phi_1 * pi / 180) + np.sqrt(n_2**2 - n_1**2 * np.sin(phi_1 * pi / 180)**2))) # TE wave reflection coefficient absolute value
plt.plot(phi_1, Gamma_perp_abs, 'k-')
# TM (p) wave
Gamma_parallel_abs = np.abs((n_2**2 * np.cos(phi_1 * pi / 180) - n_1 * np.sqrt(n_2**2 - n_1**2 * np.sin(phi_1 * pi / 180)**2)) / (n_2**2 * np.cos(phi_1 * pi / 180) + n_1 * np.sqrt(n_2**2 - n_1**2 * np.sin(phi_1 * pi / 180)**2))) # TM wave reflection coefficient absolute value
plt.plot(phi_1, Gamma_parallel_abs, 'k--')
plt.xlim(0, 90)
plt.ylim(0, 1.01)
plt.xlabel('$\phi_1~/~^{\circ}$', {'size': 16})
plt.ylabel('$|\Gamma|$', {'size': 16})
plt.legend(['$|\Gamma_{\perp}|$', '$|\Gamma_{\parallel}|$'])
plt.title('$n_1=1.5$, $n_2=1.0$')
plt.grid()
plt.show()

# Page 16 Figure 1.8
# Phase of reflection coefficient at a planar boundary between two dielectric media with n_1=1.5 and n_2=1.0
n_1 = 1.5 + 0j
n_2 = 1.0 + 0j
# TE (s) wave
Phi_perp = np.arctan(np.sqrt(n_1**2 * np.sin(phi_1 * pi / 180)**2 - n_2**2) / (n_1 * np.cos(phi_1 * pi / 180))) * 180 / pi # TE wave reflection phase
plt.plot(phi_1, Phi_perp, 'k-')
# TM (s) wave
Phi_parallel = np.real(np.arctan(n_1 * np.sqrt(n_1**2 * np.sin(phi_1 * pi / 180)**2 - n_2**2) / (n_2**2 * np.cos(phi_1 * pi / 180)))) * 180 / pi # TM wave reflection phase
Phi_parallel[Phi_parallel == 0.] = 90
plt.plot(phi_1, Phi_parallel, 'k--')
plt.xlim(0, 90)
plt.ylim(0, 90.5)
plt.xlabel('$\phi_1~/~^{\circ}$', {'size': 16})
plt.ylabel('$\Phi~/~^{\circ}$', {'size': 16})
plt.legend(['$\Phi_{\perp}$', '$\Phi_{\parallel}$'])
plt.title('$n_1=1.5$, $n_2=1.0$')
plt.grid()
plt.show()


import numpy as np
from scipy import linalg

def _residual_linear_fit(x, cov):
    coeffX = linalg.lstsq(cov.T, x)[0]
    return x - coeffX.dot(cov)

def r2(trg, srcLst):
    return 1 - np.var(_residual_linear_fit(trg, np.array(srcLst))) / np.var(trg)

def quadratic_triplet_decomp_1D(src1, src2, trg):
    '''
    :param src1:  First source channel, 1D array of floats
    :param src2:  Second source channel, 1D array of floats
    :param trg:   Target channel, 1D array of floats
    :return:      4 relative explained variances: unique source 1, unique source 2, redundant, synergistic
    '''

    # Compute first two centered moments
    c1 = src1 - np.mean(src1)
    c2 = src2 - np.mean(src2)
    cTrg = trg - np.mean(trg)
    c12 = c1 * c2

    # # Fit, compute ExpVariduals and related variances
    rev1 = r2(cTrg, [c1])
    rev2 = r2(cTrg, [c2])

    revFull = r2(cTrg, [c1, c2, c12])
    revM1   = r2(cTrg, [c2, c12])
    revM2   = r2(cTrg, [c1, c12])
    revM12  = r2(cTrg, [c1, c2])

    # Compute individual effects
    unq1 = revFull - revM1
    unq2 = revFull - revM2
    syn12 = revFull - revM12
    # red12 = revFull - unq1 - unq2 - syn12
    red12 = rev1 + rev2 - revM12

    return np.clip([unq1, unq2, red12, syn12], 0, None)  # Clip negative entries as they are meaningless


scores = [3.0, 1.0, 0.2]

import numpy as np

def softmax(x):
	"""Compute softmax values for each sets of scores in x."""
	exp = np.exp(x)
	return exp/np.sum(exp,0)

print(softmax([10*s for s in scores])) #The probabilities get closer to 0 or 1
print(softmax([s/10 for s in scores])) #The probabilities get closer to the uniform distribution

# Plot softmax curves
import matplotlib.pyplot as plt
x = np.arange(-2.0, 6.0, 0.1)
scores = np.vstack([x, np.ones_like(x), 0.2 * np.ones_like(x)])

plt.plot(x, softmax(scores).T, linewidth=2)
plt.show()


import numpy

def write_shadow_surface(s,xx,yy,outFile='presurface.dat'):
    """
      write_shadowSurface: writes a mesh in the SHADOW/presurface format
      SYNTAX:
           out = write_shadowSurface(z,x,y,outFile=outFile)
      INPUTS:
           z - 2D array of heights
           x - 1D array of spatial coordinates along mirror width.
           y - 1D array of spatial coordinates along mirror length.

      OUTPUTS:
           out - 1=Success, 0=Failure
           outFile - output file in SHADOW format. If undefined, the
                     file is names "presurface.dat"

    """
    out = 1

    try:
       fs = open(outFile, 'w')
    except IOError:
       out = 0
       print ("Error: can\'t open file: "+outFile)
       return
    else:
        # dimensions
        fs.write( repr(xx.size)+" "+repr(yy.size)+" \n" )
        # y array
        for i in range(yy.size):
            fs.write(' ' + repr(yy[i]) )
        fs.write("\n")
        # for each x element, the x value and the corresponding z(y)
        # profile
        for i in range(xx.size):
            tmps = ""
            for j in range(yy.size):
                tmps = tmps + "  " + repr(s[j,i])
            fs.write(' ' + repr(xx[i]) + " " + tmps )
            fs.write("\n")
        fs.close()
        print ("write_shadow_surface: File for SHADOW "+outFile+" written to disk.")

# calculate a Gaussian bump
# create an array of 2 cm length
npoints = 51
length = 2.0

x = numpy.linspace(-0.5*length,0.5*length,npoints)
y = numpy.linspace(-0.5*length,0.5*length,npoints)

# create a surface with pixel value its distance to the center

x1 = numpy.outer(x,numpy.ones(y.size))
y1 = numpy.outer(numpy.ones(x.size),x)

r = numpy.sqrt( (x1)**2 + (y1)**2 )

# define bump FWHM
bump_fwhm = 0.5 # cm

#pizel sizes
pixel = length / (npoints-1)

# sigma value corresponding to FWHM
sigma = (bump_fwhm / pixel) / ( 2*numpy.sqrt(2*numpy.log(2)) )
print("sigma: ",sigma)

# evaluate the 2D Gaussian
z = numpy.exp(-((r/pixel)/2/sigma)**2)
# give a heigth of 1 microns
z = z * 1e-4

#write file for SHADOW
write_shadow_surface(z,x,y,outFile='bump.dat')

#
#plot
#
from matplotlib import pylab as plt
plt.figure(1)
plt4 = plt.imshow(z.T*1e4,extent=[-0.5*length,0.5*length,-0.5*length,0.5*length])
plt.title('Gaussian Bump')
plt.xlabel('H [cm]')
plt.ylabel('V [cm]')
cbar = plt.colorbar(plt4 , format="%.2f")
cbar.ax.set_ylabel('Deformation [um]')

plt.show()


import numpy as np
from functools import lru_cache as cache

@cache(maxsize=None)
def _assert_valid_param(param):
    A, B, C = param
    assert not np.isclose(A ** 2 + B ** 2, 0), "invalid line param."
    return np.array(param) / np.sqrt(A ** 2 + B ** 2)

def assert_valid_param(param):
    return _assert_valid_param(tuple(param))

@cache(maxsize=None)
def _fit_line(pts):
    P = np.array(pts)
    P = np.hstack((P, np.ones((len(P), 1))))
    assert np.linalg.matrix_rank(P) >= 2, f"points to fit line are not valid: {P}"
    u, s, vt = np.linalg.svd(P)
    param = assert_valid_param(vt[-1])
    res = np.linalg.norm(P.dot(param)) / len(P)

    return param, res

def fit_line(pts):
    return _fit_line(tuple(tuple(pt) for pt in pts))

def dist_pts_to_line(pts, param):
    param = assert_valid_param(param)
    P = np.array([pt for pt in pts])
    P = np.hstack((P, np.ones((len(P), 1))))
    dists = np.abs(P.dot(param))

    return dists

def assert_pts_in_line(pts, param, atol=1.):
    dists = np.array(dist_pts_to_line(pts, param))
    assert np.all(dists < atol)

@cache(maxsize=None)
def _find_pt_in_line(param):
    A, B, C = assert_valid_param(param)
    if np.abs(A) > np.abs(B):
        x, y = -C / A, 0
    else:
        x, y = 0, -C / B

    return np.array([x, y])

def find_pt_in_line(param):
    return _find_pt_in_line(tuple(param))

def project_pts_on_line(pts, param):
    param = assert_valid_param(param)
    P = np.array([pt for pt in pts])
    pt0 = np.array(find_pt_in_line(param))
    e = np.array([-param[1], param[0]])
    alpha = (P - pt0).dot(e)
    P_proj = np.outer(alpha, e) + pt0

    assert P_proj.ndim == 2 and alpha.ndim == 1, f"internal error occored when project pts to line: {P_proj}, {alpha}"

    return P_proj, alpha

def find_lines_intersect(params):
    P = []
    for param in params:
        param = assert_valid_param(param)
        P.append(param)
    P = np.array(P)
    assert np.linalg.matrix_rank(P) >= 2, "lines do not intersect"
    u, s, vh = np.linalg.svd(P)
    x, y, _ = vh[-1] / vh[-1][-1]
    hpt = np.array([x, y, 1])
    dist = np.abs(P.dot(hpt)).mean()

    return np.array([x, y]), dist

@cache(maxsize=None)
def is_pt_in_line_seg(eps, pt, pt1, pt2):
    param, _ = fit_line([pt1, pt2])
    _, alphas = project_pts_on_line([pt, pt1, pt2], param)
    dist = dist_pts_to_line([pt], param)[0]
    return (dist < eps * 2) and (np.min(alphas[1:]) <= alphas[0] <= np.max(alphas[1:]))


import numpy as np
from numpy import linalg as la

np.set_printoptions(precision=3)

class DiGraph:
    """A class for representing directed graphs via their adjacency matrices.

    Attributes:
        (fill this out after completing DiGraph.__init__().)
    """
    # Problem 1
    def __init__(self, A, labels=None):
        """Modify A so that there are no sinks in the corresponding graph,
        then calculate Ahat. Save Ahat and the labels as attributes.

        Parameters:
            A ((n,n) ndarray): the adjacency matrix of a directed graph.
                A[i,j] is the weight of the edge from node j to node i.
            labels (list(str)): labels for the n nodes in the graph.
                If None, defaults to [0, 1, ..., n-1].

        Examples
        ========
        >>> A = np.array([[0, 0, 0, 0],[1, 0, 1, 0],[1, 0, 0, 1],[1, 0, 1, 0]])
        >>> G = DiGraph(A, labels=['a','b','c','d'])
        >>> G.A_hat
        array([[0.   , 0.25 , 0.   , 0.   ],
               [0.333, 0.25 , 0.5  , 0.   ],
               [0.333, 0.25 , 0.   , 1.   ],
               [0.333, 0.25 , 0.5  , 0.   ]])
        >>> steady_state_1 = G.linsolve()
        >>> { k: round(steady_state_1[k],3) for k in steady_state_1}
        {'a': 0.096, 'b': 0.274, 'c': 0.356, 'd': 0.274}
        >>> steady_state_2 = G.eigensolve()
        >>> { k: round(steady_state_2[k],3) for k in steady_state_2}
        {'a': 0.096, 'b': 0.274, 'c': 0.356, 'd': 0.274}
        >>> steady_state_3 = G.itersolve()
        >>> { k: round(steady_state_3[k],3) for k in steady_state_3}
        {'a': 0.096, 'b': 0.274, 'c': 0.356, 'd': 0.274}
        >>> get_ranks(steady_state_3)
        ['c', 'b', 'd', 'a']
        """
        self.A_hat = np.array(A, dtype=float, copy=True)
        height, width = A.shape

        if labels == None:
            self.Labels = [str(i) for i in range(0,width)]
        elif len(labels) != width:
            raise ValueError("Number of labes differ from number of nodes in graph")
        else:
            self.Labels = labels

        c = 0
        while c < width:            #Iterate columns
            r = 0
            sum = 0
            while r < height:       #Iterate column
                sum = sum + A[r,c]
                r = r + 1
            if sum == 0:            #If sum is 0 (node is a sink)
                self.A_hat[:,c] = 1 / height     #Set values of column to 1/height
            else:
                self.A_hat[:,c] = A[:,c] / sum   #Divide with sum
            c = c + 1

    def linsolve(self, epsilon=0.85):
        """Compute the PageRank vector using the linear system method.

        Parameters:
            epsilon (float): the damping factor, between 0 and 1.

        Returns:
            dict(str -> float): A dictionary mapping labels to PageRank values.
        """
        n = self.A_hat.shape[0]         #Get side length
        I = np.eye(n)                   #Identity of appropriate size
        one = np.ones(n)                #Vector of appropriate size

        A = I - epsilon * self.A_hat    #Calculate coefficients
        one = ((1-epsilon) / n) * one   #Calculate result of coefficients

        solution = la.solve(A,one)      #Calculate solution

        #Return dictionary mapping
        return {lab: rank for (lab, rank) in zip(self.Labels, solution)}

    def eigensolve(self, epsilon=0.85):
        """Compute the PageRank vector using the eigenvalue method.
        Normalize the resulting eigenvector so its entries sum to 1.

        Parameters:
            epsilon (float): the damping factor, between 0 and 1.

        Return:
            dict(str -> float): A dictionary mapping labels to PageRank values.
        """
        raise NotImplementedError("eigensolve Incomplete")

    def itersolve(self, epsilon=0.85, maxiter=100, tol=1e-12):
        """Compute the PageRank vector using the iterative method.

        Parameters:
            epsilon (float): the damping factor, between 0 and 1.
            maxiter (int): the maximum number of iterations to compute.
            tol (float): the convergence tolerance.

        Return:
            dict(str -> float): A dictionary mapping labels to PageRank values.
        """
        raise NotImplementedError("Problem 2 Incomplete")

def get_ranks(d):
    """Construct a sorted list of labels based on the PageRank vector.

    Parameters:
        d (dict(str -> float)): a dictionary mapping labels to PageRank values.

    Returns:
        (list) the keys of d, sorted by PageRank value from greatest to least.
    """
    raise NotImplementedError("Problem 3 Incomplete")

# Task 2
def rank_websites(filename="web_stanford.txt", epsilon=0.85):
    """Read the specified file and construct a graph where node j points to
    node i if webpage j has a hyperlink to webpage i. Use the DiGraph class
    and its itersolve() method to compute the PageRank values of the webpages,
    then rank them with get_ranks().

    Each line of the file has the format
        a/b/c/d/e/f...
    meaning the webpage with ID 'a' has hyperlinks to the webpages with IDs
    'b', 'c', 'd', and so on.

    Parameters:
        filename (str): the file to read from.
        epsilon (float): the damping factor, between 0 and 1.

    Returns:
        (list(str)): The ranked list of webpage IDs.

    Examples
    ========
    >>> print(rank_websites()[0:5])
    ['98595', '32791', '28392', '77323', '92715']
    """
    raise NotImplementedError("Task 2 Incomplete")

# Task 3
def rank_uefa_teams(filename, epsilon=0.85):
    """Read the specified file and construct a graph where node j points to
    node i with weight w if team j was defeated by team i in w games. Use the
    DiGraph class and its itersolve() method to compute the PageRank values of
    the teams, then rank them with get_ranks().

    Each line of the file has the format
        A,B
    meaning team A defeated team B.

    Parameters:
        filename (str): the name of the data file to read.
        epsilon (float): the damping factor, between 0 and 1.

    Returns:
        (list(str)): The ranked list of team names.

    Examples
    ========
    >>> rank_uefa_teams("psh-uefa-2018-2019.csv",0.85)[0:5]
    ['Liverpool', 'Ath Madrid', 'Paris SG', 'Genk', 'Barcelona']
    """
    raise NotImplementedError("Task 3 Incomplete")

if __name__ == "__main__":
    import doctest
    doctest.testmod()


import numpy as np
import matplotlib.pyplot as plt
from tqdm import trange

class CFG:
    n = 10
    mean = 0.0
    variance = 1.0
    t = 1000
    esp = [0, 0.01, 0.05, 0.1, 0.15, 0.2]
    c_param = [0.01, 0.03, 0.05, 0.1, 0.2, 0.3, 0.5, 1, 2, 3]
    n_try = 2000

class bandit():
    def __init__(self, m, v):
        self.m = m
        self.v = v
        self.mean = 0.0
        self.cnt = 0

    def reset(self):
        self.mean = 0.0
        self.cnt = 0

    def get_UCB_score(self, t, c):
        if self.cnt == 0:
            sq = 10000
        else:
            sq = c * np.sqrt(np.log(t)/self.cnt)
        return self.mean + sq

    def get_reward(self):
        reward = self.v * np.random.randn() + self.m
        return reward

    def update(self, reward):
        self.cnt += 1
        self.mean = self.mean + 1/self.cnt * (reward - self.mean)

def get_result_eps(e):
    bandits = [bandit(np.random.randn(),CFG.variance) for i in range(CFG.n)]
    res = []
    for _ in range(CFG.t):
        if (np.random.random()<e):
            choose = np.random.choice(CFG.n)
        else:
            choose = np.argmax([ban.mean for ban in bandits])

        val = bandits[choose].get_reward()
        res.append(val)
        bandits[choose].update(val)
    # print(res)
    return res

def get_result_UCB(c):
    bandits = [bandit(np.random.randn(),CFG.variance) for i in range(CFG.n)]
    res = []
    for t in range(CFG.t):
        # if (np.random.random()<e):
        #     choose = np.random.choice(CFG.n)
        # else:
        choose = np.argmax([ban.get_UCB_score(t, c) for ban in bandits])

        val = bandits[choose].get_reward()
        res.append(val)
        bandits[choose].update(val)
    # print(res)
    return res

plt.figure(figsize=(20, 10))

# for e in CFG.esp:
#     res = np.zeros(CFG.t)
#     for tr in trange(CFG.n_try):
#         res += get_result_eps(e)
#     res /= CFG.n_try
#     plt.plot(res, label = f'epsilon e = {e}')

for c in CFG.c_param:
    res = np.zeros(CFG.t)
    for tr in trange(CFG.n_try, desc=f'UCB c = {c}'):
        res += get_result_UCB(c)
    res /= CFG.n_try
    plt.plot(res, label = f'UCB c = {c}')

plt.xlabel('step')
plt.ylabel('average reward')
plt.legend()
plt.savefig('figure_2_2_a.png')
plt.show()


#!/usr/bin/env python3

import matplotlib.pyplot as plt
import numpy as np
from numpy.lib.stride_tricks import as_strided

def pool2d(A, kernel_size, stride, padding, pool_mode="max"):
    """
    2D Pooling

    Parameters:
        A: input 2D array
        kernel_size: int, the size of the window
        stride: int, the stride of the window
        padding: int, implicit zero paddings on both sides of the input
        pool_mode: string, 'max' or 'avg'
    """
    # Padding
    A = np.pad(A, padding, mode="constant")

    # Window view of A
    output_shape = (
        (A.shape[0] - kernel_size) // stride + 1,
        (A.shape[1] - kernel_size) // stride + 1,
    )
    kernel_size = (kernel_size, kernel_size)
    # numpy strides are offsets of the axises, since the dtype is np.int64
    # each element take 8 bytes
    assert A.strides == (32, 8)
    A_w = as_strided(
        A,
        shape=output_shape + kernel_size,
        strides=(stride * A.strides[0], stride * A.strides[1]) + A.strides,
    )
    # Create a strides view with smaller strides steps in the first two axises.
    assert A_w.strides == (64, 16, 32, 8)
    A_w = A_w.reshape(-1, *kernel_size)

    # Return the result of pooling
    if pool_mode == "max":
        return A_w.max(axis=(1, 2)).reshape(output_shape)
    elif pool_mode == "avg":
        return A_w.mean(axis=(1, 2)).reshape(output_shape)

A = np.array([[1, 1, 2, 4], [5, 6, 7, 8], [3, 2, 1, 0], [1, 2, 3, 4]], dtype=np.int64)

print(pool2d(A, kernel_size=2, stride=2, padding=0, pool_mode="max"))

def stride_1d(line_in, L, W, D, stride):
    window = as_strided(
        line_in, shape=((L - W) // stride + 1, W), strides=(stride * D, D)
    )
    # plt.plot(window)
    # plt.show()
    return window

L = 100  # sample count
W = 10  # window width
D = 8  # size of the datum in type, dtype np.int64 uses 8 bytes
signal = np.sin(np.linspace(0, 20, L))
noise = np.random.uniform(-0.1, 0.1, L)
line_in = signal + noise
stride_1d(line_in, L, W, D, 2)

L = 8
B = np.arange(L, dtype=np.int32)
window = stride_1d(B, L, 2, 4, 2)
np.testing.assert_allclose(
    window, np.array([[0, 1], [2, 3], [4, 5], [6, 7]], dtype=np.int32)
)


# coding: utf-8

# # Function phasecorr

# ## Synopse

# Computes the phase correlation of two images.
#
# - **g = phasecorr(f,h)**
#     - **OUTPUT**
#         - **g**: Image. Phase correlation map.
#     - **INPUT**
#         - **f**: Image. n-dimensional.
#         - **h**: Image. n-dimensional.

# ## Description

# Computes the phase correlation of two n-dimensional images. Notice that the input images must have
# the same dimension and size. The output is an image with same dimension and size of the input image.
# This output image is a phase correlation map were the point of maximum value corresponds to the
# translation between the input images.
#

# In[2]:

import numpy as np
def phasecorr(f,h):
    F = np.fft.fftn(f)
    H = np.fft.fftn(h)
    T = F * np.conjugate(H)
    R = T/np.abs(T)
    g = np.fft.ifftn(R)
    return g.real

# ## Examples

# In[1]:

testing = (__name__ == "__main__")
if testing:
    import numpy as np
    import sys,os
    ea979path = os.path.abspath('../../')
    if ea979path not in sys.path:
        sys.path.append(ea979path)
    import ea979.src as ia

    get_ipython().magic('matplotlib inline')
    import matplotlib.image as mpimg

# ### Example 1

# Show that the point of maximum correlation for two equal images is the origin.
#
#

# In[4]:

if testing:
    # 2D example
    f1 = mpimg.imread("../data/cameraman.tif")
    noise = np.random.rand(f1.shape[0],f1.shape[1])
    f2 = ia.normalize(ia.ptrans(f1,(-1,50)) + 300 * noise)
    g1 = ia.phasecorr(f1,f2)
    i = np.argmax(g1)
    row,col = np.unravel_index(i,g1.shape)
    v = g1[row,col]
    print(np.array(f1.shape) - np.array((row,col)))

# In[ ]:

if testing:
    print('max at:(%d, %d)' % (row,col))

    ia.adshow(ia.normalize(f1), "input image")
    ia.adshow(ia.normalize(f2), "input image")
    ia.adshow(ia.normalize(g1), "Correlation peak at (%d,%d) with %d" % (row,col,v))

# ### Exemplo 3

# Show how to perform Template Matching using phase correlation.

# In[ ]:

if testing:
    # 2D example
    w1 = f1[27:69,83:147]

    h3 = np.zeros_like(f1)
    h3[:w1.shape[0],:w1.shape[1]] = w1
    noise = np.random.rand(h3.shape[0],h3.shape[1])
    h3 = ia.normalize(h3 + 100 * noise)

    h3 = ia.ptrans(h3, - np.array(w1.shape, dtype=int)//2)

    g9 = ia.phasecorr(f1,h3)

    p3 = np.unravel_index(np.argmax(g9), g9.shape)
    g11 = ia.ptrans(h3,p3)

    ia.adshow(ia.normalize(f1), "Original 2D image - Cameraman")
    ia.adshow(ia.normalize(w1), "2D Template")
    ia.adshow(ia.normalize(h3), "2D Template same size as f1")
    ia.adshow(ia.normalize(g9), "Cameraman - Correlation peak: %s"%str(p3))
    ia.adshow(ia.normalize((g11*2.+f1)/3.), "Template translated mixed with original image")

# ## Equation

# We calculate the discrete Fourier transform of the input images $f$ and $h$:
#
# $$    F = \mathcal{F}(f); $$
#
# $$    H = \mathcal{F}(h). $$
#
# Next, the following equation compute $R$
#
# $$    R = \dfrac{F H^*}{|F H^*|}. $$
#
# Finally, the result is given by applying the inverse discrete Fourier transform to $R$
#
# $$    g = \mathcal{F}^{-1}(R). $$
#
# The displacement (not implemented in this function) can be obtained by:
#
# $$ (row, col) = arg max\{g\} $$

# ## See also

# - `ia636:iadft iadft` -- Discrete Fourier Transform.
# - `ia636:iaidft iaidft` -- Inverse Discrete Fourier Transform.
# - `ia636:iaptrans iaptrans` -- Periodic translation.
# - `ia636:iamosaic iamosaic` -- Creates a mosaic of images from the input volume (3D).
# - `ia636:iacorrdemo iacorrdemo` -- Illustrate the Template Matching technique.

# ## References

# - E. De Castro and C. Morandi "Registration of Translated and Rotated Images Using Finite Fourier Transforms", IEEE Transactions on pattern analysis and machine intelligence, Sept. 1987.

# ## Contributions

# - André Luis da Costa, 1st semester 2011


# AP Ruymgaart
import numpy as np
from numpy import sin, cos, pi
from skimage import measure
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def fun(x, y, z): return cos(x) + cos(y) + cos(z)
def gaussian(X,Y,Z,px,py,pz,w=0.1): return np.exp(-w*(X-px)**2 - w*(Y-py)**2 - w*(Z-pz)**2  )

def isosurface(vol, s=0.1, limits=[-10,10,-10,10,-10,10], labels=['x','y','z']):

	[a,b,c,d,e,f] = limits
	verts, faces, _, _ = measure.marching_cubes_lewiner(vol, spacing=(s, s, s))
	fig = plt.figure()
	ax = fig.add_subplot(111, projection='3d')

	if True:
		ax.axes.set_xlim3d(left=a, right=b)
		ax.axes.set_ylim3d(bottom=c, top=d)
		ax.axes.set_zlim3d(bottom=e, top=f)
		ax.set_xlabel(labels[0])
		ax.set_ylabel(labels[1])
		ax.set_zlabel(labels[2])

	ax.plot_trisurf(verts[:, 0], verts[:,1], faces, verts[:, 2], cmap='Spectral', lw=1)
	plt.show()

# NOTE: gives fist min,max (there need not be a single min or max!)
# also if there are 2 or more minima or maxima but one is just slightly larger
# only that one will show
def minMaxInfo(M, Mesh=None, F=None, bAbs=False):

	if bAbs: M = np.absolute(M)

	lmn = np.argmin(M)
	arrIndxs = np.where(M == np.min(M))
	nMin = len(arrIndxs[0])
	if nMin > 1: print('\t* NO SINGLE MIN: Number of points equal to minimum', nMin)
	else: print('\tSINGLE minimum')
	argMn = np.unravel_index(lmn, M.shape)

	lmx = np.argmax(M)
	arrIndxs = np.where(M == np.max(M))
	nMax = len(arrIndxs[0])
	if nMin > 1: print('\t* NO SINGLE MAX: Number of points equal to maximum', nMax)
	else: print('\tSINGLE maximum')
	argMx = np.unravel_index(lmx, M.shape)

	print('\tShape', M.shape)
	print('\tFirst min. at', lmn, argMn, 'value', M[argMn], 'First max. at', lmx, argMx, 'value', M[argMx])

	return 	[argMn, argMx, np.min(M), np.max(M)]

# Scalar valued function of vector position F(x,y,z)
# this function only picks up one maximum (in case there are multiple)
#
def minMaxInfoScalarFuncOfVec(Mesh, F, verbose=False, bPlot=False):

	if verbose: print('minMaxInfoScalarFuncOfVec')

	[X,Y,Z] = Mesh

	lmx = np.argmax(abs(F))
	argMx = np.unravel_index(lmx, F.shape)

	fx,fy,fz = F[argMx[0],:,argMx[2]], F[:,argMx[1],argMx[2]], F[argMx[0],argMx[1],:]
	gx,gy,gz = X[argMx[0],:,argMx[2]], Y[:,argMx[1],argMx[2]], Z[argMx[0],argMx[1],:]

	fMxLin = np.argmax(np.absolute(F))
	argMxF = np.unravel_index(fMxLin, F.shape)

	xIndMx = np.argmax(fx)
	yIndMx = np.argmax(fy)
	zIndMx = np.argmax(fz)
	if verbose: print('\t', xIndMx,yIndMx,zIndMx, fx[xIndMx], fy[yIndMx], fz[zIndMx] )

	if bPlot:
		f, (ax1, ax2, ax3) = plt.subplots(3, 1)
		ax1.plot(gx, fx, label='x')
		ax2.plot(gy, fy, label='y')
		ax3.plot(gz, fz, label='z')
		ax1.axvline(x=gx[xIndMx], c='r')
		ax2.axvline(x=gy[yIndMx], c='r')
		ax3.axvline(x=gz[zIndMx], c='r')
		plt.show()

	return [xIndMx,yIndMx,zIndMx,gx,gy,gz,fx,fy,fz]

if __name__ == '__main__':

	Lx,Ly,Lz,n=10,20,30, 64
	#y,z = x,x
	x = np.linspace(-Lx,Lx,n) #.astype('complex')
	y = np.linspace(-Ly,Ly,n)
	z = np.linspace(-Lz,Lz,n)
	#y,z = x,x
	[X,Y,Z] = np.meshgrid(x,y,z)

	print('---- Mesh grid X info: ----')
	minMaxInfo(X)
	print('\n---- Mesh grid Y info: ---- ')
	minMaxInfo(Y)
	print('\n---- Mesh grid Z info: ---- ')
	minMaxInfo(Z)

	#x, y, z = pi*np.mgrid[-1:1:31j, -1:1:31j, -1:1:31j]
	#vol = fun(x, y, z)
	#isosurface(vol)

	print(x[1]-x[0], 20/64, x[0], x[63])
	F = gaussian(X,Y,Z,5,-7,-2)
	print('\n---- Scalar valued function of vector position G(x,y,z) info: ---- ')
	minMaxInfoScalarFuncOfVec([X,Y,Z], F, verbose=True, bPlot=True)

	argMx = np.unravel_index(np.argmax(abs(F)), F.shape)
	print(argMx, x[argMx[1]], y[argMx[0]], z[argMx[2]])

	F += gaussian(X,Y,Z,8,8,8)
	minMaxInfoScalarFuncOfVec([X,Y,Z], F, verbose=True, bPlot=True)

	isosurface(F, s=0.15)


import numpy as np

def cosine_similarity(a, b):
    a = np.squeeze(a)
    b = np.squeeze(b)
    assert a.shape[0] == b.shape[0]
    return np.sum(a * b) / (np.linalg.norm(a) * np.linalg.norm(b))


import numpy as np

earth_mean_radius_meters = 6371000

def haversine(radius, phi_0, phi_1, theta_0, theta_1):
    """Computes the length of the arc between the given points on the surface
    of the sphere, using the haversine formula.

    Params
    ------
    radius    Radius of the sphere.
    phi_0     Latitude of first point, in radians.
    phi_1     Latitude of second point, in radians.
    theta_0   Longitude of first point, in radians.
    theta_1   Longitude of second point, in radians.
    """
    return 2.0 * radius * np.arcsin(
        np.sqrt(
            np.sin((phi_1 - phi_0) / 2.0) ** 2 +
            np.cos(phi_0) * np.cos(phi_1) * np.sin((theta_1 - theta_0) / 2.0) ** 2
        )
    )


import warnings
import collections

import numpy as np
import statsmodels.api as sm
from scipy.stats import t
from sklearn.utils import check_array

def dm_test(e1, e2, alternative='two_sided', h=1, power=2):
    """
    The Diebold-Mariano test for predictive accuracy.

    The DM test compares the forecast accuracy of different forecasting methods. This implementation is designed
    according to R implementation which could be found here: https://pkg.robjhyndman.com/forecast/reference/dm.test.html

    :param e1: forecast errors from the first method
    :param e2: forecast errors from the second method
    :param alternative: str specifying the alternative hypothesis, 'two_sided' (default one), 'less' or 'greater'
    :param h: forcasting horizon used in calculating errors (e1, e2)
    :param power: power used in the loss function (usually 1 or 2)
    :return: named tuple containing DM statistic and p-value

    Examples
    --------
    >>> e1 = np.random.rand(100)
    >>> e2 = np.random.rand(100)
    >>> dm_test(e1, e2, h=1, power=2, alternative='two_sided')
    dm_test_result(dm_stat=1.023019805162665, p_value=0.30879181140619405)

    """
    alternatives = ['two_sided', 'less', 'greater']
    if alternative not in alternatives:
        raise ValueError(f"alternative must be one of {alternatives}")

    e1 = check_array(e1, ensure_2d=False)
    e2 = check_array(e2, ensure_2d=False)

    d = np.abs(e1) ** power - np.abs(e2) ** power
    n = d.shape[0]
    d_cov = sm.tsa.acovf(d, fft=True, nlag=h - 1)
    d_var = (d_cov[0] + 2 * d_cov[1:].sum()) / n

    if d_var > 0:
        dm_stat = np.mean(d) / np.sqrt(d_var)
    elif h == 1:
        raise ValueError('Variance of DM statistic is zero')
    else:
        warnings.warn('Variance is negative, using horizon h=1', RuntimeWarning)
        return dm_test(e1, e2, alternative=alternative, h=1, power=power)

    # The corrected statistic suggested by HLN
    k = ((n + 1 - 2 * h + h / n * (h - 1)) / n) ** 0.5
    dm_stat *= k

    if alternative == 'two_sided':
        p_value = 2 * t.cdf(-abs(dm_stat), df=n - 1)
    else:
        p_value = t.cdf(dm_stat, df=n - 1)
        if alternative == 'greater':
            p_value = 1 - p_value

    dm_test_result = collections.namedtuple('dm_test_result', ['dm_stat', 'p_value'])
    return dm_test_result(dm_stat=dm_stat, p_value=p_value)

import numpy,from scipy,import statsmodels


import numpy as np

def decompLU(A):
	L = np.eye(np.shape(A)[0]) #matriz L com a dimensao de A com a diagonal principla igual a 1
	U = np.zeros(np.shape(A)) #matriz U full zero
	lim = np.shape(A)[1] #pegando o tamanho da coluna de A (matriz quadrada)
	sum = 0

	for i in range(0, lim):
		for j in range(0, lim):
			sum = 0
			#Parte do somatorio de U que vai de 0 a i-1 e acumula L[i][k]*U[k][j] sendo k a variavel iteradora
			for k in range (0, i):
				sum += L[i,k]*U[k,j]
			U[i,j] = A[i,j] - sum #atribuindo o valor de U[i][j]

		for j in range (0, lim):
			sum = 0
			#Parte do somatorio de L que vai de 0 a i-1 e acumula L[j][k]*U[k][i] sendo k a variavel iteradora
			for k in range (0, i):
				sum+= L[j,k]*U[k,i]
			L[j,i] = (A[j,i] - sum)/U[i,i] #atribuindo o valor L[i][j]

	return L, U #retornando o resultado

def solveLU(L, U, b):
	#Resolvendo o sistema A*x = b
	#Como A = L*U, entao (L*U)*x = b
	#Vamos fazer L*(U*x) = b e U*x=y, portanto L*y=b
	#Logo, para encontrarmos o resultado resolvemos o sistema U*x = y

	y = np.zeros(np.shape(b)) #matriz coluna y com a mesma dimensao de b
	x = np.zeros(np.shape(b)) #matriz coluna x (resultado do sistema) com a mesma dimensao de b

	for i in range (0, np.shape(b)[0]): #resolvendo o sistema  L*y=b
		for j in range (0,i):
			y[i] -= y[j]*L[i,j]
		y[i]+=b[i]

	for i in range (np.shape(b)[0]-1, -1, -1): #resolvendo o sistema U*x=b
		for j in range (np.shape(b)[0]-1, i, -1):
			x[i] -= x[j]*U[i,j]
		x[i]+=y[i]
		x[i]/=U[i,i]

	return x #retornando o resultado

def inverse_method(A, error_tol):
	lim = 10000 #definindo um limite de iteracoes
	error = np.inf #atribuindo infinito para o erro
	n = A.shape[1] # pegando a dimensao da matriz A quadrada
	y0 = np.zeros(n)
	y0[0] = 1 #chute inicial normalizado

	(L, U) = decompLU(A) #fazendo a decomposicao LU de A

	for k in range (0, lim):
		xk = solveLU(L, U, y0) #resolvendo o sistema Ax^k = y^(k-1) com decomposicao LU
		yk = xk/np.linalg.norm(xk) #normalizando xk
		error = np.abs(np.abs(y0.dot(yk))-1) #teste de alinhamento
		if error <= error_tol: #se o erro for menor que a tolerancia recebida como parametro, pode parar a iteracao
			break
		y0 = yk

	lambda_1 = y0.dot(A.dot(y0)) #calculando o lambda_1 = y^k * (A * y^k)

	return lambda_1, y0 #retornando o autovalor e seu autovetor associado

A = np.array([[12, 2, 3],
              [ 2, 3, 5],
              [ 3, 5,-2]], dtype='double')

(D, V) = np.linalg.eig(A) #usando uma funcao pronta para calcular todos os autovalores
print('Método do Python: {0:.9f}'.format(np.min(abs(D))))
(autovalor, autovetor) = inverse_method(A,0.000000001) #usando o metodo da potencia inversa implementado com erro em torno de 10^-9
print('Método Implementado: {0:.9f}'.format(autovalor))


##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
##                    Numpy Basics
## %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# create a 2D array
import numpy as np

arr2D = np.array([[1,4,6],[2,5,7]])

# getting information about arr2D
print(arr2D.size) # returns 6, the no. of items
print(arr2D.ndim) # returns 2, the no. of dimensions
print(arr2D.shape) # returns tuple(2,3) corresponding to 2 rows & 3 columns

# create a 1D array
arr1D = np.array([1,4,6])

# getting information about arr1D
print(arr1D.size) # returns 3, the no. of items
print(arr1D.ndim) # returns 1, the no. of dimensions
print(arr1D.shape) # returns tuple(3,) corresponding to 3 items

#%% creating numpy arrays
# creating sequence of numbers
arr1 = np.arange(3, 6) # same as Python range function; results in array([3,4,5])
arr2 = np.arange(3, 9, 2) # the 3rd argument defines the step size; results in array([3,5,7])
arr3 = np.linspace(1,7,3) # creates evenly spaced 3 values from 1 to 9; results in array([1,4,7])

# creating special arrays
arr4 = np.ones((2,1)) # array of shape (2,1) with all items as 1
arr5 = np.zeros((2,2)) # all items as zero; often used as placeholder array at beginning of script
arr6 = np.eye(2) # diagonal items as 1

# adding axis to existing arrays (e.g., converting 1D array to 2D array)
print(arr1[:, np.newaxis])
arr7 = arr1[:, None] # same as above

# combining / stacking arrays
print(np.hstack((arr1, arr2))) # horizontally stacks passed arrays
print(np.vstack((arr1, arr2))) # vertically stacks passed arrays
print(np.hstack((arr5,arr4))) # array 4 added as a column into arr5
print(np.vstack((arr5,arr6))) # rows of array 6 added onto arr5

#%% basic numpy functions
print(arr2D.sum(axis=0))
print(arr2D.sum(axis=1))

#%% indexing arrays
# accessing individual items
print(arr2D[1,2]) # returns 7

# slicing
arr8 = np.arange(10).reshape((2,5)) # rearrange the 1D array into shape (2,5)
print((arr8[0:1,1:3]))
print((arr8[0,1:3])) # note that a 1D array is returned here instead of the 2D array above

# accessing entire row or column
print(arr8[1]) # returns 2nd row as array([5,6,7,8,9]); same as arr8[1,:]
print(arr8[:, 4]) # returns items of 3rd column as a 1D array

# extract a subarray from arr8 and modify it
arr8_sub = arr8[:, :2] # columns 0 and 1 from all rows
arr8_sub[1, 1] = 1000
print(arr8) # arr8 gets modified as well!!

# use copy method for a separate copy
arr8 = np.arange(10).reshape((2,5))
arr8_sub2 = arr8[:, :2].copy()
arr8_sub2[1, 1] = 100
print(arr8)

# Fancy indexing
# combination of simple and fancy indexing
arr8_sub3 = arr8[:, [0, 1]] # note how columns are indexed via a list
arr8_sub3[1, 1] = 100 # arr8_sub3 becomes same as arr8_sub2 but arr8 is not modified here
print(arr8)

# use boolean mask to select subarray
arr8_sub4 = arr8[arr8 > 5] # returns array([6,7,8,9]), i.e., all values > 5
arr8_sub4[0] = 0 # again, arr8 is not affected
print(arr8)

#%% vectorized operations
vec1 = np.array([1,2,3,4])
vec2 = np.array([5,6,7,8])
vec_sum = vec1 + vec2 # returns array([6,8,10,12]); no need to loop through index 0 to 3

# slightly more complex operation (computing distance between vectors)
vec_distance = np.sqrt(np.sum((vec1 - vec2)**2)) # vec_distance = 8.0


# Copyright 2021 Neuron-AI GitHub Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
from typing import Iterable
from warnings import warn

from numpy import array, sqrt

def _check_distance_params(x, y, kwargs):
    x = array(x).reshape(1, -1)  # to make them 1D
    y = array(y).reshape(1, -1)

    if x.shape != y.shape and kwargs.get("suppress_warnings") != True:
        warn(
            UserWarning(
                "using sequences which do not contain equivalent numbers of items can result in unexpected results."
            )
        )

    return x,y

def euclidean_distance(x: Iterable, y: Iterable, **kwargs) -> float:
    """Euclidean distance function, returns the distance between x and y
    Pass "supress_warnings=True" to the function to avoid warnings when x and y have different lengths.

    Parameters
    ----------
    x : Iterable
            The first item to be calucated with
    y : Iterable
            The second item to be calucated with

    Returns
    -------
    float
            The euclidean distance
    """
    x, y = _check_distance_params(x, y, kwargs)

    return sqrt(sum((x - y) ** 2 for x, y in zip(x[0], y[0])))

def manhattan_distance(x, y, **kwargs):
    """Manhattan distance function, returns the manhattan distance between x and y
    Pass "supress_warnings=True" to the function to avoid warnings when x and y have different lengths.

    Parameters
    ----------
    x : Iterable
            The first item to be calucated with
    y : Iterable
            The second item to be calucated with

    Returns
    -------
    float
            The manhattan distance
    """
    x, y = _check_distance_params(x, y, kwargs)

    return sum(abs(x - y) for x, y in zip(x[0], y[0]))

distance_functions = {"euclidean": euclidean_distance, "manhattan": manhattan_distance}


"""Determine if a list of matrices are POVM elements."""
from typing import List
import numpy as np

from toqito.matrix_props import is_positive_semidefinite

def is_povm(mat_list: List[np.ndarray]) -> bool:
    r"""
    Determine if a list of matrices constitute a valid set of POVMs [WikPOVM]_.

    A valid set of measurements are defined by a set of positive semidefinite operators

    .. math::
         \{P_a : a \in \Gamma\} \subset \text{Pos}(\mathcal{X}),

    indexed by the alphabet :math:`\Gamma` of measurement outcomes satisfying the constraint that

    .. math::
        \sum_{a \in \Gamma} P_a = I_{\mathcal{X}}.

    Examples
    ==========

    Consider the following matrices:

    .. math::
        M_0 =
        \begin{pmatrix}
            1 & 0 \\
            0 & 0
        \end{pmatrix}, \qquad
        M_1 =
        \begin{pmatrix}
            0 & 0 \\
            0 & 1
        \end{pmatrix}

    our function indicates that this set of operators constitute a set of POVMs.

    >>> from toqito.measurement_props import is_povm
    >>> import numpy as np
    >>> meas_1 = np.array([[1, 0], [0, 0]])
    >>> meas_2 = np.array([[0, 0], [0, 1]])
    >>> meas = [meas_1, meas_2]
    >>> is_povm(meas)
    True

    We may also use the :code:`random_povm` function from :code:`toqito`, and can verify that a
    randomly generated set satisfies the criteria for being a POVM set.

    >>> from toqito.measurement_props import is_povm
    >>> from toqito.random import random_povm
    >>> import numpy as np
    >>> dim, num_inputs, num_outputs = 2, 2, 2
    >>> measurements = random_povm(dim, num_inputs, num_outputs)
    >>> is_povm([measurements[:, :, 0, 0], measurements[:, :, 0, 1]])
    True

    Alternatively, the following matrices

    .. math::
        M_0 =
        \begin{pmatrix}
            1 & 2 \\
            3 & 4
        \end{pmatrix}, \qquad
        M_1 =
        \begin{pmatrix}
            5 & 6 \\
            7 & 8
        \end{pmatrix}

    does not constitute a POVM set.

    >>> from toqito.measurement_props import is_povm
    >>> import numpy as np
    >>> non_meas_1 = np.array([[1, 2], [3, 4]])
    >>> non_meas_2 = np.array([[5, 6], [7, 8]])
    >>> non_meas = [non_meas_1, non_meas_2]
    >>> is_povm(non_meas)
    False

    References
    ==========
    .. [WikPOVM] Wikipedia: POVM
        https://en.wikipedia.org/wiki/POVM

    :param mat_list: A list of matrices.
    :return: Return :code:`True` if set of matrices constitutes a set of
             measurements, and :code:`False` otherwise.
    """
    dim = mat_list[0].shape[0]

    mat_sum = np.zeros((dim, dim), dtype=complex)
    for mat in mat_list:
        # Each measurement in the set must be positive semidefinite.
        if not is_positive_semidefinite(mat):
            return False
        mat_sum += mat
    # Summing all the measurements from the set must be equal to the identity.
    if not np.allclose(np.identity(dim), mat_sum):
        return False
    return True


import matplotlib.pyplot as plt
import numpy as np

moresamples = False
logistic = False
samples = [[0.47,1],[0.24,1],[0.75,1],[0.00,1],[-0.80,1],[-0.59,1],[1.09,1],[1.34,1],
           [1.01,1],[-1.02,1],[0.50,1],[0.64,1],[-1.15,1],[-1.68,1],[-2.21,1],[-0.52,1],
           [3.93,1],[4.21,1],[5.18,1],[4.20,1],[4.57,1],[2.63,1],[4.52,1],[3.31,1],
           [6.75,1],[3.47,1],[4.32,1],[3.08,1],[4.10,1],[4.00,1],[2.99,1],[3.83,1]]
n = len(samples)
m = len(samples[0])
labels = [0]*(n//2) + [1]*(n//2)

if moresamples:
    samples += [[16.2,1],[15.7,1],[15.0,1],[16.0,1],[15.4,1],[17.3,1],[15.6,1],[15.8,1],[12.8,1],[16.2,1],[14.8,1],[17.0,1],[16.1,1],[16.0,1],[16.9,1],[15.9,1]]
    n = len(samples)
    labels  += [1]*(n-len(labels))

xmin = np.min([x for x,_ in samples])
xmax = np.max([x for x,_ in samples])

if logistic:
    U = np.matrix([[1],[0]])
    for _ in range(5):
        JR = np.matrix(np.zeros((n+2, 2)))
        R  = np.matrix(np.zeros((n+2, 1)))
        for i in range(n):
            ei = np.exp(-U[1,0] - samples[i][0]*U[0,0])
            R[i,0] = -1/(1+ei) + labels[i]
            for j in range(3):
                JR[i, 0] = samples[i][0]*ei/(1+ei)**2
                JR[i, 1] = ei/(1+ei)**2
        l = .001 # regularization
        JR[n,0] = JR[n+1, 1] = 1.*l
        R[n,0] = -U[0]*l
        R[n+1,0] = -U[1]*l
        U = U + np.linalg.inv(JR.T*JR)*JR.T*R
    a,b = U.T.tolist()[0]
    print(a,b)
    sep = -b/a
    xred = np.linspace(xmin, sep, 100)
    yred = [1/(1+np.exp(-x*U[0,0] - U[1,0])) for x in xred]
    xgrn = np.linspace(sep, xmax, 100)
    ygrn = [1/(1+np.exp(-x*U[0,0] - U[1,0])) for x in xgrn]
else:
    A = np.matrix(np.zeros((n,m)))
    b = np.matrix(np.zeros((n,1)))
    for i in range(n):
        A[i,:] = samples[i]
        b[i,0] = labels[i]

    X = np.linalg.inv(A.transpose()*A)*A.transpose()*b
    k,m = X.transpose().tolist()[0]

    sep = (.5-m)/k
    xred = np.linspace(xmin, sep, 500)
    yred = k*xred + m
    xgrn = np.linspace(sep, xmax, 500)
    ygrn = k*xgrn + m

plt.rcParams["font.family"] = "serif"
plt.rcParams["mathtext.fontset"] = "dejavuserif"
plt.rcParams['text.usetex'] = True
plt.rc('font', size=20)

if moresamples:
    fig, ax = plt.subplots(1, figsize=(6.40*2,2.40),dpi=150)
else:
    fig, ax = plt.subplots(1, figsize=(6.40,2.40),dpi=150)
ax.get_yaxis().set_visible(False)

plt.ylim(-.2, .2)
plt.axvline(sep, ymin=0, ymax=1, color='black', linestyle='--', linewidth=3)
#plt.plot(xred, yred, '-r', linewidth=2)
#plt.plot(xgrn, ygrn, '-g', linewidth=2)

for sample, label in zip(samples,labels):
    c = 'g'
    if (label<.5): c = 'r'
    label = 0
    plt.scatter(sample[0], label, color=c, edgecolors='black')

plt.tight_layout()
if logistic:
    if moresamples:
        plt.savefig("logistic-1d-b.png", bbox_inches='tight')
    else:
        plt.savefig("logistic-1d-a.png", bbox_inches='tight')
else:
    if moresamples:
        plt.savefig("linear-1d-b.png", bbox_inches='tight')
    else:
        plt.savefig("linear-1d-a.png", bbox_inches='tight')
plt.show()


# -*- coding: utf-8 -*-
"""Torus.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1YZmTW-7a1htNhtKOd3VxcG7YVB9mZEPG

#Torus
This python notebook will show the proof of the formula for volume of the torus using the Jacobian Method and the Triple Integrals Method computationally

###Import init_printing to display the integral result in a pretty way
"""

from sympy import init_printing
init_printing()

"""###Import required libraries"""

import numpy as np
import sympy as sp
from sympy import integrate, Symbol, symbols, cos, sin, Matrix, pi, exp, plot, diff
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

"""###Create the torus graph and the formula for volume of torus"""

#Generate torus
angle = np.linspace(0, 2 * np.pi, 20)
theta, phi = np.meshgrid(angle, angle)

#toroidal coordinate system
r, R = 3, 8.5
X = (R + r * np.cos(phi)) * np.cos(theta)
Y = (R + r * np.cos(phi)) * np.sin(theta)
Z = r * np.sin(phi)

x = sp.Symbol('x')
y = sp.Symbol('y')

#Volume of torus
R, r, phi, theta = symbols('R r phi theta')

attributes1 = (r, 0, r)
attributes2 = (phi, 0, 2*pi)
attributes3 = (theta, 0, 2*pi)

#f variable value below was from the jacobian matrix calculation
#of the coordinate system
f = r*(R + cos(phi))
I1 = integrate(f, attributes2)
I2 = integrate(I1, attributes3)
I3 = integrate(I2, attributes1)

"""###Displaying the Toroidal shape and its volume formula"""

# Display the shape
fig = plt.figure()
ax = fig.gca(projection = '3d')
ax.set_xlim3d(-10, 10)
ax.set_ylim3d(-10, 10)
ax.set_zlim3d(-10, 10)
ax.plot_surface(X, Y, Z, color = 'c', edgecolors='w', rstride = 1, cstride = 1)
plt.show()

print('\n  The formula for volume of the torus: ')
display(I3)

"""###Jacobian Matrix Calculation"""

#toroidal coordinate system
R, r, phi, theta = symbols('R r phi theta', real=True)
X = (R + r * cos(theta)) * cos(phi)
Y = (R + r * cos(theta)) * sin(phi)
Z = r*sin(theta)

#Jacobian matrix
J = Matrix([
    [diff(X, r),diff(X, theta),diff(X,phi)],
    [diff(Y, r),diff(Y, theta),diff(Y,phi)],
    [diff(Z, r),diff(Z, theta),diff(Z,phi)]
])

display('A is: ', J)
display('Det(A) = ', J.det())


from math import sqrt
import numpy as np

def norm(x):
    return sqrt(x.dot(x))

def infinity_norm(x):
    return max(abs(x))

class Continuation:
    def __init__(self, interface, parameters):
        self.interface = interface
        self.parameters = parameters

    def newton(self, x0, tol=1.e-7, maxit=1000):
        x = x0
        for k in range(maxit):
            fval = self.interface.rhs(x)
            jac = self.interface.jacobian(x)
            dx = self.interface.solve(jac, -fval)

            x = x + dx

            dxnorm = norm(dx)
            if dxnorm < tol:
                print('Newton converged in %d steps with norm %e' % (k, dxnorm))
                break

        return x

    def newtoncorrector(self, parameter_name, ds, x, x0, mu, mu0, tol):
        # Set some parameters
        maxit = 10000
        # zeta = 1 / len(x)
        zeta = 1
        delta = 0.1  # changed. It is used to compute F_mu

        # Do the main iteration
        for k in range(maxit):
            # Compute F and F_mu (RHS of 2.2.9)
            self.interface.set_parameter(parameter_name, mu + delta)
            dflval = self.interface.rhs(x)
            self.interface.set_parameter(parameter_name, mu)
            fval = self.interface.rhs(x)
            dflval = (dflval - fval) / delta

            # Compute the jacobian at x
            jac = self.interface.jacobian(x)

            # Compute r (2.2.8)
            diff = x - x0
            rnp1 = zeta * diff.dot(diff) + (1 - zeta) * (mu - mu0) ** 2 - ds ** 2

            r_x = -2 * zeta * (x - x0)
            r_mu = -2 * (1 - zeta) * (mu - mu0)

            if self.parameters.get("Bordered Solver", False):
                # Solve the entire bordered system in one go (2.2.9)
                # dx, dmu = self.interface.solve(jac, -fval, -rnp1, dflval, 2 * zeta * diff, 2 * (1 - zeta) * (mu - mu0))
                #TODO dont use hymls, solve bordered system directly
                res = self.interface.solve_bordered(jac, fval, dflval, r_x, r_mu, rnp1)
                dx = res[:len(res) - 1]
                dmu = res[-1]

            else:
                # Solve twice with F_x (2.2.9)
                z1 = self.interface.solve(jac, -fval)
                z2 = self.interface.solve(jac, dflval)

                # Compute dmu (2.2.13)
                # dmu = (-rnp1 - 2 * zeta * diff.dot(z1)) / (2 * (1 - zeta) * (mu - mu0) - 2 * zeta * diff.dot(z2))
                dmu = (-rnp1 + r_x.dot(z1)) / (-r_mu + r_x.dot(z2))
                # Compute dx (2.2.12)
                dx = z1 - dmu * z2

            # Compute a new x and mu (2.2.10 - 2.2.11)
            x = x + dx
            mu = mu + dmu

            dxnorm = norm(dx)
            # if max(dmu, dxnorm) < tol:
            #     print('Newton corrector converged in %d steps with norm %e' % (k, dxnorm))
            #     return (x, mu)
            if dxnorm < tol:
                print('Newton corrector converged in %d steps with norm %e' % (k, dxnorm))
                num_iterations = k
                return (x, mu, num_iterations)

        print('No convergence achieved by Newton corrector')

    def continuation(self, x0, parameter_name, target, ds, maxit):
        x = x0

        # Get the initial tangent (2.2.5 - 2.2.7).
        delta = 1
        mu = self.interface.get_parameter(parameter_name)
        fval = self.interface.rhs(x)
        self.interface.set_parameter(parameter_name, mu + delta)
        dmu = (self.interface.rhs(x) - fval) / delta
        self.interface.set_parameter(parameter_name, mu)

        # Compute the jacobian at x and solve with it (2.2.5)
        jac = self.interface.jacobian(x)
        dx = -self.interface.solve(jac, dmu)

        # Scaling of the initial tangent (2.2.7)
        dmu = 1
        zeta = 1
        nrm = sqrt(zeta * dx.dot(dx) + dmu ** 2)
        dmu = dmu / nrm
        dx = dx / nrm

        dmu0 = dmu
        dx0 = dx

        # TODO wei
        paras = []
        u = []
        u_norm = []

        # see if it passes the corner
        flag = 0

        C_v = []
        iterations = []

        # Perform the continuation
        for j in range(maxit):
            mu0 = mu
            x0 = x

            # Predictor (2.2.3)
            mu = mu0 + ds * dmu0
            x = x0 + ds * dx0

            # Corrector (2.2.9 and onward)
            (x2, mu2, num_iterations) = self.newtoncorrector(parameter_name, ds, x, x0, mu, mu0, 1e-4)

            print("%s: %f" % (parameter_name, mu2))

            if flag == 0 and mu2 > 3.5:
                flag = 1

            if mu2 > 3.5:
                C_v.append(mu2)
                iterations.append(num_iterations)

            paras.append(mu2)
            u.append(x2)
            u_norm.append(infinity_norm(x2))

            # we need to find an appropriate threshold so that we end up the value in the upper branch that is close to 0
            if flag == 1 and mu2 < 0.1:
                return x2, paras, u, u_norm, C_v, iterations

            # if (mu2 >= target and mu0 < target) or (mu2 <= target and mu0 > target):
            #     # Converge onto the end point (we usually go past it, so we
            #     # use Newton to converge)
            #     mu = target
            #     self.interface.set_parameter(parameter_name, mu)
            #     x = self.newton(x, 1e-4)
            #
            #     return x

            # Set the new values computed by the corrector
            dmu = mu2 - mu0
            mu = mu2
            dx = x2 - x0
            x = x2

            # when ds is small, dmu is also small, in this case, we cannot use this condition.
            # if abs(dmu) < 1e-10:
            if abs(dmu) < ds * 1e-5:
                return x, paras, u, u_norm, C_v, iterations

            # Compute the tangent (2.2.4)
            dx0 = dx / ds
            dmu0 = dmu / ds

        return x, paras, u, u_norm, C_v, iterations


"""
Reduced row echelon form

A matrix is in reduced row echelon form(also called row canonical form) if it satisfies the
following conditions:
 * It is in row echelon form
 * Every leading coefficient is 1 and is the only nonzero entry in its column

The reduced row echelon of a matrix may be computed by Gauss-Jordan elimination.
This is an example of a matrix in reduced row echelon form, which shows that the left of the
matix is not always an identity matrix.

        | 1 0 a1 0 b1 |
    M = | 0 1 a2 0 b2 |
        | 0 0 0  0 b3 |
"""

import numpy as np

def rref(matrix):
    """transform a matrix into reduced row echelon form

    Args:
        the origial matrix
        numpy array of shape (m, n)

    Returns:
        the reduced row echelon form matrix
        numpy array of shape (m, n)

    Example:
        input: [[1, 5, 7], [-2, -7, -5]]
        >>> rref(input)
        [[1, 0, -8], [0, 1, 3]]
    """
    m, n = matrix.shape

    h, w = 0, 0 # initialization of pivot row and column

    while h < m and w < n:

        # If the entry is zero, then move to the next column
        if matrix[h][w] == 0:
            w = w + 1
        else:

            # set the entries below the pivot column to zero
            coefficient = matrix[h+1:, w] / matrix[h, w]
            element_matrix = np.identity(m - h)
            element_matrix[1:, 0] = -coefficient
            matrix[h:, :] = np.matmul(element_matrix, matrix[h:, :])
            matrix[h, :] = matrix[h, :] / matrix[h, w]

            if h > 0:
                # set the entries above the pivot column to zero
                coefficient = matrix[:h, w] / matrix[h, w]
                element_matrix = np.identity(h+1)
                element_matrix[:-1, -1] = -coefficient
                matrix[:h+1, :] = np.matmul(element_matrix, matrix[:h+1, :])

            # increase pivot row and column
            w = w + 1
            h = h + 1

    return matrix

if __name__ == "__main__":

    matrix = np.array([[1, 5, 7], [-2, -7, -5]])
    print(rref(matrix))


import numpy as np
import time
import sys
import matplotlib.pyplot as plt

a = np.array([1,2,3])
print(a)

b = np.array([[1,2,3], [4,5,6]])
print(b)

S = range(1000)
print(S)
print(sys.getsizeof(5)*len(S)) # List - size of one element multiplied by the number of all elements,
                                # so we got the size of all elements
print(sys.getsizeof(5)) #size of one element

D = np.arange(1000)
print(D.size * D.itemsize) # NumPy - size of one element multiplied by the number of all elements,
                            # so we got the size of all elements

SIZE = 1000000
L1 = range(SIZE)
L2 = range(SIZE)

A1 = np.arange(SIZE)
A2 = np.arange(SIZE)

start = time.time()
result = [(x,y) for x,y in zip(L1, L2)]

print ((time.time() - start) * 1000 ) #runtime of List

start = time.time()
result = A1 + A2

print ((time.time() - start) * 1000 ) #runtime of Numpy

a = np.array([(1,2,3),(2,3,4)])
print(a.ndim) #dimention of array
print(a.itemsize) #byte size of each element
print(a.dtype) #data type
print(a.size) #size of array
print(a.shape) #shape of array
a = a.reshape(3, 2) #reshape into 3 rows and 2 collums
print(a)

a = np.array([(1,2,3),(2,3,4),(5,6,8)])
print(a[0:2,1]) #slicing

a = np.linspace(1,3,10) #slicing
print(a)

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

print(a.max()) #maximum element in array
print(a.min()) #minimum element in array
print(a.sum(), "\n") #sum elements in array

print(a.sum(axis=1),"\n") #horizontal axis ->
print(a.sum(axis=0), "\n") #vertical axis |
print("\n", np.sqrt(a)) #square
print("\n", np.std(a)) #Standart Deviation (средне квадратическое отклонение)

b = np.array([(7,5,10),(25,34,2),(7,1,3)])

print("\n", a+b) #sum of matrices
print("\n", a-b) #substraction of matrices
print("\n", a*b) #multiply of matrices
print("\n", a/b) #divide of matrices

print("\n"*2,np.vstack((a,b))) #concatenation along the first axis ->
print("\n"*2,np.hstack((a,b))) #stack arrays in sequence horizontaly (column wise)
print("\n", a.ravel()) #into a single column

x = np.arange(0, 3 * np.pi, 0.1 )
y = np.sin(x)
z = np.cos(x)
plt.plot(x,y) #sin plot
plt.plot(x,z) #cos plot
plt.show()

ar = np.array([2, 5, 10])
print("\n", np.exp(ar)) #exponential
print("\n", np.log(ar)) #natural log с основой 2
print(np.log10(ar)) #с основой 10


import numpy as np
import math

def eigen_max(operator, shape, num_iters=10):
    """
    Compute maximum eigenvalue of the operator by the power method.
    shape: input shape for the probe operator, usually: [batch_size, d]
    num_iters: number of power method iterations to compute the maximum
        eigenval, at each call to the model, the eigenvector is cached.
    """
    def _l2normalize(v, eps=1e-12):
        return v / np.linalg.norm(v)

    u = np.ones(shape)
    for i in range(num_iters):
        u = _l2normalize(operator(u))

    result = u.T.dot(operator(u))

    return result

def ChebyshevCoefficients(operator, a, b, n):
    """
    Returns Chebyshev coefficients up to order `n` for the function `operator`
    defined in the interval [a, b]

    WARNING: Returns 2x the coefficient c0
    TODO: Figure out how to divide c0 by 2 :-/
    """
    bma = 0.5 * (b - a)
    bpa = 0.5 * (b + a)

    xk = np.cos(math.pi * (np.arange(n+1) + 0.5)/(n + 1)) * bma + bpa
    f = operator(xk)

    i = np.arange(n+1)
    xk, _ = np.meshgrid(i, i)
    fac = 2.0 / (n+1)
    c = fac * (f * np.cos(xk.T * (math.pi * (xk + 0.5) / (n+1)))).sum(axis=1)
    return c

def StochasticChebyshevTrace(operator,grad_operator, shape, coeffs):
    """
    Computes the trace of the Chebyshev expansion of the function defined by
    `coeffs` and applied to `operator`, using the Hutchinson estimator

    operator: input operator
    shape: shape of input random vector to use, typically [batch_size, d]
    coeffs: Chebyshev coefficients of the function to evaluate
    n_probe: number of rademacher samples

    WARNING: Will divide coeffs[0] by two
    """
    # Sample a rademacher tensor with desired size
    v = 1 - 2*np.random.binomial(1,0.5,shape)

    # Initialize the iteration
    w0, w1 = v, operator(v)
    gw0 = 0*grad_operator(v)
    gw1 = grad_operator(v)

    s  = 0.5*coeffs[0]*w0  + coeffs[1]*w1
    gs = 0.5*coeffs[0]*gw0 + coeffs[1]*gw1

    for i in range(2, coeffs.shape[0]):
        wi = 2.*operator(w1) - w0
        s = s + coeffs[i]*wi

        gwi = 2.*grad_operator(w1) + 2*operator(gw1) - gw0
        gs = gs + coeffs[i]*gwi

        w0=w1*1.0
        w1=wi*1.0
        gw0=gw1*1.0
        gw1=gwi*1.0

    return v.T.dot(s), v.T.dot(gs)

def chebyshev_logdet(operator, grad_operator, shape, deg=20, num_iters=20,
                     g=1.1, eps=1e-6, m=100):
    """
    Computes the spectral sum tr(f(A)), where A is the operator,
    f is the func, [a, b] is the range of eigenvalues of A.

    shape: Shape of input vectors to the operator, typically [batch, d]
    deg: Degree of the Chebyshev approximation
    m: Number of random vectors to probe the trace

    This corresponds to Algorithm 1 in Han et al. 2017

    Returns the logdet, as well as the gradient
    """
    # Find the largest eigenvalue amongst the batch
    lmax = eigen_max(operator, shape, num_iters)
    a, b = eps, g*lmax

    # Rescales the operator
    def scaled_op(x):
        return operator(x) / (a+b)

    def scaled_gop(x):
        return grad_operator(x) / (a+b)

    a1 = a / (a + b)
    b1 = b / (a + b)

    # Compute the chebyshev coefficients of the operator
    c = ChebyshevCoefficients(np.log, a1, b1, deg)

    # Rescales the operator
    def scaled_op1(x):
        return 2. * scaled_op(x) / (b1 - a1) - (b1 + a1)/(b1 - a1) * x

    def scaled_gop1(x):
        return 2. * scaled_gop(x) / (b1 - a1)

    res = []
    resg = []
    for i in range(m):
        Gamma, grad_Gamma = StochasticChebyshevTrace(scaled_op1, scaled_gop1, shape=shape, coeffs=c)
        res.append( Gamma + shape[0]*np.log(a1+b1))
        resg.append(grad_Gamma)

    return np.concatenate(res), np.concatenate(resg)


## 3. Converting Categorical Variables ##

# Convert a single column from text categories to numbers
col = pandas.Categorical(income["workclass"])
income["workclass"] = col.codes
print(income["workclass"].head(5))
for name in ["education", "marital_status", "occupation", "relationship", "race", "sex", "native_country", "high_income"]:
    col = pandas.Categorical(income[name])
    income[name] = col.codes

## 5. Creating Splits ##

# Enter your code here
private_incomes = income[income["workclass"] == 4]
public_incomes = income[income["workclass"] != 4]

print(private_incomes.shape)
print(public_incomes.shape)

## 9. Overview of Data Set Entropy ##

import math
# We'll do the same calculation we did above, but in Python
# Passing in 2 as the second parameter to math.log will take a base 2 log
entropy = -(2/5 * math.log(2/5, 2) + 3/5 * math.log(3/5, 2))
print(entropy)
prob_0 = income[income["high_income"] == 0].shape[0] / income.shape[0]
prob_1 = income[income["high_income"] == 1].shape[0] / income.shape[0]
income_entropy = -(prob_0 * math.log(prob_0, 2) + prob_1 * math.log(prob_1, 2))

## 11. Information Gain ##

import numpy

def calc_entropy(column):
    """
    Calculate entropy given a pandas series, list, or numpy array.
    """
    # Compute the counts of each unique value in the column
    counts = numpy.bincount(column)
    # Divide by the total column length to get a probability
    probabilities = counts / len(column)

    # Initialize the entropy to 0
    entropy = 0
    # Loop through the probabilities, and add each one to the total entropy
    for prob in probabilities:
        if prob > 0:
            entropy += prob * math.log(prob, 2)

    return -entropy

# Verify that our function matches our answer from earlier
entropy = calc_entropy([1,1,0,0,1])
print(entropy)

information_gain = entropy - ((.8 * calc_entropy([1,1,0,0])) + (.2 * calc_entropy([1])))
print(information_gain)
income_entropy = calc_entropy(income["high_income"])

median_age = income["age"].median()

left_split = income[income["age"] <= median_age]
right_split = income[income["age"] > median_age]

age_information_gain = income_entropy - ((left_split.shape[0] / income.shape[0]) * calc_entropy(left_split["high_income"]) + ((right_split.shape[0] / income.shape[0]) * calc_entropy(right_split["high_income"])))

## 12. Finding the Best Split ##

def calc_information_gain(data, split_name, target_name):
    """
    Calculate information gain given a data set, column to split on, and target
    """
    # Calculate the original entropy
    original_entropy = calc_entropy(data[target_name])

    # Find the median of the column we're splitting
    column = data[split_name]
    median = column.median()

    # Make two subsets of the data, based on the median
    left_split = data[column <= median]
    right_split = data[column > median]

    # Loop through the splits and calculate the subset entropies
    to_subtract = 0
    for subset in [left_split, right_split]:
        prob = (subset.shape[0] / data.shape[0])
        to_subtract += prob * calc_entropy(subset[target_name])

    # Return information gain
    return original_entropy - to_subtract

# Verify that our answer is the same as on the last screen
print(calc_information_gain(income, "age", "high_income"))

columns = ["age", "workclass", "education_num", "marital_status", "occupation", "relationship", "race", "sex", "hours_per_week", "native_country"]
information_gains = []
# Loop through and compute information gains
for col in columns:
    information_gain = calc_information_gain(income, col, "high_income")
    information_gains.append(information_gain)

# Find the name of the column with the highest gain
highest_gain_index = information_gains.index(max(information_gains))
highest_gain = columns[highest_gain_index]

Data Scientist In Python/Step 6 - Machine Learning Introduction/6. Decision Trees/1. Introduction to Decision Trees.py


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

# Load the dataframe
df = pd.read_csv(path)
print(df.head())

#Code starts here

#Task-1 => Check if events are independent
# p(A)
p_a = len(df[df['fico']>700])/len(df)
print('probability p(A) that fico score > 700:',p_a)

# p(B)
p_b = len(df[df['purpose'] == 'debt_consolidation'])/len(df)
print('probability p(B) that purpose is debt consolidation:',p_b)

# p(A intersection B)
p_a_intersection_b = len(df[(df['purpose'] == 'debt_consolidation')&(df['fico']>700)])/len(df)

# p(A|B)
p_a_b = p_a_intersection_b/p_b
print('probability of A given B:',p_a_b)

if (p_a_b == p_a):
    result = True
else:
    result = False
print(result)

#Task-2
# p(A)
prob_lp = len(df[df['paid.back.loan'] == 'Yes'])/len(df)
print('probability p(A) that loan is paid back:',prob_lp)

# p(B)
prob_cs = len(df[df['credit.policy'] == 'Yes'])/len(df)
print('probability p(B) that credit policy is Yes:',prob_cs)

c = np.array(df['credit.policy'])
p = np.array(df['paid.back.loan'])
table = pd.crosstab(c,p)
print(table)

# p(A intersection B)
prob_pd_intersection_cs = table.iloc[1,1]/len(df)

# p(B|A)
prob_pd_cs = prob_pd_intersection_cs/prob_lp
print('probability of B given A:',prob_pd_cs)

# p(A|B)=(p(B|A)*p(A))/p(B)
bayes = (prob_pd_cs*prob_lp)/prob_cs
print('Bayes:',bayes)

# the codes below gives the same value, so both can be used
# print(table.iloc[1,1])
# print(len(df[(df['paid.back.loan'] == 'Yes')&(df['credit.policy']=='Yes')]))

#Task-3
df['purpose'].value_counts().plot(kind='bar')
plt.show()

df1 = df[df['paid.back.loan'] == 'No']
df1['purpose'].value_counts().plot(kind='bar')
plt.show()

#Task-4
inst_median = df['installment'].median()
print('Installment Median:',inst_median)

inst_mean = df['installment'].mean()
print('Installment Mean:',inst_mean)

df['installment'].plot(kind='hist')
plt.xlabel('Installment')
plt.show()

df['log.annual.inc'].plot(kind='hist')
plt.xlabel('Log Annual Income')
plt.show()


import numpy as np
import matplotlib.pyplot as plt

LR = .1 # learning rate

def activate_fn(n):
    return 1 if n>=0 else 0

def plot_fn(x, w_all, output_all=True):
    for j in range(len(w_all)):
        if not output_all:
            j = len(w_all) - 1
        xx = np.linspace(-0.5, 1.5, 20)
        yy = -(w_all[j][0] + w_all[j][1]*xx) / w_all[j][2]
        plt.figure()
        plt.plot(xx, yy)
        plt.scatter(0, 0, color='r')
        plt.scatter(0, 1, color='r')
        plt.scatter(1, 0, color='r')
        plt.scatter(1, 1, color='b')
        plt.xlim(-0.5, 1.5)
        plt.ylim(-0.5, 1.5)
        plt.xlabel('x1')
        plt.ylabel('x2')
        plt.axis('equal')
        plt.title('Iteration:{}/{}'.format(j+1, len(w_all)))
        plt.grid()
        plt.show()
        if not output_all:
            return 0

def plot_tr(w_all):
    n = len(w_all)
    plt.figure()
    for i in range(3):
        plt.plot(list(range(n)), [w_all[j][i] for j in range(n)])
    plt.legend(['w0','w1','w2'])
    plt.title('Parameters trajectory')
    plt.xlabel('Iteration')
    plt.ylabel('Weight')
    plt.show()

if __name__ == '__main__':
    w = np.random.uniform(-1, 1, size=3)
    w_rollout = []
    x = np.array([[1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]])
    y = np.array([0, 0, 0, 1]) # and
    # y = np.array([0, 1, 1, 1]) # or
    # y = np.array([1, 1, 1, 0]) # nand
    # y = np.array([0, 1, 1, 0]) # xor
    while True:
        w_rollout.append(w.copy())
        y_predict = np.squeeze(np.asarray([activate_fn(w.dot(x[i])) for i in range(4)]))
        err = y - y_predict
        delta_w = LR * np.array([err[i] * x[i] for i in range(4)])
        w += np.sum(delta_w, axis=0)
        if np.sum(np.square(err)) == 0 or len(w_rollout) > 100:
            break
    print('Weights:{}\nNumber of iters:{}'.format(w, len(w_rollout)))
    plot_fn(x, w_rollout, output_all=False)
    plot_tr(w_rollout)


# poisson distribution
# poisson distribution is a discrete distribution

# it estimates how many times an event can happen in a specified time.
# e.g. If someone eats twice a day what is probability he will eat thrice?
# It has two parameters
# lam - rate or known number of occurences e.g. 2 for above problem
# size - The shape of the returned array

# generate a random 1x10 distribution for occurence 2

from numpy import random

x = random.poisson(lam = 2, size = 10)

print(x)

# visualization of poisson distribution

# from numpy import random
import matplotlib.pyplot as plt
import seaborn as sns

sns.distplot(random.poisson(lam =2, size = 1000), kde = False, label = 'Poisson')

plt.show()

# differnce between normal and poisson distribution

# normal distribution is continuous whereas poisson is discrete
# for a large enough poisson distribution it will become similar to normal distribution with certain std dev and mean

# from numpy import random
# import matplotlib.pyplot as plt
# import seaborn as sns

sns.distplot(random.normal(loc = 50, scale = 7, size = 1000), hist = False, label = 'Normal')
sns.distplot(random.poisson(lam = 50, size = 1000), hist = False, label = 'Poisson')
plt.show()

# Difference between Possion and Binomial Distribution

"""
The difference is very subtle it is that,
binomial distribution is for discrete trials, whereas poisson distribution is for continuous trials.
But for very large n and near-zero p binomial distribution is near
identical to poisson distribution such tht n * p is nearly equal to lam.
"""
# from numpy import random
# import matplotlib.pyplot as plt
# import seaborn as sns
sns.distplot(random.binomial(n = 1000, p = 0.01, size = 10000), hist = False, label ='Binomial')
sns.distplot(random.poisson(lam = 10, size = 1000), hist = False, label = 'Poisson')

plt.show()


import numpy as np
from numbers import Number

ACTIVATIONS = {}

def parser(func):

	def wrapper(x, deriv=False, **kwargs):
		if not isinstance(deriv, bool):
			raise TypeError("Expected the parameter \'deriv\' to be a boolean, but received {type} instead!".format(type=type(deriv)))
		elif not isinstance(x, np.ndarray) and not isinstance(x, Number):
			raise TypeError("Expected the parameter \'x\' to be a numpy array or a number, but received {type} instead!".format(type=type(x)))
		return func(x, deriv, kwargs)

	wrapper.__name__ = func.__name__
	ACTIVATIONS[wrapper.__name__.lower()] = wrapper
	return wrapper

@parser
def linear(x, deriv=False, *args):
	return x if not deriv else np.ones(x.shape)

@parser
def sigmoid(x, deriv=False, *args):
	return 1/(1+np.exp(-x)) if not deriv else sigmoid(x)*(1-sigmoid(x))

@parser
def tanh(x, deriv=False, *args):
	return np.tanh(x) if not deriv else 1-np.tanh(x)**2

@parser
def ReLu(x, deriv=False, *args):
	return (np.abs(x)+x)/2 if not deriv else (np.sign(x)+1)/2

@parser
def ELU(x, deriv=False, *args):
	alpha = 1 if 'alpha' not in args[0] else args[0]['alpha']
	return np.where(x>0, x, alpha*(np.exp(x)-1)) if not deriv else np.where(x>0, 1, alpha*np.exp(x))

@parser
def LeakyReLu(x, deriv=False, *args):
	alpha = 1 if 'alpha' not in args[0] else args[0]['alpha']
	return np.where(x>0, x, alpha*x) if not deriv else np.where(x>0, 1, alpha)

@parser
def atan(x, deriv=False, *args):
	return np.arctan(x) if not deriv else 1/(x**2+1)

@parser
def BentIdentity(x, deriv=False, *args):
	return x+(np.sqrt(x**2+1)-1)/2 if not deriv else x/(2*np.sqrt(x**2+1))+1

@parser
def BipolarSigmoid(x, deriv=False, *args):
	return (1-np.exp(-x))/(1+np.exp(x)) if not deriv else (2+np.exp(-x)-np.exp(x))/((1+np.exp(x))**2)

@parser
def gaussian(x, deriv=False, *args):
	return np.exp(-x**2) if not deriv else -2*x*np.exp(-x**2)

@parser
def hardtanh(x, deriv=False, *args):
	return np.where(np.abs(x)>1, np.sign(x), x)	 if not deriv else np.where(np.abs(x)>1, 0, 1)

@parser
def InverseSqrt(x, deriv=False, *args):
	alpha = 0.1 if 'alpha' not in args[0] else args[0]['alpha']
	return x/(np.sqrt(1+alpha*x**2)) if not deriv else 1/(1+alpha*x**2)**(3/2)

@parser
def LeCunTanh(x, deriv=False, *args):
	alpha = 1.7159 if 'alpha' not in args[0] else args[0]['alpha']
	return alpha*np.tanh(2*x/3) if not deriv else 2*alpha/(3*np.cosh(2*x/3)**2)

@parser
def LogLog(x, deriv=False, *args):
	return 1-np.exp(-np.exp(x)) if not deriv else np.exp(x)*(LogLog(x)-1)

@parser
def LogSigmoid(x, deriv=False, *args):
	return np.log(sigmoid(x)) if not deriv else 1-sigmoid(x)

@parser
def SELU(x, deriv=False, *args):
	alpha = 1.67326 if 'alpha' not in args[0] else args[0]['alpha']
	beta = 1.0507 if 'beta' not in args[0] else args[0]['beta']
	return beta*np.where(x>0, x, alpha*(np.exp(x-1))) if not deriv else beta*np.where(x>0, 1, alpha*np.exp(x))

@parser
def sinc(x, deriv=False, *args):
	return np.where(x!=0, np.sin(x)/x, 1) if not deriv else np.where(x!=0, np.cos(x)/x-np.sin(x)/(x**2), 0)

@parser
def swish(x, deriv=False, *args):
	return x/(1+np.exp(-x)) if not deriv else np.exp(x)*(x+np.exp(x)+1)/(np.exp(x)+1)**2

@parser
def softsign(x, deriv=False, *args):
	return x/(1+np.abs(x)) if not deriv else 1/(1+np.abs(x))**2

@parser
def softplus(x, deriv=False, *args):
	return np.log(1+np.exp(x)) if not deriv else np.exp(x)/(1+np.exp(x))

@parser
def softmax(x, *args):
	return np.exp(x)/np.sum(np.exp(x))


#codes are seperated by """ as multiline comments

"""
import numpy as np
import panda sas pd
import scipy
from scipy import stats
mu=85
x_=55
se=6.5
#CalculatingtheZvaluetocompletetheztesting
z_stat=(x_-mu)/(se/np.sqrt(25))
#calculatingthep-value
p_val=2*(1-stats.norm.cdf(z_stat))
print('Z=',z_stat,"pValue=",p_val)

"""

"""
importnumpyasnp
importpandasaspd
importscipy
fromscipyimportstats

x=[30,28,32,26,33,25,28,30]
mu=31
t_critical=2.093#foralphalevel0.05
x_=np.array(x).mean()
#subtract1fromNtogetunbiasedestimateofsamplestandarddeviation
N=len(x)

t_stat=(x_-mu)*np.sqrt(N)/np.array(x).std(ddof=1)
print("t-statistic:",t_stat)

#aonesamplet-testthatgivesyouthep-valuetoocanbedonewithscipyasfollows:
t,p=stats.ttest_1samp(x,mu)
print("t=",t,",p=",p)
"""

"""
importnumpyasnp
importpandasaspd
importscipy
fromscipyimportstats

t_critical=1.677#foralpha=0.05

dof_x1=7
dof_x2=7

dof=dof_x1+dof_x2

std_x1=1.2
std_x2=0.9
x1_=10.2
x2_=11.8

SE=np.sqrt(((dof_x1*std_x1**2+dof_x2*std_x2**2)/dof))

t_stat=(x2_-x1_)/var*np.sqrt(1/len(x1)+1/len(x2))
print("t-statistic",t_stat)

t,p=stats.ttest_ind(x2,x1,equal_var=True)
print("t=",t,",pvalue=",p)
"""

"""
importnumpyasnp
importpandasaspd
importscipy
fromscipyimportstats

x1=[39,45,21,11,38,36]#Examinationmarksbeforevacations
x2=[22,21,13,13,49,20]#Examinationmarksaftervacations
t,p=stats.ttest_rel(x2,x1)
print("t=",t,",pvalue=",p)
"""

"""
importnumpyasnp
importpandasaspd
importscipy
fromscipyimportstats

a=['rigged',3,4]
b=['fair',2,5]

Observed_Values=np.append(a[1:3],b[1:3])
print(Observed_Values)

Expected_Values=np.outer(Observed_Values,1.5)#.T[0]
print(Expected_Values)
df=1
print("DegreeofFreedom:-",df)

alpha=0.05

chi_square=sum([(o_v-e_v)**2./e_vforo_v,e_vinzip(Observed_Values,Expected_Values)])

#chi_square_statistic=chi_square[0]+chi_square[1]
print("chi-squarestatistic:-",chi_square)

#implementingchisuqaretestusingscipymodule
chi_square_stat=stats.chisquare(Observed_Values,[4.5,6,3,7.5])
print("ChiSquareStatistic=",chi_square_stat[0])

#critical-value
critical_value=scipy.stats.chi2.ppf(q=1-alpha,df=df)

#p-value
p_value=1-scipy.stats.chi2.cdf(x=chi_square_stat,df=df)

print('Criticalvalue=',critical_value,'Pvalue=',p_value)
"""

"""
a=['rigged',2,3,5]
b=['fair',4,4,8]
c=['Sum',6,7,13]
df=pd.DataFrame([a,b,c])

df.columns=['dice','2','3','Sum']
df.set_index('dice',inplace=True)
obs=np.array([df.iloc[0][0:2].values,df.iloc[1][0:2].values])

observed_values=np.append(df.iloc[0][0:2].values,
df.iloc[1][0:2].values)

print("DegreesofFreedom:",2)

exp=scipy.stats.chi2_contingency(obs)[3]
exp_values=np.append(exp[0],exp[1])

chi_squared_statistic=((observed_values-exp_values)**2/exp_values).sum()
print('Chi-squaredStatistic=',chi_squared_statistic)

#implementingchisquaretestusingscipymodule
chi_stat,p_val,dof=scipy.stats.chi2_contingency(obs)[0:3]
print('Chi_square_stat',chi_stat,"p-Value",p_val,"DegreeofFreedom",dof)
"""


from math import *
from numpy import *
from myarray import *
import parameters
kEps = 1e-7

## The bezier curve's coefficient matrix, P(t) = tbar*M*P
M = matrix('-1. 3. -3. 1.; 3. -6. 3. 0.; -3. 3. 0. 0.; 1. 0. 0. 0.')

def sample_cubic_bezier_curve_chain( Cset, num_samples = 100 ):
	'''
	Given a set of bezier curve chain control points
	and a positive integer representing the number of samples per curve in the chain,
	returns a list of pairs ( samples, ts ) as would be returned by sample_cubic_bezier_curve().

	if curves are open, add samples of the straight line that connect the begin and the end.
	'''
	all_pts = []
	all_ts = []
	all_dts = []

	for P in Cset:
		samples, ts, dts = sample_cubic_bezier_curve( P, num_samples )
		all_pts.append( samples )
		all_ts.append( ts )
		all_dts.append( dts )

	return all_pts, all_ts, all_dts

def sample_cubic_bezier_curve( P, num_samples = 100 ):
	'''
	a 4-by-k numpy.array P containing the positions of the control points as the rows,
	return two lists: sample points of the bezier curve denoted in P, and corresponding t values
	'''
	if num_samples is None:
		num_samples = max(int(length_of_cubic_bezier_curve(P) / 1), 2)

	P = asarray( P )
	result = []
	tbar = ones( (4,1) )
	ts = linspace( 0, 1, num_samples )
	for t in ts:

		tbar[0] = t**3
		tbar[1] = t**2
		tbar[2] = t

		point = dot( P.T, dot( M.T, tbar ) )
		result.append( asarray(point).squeeze().tolist() )

	dts = ones( num_samples-1 ) * (1./(num_samples-1) )

	return asarray( result ), asarray( ts ), asarray( dts )

def sample_straight_line( begin, end, num_samples = 100 ):

	begin = asarray(begin)
	end = asarray(end)
	result = []

	if num_samples is None:
		num_samples = max(int(mag(begin - end) / 1), 2)

	ts = linspace( 0, 1, num_samples )
	for t in ts:

		point = (end - begin)*t + begin
		result.append(asarray(point).squeeze())

	return asarray( result ), asarray( ts )

def length_of_cubic_bezier_curve( P, num_samples = 100 ):
	'''
	a 4-by-k numpy.array P containing the positions of the control points as the rows,
	return a list of sample points of the bezier curve denoted in P
	'''

	P = asarray( P )
	assert P.shape[0] == 4

	samples = []
	tbar = ones( 4 )
	for t in linspace( 0, 1, num_samples ):
		tbar[0] = t**3
		tbar[1] = t**2
		tbar[2] = t
		tbar = tbar.reshape( (4,1) )
		point = dot( P.T, dot( M.T, tbar ) )
		samples.append( asarray(point).reshape(-1) )

	samples = asarray(samples)
	lengths = [mag(samples[i]-samples[i+1]) for i in range(len(samples)-1)]

	return sum( lengths )

def make_control_points_chain( controls, close = True ):
	'''
	make every control points a group from a chain of control points. Discard the remainder control points if there is any extra.
	'''
	controls = asarray(controls)
	remain_num = len( controls ) % 3

	if remain_num == 2:
		print 'wrong num for controls: ', len(controls), ': ', controls, 'closed:', close
	elif remain_num == 1:
		if close and not array_equal( controls[0], controls[-1] ):
			print 'wrong num for closed controls: ', len(controls), ': ', controls
	else:
		if not close:
			debugger()
			print 'wrong num for open controls: ', len(controls), ': ', controls

	num_segment = len( controls )//3
	if close:
		controls = concatenate( (controls[:num_segment*3], controls[0].reshape(1,2) ), axis=0 )
	elif not close and remain_num == 0:
		num_segment -= 1

	controls = controls[ :3*num_segment+1 ]

	Cset = []
	for i in range( num_segment ):
		Cset.append(controls[i*3:i*3+4].tolist())

	Cset = asarray( Cset )

	return Cset

def make_constraints_from_control_points( control_group, close=True ):
	'''
	Make default constraints based on the following assumptions:
	- if upon loading they appear to be G1, they should stay G1
	- C1 is G1
	- 90 degrees defaults to fixed angle
	- pinning should never be done; user can add later or handles placed near control points have that effect
	'''
	control_group = asarray( control_group )
	num = len( control_group )
	constraints = [ ['C0', False] for i in range( num ) ]

	for i in range( num ):
		## find the first control point in the same segment that has a different value with the first and last ones
		dir1, dir2 = zeros( 2 ), zeros( 2 )
		for j in range( 2, 5 ):
			if not array_equal( control_group[i, -1], control_group[i,-j] ):
				dir1 = dir( control_group[i,-1] - control_group[i,-j] )
				break

		for j in range( 1, 4 ):
			if not array_equal( control_group[(i+1)%num,j], control_group[(i+1)%num,0 ] ):
				dir2 = dir( control_group[(i+1)%num,j] - control_group[(i+1)%num,0 ] )
				break

# 		if i == 5 or i ==10:
# 			debugger()

		if allclose( dot( dir1, dir2 ), 1.0, atol=1e-02 ) and mag(dir1) != 0 and mag(dir2) != 0:
			if parameters.kG1andAconstraints:
				## G1
				constraints[ (i+1)%num ][0] = 'G1'
			else:
				## C1
				constraints[ (i+1)%num ][0] = 'C1'
		elif allclose( dot( dir1, dir2 ), 0, atol=1e-03 ) and mag(dir1) != 0 and mag(dir2) != 0:
			if parameters.kG1andAconstraints:
				## fixed angle
				constraints[ (i+1)%num ][0] = 'A'
			else:
				## C0
				constraints[ (i+1)%num ][0] = 'C0'

	if not close:
		constraints[0][0] = 'None'
		constraints.append( ['None', False] )

	return constraints

def split_cublic_beizer_curve( controls, partition ):
	'''
	controls are the four control points of an cubic bezier curve
	partition is an array has each splitted curve's portion
	e.g. [0.5, 0.3, 0.2] is an partition
	'''
	controls = asarray( controls )
	partition = asarray( partition )

	assert len(controls.shape) == 2
	assert controls.shape[0] == 4
	assert sum( partition ) == 1
	assert partition.any() > 0

	# num = len( partition )

	ref = 1.0
	for i in range( len(partition) ):
		partition[i] = partition[i] / ref
		ref = ( 1 - partition[i] ) * ref

	result = []
	for i, k in enumerate( partition[:-1] ):

		r1 = controls[:-1]*(1.-k) + controls[1:]*k
		r2 = r1[:-1]*(1.-k) + r1[1:]*k
		r3 = r2[:-1]*(1.-k) + r2[1:]*k

		result.append([controls[0].tolist(), r1[0].tolist(), r2[0].tolist(), r3[0].tolist()])

		controls = array( [r3[-1], r2[-1], r1[-1], controls[-1]] )

	result.append(controls)

	return asarray(result)


import sys
import math
import numpy as np
from random import seed, shuffle
from scipy.stats import multivariate_normal # generating synthetic data

def generate_synthetic_data(data_type, n_samples):

    """
        Code for generating the synthetic data.
        We will have two non-sensitive features and one sensitive feature.
        A sensitive feature value of 0.0 means the example is considered to be in protected group (e.g., female) and 1.0 means it's in non-protected group (e.g., male).
    """

    # in the same program run, we might generate this data multiple times (once for single and once for two models)
    # so make sure that the seed is always the same
    SEED = 1234
    seed(SEED)
    np.random.seed(SEED)

    def gen_gaussian(mean_in, cov_in, class_label):
        nv = multivariate_normal(mean = mean_in, cov = cov_in)
        X = nv.rvs(n_samples)
        y = np.ones(n_samples, dtype=float) * class_label
        return nv,X,y

    if data_type == 1:

        disc_factor = math.pi / 8.0 # this variable determines the initial discrimination in the data -- decraese it to generate more discrimination

        """ Generate the non-sensitive features randomly """
        # We will generate one gaussian cluster for each class
        mu1, sigma1 = [2, 2], [[5, 1], [1, 5]]
        mu2, sigma2 = [-2,-2], [[10, 1], [1, 3]]
        nv1, X1, y1 = gen_gaussian(mu1, sigma1, 1) # positive class
        nv2, X2, y2 = gen_gaussian(mu2, sigma2, -1) # negative class

        # join the posisitve and negative class clusters
        X = np.vstack((X1, X2))
        y = np.hstack((y1, y2))

        # shuffle the data
        perm = range(0,n_samples*2)
        shuffle(perm)
        X = X[perm]
        y = y[perm]

        rotation_mult = np.array([[math.cos(disc_factor), -math.sin(disc_factor)], [math.sin(disc_factor), math.cos(disc_factor)]])
        X_aux = np.dot(X, rotation_mult)

        """ Generate the sensitive feature here """
        x_control = [] # this array holds the sensitive feature value
        for i in range (0, len(X)):
            x = X_aux[i]

            # probability for each cluster that the point belongs to it
            p1 = nv1.pdf(x)
            p2 = nv2.pdf(x)

            # normalize the probabilities from 0 to 1
            s = p1+p2
            p1 = p1/s
            p2 = p2/s

            r = np.random.uniform() # generate a random number from 0 to 1

            if r < p1: # the first cluster is the positive class
                x_control.append(1.0) # 1.0 means its male
            else:
                x_control.append(0.0) # 0.0 -> female

        x_control = np.array(x_control)

    elif data_type == 2:

        ## More datasets here.
        pass

    return X,y,x_control


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

@author: Paolo
"""

## IMPORT APPROPRIATE MODULES
import numpy as np
from scipy.stats import gamma
import time

## 2) CODING AND APPLIED STATISTICS ##
### Drawn random samples from a given distribution

## Uniform random variables
def Draw_samples_from_Uni(lower_limit, upper_limit, size):
    ## constants based on ANSI C implementation (Saucier, 2000)
    mod = 2**32
    multiplier = 1103515245
    increment = 12345
    U = np.zeros(size) ## setup initial numpy vector to reserve memory space
    seed = round(time.time()) ## set seed according to current time
    for i in range(size):
        seed = (multiplier*seed + increment)%mod
        U[i] = lower_limit + (upper_limit - lower_limit) * seed/mod
    return U

## Draw samples from given distribution
def Draw_samples_from_dist(num_samples, dist, **params):
    uniform_samps = Draw_samples_from_Uni(0, 1, num_samples)

    ## Using inverse transform sampling and the Box-Muller transform
    if dist == "Normal":

        if "mean" in params:
            mean = params["mean"]
        else:
            print("Please specify 'mean' parameter.")
            return
        if "std" in params:
            std = params["std"]
        else:
            print("Please specify 'std' parameter.")
            return

        uniform_samps_pairs = Draw_samples_from_Uni(0, 1, num_samples)
        z = np.zeros(num_samples)
        for i in range(num_samples):
            z0 = np.sqrt(-2*np.log(uniform_samps[i]))*np.cos(2*np.pi*uniform_samps_pairs[i])
            # z1 = np.sqrt(-2*np.log(uniform_samps[i]))*np.sin(2*np.pi*uniform_samps_pairs[i]) ## however we can just use z0, we don't need pairs
            z[i] = z0*std + mean
        return z

    elif dist == "Exponential":

        if "lamb" in params:
            lamb = params["lamb"]
        else:
            print("Please specify 'lamb' parameter.")
            return

        x = np.zeros(num_samples)
        for i in range(num_samples):
            x[i] = -(1/lamb) * (np.log(1 - uniform_samps[i])) ## take the inverse Exponential CDF and apply inverse transform sampling
        return x

    elif dist == "Gamma":

        if "shape" in params:
            shape = params["shape"]
        else:
            print("Please specify 'shape' parameter.")
            return
        if "loc" in params:
            loc = params["loc"]
        else:
            print("Please specify 'loc' parameter.")
            return
        if "scale" in params:
            scale = params["scale"]
        else:
            print("Please specify 'scale' parameter.")
            return

        g = np.zeros(num_samples)
        for i in range(num_samples):
            g[i] = gamma.ppf(uniform_samps[i], shape, loc, scale)
        return g
    else:
        print("Please input a correct distribution. Type either 'Normal', 'Exponential' or 'Gamma'")

norm_samps = Draw_samples_from_dist(100, "Normal", mean = 0, std = 1)
expon_samps = Draw_samples_from_dist(100, "Exponential", lamb = 1)
gamma_samps = Draw_samples_from_dist(100, "Gamma", shape = 1, loc = 1, scale = 1)


import numpy as np
from mdsim.force.md_force import MDForce

class LJForce(MDForce):
    """
    LJForce class implements the Lennard-Jones force

    Reference
    ---------
    The LJ force takes the formula:
        Fij = (-12 x sigma^12 / rij^13 + 6 x sigma^6 / rij^7) * 4 * epsilon * [rij]/rij
    """
    def __init__(self):
        super().__init__()
        # set default sigma and epsilon parameter
        self._params = {'sigma': 1.0, 'epsilon': 1.0}

    def compute_force(self, coords):
        """
        Compute the Lennard-Jones force vector on current geometry

        Parameters
        ----------
        coords: numpy.ndarray of shape (Natoms, 3)
            Numpy array of atomic coordinates

        Returns
        -------
        force: numpy.ndarray of shape (Natoms, 3)
            Numpy array of gradients on each atom
        """
        sigma = self._params['sigma']
        epsilon = self._params['epsilon']
        # compute the distance between each atom pairs
        c_diff = coords[:,np.newaxis,:] - coords[np.newaxis,:,:]
        r2_mat = np.sum(np.square(c_diff), axis=-1)
        # prepare values for the LJ force formula
        s6 = sigma**6
        r2_mat2 = np.square(r2_mat)
        np.fill_diagonal(r2_mat2, 1.0) # prevent 1/0 error in equation
        r2_matn4 = 1.0 / np.square(r2_mat2)
        r2_matn7 = np.square(r2_matn4) * r2_mat
        # compute the magnitude of the gradients
        f_lj_mat = (-12.0*r2_matn7 * s6 + 6.0*r2_matn4) * 4 * epsilon * s6
        # contract with coordinates dR to get gradient vectors
        return np.einsum('ijk,ij->ik', c_diff, f_lj_mat)

    def compute_force_ref(self, coords):
        """
        Compute the Lennard-Jones force vector on current geometry

        Parameters
        ----------
        coords: numpy.ndarray of shape (Natoms, 3)
            Numpy array of atomic coordinates

        Returns
        -------
        force: numpy.ndarray of shape (Natoms, 3)
            Numpy array of gradients on each atom
        """

        noa = len(coords)
        s6 = self._params['sigma']**6
        epsilon = self._params['epsilon']
        forces = np.zeros((noa,3), dtype=float)
        for i in range(noa):
            for j in range(i+1,noa):
                dc = coords[i] - coords[j]
                r2 = dc[0]*dc[0] + dc[1]*dc[1] + dc[2]*dc[2]
                f = (-12 / r2**7 * s6 + 6 / r2**4) * 4 * epsilon * s6
                forces[i] += f * dc
                forces[j] -= f * dc
        return forces


"""
Implements test functions from
https://en.wikipedia.org/wiki/Test_functions_for_optimization

Plots the average func(x)'s each iteration.

Run main() to run the tests.
"""
from optimizer import *
from typing import Any
import numpy as np
import matplotlib.pyplot as plt

def plot_objective(title: str, *args: Any, log_scale: bool = False, show: bool = False, learning_rate: float = 3e-1, **kwargs: Any):
    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    plt.title(title)
    plt.xlabel("iteration")
    plt.ylabel("y")
    (*_, x), avg_f, avg_f_slow = zip(*iter_optimize(*args, learning_rate=learning_rate, return_var=["x", "avg_func(x)", "avg_f_slow"], **kwargs))
    print(title, "Solution:", x)
    print(title, "Objective:", avg_f[-1])
    ax.plot(avg_f)
    ax.plot(avg_f_slow)
    if log_scale:
        ax.set_yscale("log")
    if show:
        plt.show()

test_cases = {}

def rastrigin(x):
    return np.mean(x**2 + 10 * (1 - np.cos(2*np.pi*x)))

test_cases["Rastrigin"] = dict(
    func=rastrigin,
    x=random_array(-5.12, 5.12, 25),
)

def ackley(x):
    return (
        -20 * np.exp(-0.2 * np.sqrt(0.5) * np.linalg.norm(x))
        - np.exp(np.mean(np.cos(2*np.pi*x)))
        + np.e
        + 20
    )

test_cases["Ackley"] = dict(
    func=ackley,
    x=random_array(-5, 5, 2),
    learning_rate=1e-1,
)

def sphere(x):
    return np.linalg.norm(x) ** 2

test_cases["Sphere"] = dict(
    func=sphere,
    x=random_array(-5, 5, 25),
    log_scale=True,
)

def rosenbrock(x):
    return np.mean(100*(x[1:] - x[:-1]**2)**2 + (1-x[:-1])**2)

test_cases["Rosenbrock"] = dict(
    func=rosenbrock,
    x=random_array(-2, 2, 25),
    learning_rate=3e-1,
    log_scale=True,
)

def beale(x):
    x, y = x
    return (
        (1.5 - x + x * y) ** 2
        + (2.25 - x + x * y**2) ** 2
        + (2.625 - x + x * y**3) ** 2
    )

test_cases["Beale"] = dict(
    func=beale,
    x=random_array(-4.5, 4.5, 2),
    log_scale=True,
)

def goldstein_price(x):
    x, y = x
    return (
        (1 + (x+y+1)**2 * (19 - 14*x + 3*x**2 - 14*y + 6*x*y + 3*y**2))
        * (30 + (2*x-3*y)**2 * (18 - 32*x + 12*x**2 + 48*y - 36*x*y + 27*y**2))
    )

test_cases["Goldstein-Price"] = dict(
    func=goldstein_price,
    x=random_array(-2, 2, 2),
    log_scale=True,
)

def booth(x):
    x, y = x
    return (x + 2*y - 7) ** 2 + (2*x + y - 5) ** 2

test_cases["Booth"] = dict(
    func=booth,
    x=random_array(-2, 2, 2),
    log_scale=True,
)

def bukin_n6(x):
    x, y = x
    return 100 * np.sqrt(abs(y - 0.01*x**2)) + 0.01*abs(x+10)

test_cases["Bukin function N.6"] = dict(
    func=bukin_n6,
    x=random_array(-2, 2, 2),
)

def matyas(x):
    x, y = x
    return 0.26 * (x**2 + y**2) - 0.48*x*y

test_cases["Matyas"] = dict(
    func=matyas,
    x=random_array(-10, 10, 2),
    perturbation=1e-2,
    log_scale=True,
)

def levi_n13(x):
    x, y = x
    return (
        np.sin(3*np.pi*x) ** 2
        + (x-1) ** 2 * (1 + np.sin(3*np.pi*y) ** 2)
        + (y-1) ** 2 * (1 + np.sin(2*np.pi*y) ** 2)
    )

test_cases["Levi function N.13"] = dict(
    func=levi_n13,
    x=random_array(-10, 10, 2),
)

def himmelblau(x):
    x, y = x
    return (x**2 + y - 11) ** 2 + (x + y**2 - 7) ** 2

test_cases["Himmelblau"] = dict(
    func=himmelblau,
    x=random_array(-5, 5, 2),
    log_scale=True,
)

def three_hump_camel(x):
    x, y = x
    return 2*x**2 - 1.05*x**4 + x**6/6 + x*y + y**2

test_cases["3-Hump Camel"] = dict(
    func=three_hump_camel,
    x=random_array(-5, 5, 2),
    log_scale=True,
)

def easom(x):
    return -np.prod(np.cos(x)) * np.exp(-np.linalg.norm(x-np.pi)**2)

test_cases["Easom"] = dict(
    func=easom,
    x=random_array(-10, 10, 2),
    learning_rate=1e-2,
    perturbation=1e-2,
    abs_tol_x=1e-3,
    abs_tol_f=1e-3,
)

def cross_in_tray(x):
    return -1e-4 * (-holder_table(x) + 1) ** 0.1

test_cases["Cross-in-Tray"] = dict(
    func=cross_in_tray,
    x=random_array(-10, 10, 2),
    abs_tol_x=1e-6,
    abs_tol_f=1e-6,
)

def eggholder(x):
    x, y = x
    return -y * np.sin(np.sqrt(abs(x/2 + y))) - x * np.sin(np.sqrt(abs(x - y)))

test_cases["Eggholder"] = dict(
    func=eggholder,
    x=random_array(-512, 512, 2),
)

def holder_table(x):
    return -abs(np.prod(np.sin(x))) * np.exp(abs(1 - np.linalg.norm(x) / np.pi))

test_cases["Holder Table"] = dict(
    func=holder_table,
    x=random_array(-10, 10, 2),
)

def mc_cormick(x):
    x, y = x
    return np.sin(x+y) + (x-y)**2 - 1.5*x + 2.5*y + 1

test_cases["McCormick"] = dict(
    func=mc_cormick,
    x=random_array(-1.5, 4, 2),
)

def schaffer_n2(x):
    x, y = x
    return 0.5 + (np.sin(x**2 - y**2) ** 2 - 0.5) / (1 + 0.001*(x**2 + y**2)) ** 2

test_cases["Schaffer function N.2"] = dict(
    func=schaffer_n2,
    x=random_array(-100, 100, 2),
    learning_rate=1e-3,
    perturbation=3e-3,
)

def schaffer_n4(x):
    x, y = x
    return 0.5 + (np.cos(np.sin(x**2 - y**2)) ** 2 - 0.5) / (1 + 0.001*(x**2 + y**2)) ** 2

test_cases["Schaffer function N.4"] = dict(
    func=schaffer_n4,
    x=random_array(-100, 100, 2),
    learning_rate=1e-2,
    perturbation=3e-3,
)

def styblinski_tang(x):
    return np.mean(x**4 - 16*x**2 + 5*x)

test_cases["Styblinski-Tang"] = dict(
    func=styblinski_tang,
    x=random_array(-5, 5, 25),
)

def main():
    for title, settings in test_cases.items():
        plot_objective(title, **settings)
    plt.show()

if __name__ == "__main__":
    main()


import numpy as np

# input fungsi ODE yang diinginkan
def dydx(x, y):
    # return (1 + y**2)
    return((0.7*y*(1-(y/750)))-20)

def rungekutta(x0, y0, x, h):
    # step height h
    n = np.int((x - x0) / h)
    # iterasi
    y = y0
    for i in range(1, n + 1):

        # formulasi runge-kutta 4th order
        k1 = h * dydx(x0, y)
        k2 = h * dydx(x0 + 0.5 * h, y + 0.5 * k1)
        k3 = h * dydx(x0 + 0.5 * h, y + 0.5 * k2)
        k4 = h * dydx(x0 + h, y + k3)
        y = y + (1.0 / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)

        x0 = x0 + h
    return y

# input nilai yang diinginkan
x0 = 0
y0 = 30
x = 100
h = 0.1
hasil = rungekutta(x0, y0, x, h)
print(hasil)