from __future__ import print_function
# or integrate(y, (x, -3.25, 7.32))

from sympy import *
from pyDOE import lhs
import matplotlib.pyplot as plt

# ========== HW10 SOLUTION [Python2/3] ========== #

# ========== 1 ========== #

x, y = symbols('x y')
w = 3.0*x**2*y + 2*y + y*sin(x)
ans = w.diff(x)
# or diff(w, x)
print(ans)

# ========== 2 ========== #

y = 3.3*x**6 + 1.7*x**4 - 7.3*x**3 + 6.7*x
ans = y.integrate((x, -3.25, 7.32))
# or integrate(y, (x, -3.25, 7.32))
print(ans)

# ========== 3 ========== #

# option 1

y = 5.0*x**8 + 3.0*x**3 + 3.0*x**2 - 2.5*x + 1.1
ans = solve(y, x)

# option 2

eqn = Eq(5.0*x**8 + 3.0*x**3 + 3.0*x**2 - 2.5*x + 1.1, 0)
ans = solve(eqn, x)

print(ans)

# ========== 4 ========== #

m = Symbol('m')

M = Matrix([[m**2, 2.0*m, 1.0, 2.3],
            [m, 2.3, -5.0, 2.1],
            [2.1, 73.0, -56.0, 1.1],
            [-12.0, -1.0, 13.0, m]])

ans = M.det()
# or det(M)
print(ans)

# optionally, this is how to fix the rounding issue:

M_det_fix = M.det()
for x in postorder_traversal(M.det()):
    if isinstance(x, Float):
        M_det_fix = M_det_fix.subs(x, round(x, 1))

# ========== 5 ========== #

lhd = lhs(2, samples=10, criterion='c')
x_1 = lhd[:,0]
x_2 = lhd[:,1]

plt.figure()
plt.plot(x_1,x_2, 'o')
plt.grid(True)
plt.xlim((0,1))
plt.ylim((0,1))
plt.show()


import numpy as np
import matplotlib.pyplot as plt

import matplotlib as mpl
import matplotlib.animation as an

import networkx as nx

#Butcher Tableau
alpha = np.array([0.0, 1.0/4.0, 3.0/8.0, 12.0/13.0, 1.0, 1.0/2.0])
beta = np.array([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
		[1.0/4.0, 0.0, 0.0, 0.0, 0.0, 0.0],
		[3.0/32.0, 9.0/32.0, 0.0, 0.0, 0.0, 0.0],
		[1932.0/2197.0, (-7200.0)/2197.0, 7296.0/2197.0, 0.0, 0.0, 0.0],
		[439.0/216.0, -8.0, 3680.0/513.0, (-845.0)/4104.0, 0.0, 0.0],
		[(-8.0)/27.0, 2.0, (-3544.0)/2565.0, 1859.0/4104.0, (-11.0)/40.0, 0.0]])
c = np.array([25.0/216.0, 0.0, 1408.0/2565.0, 2197.0/4104.0, (-1.0)/5.0, 0.0]) # coefficients for 4th order method
c_star = np.array([16.0/135.0, 0.0, 6656.0/12825.0, 28561.0/56430.0, (-9.0)/50.0, 2.0/55.0]) # coefficients for 5th order method
cerr=c-c_star

def rkf(f,x,h,alpha,beta,cerr,c_star,*args,**kwargs):
    '''
    To perform integration stepwise using the Runge-Kutta-Fehlberg 4(5) method with adaptive step size control
    Parameters
    ----------
    f : function
        calculating the rhs of the ode
    x : numpy array
        initial state of the system
    h : float
        initial stepsize
    alpha : numpy array
        alpha values of the RK-4(5) method
    beta : numpy array
        beta values of the RK-4(5) method
    cerr : numpy array
        difference in the c cooefficients of the RK-4 and RK-5 methods
    c_star : numpy array
        c values of the RK-5 method
    *args : additional arguments to passdown to function f
    **kwargs : additional keywork arguments to pass down to function f
    Returns
    -------
    X : numpy array
        calculated values of the system for each time step
    T : numpy array
        time values corresponding to each time step in the system
    H : numpy array
        adaptive step size values for every calculated step
    EPS : numpy array
        error epsilon at each step(between the calculated values from RK-4 and RK-5 method)
    '''
    T = np.zeros(N_t)
    X = np.zeros((N,N_t))
    H = np.zeros(N_t)
    EPS = np.zeros(N_t)
    X[:,0] = x
    H[0] = h

    i = 0
    while i < N_t-1 :
        k1 = f(X[:,i],*args,**kwargs)
        k2 = rhs(X[:,i]+h*beta[1,0]*k1, *args,**kwargs)
        k3 = rhs(X[:,i]+h*(beta[2,0]*k1 + beta[2,1]*k2),*args,**kwargs)
        k4 = rhs(X[:,i]+h*(beta[3,0]*k1 + beta[3,1]*k2 + beta[3,2]*k3),*args,**kwargs)
        k5 = rhs(X[:,i]+h*(beta[4,0]*k1 + beta[4,1]*k2 + beta[4,2]*k3 + beta[4,3]*k4),*args,**kwargs)
        k6 = rhs(X[:,i]+h*(beta[5,0]*k1 + beta[5,1]*k2 + beta[5,2]*k3 + beta[5,3]*k4 + beta[5,4]*k5),*args,**kwargs)
        errorfield = h*(cerr[0]*k1 + cerr[1]*k2 + cerr[2]*k3 + cerr[3]*k4 + cerr[4]*k5 + cerr[5]*k6)
        max_error = np.absolute(errorfield).max()

        if (max_error <= tol):
            X[:,i+1] = X[:,i] + h*(c_star[0]*k1 + c_star[1]*k2 + c_star[2]*k3 + c_star[3]*k4 + c_star[4]*k5 + c_star[5]*k6)
            h = safe * h* (tol/max_error)**0.2
            H[i+1]=h
            T[i+1]=T[i]+h
            EPS[i+1]=max_error
            i+=1
        else:
            h=safe*h*(tol/max_error)**0.25

    return X,T,H,EPS

def rhs(x,A,d,u,al,gm,b):
    '''
    TO calculate the RHS of the given ode
    Parameters
    ----------
    x : numpy array
        curent state of the system
    A : numpy array
        the adjacency matrix of the system graph
    d : float
        parameter, resistance to becoming opinionated
    u : float
        control parameter, social influence
    al : float
        parameter, self reinforcement
    gm : float
        parameter, cooperative/competitive
    b : float
        parameter, input bias
    Returns
    -------
    x_dot : numpy array
        RHS of the ode. The derivative of the current state.
    '''
    x1=np.zeros(np.shape(x))
    for i in range(np.shape(A)[0]):
        for j in range(np.shape(A)[1]):
            if (i!=j):
                x1[i]+=gm*A[i,j]*x[j]
            else:
                x1[j]+=al*x[j]
    x_dot=-d*x+u*np.tanh(x1)+b
    return x_dot

N_t = 200  #no. of steps taken in the adaptive step size
N = 10   #no. of nodes
tol = 0.0000001     #desired accuracy/tolerance
safe = 0.84     #safety factor

#Adjacency Matrix :

A=np.zeros((N,N))

#TOPOLOGIES

# #Circle
# for i in range(N-1):
#     A[i,i+1]=1
#     A[i+1,i]=1

#     A[N-1,0]=1
#     A[0,N-1]=1

# #Wheel
# for i in range(N-2):
#     A[i,i+1]=1
#     A[i+1,i]=1
#     A[i,N-1]=1
#     A[N-1,i]=1
# A[N-2,N-1]=1
# A[N-1,N-2]=1
# A[N-2,0]=1
# A[0,N-2]=1

# #Path
# for i in range(N-1):
#     A[i,i+1]=1
#     A[i+1,i]=1

#Star
for i in range(N-1):
    A[i,N-1]=1
    A[N-1,i]=1

#Plotting :

h_in = 0.05 #initial stepsize
x0=(np.random.uniform(-1.0,1.0,size=N)) #inital random state of the system

x,t,h_val,err_val = rkf(rhs,x0,h_in,alpha,beta,cerr,c_star,A,d=0.5,u=0.26,al=1.2,gm=1.3,b=0.0)
#Disagreement when gm(gamma) is negative and Agreement when gm(gamma) is positive

# for i in range(N):
#     plt.plot(t,x[i,:])

# plt.xlabel("time t")
# plt.ylabel("opinions x")
# plt.title("Star Topology Disagreement")
# #plt.savefig("Star_Topology_Disagreement.jpg")
# plt.show()

# plt.plot(t,h_val)
# plt.xlabel("time t")
# plt.ylabel("stepsize h")
# plt.title("Adaptive Step Size")
# plt.show()

# plt.plot(t,err_val)
# plt.xlabel("time t")
# plt.ylabel("error $\epsilon$")
# plt.title("Change in error")
# plt.show()

#Graph :

rows,cols=np.where(A==1.)
edges=zip(rows.tolist(),cols.tolist())
G=nx.Graph()
G.add_edges_from(edges)
plot_positions=nx.drawing.spring_layout(G)

vmin=-1
vmax=1
norm=mpl.colors.Normalize(vmin=vmin,vmax=vmax)
cmap=plt.get_cmap('coolwarm')
sm=plt.cm.ScalarMappable(cmap=cmap,norm=norm)
sm.set_array([])

#Animation. Save Video

fig=plt.gcf()

def animate(i):
    '''
    Function to iterate over in order to animate the graphs
    Parameters
    ----------
    i : int
        interative variable for the FuncAnimation() function to animate the graph
    Returns
    -------
    None.
    '''
    nx.draw(G,pos=plot_positions,node_size=500,node_color=x[:,i],cmap='coolwarm',vmin=vmin,vmax=vmax)

plt.colorbar(sm)
anim = an.FuncAnimation(fig, animate, frames=200, blit=False)
writervideo = an.FFMpegWriter(fps=10)
anim.save('Star_Topology_Agreement.mp4', writer=writervideo)


from elegy import types
import typing as tp

import jax.numpy as jnp

from elegy.losses.loss import Loss, Reduction

def huber(y_true: jnp.ndarray, y_pred: jnp.ndarray, delta: float) -> jnp.ndarray:
    r"""
    Computes the Huber loss between labels and predictions.

    For each value x in error = y_true - y_pred:

    $$
    loss =
    \begin{cases}
    \ 0.5 \times x^2,\hskip8em\text{if } |x|\leq d\\
    0.5 \times d^2 + d \times (|x| - d),\hskip1.7em \text{otherwise}
    \end{cases}
    $$

    where d is delta. See: https://en.wikipedia.org/wiki/Huber_loss

    Usage:

    ```python
    rng = jax.random.PRNGKey(42)

    y_true = jax.random.randint(rng, shape=(2, 3), minval=0, maxval=2)
    y_pred = jax.random.uniform(rng, shape=(2, 3))

    loss = elegy.losses.huber(y_true, y_pred, delta=1.0)
    assert loss.shape == (2,)

    y_pred = y_pred.astype(float)
    y_true = y_true.astype(float)
    delta = 1.0
    error = jnp.subtract(y_pred, y_true)
    abs_error = jnp.abs(error)
    quadratic = jnp.minimum(abs_error, delta)
    linear = jnp.subtract(abs_error, quadratic)
    assert jnp.array_equal(loss, jnp.mean(
      jnp.add(
          jnp.multiply(
              0.5,
              jnp.multiply(quadratic, quadratic)
              ),
              jnp.multiply(delta, linear)), axis=-1
    ))
    ```

    Arguments:
        y_true: Ground truth values. shape = `[batch_size, d0, .. dN]`.
        y_pred: The predicted values. shape = `[batch_size, d0, .. dN]`.
        delta: A float, the point where the Huber loss function changes from a quadratic to linear.

    Returns:
          huber loss Values. If reduction is NONE, this has
         shape [batch_size, d0, .. dN-1]; otherwise, it is scalar.
         (Note dN-1 because all loss functions reduce by 1 dimension, usually axis=-1.)
    """
    y_pred = y_pred.astype(float)
    y_true = y_true.astype(float)
    delta = float(delta)
    error = jnp.subtract(y_pred, y_true)
    abs_error = jnp.abs(error)
    quadratic = jnp.minimum(abs_error, delta)
    linear = jnp.subtract(abs_error, quadratic)
    return jnp.mean(
        jnp.add(
            jnp.multiply(0.5, jnp.multiply(quadratic, quadratic)),
            jnp.multiply(delta, linear),
        ),
        axis=-1,
    )

class Huber(Loss):
    r"""
    Computes the Huber loss  between labels and predictions.

    For each value x in error = y_true - y_pred:

    $$
    loss =
    \begin{cases}
    \ 0.5 \times x^2,\hskip8em\text{if } |x|\leq d\\
    0.5 \times d^2 + d \times (|x| - d),\hskip1.7em \text{otherwise}
    \end{cases}
    $$

    where d is delta. See: https://en.wikipedia.org/wiki/Huber_loss

    Usage:

    ```python
    y_true = jnp.array([[0, 1], [0, 0]])
    y_pred = jnp.array([[0.6, 0.4], [0.4, 0.6]])

    # Using 'auto'/'sum_over_batch_size' reduction type.
    huber_loss = elegy.losses.Huber()
    assert huber_loss(y_true, y_pred) == 0.155

    # Calling with 'sample_weight'.
    assert (
        huber_loss(y_true, y_pred, sample_weight=jnp.array([0.8, 0.2])) == 0.08500001
    )

    # Using 'sum' reduction type.
    huber_loss = elegy.losses.Huber(
        reduction=elegy.losses.Reduction.SUM
    )
    assert huber_loss(y_true, y_pred) == 0.31

    # Using 'none' reduction type.
    huber_loss = elegy.losses.Huber(
        reduction=elegy.losses.Reduction.NONE
    )

    assert jnp.equal(huber_loss(y_true, y_pred), jnp.array([0.18, 0.13000001])).all()
    ```
    Usage with the Elegy API:

    ```python
    model = elegy.Model(
        module_fn,
        loss=elegy.losses.Huber(delta=1.0),
        metrics=elegy.metrics.Mean(),
    )
    ```
    """

    def __init__(
        self,
        delta: float = 1.0,
        reduction: tp.Optional[Reduction] = None,
        weight: tp.Optional[float] = None,
        on: tp.Optional[types.IndexLike] = None,
        **kwargs
    ):
        """
        Initializes `Mean` class.

        Arguments:
            delta: (Optional) Defaults to 1.0. A float, the point where the Huber loss function changes from a quadratic to linear.
            reduction: (Optional) Type of `elegy.losses.Reduction` to apply to
                loss. Default value is `SUM_OVER_BATCH_SIZE`. For almost all cases
                this defaults to `SUM_OVER_BATCH_SIZE`.
            weight: Optional weight contribution for the total loss. Defaults to `1`.
            on: A string or integer, or iterable of string or integers, that
                indicate how to index/filter the `y_true` and `y_pred`
                arguments before passing them to `call`. For example if `on = "a"` then
                `y_true = y_true["a"]`. If `on` is an iterable
                the structures will be indexed iteratively, for example if `on = ["a", 0, "b"]`
                then `y_true = y_true["a"][0]["b"]`, same for `y_pred`. For more information
                check out [Keras-like behavior](https://poets-ai.github.io/elegy/guides/modules-losses-metrics/#keras-like-behavior).
        """
        self.delta = delta
        return super().__init__(reduction=reduction, weight=weight, on=on, **kwargs)

    def call(
        self,
        y_true: jnp.ndarray,
        y_pred: jnp.ndarray,
        sample_weight: tp.Optional[
            jnp.ndarray
        ] = None,  # not used, __call__ handles it, left for documentation purposes.
    ) -> jnp.ndarray:
        """
        Invokes the `Huber` instance.

        Arguments:
            y_true: Ground truth values. shape = `[batch_size, d0, .. dN]`, except
                sparse loss functions such as sparse categorical crossentropy where
                shape = `[batch_size, d0, .. dN-1]`
            y_pred: The predicted values. shape = `[batch_size, d0, .. dN]`
            sample_weight: Optional `sample_weight` acts as a
                coefficient for the loss. If a scalar is provided, then the loss is
                simply scaled by the given value. If `sample_weight` is a tensor of size
                `[batch_size]`, then the total loss for each sample of the batch is
                rescaled by the corresponding element in the `sample_weight` vector. If
                the shape of `sample_weight` is `[batch_size, d0, .. dN-1]` (or can be
                broadcasted to this shape), then each loss element of `y_pred` is scaled
                by the corresponding value of `sample_weight`. (Note on`dN-1`: all loss
                functions reduce by 1 dimension, usually axis=-1.)

        Returns:
            Weighted loss float `Tensor`. If `reduction` is `NONE`, this has
                shape `[batch_size, d0, .. dN-1]`; otherwise, it is scalar. (Note `dN-1`
                because all loss functions reduce by 1 dimension, usually axis=-1.)

        Raises:
            ValueError: If the shape of `sample_weight` is invalid.
        """
        return huber(y_true, y_pred, self.delta)


from __future__ import print_function

from sympy import Symbol, symbols, sin, cos, Rational, expand, simplify, collect
from sympy import Rational as Rat
from galgebra.printer import Format, Eprint
from galgebra.ga import Ga
from math import sqrt

global n,nbar,I

def radius(T):
    '''
    This retrieves the radius from a trivector representing a circle
    '''
    a=(T*T).scalar()
    b=((T^n)*(T^n)).scalar()
    return (-1*a/b)**0.5

def center(T):
    global n
    '''returns the center of a given circle trivector'''
    return T*n*T

def split_bivector(B):
    global ebar
    '''Implements the algorithm described in Doran and Lasenby to recover null vectors wedging to B'''
    print('B =',B)
    print('B**2 =',B*B)
    NB = B.norm()
    print('NB =',NB)
    Bh = B/NB
    ap = ebar - ((ebar^Bh)*Bh)
    a1 = ap + (ap*Bh)
    a2 = ap - (ap*Bh)
    #print '#a1 = ',a1
    #print '#a2 = ',a2
    return [a1,a2]

def norm(X):
    Y=sqrt((X*X).scalar())
    return Y

Eprint()

g='1 0 0 0, \
   0 1 0 0, \
   0 0 0 2, \
   0 0 2 0'

c2d = Ga('e_1 e_2 n \\bar{n}',g=g)
(e1,e2,n,nbar) = c2d.mv()

def F(x):
    global n,nbar
    Fx = ((x*x)*n+2*x-nbar) / 2
    return(Fx)

e = (n+nbar)/2
ebar = n - e
I=e1*e2*e*ebar

def intersect_lines(L1,L2):
    global I
    '''
    Computes the intersection bivector of two conformal lines L1, L2
    '''
    C = I*((I*L1)^(I*L2))
    return C

A=F(Rat(1,2)*e1)
B=F(2*e1)
C=F(Rat(4,5)*e1+Rat(3,5)*e2)
D=F(Rat(4,5)*e1-Rat(3,5)*e2)

print('A =',A)
print('B =',B)
print('C =',C)
print('D =',D)

T=A^B^C
print('T =',T)
U=F(e1)^(F(e2))^F(-1*e1)
print('U =',U)
inter=intersect_lines(U,T)
print('inter =',inter)

x,y = split_bivector(inter)

bases = (e1,e2)
print(x.proj(bases))
print(y.proj(bases))

print('One intersection point x = ',x)
print('The other intersection point y = ',y)
print('x**2 = ',x*x)
print('y**2 = ',y*y)
print('T^x = ',T^x)
print('T^y = ',T^y)
print('U^x = ',U^x)
print('U^y = ',U^y)


import numpy as np
from sparsesvd import sparsesvd
from scipy.sparse import csc_matrix
import seaborn as sns; sns.set()

def SSVD_python(X, gamma1 = 2, gamma2 = 2, tol = 1e-4, max_iter = 100):
    """SSVD for first layer with python optimization

    X = data matrix
    gamma1, gamma2 = weight parameters, default set to 2
    tol = tolerance for convergence, default set to 1e-4
    max_iter = maximum number of iterations, default set to 100

    If converged, return:

    u = u vector of Sparse SVD
    s = largest singular value from SVD
    v = v vector of Sparse SVD
    niter = number of iteration until convergence

    If not converged, print:

    "Fail to converge! Increase the max_iter!"

    Example usage:

    u, s, v, niter = SSVD_primary(X)
    """
    u, s, v = sparsesvd(csc_matrix(X), k=1)

    # initializations
    n = X.shape[0]
    d = X.shape[1]
    u = u.reshape((n,1))
    v = v.reshape((d,1))
    u_delta = 1
    v_delta = 1
    niter = 0
    SST = np.sum(X**2)

    while((u_delta > tol) or (v_delta > tol)):
        niter += 1

        # update v
        Xu = X.T @ u
        w2_inv = np.abs(Xu)**gamma2
        sigma_sq = np.abs(SST - sum(Xu**2))/(n*d-d)   #np.trace((X-s*u@v.T) @ (X-s*u@v.T).T)/(n*d-d)

        lambda2s = np.unique(np.append(np.abs(Xu*w2_inv), 0))
        lambda2s.sort()                                   # possible lambda2/2

        # best BIC
        BICs = np.ones(lambda2s.shape[0]-1)*np.Inf
        for i in range(BICs.shape[0]):
            v_temp = np.sign(Xu)*(np.abs(Xu) >= lambda2s[i] / w2_inv)*(np.abs(Xu) - lambda2s[i] / w2_inv)
            BICs[i] = np.sum((X-u@v_temp.T)**2)/sigma_sq + np.sum(v_temp!=0)*np.log(n*d)
        best = np.argmin(BICs)

        lambda2 = lambda2s[best]
        v_new = np.sign(Xu)*(np.abs(Xu) >= lambda2 / w2_inv)*(np.abs(Xu) - lambda2 / w2_inv)
        v_new = v_new/np.sqrt(np.sum(v_new**2))

        v_delta = np.sqrt(np.sum((v-v_new)**2))
        v = v_new

        # update u
        Xv = X @ v
        w1_inv = np.abs(Xv)**gamma1
        sigma_sq = np.abs(SST - sum(Xv**2))/(n*d-n)

        lambda1s = np.unique(np.append(np.abs(Xv*w1_inv), 0))
        lambda1s.sort()                                   # possible lambda1/2

        # best BIC
        BICs = np.ones(lambda1s.shape[0]-1)*np.Inf
        for i in range(BICs.shape[0]):
            u_temp = np.sign(Xv)*(np.abs(Xv) >= lambda1s[i] / w1_inv)*(np.abs(Xv) - lambda1s[i] / w1_inv)
            BICs[i] = np.sum((X-u_temp@v.T)**2)/sigma_sq + np.sum(u_temp!=0)*np.log(n*d)
        best = np.argmin(BICs)

        lambda1 = lambda1s[best]
        u_new = np.sign(Xv)*(np.abs(Xv) >= lambda1 / w1_inv)*(np.abs(Xv) - lambda1 / w1_inv)
        u_new = u_new/np.sqrt(np.sum(u_new**2))

        u_delta = np.sqrt(np.sum((u-u_new)**2))
        u = u_new

        # check iterations
        if niter > max_iter:
            print("Fail to converge! Increase the max_iter!")
            break
    return(np.ravel(u), s, np.ravel(v), niter)

def plotClusters(u, s, v, group, tresh):
    """Plotting Clusters for rank 1 approximation

    u, s, v = return values of SSVD function
    group = vector of known groups, None if no grouping is known
    tresh = value for discarding insignificant rows.

    return:
    Heatmap of rank 1 approximated clusters

    Example usage:

    u, s, v, niter = SSVD_primary(X)
    plotClusters(u,s,v,None,0)
    """
    first = s*u.reshape((-1, 1))@v.reshape((1, -1))

    groups = np.unique(group)
    row_idx = np.empty(0, dtype = 'int')
    for i in range(len(groups)):
        idx, = np.where(group == groups[i])
        idx_ = idx[np.argsort(u[idx])]
        row_idx = np.concatenate((row_idx, idx_))

    col_nonzero = np.argsort(np.abs(v))[tresh:]
    v_nonzero = v[col_nonzero]
    first_nonzero = first[:,col_nonzero]
    col_idx = np.argsort(v_nonzero)

    ax = sns.heatmap(first_nonzero[np.ix_(row_idx, col_idx)], vmin=-1, vmax=1, cmap = 'bwr')


import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
import numpy as np

import sys
sys.path.append('../')
from src.utils.plotting import latexconfig
latexconfig()

h = 6.626e-34    # Planck constant [J⋅s]
c = 3.0e+8       # speed of light [m/s]
k = 1.38e-23     # Boltzmann constant [J⋅K^−1]

def B_rj(labda, T):
    """Rayleigh-Jeans black-body radiation interpretation"""
    return (2.0 * c * k * T) / (labda**4)

def B_p(labda, T):
    """Planck black-body radiation formula"""
    return (2.0 * h * c**2) / (labda**5 * (np.exp(h*c/(labda*k*T)) - 1.0))

labda = np.arange(1e-9, 3e-6, 1e-9, dtype=np.float128)
Ts = [4000., 5000., 6000., 7000.]

plt.figure('Black-body Radiation', figsize=(8, 4))
for T in Ts:
    plt.plot(labda*1e9, B_p(labda, T), label=f'$T={int(T)}$K')

plt.plot(labda*1e9, B_rj(labda, T=5000), label=f'Rayleigh-Jeans (T={5000}K)', color='black')

plt.vlines(x=380, ymin=0, ymax=7e13, color='black', linestyle=':')
plt.vlines(x=740, ymin=0, ymax=7e13, color='black', linestyle=':',
           label='visible light borders')

#visible spectrum coloring
plt.axvspan(380, 450, alpha=0.3, color='violet')
plt.axvspan(451, 485, alpha=0.3, color='blue')
plt.axvspan(486, 500, alpha=0.3, color='cyan')
plt.axvspan(501, 565, alpha=0.3, color='green')
plt.axvspan(566, 590, alpha=0.3, color='yellow')
plt.axvspan(591, 625, alpha=0.3, color='orange')
plt.axvspan(626, 740, alpha=0.3, color='red')

plt.ylim([0, 7e13])
plt.legend(loc='best')
plt.xlabel('$\lambda$ [nm]')
plt.ylabel('$B(\lambda, T)$ ')
plt.show()


from math import log10, sqrt
import cv2
import numpy as np

def PSNR(original, compressed):
    '''
    Calculates the Peak signal to noise ratio between a ground truth image and predicted image.

    see https://www.geeksforgeeks.org/python-peak-signal-to-noise-ratio-psnr/
    for reference

    Parameters
    ----------
        true image (cv2 image)
        predicted image (cv2 image)

    Returns
    -------
        PSNR score
    '''
    mse = np.mean((original - compressed) ** 2)
    if(mse == 0):  # MSE is zero means no noise is present in the signal .
                  # Therefore PSNR have no importance.
        return 100
    max_pixel = 255.0
    psnr = 20 * log10(max_pixel / sqrt(mse))
    return psnr

def colourfulnessMetric(img):
    """
    Created on Mon Nov 15 10:55:16 2021

    @author: Yucheng

    Parameters
    ----------
    img : cv2 RGB image

    Returns
    -------
    M : colourness metric
    -----------------------------
    |not colourful        | 0   |
    |slightly colorful    | 15  |
    |moderately colourful | 33  |
    |averagely colourful  | 45  |
    |quite colourful      | 59  |
    |highly colourful     | 82  |
    |extremely colourful  | 109 |
    -----------------------------

    """
    # Get RGB components
    R,G,B = cv2.split(img.astype("float"))

    # colourfulness metric from Hasler et al., section 7
    rg = R - G
    yb = (1/2) * (R+G) - B

    sigma_rgyb = np.sqrt(np.var(rg) + np.var(yb))
    mu_rgyb = np.sqrt(np.mean(rg)**2 + np.mean(yb)**2)

    M = sigma_rgyb + 0.3 * mu_rgyb

    return M

def main():
    import matplotlib.pyplot as plt
    original = cv2.imread("test_imgs/original_image.png")
    compressed = cv2.imread("test_imgs/compressed_image1.png", 1)
    value = PSNR(original, compressed)
    print(f"PSNR value is {value} dB")

    img2 = cv2.imread("rainbow.jpg") # opens as BGR
    img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)

    plt.imshow(img2[:,:,:])
    plt.show()

    M = colourfulnessMetric(img2)
    print(M)

if __name__ == "__main__":
    main()


import math
import numpy as np

def solve_homography(A, B):
    '''
        Taken from: https://math.stackexchange.com/questions/494238/how-to-compute-homography-matrix-h-from-corresponding-points-2d-2d-planar-homog
    '''
    [[a1x, a1y], [a2x, a2y], [a3x, a3y], [a4x, a4y]] = A
    [[b1x, b1y], [b2x, b2y], [b3x, b3y], [b4x, b4y]] = B

    P = np.array([
        [-a1x, -a1y, -1, 0, 0, 0, a1x*b1x, a1y*b1x, b1x],
        [0, 0, 0, -a1x, -a1y, -1, a1x*b1y, a1y*b1y, b1y],

        [-a2x, -a2y, -1, 0, 0, 0, a2x*b2x, a2y*b2x, b2x],
        [0, 0, 0, -a2x, -a2y, -1, a2x*b2y, a2y*b2y, b2y],

        [-a3x, -a3y, -1, 0, 0, 0, a3x*b3x, a3y*b3x, b3x],
        [0, 0, 0, -a3x, -a3y, -1, a3x*b3y, a3y*b3y, b3y],

        [-a4x, -a4y, -1, 0, 0, 0, a4x*b4x, a4y*b4x, b4x],
        [0, 0, 0, -a4x, -a4y, -1, a4x*b4y, a4y*b4y, b4y],

        [0, 0, 0, 0, 0, 0, 0, 0, 1]
    ], dtype=np.float)

    b = np.zeros(9, dtype=np.float)
    b[-1] = 1

    H = np.linalg.solve(P, b)

    H = H.reshape(3, 3)

    return H

if __name__ == "__main__":
    A = np.array([
        [467, 683],
        [819, 680],
        [775, 570],
        [519, 573]
    ], dtype=np.float)

    B = np.array([
        [0, 0],
        [80, 0],
        [80, 80],
        [0, 80]
    ], dtype=np.float)

    H = solve_homography(A, B)
    H /= H[2, 2]
    print(H.round(2))

    x = np.array([880, 449, 1], dtype=np.float).reshape(3, 1)

    y = H @ x
    y /= y[2]
    print(y[:2])


"""
seirpinski.py

Description:

A program that draws the Sierpinski triangle.

Author: Mahesh Venkitachalam
Website: electronut.in

"""

import time
import turtle
import math
import sys, argparse
import numpy as np

def draw_triangle(t, A, B, C):
    if np.linalg.norm(A-B) > 10.0 :
        draw_triangle(t, A, 0.5*(A + B), 0.5*(A + C))
        draw_triangle(t, 0.5*(A + B), B, 0.5*(B + C))
        draw_triangle(t, 0.5*(A + C), 0.5*(B + C), C)
    else:
        # start fill
        t.begin_fill()
        # draw
        t.up()
        t.setpos(A[0], A[1])
        t.down()
        t.setpos(B[0], B[1])
        t.setpos(C[0], C[1])
        t.setpos(A[0], A[1])
        t.up()
        # end fill
        t.end_fill()

# main() function
def main():
    print('Drawing the Sierpinski triangle...')

    # draw
    a = 200
    t = turtle.Turtle()
    t.hideturtle()
    A = np.array([-a/2, 0])
    B = np.array([a/2, 0])
    C = np.array([0, a*math.sqrt(3)/2.0])
    draw_triangle(t, A, B, C)

    win = turtle.Screen()
    win.exitonclick()

# call main
if __name__ == '__main__':
    main()


# load the data for time-series
import numpy as np
from scipy import signal

from load_time_series import load_data

np.random.seed(231)

dirname = "50words"
datasets = load_data(dirname)

train_set_x, train_set_y = datasets[0]
valid_set_x, valid_set_y = datasets[1]
test_set_x, test_set_y = datasets[2]

x = train_set_x[0]
# print("train_set_x[0]: ", x)
print("len of x: ", len(x))

filter_size = 10
corr_filter = np.random.randn(filter_size)

standard_corr = signal.correlate(x, corr_filter, 'valid')
print("len of standard corr: ", len(standard_corr))

# print("standard_corr:", standard_corr)

xfft = np.fft.fft(x)
filterfft = np.conj(np.fft.fft(corr_filter, len(xfft)))
# element-wise multiplication in the frequency domain
out = xfft * filterfft
# take the inverse of the output from the frequency domain and return the modules of the complex numbers
out = np.fft.ifft(out)
output = np.array(out, np.double)
#output = np.absolute(out)

output = output[:len(standard_corr)]
# print("output of cross-correlation via fft: ", output)
print("is the fft cross_correlation correct: ", np.allclose(output, standard_corr, atol=1e-12))
print("absolute error: ", np.sum(np.abs(output - standard_corr)))


# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt

def gd(X, Y, alpha=0.01, epsilon=1e-8):
    m = len(X)
    a, b, sse2 = 0, 0, 0
    while True:
        grad_a, grad_b = 0, 0
        for i in range(m):
            diff = a * X[i] + b - Y[i]
            grad_a += X[i] * diff
            grad_b += diff

        a -= alpha * grad_a / m
        b -= alpha * grad_b / m

        sse = 0
        for j in range(m):
            sse += (a * X[j] + b - Y[j]) ** 2 / (2 * m)

        if abs(sse2 - sse) < epsilon:
            break
        else:
            sse2 = sse
    return a, b

X = [1, 2, 3, 4, 5, 6, 7, 8, 9]
Y = [0.199, 0.389, 0.580, 0.783, 0.980, 1.177, 1.380, 1.575, 1.771]

a, b = gd(X, Y, 0.05, 1e-6)

print('y = {0} * x + {1}'.format(a, b))

x = np.array(X)
plt.plot(x, Y, 'o', label='Original data', markersize=5)
plt.plot(x, a * x + b, 'r', label='Fitted line')
plt.show()


from numpy.linalg import svd
import numpy as np

# Recommend centering the embedding prior to applying the distance/similarity calculations.

def get_rotation_matrix(E_prev, E_now):
    """
    This method is used to get the rotation matrix. The problem formulation and solution can be found on:
    https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem

    R = argmin(Q) ||QE_new - E_prev||
    Solution:
    M = E_prev x E_new^T
    U, S, V^T = SVD(M)
    R = U x V^T

    :param E_prev: The embedding of the previous TimeSpell, size of n x d, where n is the number of nodes, d is the dimension of the embedding space.
    :param E_now: The embedding of the current TimeSpell, size of n x d, where n is the number of nodes, d is the dimension of the embedding space.
    :return: Returns the rotation matrix of the embedding space.
    """
    T_prev = E_prev.T
    T_now = E_now.T
    M = np.dot(T_prev, T_now.T)
    U, S, Vh = svd(M)
    # print(U.shape, S.shape, Vh.shape)
    R = np.dot(U, Vh)

    return R

def get_rotated_embedding(E_prev, E_now, ind):
    """
    This method returns the rotated embedding of E_now.
    """
    # Use the anchor nodes to compute the rotation matrix for the embedding space.
    E_prev_anchor = np.zeros((len(ind), E_prev.shape[1]), dtype=np.float)
    for i, idx in enumerate(ind):
        E_prev_anchor[i] = E_prev[int(idx)]
    E_now_anchor = np.zeros((len(ind), E_prev.shape[1]), dtype=np.float)
    for i, idx in enumerate(ind):
        E_now_anchor[i] = E_now[int(idx)]

    R = get_rotation_matrix(E_prev_anchor, E_now_anchor)
    # Transpose E_now first to align the dimensions, apply the rotation to it, and transpose the result back.
    E_now_rotated = np.dot(R, E_now.T).T

    return E_now_rotated

def get_embedding_distance(E_prev, E_now_rotated):
    n = E_prev.shape[0]
    E_now_rotated_first_n = E_now_rotated[:n, :]
    temp = np.square(np.subtract(E_prev, E_now_rotated_first_n))
    # Measuring the Euclidean distance for each node existed in E_prev
    dist = np.sqrt(np.sum(temp, axis=1))

    return dist

def get_embedding_cosine(E_prev, E_now_rotated):
    n = E_prev.shape[0]
    E_now_rotated_first_n = E_now_rotated[:n, :]
    mode_prev = np.sqrt(np.sum(np.square(E_prev), axis=1))
    mode_now = np.sqrt(np.sum(np.square(E_now_rotated_first_n), axis=1))
    product = np.sum(np.multiply(E_prev, E_now_rotated_first_n), axis=1)
    # Measuring the Cosine Similarity for each node existed in E_prev
    sim = np.divide(np.divide(product, mode_prev), mode_now)

    return sim


import numpy as np
def linearRegCostFunction(X, y, theta, Lambda):
    """computes the
    cost of using theta as the parameter for linear regression to fit the
    data points in X and y. Returns the cost in J and the gradient in grad
    """
# Initialize some useful values

    m = y.size # number of training examples

# ====================== YOUR CODE HERE ======================
# Instructions: Compute the cost and gradient of regularized linear
#               regression for a particular choice of theta.
#
#               You should set J to the cost and grad to the gradient.
#
    J = (np.sum(np.square(X.dot(theta) - y)) + Lambda * np.sum(np.square(theta[1:]))) / float(2 * m)
    grad = (X.T.dot(X.dot(theta) - y) + Lambda * np.concatenate([[0], theta[1:]])) / float(m)
    return J, grad
# =========================================================================


# Dada uma placa quadrada 1x1 m^2 e as temperaturas de fronteiras
# calcule a temperatura nesta placa e visualize a distribuição
# usando Gauss-Seidel e um grid nxn

# Aluno: Leonardo Meireles Murtha Oliveira
# NºUSP: 4182085

import GaussSeidel as gsd
import numpy as np
import matplotlib.pyplot as plt

# Retorna a matriz de coeficientes e o vetor B
def createLinearSystem(grid, n):
    m = (n-2)**2
    A = 4*np.identity(m, float) # Vezes quatro pois a variavel é a média entre 4
    b = np.zeros(m, float)

    for i in range(1, n-1):
        for j in range(1,n-1):
            g = 0

            if(grid[i,j-1] == 3.14):
                A[(i-1)*(n-2) + (j-1)][(i-1)*(n-2) + (j-2)] = -1
                #print('{} = {} {}'.format(((i-1)*(n-2) + (j-1)),i-1, (i-1)*(n-2) + (j-2)))
            else: g = g + grid[i, j-1]

            if(grid[i-1,j] == 3.14):
                A[(i-1)*(n-2) + (j-1)][(i-2)*(n-2) + (j-1)] = -1
                #print('{} = {} {}'.format(((i-1)*(n-2) + (j-1)),i-1, (i-2)*(n-2) + (j-1)))
            else: g = g + grid[i-1, j]

            if(grid[i,j+1] == 3.14):
                A[(i-1)*(n-2) + (j-1)][(i-1)*(n-2) + j] = -1
                #print('{} = {} {}'.format(((i-1)*(n-2) + (j-1)),i-1, (i-1)*(n-2) + j))
            else: g = g + grid[i, j+1]

            if(grid[i+1,j] == 3.14):
                A[(i-1)*(n-2) + (j-1)][i*(n-2) + ((j)-1)] = -1
                #print('{} = {} {}'.format(((i-1)*(n-2) + (j-1)),i-1, i*(n-2) + ((j)-1)))
            else: g = g + grid[i+1, j]

            b[(i-1)*(n-2) + (j-1)] = g

    return A, b

# Inicializa o grid com as temp. fronteiras
def initGrid(dR, n):
    grid = np.full((n, n), float(3.14),float)

    print('>>Inicializando o grid.')
    grid[0,0] = (dR[0] + dR[1])/2
    grid[n-1,0] = (dR[1] + dR[2])/2
    grid[n-1,n-1] = (dR[2] + dR[3])/2
    grid[0, n-1] = (dR[3] + dR[0])/2

    for i in range(1,n-1):
        grid[i,0] = dR[0] # Parte esquerda
        grid[0,i] = dR[1] # Parte do topo
        grid[i,n-1] = dR[2] # Parte direita
        grid[n-1,i] = dR[3] # Parde de baixo

    return grid

# Atualiza o grid com a solução
def updateGrid(grid, xf, n):
    for i in range(1, n-1):
        for j in range(1,n-1):
            grid[i][j] = xf[(i-1)*(n-2) + (j-1)]

def main():

    print('>>>A coleta de fronteira é sentido horário começando pelo lado esquerdo.')
    print('>>>Lembrando que a dimensão do grid é n gera (n-2)^2 variaveis.')
    n = int(input('>>>Digite a dimensão do grid.\n'))
    dR = np.zeros(4) # Vetor de temperaturas das fronteiras

    for i in range(4):
        dR[i] = float(input('>>>Digite a temp da fronteira [{}].\n'.format(i+1)))

    grid = initGrid(dR, n) # Inicia o grid com as temperatura de fronteiras em equilibrio
    A, b = createLinearSystem(grid, n)  # Cria a matriz a partir das equações
    xi = gsd.initialX(A, b) # Pega o chute inicial
    t = 20 # Número de iterações do Gauss-Seidel
    xf = gsd.solve(A, b, xi, t) # Resolve o sistema
    updateGrid(grid, xf, n) # Atualiza o grid com a solução aproximada

    # Parte de visualização
    plt.title('Heat Distribution')
    plt.xlabel('Coluna do grid')
    plt.ylabel('Linha do grid')
    plt.imshow(grid, cmap='plasma', interpolation='nearest')
    plt.colorbar()
    plt.show()

if __name__ == "__main__":
    main()


import numpy as np

from si.unsupervised.unsupervised_model import UnsupervisedModel
from si.data.scale import StandardScaler

def evd(X):
    C = np.cov(X.T)
    # EVD
    eigen_values, eigen_vectors = np.linalg.eig(C)
    idx = eigen_values.argsort()[::-1]
    eigen_values = eigen_values[idx]
    eigen_vectors = eigen_vectors[:, idx]

    # Return principal components and eigenvalues to calculate the portion of sample variance explained
    return np.dot(X, eigen_vectors), eigen_values

def svd(X):
    # SVD
    u, sigma, vh = np.linalg.svd(X, full_matrices=False)
    # Return principal components and eigenvalues to calculate the portion of sample variance explained
    return u.dot(np.diag(sigma)), (sigma ** 2) / (X.shape[0] - 1)

class PCA(UnsupervisedModel):
    # Must use scalar / centralize points
    def __init__(self, k, function=svd):
        super().__init__()
        self.k = k
        self._func = function
        self.scaler = None
        self.eigen = None
        self.var_exp = None

    def fit(self, dataset):
        self.scaler = StandardScaler()
        self.scaler.fit(dataset)
        self.is_fitted = True
        return self.scaler

    def transform(self, dataset):
        if not self.is_fitted:
            raise Exception("The model hasn't been fitted yet.")
        centered = self.scaler.transform(dataset)
        pc, self.eigen = self._func(centered.X)
        self.variance_explained()
        return pc[:, 0:self.k]

    def fit_transform(self, dataset):
        self.fit(dataset)
        return self.transform(dataset)

    def variance_explained(self):
        self.var_exp = list(self.eigen / self.eigen.sum())
        return self.var_exp

    def cumulative_variance_explained(self):
        percent_dict = {}
        self.variance_explained()
        for i, value in enumerate(self.var_exp):
            if i == 0:
                percent_dict[i] = value
            else:
                percent_dict[i] = value + percent_dict[i - 1]
        return percent_dict

    def inverse_transform(self):
        pass


import numpy as np
import matplotlib.pylab as plt

print("######")

# step
def step_function(x):
    # x : numpy.ndarray
    return np.array(x > 0, dtype=np.int)

x = np.arange(-5, 5, 0.1)
y = step_function(x)
print(type(x), x, y)

plt.plot(x, y)
plt.ylim(-0.1, 1.1)  # range of y axis on fig.
plt.show()

print("######")

# sigmoid

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

x = np.arange(-10, 10, 0.1)
# x = np.array([-1,1,2])
y = sigmoid(x)
y2 = step_function(x)
print(x, y, y2)

plt.plot(x, y)
plt.ylim(-0.1, 1.1)  # range of y axis on fig.
plt.show()

print("######")

# ReLU

def relu(x):
    return np.maximum(0, x)

x = np.arange(-5, 5, 0.1)
y = relu(x)
print(x, y)

plt.plot(x, y)
plt.ylim(-0.1, 10)  # range of y axis on fig.
plt.show()

print("######")
A = np.array([1, 2, 3, 4])
print(A, np.ndim(A), A.shape)
B = np.array([[1, 2], [3, 4], [5, 6]])
print(B, np.ndim(B), B.shape)

print("###")
C1 = np.array([[1, 2], [3, 4]])
C2 = np.array([[5, 6], [7, 8]])
C1C2 = np.dot(C1, C2)
print("C1", C1, "shape", C1.shape)
print("C2", C2, "shape", C2.shape)
print("C1*C2:", C1 * C2, "shape", (C1 * C2).shape)
print("C1C2:", C1C2, "shape", C1C2.shape)

print("###")
D1 = np.array([[1, 2, 3], [4, 5, 6]])
D2 = np.array([[1, 2], [3, 4], [5, 6]])
D1D2 = np.dot(D1, D2)
print("D1", D1, "shape", D1.shape)
print("D2", D2, "shape", D2.shape)
# print("D1*D2:",D1*D2,"shape",(D1*D2).shape ) # error
print("D1D2:", D1D2, "shape", D1D2.shape)

print("######")
x = np.array([1, 2])
w = np.array([[1, 3, 5], [2, 4, 6]])
print("x", x, 'x.shape', x.shape)
print("w", w, 'w.shape', w.shape)
y = np.dot(x, w)
print('y', y)

print("######")
x = np.array([1.0, 0.5])
w1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
b1 = np.array([0.1, 0.2, 0.3])
a1 = np.dot(x, w1) + b1
print("x", x, 'x.shape', x.shape)
print("w1", w1, 'w1.shape', w1.shape)
print("b1", b1, 'b1.shape', b1.shape)
print('a1', a1, 'a1.shape', a1.shape)
z1 = sigmoid(a1)
print('z1', z1, 'z1.shape', z1.shape)

w2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
b2 = np.array([0.1, 0.2])
a2 = np.dot(z1, w2) + b2
print("w2", w2, 'w2.shape', w2.shape)
print("b2", b2, 'b2.shape', b2.shape)
print('a2', a2, 'a2.shape', a2.shape)
z2 = sigmoid(a2)
print('z2', z2, 'z2.shape', z2.shape)

def identify_function(x):
    return x

w3 = np.array([[0.1, 0.3], [0.2, 0.4]])
b3 = np.array([0.1, 0.2])
a3 = np.dot(z2, w3) + b3
print("w3", w3, 'w3.shape', w3.shape)
print("b3", b3, 'b3.shape', b3.shape)
print('a3', a3, 'a3.shape', a3.shape)
y = identify_function(a3)
print('y', y, 'y.shape', y.shape)

print("######")

# Newral network ( foward)

def init_network():
    network = {}

    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])

    network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])

    network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])

    return network

def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = identify_function(a3)
    return y

network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)

print("######")

# Softmax
def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a - c)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y

x = np.array([0.3, 2.9, 4.0])
y = softmax(x)
print(y)
print(np.sum(y))
print("###")
x = np.array([1010, 1000, 990])
y = softmax(x)
# y = np.exp(x) / np.sum(np.exp(x)) # [nan nan nan] Overflow Error
print(y)
print(np.sum(y))


#!/usr/bin/python

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

def log(t):
    return [math.log(x, 2) for x in t]

def sqrt(t):
    return [math.sqrt(x) for x in t]

def linear(t):
    return t

def squared(t):
    return [x ** 2 for x in t]

def exponential(t):
    return [2 ** x for x in t]

def factorial(t):
    return [math.factorial(x) for x in t]

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    elif n == 2:
        return 1
    else:
        i = 1 # fib(n-1)
        j = 1 # fib(n-2)
        for x in xrange(3, n+1):
            k = i + j # fib(n) = fib(n-1) + fib(n-2)
            j = i
            i = k
        return i

def fibonacci(t):
    return [fib(x) for x in t]

def plot_functions():
    fns = [
        ['log n', log],
        ['sqrt n', sqrt],
        ['n', linear],
        ['n^2', squared],
        ['2^n', exponential],
        ['n!', factorial],
        ['fib n', fibonacci],
    ]

    t = range(1, 101)
    for label, fn in fns:
        plt.plot(t, fn(t), label=label)
    plt.yscale('log')
    plt.legend(loc='upperleft')
    plt.show()

if __name__ == '__main__':
    plot_functions()


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

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

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

def bestFitSlopeAndIntercept(xs, ys):
    ### this is the slope of the best fit line
    m= ((mean (xs) * mean(ys))- mean(xs*ys)) / ((mean(xs)**2)-mean(xs **2))
    b = mean(ys)-m * mean(xs)
    return m, b

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

def coeffOfDetermination(ys_orig, ys_line):
    y_mean_line= [mean(ys_orig) for y in ys_orig]
    squaredError_regr = squaredError(ys_orig, ys_line)
    squaredError_yMean= squaredError(ys_orig, y_mean_line)
    return 1 - (squaredError_regr / squaredError_yMean)

#create a test dataset
##decrease the variance in the test dataset to decrease the rendomness in the data
###chage the correlation to negative to show that it is not a dataset to linear regression
xs, ys = createDataset(40, 40, 2, correlation='pos')

m, b= bestFitSlopeAndIntercept(xs, ys)

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

# use this best fit line to make a prediction

predict_x= 8
predict_y = (m* predict_x)+b

#to determine the accuracy
#use rsquared error

r_squared = coeffOfDetermination(ys, regression_line)
print(r_squared)

plt.scatter(predict_x,predict_y,s=150,  color='g')
plt.scatter(xs,ys)
plt.plot(xs, regression_line)
plt.show()


import numpy as np
import matplotlib.pyplot as plt
from scipy import fftpack
from matplotlib.colors import LogNorm
import cv2
import time

start = time.time()
#Load input image
image_source = cv2.imread('C:/FaksGit/FourierFilter/TestImages/man.png')
gray_image = cv2.cvtColor(image_source, cv2.COLOR_BGR2GRAY)

#Plot input image
plt.figure()
plt.imshow(gray_image, plt.cm.gray)
plt.xticks([]), plt.yticks([])
plt.title("Original image")

#Return the two-dimensional discrete Fourier transform of the 2-D argument x.
image_fft = fftpack.fft2(gray_image)

#Logaritmic map
def show_spectrum(image_fft):
    plt.imshow(np.abs(image_fft), norm=LogNorm(vmin=5))
    plt.colorbar()  #Add colorbar

#Plot FT input image
plt.figure()
show_spectrum(image_fft)
plt.title("Fourier transform")

keep_fraction = 0.3  #keep fraction (u oba smijera)
image_fft2 = image_fft.copy()
row, col = image_fft2.shape  #get the current shape of an array

#Set on zero all rows with index between row*keep_fraction and row*(1-keep_fraction)
image_fft2[int(row*keep_fraction):int(row*(1-keep_fraction))] = 0
#Similar for columns
image_fft2[:, int(col*keep_fraction):int(col*(1-keep_fraction))] = 0

#Plot spectrum
plt.figure()
show_spectrum(image_fft2)
plt.title('Filtered Spectrum')

#Return inverse two-dimensional discrete Fourier transform of arbitrary type sequence x
image_new = fftpack.ifft2(image_fft2).real

end = time.time()
print("Time:" ,end - start)

fig = plt.figure(figsize=(20,10))
plt.imshow(image_new, plt.cm.gray)
plt.xticks([]), plt.yticks([])
plt.title('Reconstructed Image FFT')
fig.savefig('baboon_gauss60.3FFT.png')


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

def gradDescent(grad, x, parameters, residual, lr):
    return np.matmul(-lr * grad(x, *parameters[0]), residual).transpose()

def gaussNewton(grad, x, parameters, residual, lr):
    J = grad(x, *parameters[0])
    a = np.linalg.inv(np.matmul(J, J.transpose()))
    return np.matmul(a, np.matmul(-J, residual)).transpose()

def LM(grad, x, parameters, residual, lr):
    J = grad(x, *parameters[0])
    a = np.matmul(J, J.transpose()) + lr * np.identity(np.shape(J)[0])
    return np.matmul(np.linalg.inv(a), np.matmul(-J, residual)).transpose()

def optimize(obj, grad, x, y, updateFunc=gradDescent, pltTitle='Gradient Descent', init=[10, 13, 19.12], lr=0.01, tolerance=1e-5, iterNum=200):
    parameters = np.reshape(init, (1, -1))
    lastIter = iterNum
    lossHis = []

    for i in range(iterNum):
        residual = np.reshape(obj(x, *parameters[0]), (-1, 1)) - np.reshape(y, (-1, 1))
        lossHis.append(np.sum(abs(residual)))
        if np.max(abs(residual)) < tolerance:
            lastIter = i + 1
            break

        parameters += updateFunc(grad, x, parameters, residual, lr)

    plt.figure()
    plt.plot(range(lastIter), lossHis)
    plt.title('Loss vs Number of Iterations for ' + pltTitle)
    return parameters[0]

def gaussian_grad(x, a, mean, std):
    Ja = np.exp(-(x - mean) ** 2 / (2 * std ** 2))
    Jm = a * (x - mean) * np.exp(-(x - mean) ** 2 / (2 * std ** 2)) / (std ** 2)
    Js = a * ((x - mean) ** 2) * np.exp(-(x - mean) ** 2 / (2 * std ** 2)) / (std ** 3)
    return np.vstack((Ja, Jm, Js))

def make_gaussian(x, a, mean, std):
    y = a * np.exp(-(x-mean)**2/(2*std**2))
    return y

gt = [10, 0, 20]
obsNum = 50
x = np.linspace(-25, 25, obsNum)
y = make_gaussian(x, *gt)
predsGD = make_gaussian(x, *optimize(make_gaussian, gaussian_grad, x, y))
predsGN = make_gaussian(x, *optimize(make_gaussian, gaussian_grad, x, y, gaussNewton, 'Gauss-Newton'))
predsLM = make_gaussian(x, *optimize(make_gaussian, gaussian_grad, x, y, LM, 'LM', lr=1))

plt.figure()
plt.plot(x, y, '-o', label='Ground Truth')
plt.plot(x, predsGD, '-o', label='Gradient Descent')
plt.plot(x, predsGN, '-o', label='Gauss-Newton')
plt.plot(x, predsLM, '-o', label='LM')
plt.title('Predicted vs GT Gaussians by Gradient Descent')
plt.legend()
plt.show()


# standard library
from functools import lru_cache

# third-party libraries
from scipy.stats import norm as normal_distribution, binom as binomial_distribution # type: ignore # mistakenly, pylance doesn't recognize scipy.stats.norm and scipy.stats.binom

@lru_cache(10000)
def normal_z_score(p: float = 0.95) -> float:
    """
    Answers the question: "At what point in terms of number of standard deviations from the mean
    the area spanning from -inf to that point on the normal distribution cover `p` of its area

    https://en.wikipedia.org/wiki/Standard_score#/media/File:Z_score_for_Students_A.png
    https://mat117.wisconsin.edu/wp-content/uploads/2014/12/Sec03.-z-score-5.png
    https://cdn1.byjus.com/wp-content/uploads/2017/09/word-image2.png
    """
    return normal_distribution.ppf(p)

@lru_cache(10000)
def normal_z_score_two_tailed(p: float = 0.95) -> float:
    """
    Answers the question: "How many standard deviations from the mean we have to span
    equally in both sides in a normal distribution to cover `p` of the area"

    https://www.freecodecamp.org/news/content/images/2020/08/normal_dist_68_rule.jpg
    https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Standard_deviation_diagram.svg/1920px-Standard_deviation_diagram.svg.png
    """
    return normal_distribution.ppf(1-(p/2)) # (1+p)/2  is a more concervative formula

@lru_cache(10000)
def normal_p_area(z: float) -> float:
    """
    Answers the question: "How much area under the normal distribution curve does
    the range from -inf to `z` cover (`z` - number of standard deviations from the mean)"

    https://en.wikipedia.org/wiki/Standard_score#/media/File:Z_score_for_Students_A.png
    https://mat117.wisconsin.edu/wp-content/uploads/2014/12/Sec03.-z-score-5.png
    https://cdn1.byjus.com/wp-content/uploads/2017/09/word-image2.png
    """
    return normal_distribution.cdf(z)

@lru_cache(10000)
def normal_p_area_two_tailed(z: float) -> float:
    """
    Answers the question: "How much area does the range cover that spans
    `z` standard deviations from the mean in equally both sides in a normal distribution"

    https://www.freecodecamp.org/news/content/images/2020/08/normal_dist_68_rule.jpg
    https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Standard_deviation_diagram.svg/1920px-Standard_deviation_diagram.svg.png
    """
    return (-normal_distribution.cdf(z)+1) * 2  # (2*z)-1  is a more concervative formula


import numpy as np

def distance2(A, B):

    M = A.shape[0]
    N = B.shape[0]

    A_dots = (A*A).sum(axis=1).reshape((M, 1))*np.ones(shape=(1, N))
    B_dots = (B*B).sum(axis=1)*np.ones(shape=(M, 1))
    D_squared = A_dots + B_dots - 2*A.dot(B.T)

    zero_mask = np.less(D_squared, 0.0)
    D_squared[zero_mask] = 0.0
    return np.sqrt(D_squared)

if __name__ == '__main__':
    f = open("x.txt", "r")
    lst = []
    for line in f:
        strd = line.strip().split(',')
        lst.append([float(x) for x in strd])

    arr = np.array(lst)
    rs = distance2(arr, arr)
    print(rs)
    np.savetxt("rs.txt", rs, delimiter=",", fmt="%.4e")


"""Adapted from ``scipython.com/book/chapter-8-scipy/additional-examples/the-sir-epidemic-model/``"""
from corona.utils import *
from scipy.integrate import odeint

# The SIR model differential equations.
def dxdt_param(y, t, beta, gamma):
    S, I, R = y
    N = sum(y)
    dSdt = -beta * S * I / N
    dIdt = beta * S * I / N - gamma * I
    dRdt = gamma * I
    return dSdt, dIdt, dRdt

# Total population, N.
N = 1000

# Initial numbers
I0 = 1           # Infected
R0 = 0           # Recover
S0 = N - I0 - R0 # Susceptible

# Params
beta  = 0.2 # Contact rate
gamma = 0.1 # Mean recovery rate (in people/days).

# A grid of time points (in days)
tt = np.linspace(0, 365, 365+1)

# Fix params
from functools import partial
dxdt = partial(dxdt_param,beta=beta,gamma=gamma)

# Initial conditions vector
x0 = S0, I0, R0

# Integrate the SIR equations over the time grid, t.
ret = odeint(dxdt, x0, tt)
SS, II, RR = ret.T

# Plot the data on three separate curves for S(t), I(t) and R(t)
plt.ion()
fig, ax = freshfig(1)
ax.plot(tt, SS, label='Susceptible')
ax.plot(tt, II, label='Infected')
ax.plot(tt, RR, label='Recovered with immunity')
ax.set_xlabel('Time (days)')
ax.set_ylabel('People')
legend = ax.legend()


from numpy import *
from math import log

# x: features, y: classes
def infogain(x, y):
    info_gains = zeros(x.shape[1]) # features of x

    # calculate entropy of the data *hy*
    # with regards to class y
    cl = unique(y)
    hy = 0
    for i in range(len(cl)):
        c = cl[i]
        py = float(sum(y==c))/len(y) # probability of the class c in the data
        hy = hy+py*log(py,2)

    hy = -hy
    # compute IG for each feature (columns)
    for col in range(x.shape[1]): # features are on the columns
        values = unique(x[:,col]) # the distinct values of each feature
        # calculate conditional entropy *hyx = H(Y|X)*
        hyx = 0
        for i in range(len(values)): # for all values of the feature
            f = values[i] # value of the specific feature
            yf = y[where(x[:,col]==f)] # array with the the data points index where feature i = f
            # calculate h for classes given feature f
            yclasses = unique(yf) # number of classes
            # hyx = 0; # conditional class probability initialization
            for j in range(len(yclasses)):
                yc = yclasses[j]
                pyf = float(sum(yf==yc))/len(yf) # probability calls condition on the feature value
                hyx = hyx+pyf*log(pyf,2) # conditional entropy

        hyx = -hyx
        # Information gain
        info_gains[col] = hy - hyx

    return info_gains


import numpy as np
from scipy import special
import matplotlib.pyplot as plt

def plane_diff_1d(t, D, x0=0, xmin=-1, xmax=1, c_left=1, c_right=0, num_points=200):

     n = num_points
     x = np.linspace(xmin, xmax, n)
     delta_c = c_left - c_right

     c0 = np.piecewise(x, [x < x0, x >= x0], [c_left, c_right])
     c = 0.5 * delta_c * (special.erfc((x - x0)/(2 * np.sqrt(D * t))))

     return x, c, c0

D = 0.01 # Diffusion coefficient

fig, ax = plt.subplots()

for t in range(1, 14, 3):

    x, c, c0 = plane_diff_1d(t=t, D=D)
    if t==1:
        leg = "t = " + str(t)
        plt.plot(x, c0, label="t = 0")
    leg = "t = " + str(t)
    ax.plot(x, c, label=leg)

ax.grid()
ax.set_xlabel('x')
ax.set_ylabel('C')
ax.legend()


import numpy as np
import matplotlib.pyplot as plt
import time
from FastFourierTransform import fft_own

if __name__ == "__main__":
    ekg = np.loadtxt('ekg.csv', delimiter=",")
    dataLen = len(ekg[0])
    # fft_own requires pow of 2 number of samples
    N = 2 ** (int(np.log2(len(ekg[0]))) + 1)
    pad_len = N - dataLen
    t = np.append(ekg[0], np.zeros(pad_len))
    ekg_val = np.append(ekg[1], np.zeros(pad_len))

    # Plot the signal
    plt.subplot(311)
    plt.plot(t, ekg_val)

    fs = 1000
    start_time = time.time()
    sp = np.fft.fft(ekg_val)
    stop_time = time.time()
    elapsed_numpy = stop_time - start_time
    sp = sp[0:int(N/2)]
    f = np.linspace(0, fs / 2, int(N / 2))

    # Compute the FFT with time measurement
    start_time = time.time()
    X_f = fft_own(ekg_val)
    stop_time = time.time()

    # Magnitude
    A_f = np.sqrt(X_f.imag ** 2 + X_f.real ** 2)[0:int(N/2)]
    sp = np.sqrt(sp.imag ** 2 + sp.real ** 2)[0:int(N/2)]

    elapsed_time = stop_time - start_time
    print("The time of execution of our DFT (own) is: %.5f s." % elapsed_time)
    print("The time of execution of our DFT (np) is: %.5f s." % elapsed_numpy)

    plt.subplot(312)
    plt.plot(f, sp)

    plt.subplot(313)
    plt.plot(f, A_f)

    plt.show()


'''
Assume df is a pandas dataframe object of the dataset given
'''
import numpy as np
import pandas as pd
import random

'''
Calculate the entropy of a target attribute
'''
def entropy(target_attribute):
	elements, counts = np.unique(target_attribute,return_counts = True)
	entropy = np.sum([(-counts[i]/np.sum(counts))*np.log2(counts[i]/np.sum(counts)) for i in range(len(elements))])
	return entropy

'''
Calculate the entropy of the entire dataset
	#input:pandas_dataframe
	#output:int/float/double/large
'''
def get_entropy_of_dataset(df):
	elements, counts = np.unique(df[df.columns[-1]], return_counts = True)
	total_entropy = np.sum([(-counts[i]/np.sum(counts))*np.log2(counts[i]/np.sum(counts)) for i in range(len(elements))])
	return total_entropy

'''
Return entropy of the attribute provided as parameter
	#input:pandas_dataframe,str   {i.e the column name ,ex: Temperature in the Play tennis dataset}
	#output:int/float/double/large
'''
def get_entropy_of_attribute(df,attribute):
	vals, counts= np.unique(df[attribute],return_counts=True)
	entropy_of_attribute = np.sum([(counts[i]/np.sum(counts))*entropy(df.where(df[attribute]==vals[i]).dropna()[df.columns[-1]]) for i in range(len(vals))])
	return abs(entropy_of_attribute)

'''
Return Information Gain of the attribute provided as parameter
	#input:int/float/double/large,int/float/double/large
	#output:int/float/double/large
'''
def get_information_gain(df,attribute):
	return (get_entropy_of_dataset(df) - get_entropy_of_attribute(df,attribute))

'''
Returns Attribute with highest info gain
	#input: pandas_dataframe
	#output: ({dict},'str')
	Return a tuple with the first element as a dictionary which has IG of all columns
	and the second element as a string with the name of the column selected
'''
def get_selected_attribute(df):
	information_gains = {}

	for i in range(len(df.columns)-1):
		information_gains[df.columns[i]]=get_information_gain(df,df.columns[i])
	selected_column = max(information_gains, key = information_gains.get)

	return (information_gains,selected_column)


# Задание 7: Конструкция Пэли и коды Боуза—Шриханде

# Реализуйте алгоритм построения кодов Боуза—Шриханде второго типа, которые построены по матрице Адамара,
# построенной в свою очередь на основе конструкции Пэли. На вход программы подаётся единственное целое число n.
# Гарантируется, что n делится на 4 без остатка, и что (n−1) — простое число.
# Программа должна выдать 2n строчек длины n, содержащих нули и единицы. Строчки должны быть все различны,
# причём количество различных позиций в любой паре различных строчек должно быть не менее n/2.

# Вычислительной оптимальности не требуется, но полезно, например, предвычислить таблицу значений символа Лежандра.

import numpy as np

def inv_matrix(matrix):
    matrix_ = matrix.copy()
    return np.array([1 * (i + 1) % 2 for i in matrix_])

def build_Hadamard_matrix(n):
    result = np.zeros((n, n))
    p = n - 1

    residues = np.zeros(2 * p)

    residues[:p] = -1

    for i in range(1, ((p - 1) // 2) + 1):
        residues[i * i % p] = 1

    residues[p:2 * p] = residues[: - p]

    result[0, :] = 1

    result[:, 0] = 1

    for i in range(1, n):
        for j in range(1, n):
            result[i, j] = residues[j - i + p]

    return result

def print_matrix(matrix):
    '''
    Print matrix
    Input: nxn matrix, optional - n=shape
    '''
    n = matrix.shape[0]
    for line in matrix[:n, :n].astype(np.int32):
        print("".join(map(str, line)))

def main():
    n = int(input())

    hadamard_matrix = build_Hadamard_matrix(n)

    hadamard_matrix[hadamard_matrix < 0] = 0

    hadamard_matrix2 = inv_matrix(hadamard_matrix)

    hadamard_matrix = hadamard_matrix[::-1]

    result_code = np.vstack((hadamard_matrix2, hadamard_matrix))

    print_matrix(result_code)

main()


import numpy as np

# Converts arrays of xyz-coordinates to spherical coordinates
def cart2sph(xv,yv,zv):
    rho = np.sqrt(xv**2+yv**2+zv**2)
    phi = np.arctan2(yv,xv)
    theta = np.arctan2(zv,np.sqrt(xv**2+yv**2))
    return rho,phi,theta

# Converts arrays of spherical coordinates to xyz-coordinates
def sph2cart(rho,phi,theta):
    xv = rho*np.sin(theta)*np.cos(phi)
    yv = rho*np.sin(theta)*np.sin(phi)
    zv = rho*np.cos(theta)
    return xv,yv,zv

# Euclidean distance between two 1D arrays, used for lists of x-coordinates
def dist(r1,r2):
    return np.sqrt((r1-r2)**2)

# Euclidean distance between two 2D arrays, used for lists of xyz-coordinates
def arrayDist(r1,r2):
    return np.sqrt(np.sum((r1-r2)**2,axis=1))


import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from scipy.stats import binom
from scipy import linalg
from scipy.sparse.linalg import expm_multiply
from scipy.sparse import csc_matrix
from scipy.special import comb
import math
import time

def hypercube(n_dim):
    n = n_dim - 1
    sigma_x = np.array([[0, 1],
                        [1, 0]])
    A = sigma_i(sigma_x, 0, n)

    for i in range(1, n_dim):
        A += sigma_i(sigma_x, i, n)
    return -1 * A

def sigma_i(sigma_x, i, n):
    if i > 0:
        out = np.eye(2)
        for j_before in range(i - 1):
            out = np.kron(out, np.eye(2))
        out = np.kron(out, sigma_x)
    else:
        out = sigma_x
    for j_after in range(n - i):
        out = np.kron(out, np.eye(2))
    return out

def quantum_walk_hypercube(N, H, psi0, timesteps, normalise):
    psiN = expm_multiply(-(1j) * timesteps * H, psi0)

    prob = np.real(np.conj(psiN) * psiN)

    result = np.zeros(N + 1)
    normalise_array = np.zeros(N+1)

    for i, probability in enumerate(prob):
        binary_i = bin(i)
        i_ones = [ones for ones in binary_i[2:] if ones == '1']
        num_ones = len(i_ones)
        result[num_ones] += probability
        if normalise:
            normalise_array[num_ones] += 1

    if normalise:
        result = result/normalise_array

    return result

def run_walks(n, time_limit, gamma, normalise=False):
    P = 2 ** n  # number of positions
    A = hypercube(n)
    marked_state = np.zeros((2 ** n, 2 ** n))
    marked_state[0, 0] = 1
    H = gamma * (A - n * np.eye(2 ** n)) - marked_state  # not sure if marked_node should also be multiplied by gamma

    psi0 = np.ones(P) * (1 / np.sqrt(2 ** n))
    output = np.zeros((time_limit + 1, n + 1))
    for timesteps in range(0, time_limit + 1):
        output[timesteps] = quantum_walk_hypercube(n, H, psi0, timesteps, normalise=normalise)
        print(sum(output[timesteps]))
    return output

def plot_furthest_qubit_prob(timesteps, data):
    plt.figure()
    plt.plot(range(timesteps + 1), data[:, -1])
    plt.xlim(0, timesteps)
    plt.xticks(range(0, timesteps + 1, 5))
    plt.ylim(0, 1)
    plt.yticks([0, 0.2, 0.4, 0.6, 0.8, 1])
    plt.show()

def plot_nearest_qubit_prob(timesteps, data):
    plt.figure()
    plt.plot(range(timesteps + 1), data[:, 0])
    plt.xlim(0, timesteps)
    plt.xticks(range(0, timesteps + 1, 5))
    plt.ylim(0, 1)
    plt.yticks([0, 0.2, 0.4, 0.6, 0.8, 1])
    plt.show()

def plot_prob_heatmap(data, N, timesteps):
    fig, ax = plt.subplots()
    im = ax.imshow(data, interpolation="gaussian", cmap=plt.get_cmap('plasma'))
    ax.set_xlabel("Steps away from origin, x")
    ax.set_ylabel("Time, t", rotation=0, labelpad=18)
    plt.xticks(range(0, N + 1, 2))
    plt.yticks(range(0, timesteps + 1, 3))
    ax.invert_yaxis()

    cb = fig.colorbar(cm.ScalarMappable(cmap=plt.get_cmap('plasma')))
    cb.set_label('Probability, P(x, t)')

    plt.show()

def optimal_gamma(n):
    N = 2**n
    gam = 0
    for r in range(1, n+1):
        gam += comb(n, r) * (1/r)
    gam = (1/2) * (1/N) * gam
    return gam

def first_max_index(array):
    index = len(array) - 1
    previous_value = array[0] - 1
    for i in range(len(array)):
        if array[i] < previous_value:
            index = i - 1
            break
        previous_value = array[i]
    return index, np.max(array)

if __name__ == '__main__':
    time_start = time.time()
    max_timesteps = [10, 10, 10, 20, 20, 20, 30, 40, 50, 60]
    data = []
    num_dims = 10

    for n in range(1, num_dims+1):
        gamma = optimal_gamma(n)    # hopping rate
        print("gamma:", gamma)
        print("n:", n)
        data.append(run_walks(n, max_timesteps[n-1], gamma))  # 2D array of probability with dims [time, distance]

    time_end = time.time()
    print("runtime:", time_end - time_start)

    peak_times = np.zeros(num_dims)
    peak_probs = np.zeros(num_dims)
    for i in range(num_dims):
        peak_times[i] = first_max_index(data[i][:, 0])[0]
        peak_probs[i] = first_max_index(data[i][:, 0])[1]

    plt.figure()

    # plt.plot(np.arange(num_dims)+1, peak_probs)
    # plt.xlim(1, num_dims)
    # plt.xlabel("Number of qubits, n")
    # plt.ylabel("Maximum probability of marked state, $P_{max}(n)$")

    n_array = np.arange(float(num_dims)) + 1
    for index, num in enumerate(n_array):
        n_array[index] = np.sqrt(2**num)

    plt.plot(n_array, peak_times)
    plt.scatter(n_array, peak_times)
    #plt.xlim(0, n_array[-1])
    # plt.xticks(range(0, timesteps + 1, 5))
    plt.xlabel("$\sqrt{2^{n}}$")
    plt.ylim(0, max_timesteps[-1])
    # plt.yticks([0, 0.2, 0.4, 0.6, 0.8, 1])
    plt.ylabel("Time until first probability maximum of marked state")

    plt.show()


import numpy as np

def ReLU(z):
    return np.maximum(z, 0)

def sigmoid(z):
    return (1+np.exp(-z))**(-1)

def TanH(z):
    return (np.exp(z) - np.exp(-z))/2

def softmax(z):
    return np.exp(z)/np.sum(np.exp(z))

##### Derivatives ######

def ReLU_deriv(z):
    z[z <= 0] = 0
    z[z > 0] = 1
    return z

def sigmoid_deriv(z):
    return sigmoid(z)*(1-sigmoid(z))

def TanH_deriv(z):
    return 1 - TanH(z)**2

def softmax_deriv(z):
    exp = np.exp(z)
    return exp*(1/np.sum(exp) - exp/np.sum(exp)**2)


#######################################################################################
#
#	Copyright (c) 2018, Wiphoo (Terng) Methachawalit, All rights reserved.
#
#######################################################################################

#######################################################################################
#
#	STANDARD IMPORTS
#

import numpy

#######################################################################################
#
#	LOCAL IMPORTS
#

#######################################################################################
#
#	GLOBAL VARIABLES
#

#######################################################################################
#
#	HELPER FUNCTIONS
#

#######################################################################################
#
#	CLASS DEFINITIONS
#

class ActivationFuncs:
	''' this class is designed storing an actication fucntion
			and activation function derivative
	'''

	@staticmethod
	def activationFunc( x ):
		return ActivationFuncs.sigmoidFunc( x )

	@staticmethod
	def activationFuncDerivative( val ):
		return ActivationFuncs.sigmoidFuncDerivative( val )

	#	sigmoid functions
	@staticmethod
	def sigmoidFunc( x ):
		return 1.0 / ( 1.0 + numpy.exp( -x ) )

	@staticmethod
	def sigmoidFuncDerivative( val ):
		return val * ( 1.0 - val )


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

# # Week 8 Wednesday
#
# [Yuja recording](https://uci.yuja.com/V/Video?v=4446584&node=14938258&a=1900985263&autoplay=1)
#
# Before the recording, at the board we went over some different components related to Neural Networks and PyTorch, and especially we went over an example of performing gradient descent.
#
# The goal of today's class is to get more comfortable with the various components involved in building and training a neural network using PyTorch.

# In[1]:

import numpy as np
import torch
from torch import nn
from torchvision.transforms import ToTensor

# ## Gradient descent

# Gradient descent can be used to try to find a minimum of any differentiable function.  (Often it will only find a local minimum, not a global minimum, even if a global minimum exists.)  We usually use gradient descent for very complicated functions, but here we give an example of performing gradient descent to attempt to find a minimum of the function
#
# $$
# f(x,y) = (x-3)^2 + (y+2)^2 + 8.
# $$
#
# We call this function `loss_fn` so that the syntax is the same as what we're used to in PyTorch.

# In[2]:

loss_fn = lambda t: (t[0] - 3)**2 + (t[1] + 2)**2 + 8

# To perform gradient descent, you need to begin with an initial guess.  We guess (10,10) and then gradually adjust this, hoping to move towards a minimum.  Notice the decimal point after 10... this is a shortcut for telling PyTorch that these should be treated as floats.

# In[3]:

a = torch.tensor([10.,10], requires_grad=True)
a

# In[4]:

loss_fn([10,10])

# In[5]:

loss_fn(a)

# In[6]:

type(loss_fn)

# Because we specified `requires_grad=True` as a keyword argument, we will be able to find gradients of computations involving `a`.  There isn't any gradient yet because we haven't computed one.

# In[7]:

a.grad

# Here we define a stochastic gradient descent optimizer like usual in PyTorch.  The first input is usually something like `model.parameters()`.  Here we try to use `a` as the first argument.  That is almost right, but we need to put it in a list (or some other type of *iterable*).

# In[8]:

optimizer = torch.optim.SGD(a, lr = 0.1)

# In[9]:

optimizer = torch.optim.SGD([a], lr = 0.1)

# In[10]:

loss = loss_fn(a)

# This next `optimizer.zero_grad()` is not important yet, but it is good to be in the habit, because otherwise multiple gradient computations will accumulate, and we want to start over each time.

# In[11]:

optimizer.zero_grad()

# In[12]:

type(loss)

# Next we compute the gradient.  This typically uses an algorithm called *backpropagation*, which is where the name `backward` comes from.

# In[13]:

loss.backward()

# In[14]:

a

# Now the `grad` attribute of `a` has a value.  You should be able to compute this value by hand in this case, since our `loss_fn` is so simple.

# In[15]:

a.grad

# Now we replace add a multiple (the learning rate `lr`) of the negative gradient to `a`.  Again, you should be able to compute this by hand in this case.  The formula is
#
# $$
# a \leadsto a - lr \cdot \nabla
# $$

# In[16]:

optimizer.step()

# In[17]:

a

# In[18]:

loss_fn = lambda t: (t[0] - 3)**2 + (t[1] + 2)**2 + 8

# Notice how the value of `a` is approaching the minimum (3,-2), and notice how `loss` is approaching the minimum of our `loss_fn`, which is 8.  (The only reason we're using the terms `loss` and `loss_fn` is because those are the terms we usually use in PyTorch.  In this case, `loss_fn` is just an ordinary two-variable function like from Math 2D which we are trying to minimize.)

# In[19]:

epochs = 20
a = torch.tensor([10.,10], requires_grad=True)
optimizer = torch.optim.SGD([a], lr = 0.1)
for i in range(epochs):
    loss = loss_fn(a)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print("Epoch " + str(i))
    print(a)
    print(loss)
    print("")

# If we want `a` to approach the minimum (3,-2) faster, we can make the learning rate bigger, but here is an example of what can go wrong if we make the learning rate too big.

# In[20]:

epochs = 20
a = torch.tensor([10.,10], requires_grad=True)
optimizer = torch.optim.SGD([a], lr = 10)
for i in range(epochs):
    loss = loss_fn(a)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print("Epoch " + str(i))
    print(a)
    print(loss)
    print("")

# Here is an example for what seems to be a good choice of `lr`.

# In[21]:

epochs = 20
a = torch.tensor([10.,10], requires_grad=True)
optimizer = torch.optim.SGD([a], lr = 0.25)
for i in range(epochs):
    loss = loss_fn(a)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print("Epoch " + str(i))
    print(a)
    print(loss)
    print("")


# -*- coding: utf-8 -*-
"""
Created on Mon Jun 22 08:49:49 2020

@author: simon
"""

#%%
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt

# Quantizer
# quant(0.111, [-0.125, 0.125]) ==> 1  out of range[0, 1, 2]
# returns value in range [0 to n+1] where n = len(references)
def quant(x, thresholds):
   # Concatenate a very high value to the vector of thresholds such that
   # the line afterwards does not give empty sets
   t = np.concatenate( ( thresholds, [1e10] ) )
   # The code [(idx,val) for (idx,val) in enumerate(t) if val>x] gives back
   # all pairs of indices and values for values that are larger than x.
   # The min() function extracts the (idx,val) pair with the lowest idx.
   # min()[0] is the just the index. This is our decision region.
   # Possible values go from 0 to the number of thresholds.
   r = min( [(idx,val) for (idx,val) in enumerate(t) if val>x] )[0]
   return r

# MDAC
# mdac(0.123, [-0.125, 0.125], [-0.25, 0, 0.25], 2) ==> [1, 0.246]
#
# thresholds => for quantizer
# reference  => for DAC
def mdac(x, thresholds, references, gain):
   # The MDAC first uses a quantizer to determine the region.
   r = quant(x,thresholds)
   # It then subtracts the appropriate reference voltage for that region
   # and scales the result with the MDAC gain.
   y = ( x - references[r] ) * gain
   return [r,y]

# pipeline ADC
def pipeA(x, Nstage):
   # First a simple pipeline ADC with Nstage stages. Nstage-1 are 1.5-bit
   # stages, the last one is a 1-bit stage for the final decision.

   # np.arange(a,b,d) makes an array filled with numbers from a to b, in
   # steps of d, but NOT including b. So we add 0.001 to the upper limit such
   # that it is inlcuded.

   # The result of the liner below is array([-0.25,  0.  ,  0.25])
   re15 = np.arange( -1/4, 1/4+0.01, 1/4 )

   # The thresholds are exactly in the middle of the reference voltages.
   # The convolution with [1/2,1/2] is a moving average which gives one
   # element too many at each end of the array. Adressing an array
   # with a[1:-1] gives all elements except the first and the last.

   # The result of the line below is array([-0.125,  0.125])
   th15 = np.convolve( re15, np.array([1/2,1/2]) )[1:-1]
   ga15 = 2

   # The vector xs will store the input voltage of each stage:
   xs = np.zeros(Nstage)
   xs[0] = x

   # The vector rs will store the region that each stage has determined.
   # The stage must scale the region such that it is between 0 and 1.
   rs = np.zeros(Nstage)

   # The number cumgain is all the gain accumulated after each stage.
   # The logic behind this is that every time a stage multiplies the
   # voltage, the following stage is less important by the same
   # factor. We initialize this number with 1:
   cumgain = 1

   # All but the last stage are 1.5-bit-stages:
   for k in np.arange(Nstage-1):
       # Use the appropriate MDAC:
       [rs[k],xs[k+1]] = mdac(xs[k],th15,re15,ga15)
       # The cumulative gain is  multiplied by the gain of the stage:
       cumgain *= 2
       # The region is then scaled by 1/cumgain; therefore the first stage
       # just ouputs its region number divided by its gain.
       rs[k] = rs[k]/cumgain

   # The last stage is a normal 1-bit stage with a gain of 1.  The line
   # cumgain = cumgain*1 is totally useless, but didactically necessary.
   k=Nstage-1
   rs[k] = quant(xs[k],[0])
   cumgain = cumgain*1
   rs[k] = rs[k]/cumgain

   # The output of the ADC is the sum of all region numbers, but if we want
   # integers, they need to be scaled back by cumgain.
   b = np.sum(rs)*cumgain
   return b

# 1.5 Bit
def pipeAnonid(x, Nstage):
   # This is the same as pipeA, but with an incorrect gain and a shifted
   # reference:
   re15 = np.arange( -1/4, 1/4+0.01, 1/4 )
   th15 = np.convolve( re15, np.array([1/2,1/2]) )[1:-1]
   # ------  INPUT HERE  ------
   ga15 = 1.98
   re15[0] = re15[0]-0.01
   # ------
   xs = np.zeros(Nstage)
   xs[0] = x
   rs = np.zeros(Nstage)
   cumgain = 1
   for k in np.arange(Nstage-1):
       [rs[k],xs[k+1]] = mdac(xs[k],th15,re15,ga15)
       cumgain *= 2
       rs[k] = rs[k]/cumgain
   # last stage
   k=Nstage-1
   rs[k] = quant(xs[k],[0])
   cumgain = cumgain*1
   rs[k] = rs[k]/cumgain
   # output scale
   b = np.sum(rs)*cumgain
   return b

# 1.5 Bit
def pipe1721(x,Nstage):
   # This is the ADC from Fig. 17.21 in the textbook; the only difference is that
   # the last stage is a two-bit quantizer
   re15 = np.arange( -1/4, 1/4+0.01, 1/4 )
   th15 = np.convolve( re15, np.array([1/2,1/2]) )[1:-1]
   ga15 = 2
   xs = np.zeros(Nstage)
   xs[0] = x
   rs = np.zeros(Nstage)
   cumgain = 1
   for k in np.arange(Nstage-1):
       [rs[k],xs[k+1]] = mdac(xs[k],th15,re15,ga15)
       cumgain *= 2
       rs[k] = rs[k]/cumgain
   k=Nstage-1

   # The last stage is a two-bit stage.
   # This two-bit stage needs to have one threshold at 0!
   # Being a two-bit stage, it implicitly has a gain of 2
   rs[k] = quant(xs[k],[-0.5,0,0.5])
   cumgain = cumgain*2
   rs[k] = rs[k]/cumgain

   # output scale
   b = np.sum(rs)*cumgain
   return b

# 1.5 Bit
def pipe1721nonid(x,Nstage):
   # This is the ADC from Fig. 17.21 in the textbook; the only difference is that
   # the last stage is a two-bit quantizer
   re15 = np.arange( -1/4, 1/4+0.01, 1/4 )
   th15 = np.convolve( re15, np.array([1/2,1/2]) )[1:-1]
   # ------  INPUT HERE  ------
   ga15 = 1.98
   re15[0] = re15[0]-0.01
   # ------
   xs = np.zeros(Nstage)
   xs[0] = x
   rs = np.zeros(Nstage)
   cumgain = 1
   for k in np.arange(Nstage-1):
       [rs[k],xs[k+1]] = mdac(xs[k],th15,re15,ga15)
       cumgain *= 2
       rs[k] = rs[k]/cumgain
   k=Nstage-1

   # The last stage is a two-bit stage.
   # This two-bit stage needs to have one threshold at 0!
   # Being a two-bit stage, it implicitly has a gain of 2
   rs[k] = quant(xs[k],[-0.5,0,0.5])
   cumgain = cumgain*2
   rs[k] = rs[k]/cumgain

   # output scale
   b = np.sum(rs)*cumgain
   return b

# 2.5 Bit Stages followed by 1.5 Bit Stages and a final 1 Bit stage
def pipe25b15b(x,N25,N15):
   # This is again the same as pipeA, but it has N25 2.5-bit stages followed
   # by N15 1.5-bit stages and one 1-bit stage.
   re15 = np.arange( -1/4, 1/4+0.01, 1/4 )
   th15 = np.convolve( re15, np.array([1/2,1/2]) )[1:-1]
   ga15 = 2

   # So we also need the thresholds and gains of the 2.5-bit MDAC:
   re25 = np.arange( -6/16, 6/16+0.01, 1/8 )
   th25 = np.convolve( re25, np.array([1/2,1/2]) )[1:-1]
   ga25 = 4

   # The total number of stages is now
   Nstage = N25+N15+1

   xs = np.zeros(Nstage)
   xs[0] = x
   rs = np.zeros(Nstage)
   cumgain = 1

   # First the 2.5-bit stages.
   for k in np.arange(N25):
       [rs[k],xs[k+1]] = mdac(xs[k],th25,re25,ga25)
       cumgain *= 4
       rs[k] = rs[k]/cumgain

   # Then the 1.5-bit stages: their indices go from N25 to Nstage-1:
   for k in np.arange(N25,Nstage-1):
       [rs[k],xs[k+1]] = mdac(xs[k],th15,re15,ga15)
       cumgain *= 2
       rs[k] = rs[k]/cumgain

   # We finish with the one-bit stage
   k=Nstage-1
   rs[k] = quant(xs[k],[0])
   cumgain = cumgain*1
   rs[k] = rs[k]/cumgain

   # output scale
   b = np.sum(rs)*cumgain
   return b

# 2.5 Bit Stages followed by a final 1 Bit stage
def pipe25b(x,N25):
   # So we also need the thresholds and gains of the 2.5-bit MDAC:
   re25 = np.arange( -6/16, 6/16+0.01, 1/8 )
   th25 = np.convolve( re25, np.array([1/2,1/2]) )[1:-1]
   ga25 = 4

   # The total number of stages is now
   Nstage = N25+1

   xs = np.zeros(Nstage)
   xs[0] = x
   rs = np.zeros(Nstage)
   cumgain = 1

   # First the 2.5-bit stages.
   for k in np.arange(N25):
       [rs[k],xs[k+1]] = mdac(xs[k],th25,re25,ga25)
       cumgain *= 4
       rs[k] = rs[k]/cumgain

   # We finish with the one-bit stage
   k=Nstage-1
   rs[k] = quant(xs[k],[0])
   cumgain = cumgain*1
   rs[k] = rs[k]/cumgain

   # output scale
   b = np.sum(rs)*cumgain
   return b

# 2.5 Bit Stages followed by a final 1 Bit stage
def pipe25bNonid(x,N25):
   eps = 0.07  # comparator offset

   # So we also need the thresholds and gains of the 2.5-bit MDAC:
   re25 = np.arange( -6/16, 6/16+0.01, 1/8 )
   th25 = np.convolve( re25, np.array([1/2,1/2]) )[1:-1]
   ga25 = 4.0

   th25 = th25 + eps

   # The total number of stages is now
   Nstage = N25+1

   xs = np.zeros(Nstage)
   xs[0] = x
   rs = np.zeros(Nstage)
   cumgain = 1

   # First the 2.5-bit stages.
   for k in np.arange(N25):
       [rs[k],xs[k+1]] = mdac(xs[k],th25,re25,ga25)
       cumgain *= 4
       rs[k] = rs[k]/cumgain

   # We finish with the one-bit stage
   k=Nstage-1
   rs[k] = quant(xs[k],[0])
   cumgain = cumgain*1
   rs[k] = rs[k]/cumgain

   # output scale
   b = np.sum(rs)*cumgain
   return b

# 2.5 Bit Stages followed by a final 1 Bit stage
def pipe25bNonid_vs(x,N25, offset):
   eps = offset  # threshold offset

   # So we also need the thresholds and gains of the 2.5-bit MDAC:
   re25 = np.arange( -6/16, 6/16+0.01, 1/8 )
   th25 = np.convolve( re25, np.array([1/2,1/2]) )[1:-1]
   ga25 = 4.0

   th25 = th25 + eps

   # The total number of stages is now
   Nstage = N25+1

   xs = np.zeros(Nstage)
   xs[0] = x
   rs = np.zeros(Nstage)
   cumgain = 1

   # First the 2.5-bit stages.
   for k in np.arange(N25):
       [rs[k],xs[k+1]] = mdac(xs[k],th25,re25,ga25)
       cumgain *= 4
       rs[k] = rs[k]/cumgain

   # We finish with the one-bit stage
   k=Nstage-1
   rs[k] = quant(xs[k],[0])
   cumgain = cumgain*1
   rs[k] = rs[k]/cumgain

   # output scale
   b = np.sum(rs)*cumgain
   return b

# 1.5 Bit
def pipe15bNonid_vs(x, Nstage, offset):
   eps = offset  # threshold offset
   # This is the same as pipeA, but with an incorrect gain and a shifted
   # reference:
   re15 = np.arange( -1/4, 1/4+0.01, 1/4 )
   th15 = np.convolve( re15, np.array([1/2,1/2]) )[1:-1]
   # ------  INPUT HERE  ------
   ga15 = 2
   th15 = th15 + eps
   # ------
   xs = np.zeros(Nstage)
   xs[0] = x
   rs = np.zeros(Nstage)
   cumgain = 1
   for k in np.arange(Nstage-1):
       [rs[k],xs[k+1]] = mdac(xs[k],th15,re15,ga15)
       cumgain *= 2
       rs[k] = rs[k]/cumgain
   # last stage
   k=Nstage-1
   rs[k] = quant(xs[k],[0])
   cumgain = cumgain*1
   rs[k] = rs[k]/cumgain
   # output scale
   b = np.sum(rs)*cumgain
   return b

# DAC
# dacA(0, 16)      = -0.49999237060546875
# dacA(0, 2**16-1) = 0.5
def dacA(b, Nbit):
   # This is the correct DAC for an ideal Nbit ADC
   # The full range is 2**Nbit steps, 0 is -0.5+LSB/2, 15 is +0.5-LSB/2, so:
   y = -0.5 + b/2**Nbit + 1/2**(Nbit+1)
   return y

#%% Block tests

#%%
# what role plays commparator offset in a 1.5bit stage ?
# reproduce Fig. 17.20 b)

eps = 0.04 # comparator offset

re15 = np.arange( -1/4, 1/4+0.01, 1/4 )
th15 = np.convolve( re15, np.array([1/2,1/2]) )[1:-1]
th15[0] = th15[0] - eps
th15[1] = th15[1] + eps
ga15 = 2
vi = np.arange(0,1.0001,0.0001)-0.5
mo = np.array( [ mdac(x,th15,re15,ga15) for x in vi] )

fig,ax1 = plt.subplots()
pcolor = 'tab:red'
ax1.set_title('Figure 17.20 - a 1.5-bit pipelined converter stage')
ax1.set_xlabel('MDAC input voltage')
ax1.set_ylabel('MDAC digital value', color=pcolor)
ax1.plot(vi, mo[:,0], color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)
ax1.grid(axis='x')

ax2 = ax1.twinx()
pcolor = 'tab:blue'
ax2.set_ylabel('MDAC output voltage', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, mo[:,1], color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.grid()

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()

#%%
# what role plays commparator offset in 2.5bit stages
N25 = 4
Nbits = 2*N25+1
vi = np.arange(0,1.001,0.001)-0.5
ro = np.array( [ pipe25bNonid(x,N25) for x in vi ] )
vo = np.array( [ dacA(b,Nbits) for b in ro ] )

# Diagnostic output
print('This is a %d-bit converter.'%(Nbits))
print('The five smallest an five largest numbers in the output vector are:')
print(np.unique(ro)[[0,1,2,3,4,-5,-4,-3,-2,-1]] )
print('If all is well, these numbers should be bounded by',([0,2**Nbits-1]))

fig,ax1 = plt.subplots()

pcolor = 'tab:red'
ax1.set_xlabel('ADC input voltage')
ax1.set_ylabel('ADC input and DAC ouput voltage', color=pcolor)
ax1.plot(vi, vi, color=pcolor)
ax1.plot(vi, vo, color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)
ax1.grid(axis='x')

ax2 = ax1.twinx()
pcolor = 'tab:blue'
ax2.set_ylabel('Quantization error in LSB', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, (vo-vi)*2**Nbits, color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.set_ylim(-8,8)
plt.grid()

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()

"""
This is a 10-bit converter.
The five smallest an five largest numbers in the output vector are:
[0.000e+00 1.000e+00 2.000e+00 3.000e+00 4.000e+00 1.019e+03 1.020e+03
 1.021e+03 1.022e+03 1.023e+03]
If all is well, these numbers should be bounded by [0, 1023]
"""

#%%
# Quantizer
# The quantizer is tested by reproducing Fig. 15.6 in the textbook.

Nbits = 3
thresholds = ( np.arange(2**Nbits-1)+0.5 ) / 2**Nbits
vi = np.arange(0,1.0001,0.0001)
bo = np.array( [ quant(v,thresholds) for v in vi ] )
vo = bo/2**Nbits

fig,ax1 = plt.subplots()
pcolor = 'tab:red'
ax1.set_title('Figure 15.6 - Ramp Signal')
ax1.set_xlabel('Quantizer input voltage')
ax1.set_ylabel('MDAC digital value', color=pcolor)
ax1.plot(vi, bo, color=pcolor)
ax1.plot(vi, vi*2**Nbits, '--', color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)
ax1.grid(axis='x')

ax2 = ax1.twinx()
pcolor = 'tab:blue'
ax2.set_ylabel('Quantization error in LSBs', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, (vo-vi)*2**Nbits, color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.set_ylim(-0.6,0.6)
ax2.grid()

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()

#%%
# The 1.5-bit MDAC is tested by reproducing Fig. 17.20 in the textbook.

re15 = np.arange( -1/4, 1/4+0.01, 1/4 )
th15 = np.convolve( re15, np.array([1/2,1/2]) )[1:-1]
ga15 = 2
vi = np.arange(0,1.0001,0.0001)-0.5
mo = np.array( [ mdac(x,th15,re15,ga15) for x in vi] )

fig,ax1 = plt.subplots()
pcolor = 'tab:red'
ax1.set_title('Figure 17.20 - a 1.5-bit pipelined converter stage')
ax1.set_xlabel('MDAC input voltage')
ax1.set_ylabel('MDAC digital value', color=pcolor)
ax1.plot(vi, mo[:,0], color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)
ax1.grid(axis='x')

ax2 = ax1.twinx()
pcolor = 'tab:blue'
ax2.set_ylabel('MDAC output voltage', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, mo[:,1], color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.grid()

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()

#%%
# The 2.5-bit MDAC is tested by reproducing Fig. P17.20 in the textbook.

re25 = np.arange( -6/16, 6/16+0.01, 1/8 )
th25 = np.convolve( re25, np.array([1/2,1/2]) )[1:-1]
ga25 = 4
vi = np.arange(0,1.0001,0.0001)-0.5
mo = np.array( [ mdac(x,th25,re25,ga25) for x in vi] )

fig,ax1 = plt.subplots()
pcolor = 'tab:red'
ax1.set_title('Figure P17.20')
ax1.set_xlabel('MDAC input voltage')
ax1.set_ylabel('MDAC digital value', color=pcolor)
ax1.plot(vi, mo[:,0], color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)
ax1.grid(axis='x')

ax2 = ax1.twinx()
pcolor = 'tab:blue'
ax2.set_ylabel('MDAC output voltage', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, mo[:,1], color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.grid(axis='both')

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()

#%%
# Ideal and nonideal versions of pipeA()
# Try different numbers of stages as well as different nonidealities
#
# The Pipeline ADC fits the DAC if, in the ideal form, the quantization
# error curve is the expected sawtooth between -0.5 LSB an 0.5 LSB
# and the digital output contains the correct integers.

Nstage = 7
Nbits = Nstage
vi = np.arange(0,1.0001,0.0001)-0.5

ro = np.array( [ pipeA(x,Nstage) for x in vi ] )
vo = np.array( [ dacA(b,Nbits) for b in ro ] )

# NONIDEAL
roni = np.array( [ pipeAnonid(x,Nstage) for x in vi ] )
voni = np.array( [ dacA(b,Nbits) for b in roni ] )

# Diagnostic output
print('This is a %d-bit converter.'%(Nbits))
print('The five smallest an five largest numbers in the output vector are:')
print(np.unique(ro)[[0,1,2,3,4,-5,-4,-3,-2,-1]] )
print('If all is well, these numbers should be bounded by',([0,2**Nbits-1]))

fig,ax1 = plt.subplots()

pcolor = 'tab:orange'
ax1.plot(vi, voni, color=pcolor)
pcolor = 'tab:red'
ax1.set_xlabel('ADC input voltage')
ax1.set_ylabel('ADC input and DAC ouput voltage', color=pcolor)
ax1.plot(vi, vi, color=pcolor)
ax1.plot(vi, vo, color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)

ax2 = ax1.twinx()
pcolor = 'tab:cyan'
ax2.plot(vi, (voni-vi)*2**Nstage, color=pcolor)
pcolor = 'tab:blue'
ax2.set_ylabel('Quantization error in LSB', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, (vo-vi)*2**Nstage, color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.set_ylim(-8,8)
plt.grid()

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()

#%%
# Try different values for Nstage
Nstage = 3
Nbits = Nstage+1
vi = np.arange(0,1.001,0.001)-0.5
ro = np.array( [ pipe1721(x,Nstage) for x in vi ] )
vo = np.array( [ dacA(b,Nbits) for b in ro ] )

# Diagnostic output
print('This is a %d-bit converter.'%(Nbits))
print('The five smallest an five largest numbers in the output vector are:')
print(np.unique(ro)[[0,1,2,3,4,-5,-4,-3,-2,-1]] )
print('If all is well, these numbers should be bounded by',([0,2**Nbits-1]))
fig,ax1 = plt.subplots()

pcolor = 'tab:red'
ax1.set_xlabel('ADC input voltage')
ax1.set_ylabel('ADC input and DAC ouput voltage', color=pcolor)
ax1.plot(vi, vi, '--', color=pcolor)
ax1.plot(vi, vo, color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)

ax2 = ax1.twinx()
pcolor = 'tab:blue'
ax2.set_ylabel('Quantization error in LSB', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, (vo-vi)*2**Nbits, color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.set_ylim(-8,8)
plt.grid()
fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()

"""
This is a 4-bit converter.
The five smallest an five largest numbers in the output vector are:
[ 1.  2.  3.  4.  5. 11. 12. 13. 14. 15.]
If all is well, these numbers should be bounded by [0, 15]
"""

#%%
# Try different values for N25 and N15
N25 = 3
N15 = 3
Nbits = 2*N25+N15+1
vi = np.arange(0,1.001,0.001)-0.5
ro = np.array( [ pipe25b15b(x,N25,N15) for x in vi ] )
vo = np.array( [ dacA(b,Nbits) for b in ro ] )

# Diagnostic output
print('This is a %d-bit converter.'%(Nbits))
print('The five smallest an five largest numbers in the output vector are:')
print(np.unique(ro)[[0,1,2,3,4,-5,-4,-3,-2,-1]] )
print('If all is well, these numbers should be bounded by',([0,2**Nbits-1]))

fig,ax1 = plt.subplots()

pcolor = 'tab:red'
ax1.set_xlabel('ADC input voltage')
ax1.set_ylabel('ADC input and DAC ouput voltage', color=pcolor)
ax1.plot(vi, vi, color=pcolor)
ax1.plot(vi, vo, color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)
ax1.grid(axis='x')

ax2 = ax1.twinx()
pcolor = 'tab:blue'
ax2.set_ylabel('Quantization error in LSB', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, (vo-vi)*2**Nbits, color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.set_ylim(-8,8)
plt.grid()

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()

"""
This is a 10-bit converter.
The five smallest an five largest numbers in the output vector are:
[0.000e+00 1.000e+00 2.000e+00 3.000e+00 4.000e+00 1.019e+03 1.020e+03
 1.021e+03 1.022e+03 1.023e+03]
If all is well, these numbers should be bounded by [0, 1023]
"""

#%%
"""

SIMONS STUFF BELOW

"""
#%%
# compare 1.5bit to 2.5bit comparator threshold offset error
# offset < 1/16 no issues
# offset = 2/16 --> 2.5 Bit (dark  blue) makes errors
# offset = 3/16 --> 1.5 Bit (light blue) makes errors too

# 2.5 bit ==> dark  blue / red
# 1.5 bit ==> light blue / orange

offset = 2/16
Nbits = 5  # only odd numbers
N25 = int((Nbits-1)/2)
N15 = Nbits

vi = np.arange(0,1.0001,0.0001)-0.5

ro15 = np.array( [ pipe15bNonid_vs(x,N15,offset) for x in vi ] )
vo15 = np.array( [ dacA(b,Nbits) for b in ro15 ] )

# NONIDEAL
ro25 = np.array( [ pipe25bNonid_vs(x,N25,offset) for x in vi ] )
vo25 = np.array( [ dacA(b,Nbits) for b in ro25 ] )

# Diagnostic output
print('This is a %d-bit converter.'%(Nbits))
print('The five smallest an five largest numbers in the output vector are:')
print(np.unique(ro15)[[0,1,2,3,4,-5,-4,-3,-2,-1]] )
print('If all is well, these numbers should be bounded by',([0,2**Nbits-1]))
print(np.unique(ro25)[[0,1,2,3,4,-5,-4,-3,-2,-1]] )
print('If all is well, these numbers should be bounded by',([0,2**Nbits-1]))

fig,ax1 = plt.subplots()

pcolor = 'tab:orange'
ax1.plot(vi, vo15, color=pcolor)
pcolor = 'tab:red'
ax1.set_xlabel('ADC input voltage')
ax1.set_ylabel('ADC input and DAC ouput voltage', color=pcolor)
ax1.plot(vi, vi, color=pcolor)
ax1.plot(vi, vo25, color=pcolor)
ax1.tick_params(axis='y', labelcolor=pcolor)

ax2 = ax1.twinx()
pcolor = 'tab:cyan'
ax2.plot(vi, (vo15-vi)*2**Nbits, color=pcolor)
pcolor = 'tab:blue'
ax2.set_ylabel('Quantization error in LSB', color=pcolor)  # we already handled the x-label with ax1
ax2.plot(vi, (vo25-vi)*2**Nbits, color=pcolor)
ax2.tick_params(axis='y', labelcolor=pcolor)
ax2.set_ylim(-8,8)
plt.grid()

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()


import numpy as np

def apen(sequence, m=1, r=0):
    """
    Calculates the Approximate Entropy, or ApEn, of the given sequence, as described in:

    .. code-block:: text

      Pincus, S. M.; Gladstone, I. M.; Ehrenkranz, R. A. (1991).
      "A Regularity Statistic For Medical Data Analysis".
      Journal of Clinical Monitoring and Computing. 7 (4): 335-345.

    The implementation here is based on the Python implementation described in:
    https://en.wikipedia.org/wiki/Approximate_entropy

    :param sequence: a string of whole numbers, such as '012301', or an array of whole numbers, such as [0,1,2,3,0,1],
                     or a numpy array of whole numbers

    :param m: the length of compared runs of data

    :param r: a filtering level

    :return: a real number, representing the approximate entropy (ApEn) for the given sequence
    """
    if type(sequence) is str:
        U = np.array([int(x) for x in sequence])
    elif type(sequence) is list:
        U = np.array(sequence)
    elif type(sequence) is np.ndarray:
        U = sequence
    else:
        raise Exception("unsupported sequence type: %s" % type(sequence))

    N = len(U)

    def maximum_distance(x_i, x_j):
        return max([abs(ua - va) for ua, va in zip(x_i, x_j)])

    def phi(m):
        x = [[U[j] for j in range(i, i + m - 1 + 1)] for i in range(N - m + 1)]
        C = [len([1 for x_j in x if maximum_distance(x_i, x_j) <= r]) / (N - m + 1.0) for x_i in x]
        return (1 / (N - m + 1.0)) * sum(np.log(C))

    return abs(phi(m + 1) - phi(m))


import matplotlib.pyplot as plt # Import Required Modules
import numpy as np

x = np.arange(0, 4, 0.1) # Set a-axis values, increments of "0.1" provdies a smoother graph

y1 = x  # Set y-axis Values
y2= x**2
y3 = x**3

plt.plot(y1, label = '$f(x) = x$') # Plot the provided points and label them in relation to their function
plt.plot(y2, label = '$g(x) = x^2$')
plt.plot(y3, label = '$h(x) = x^3$')

# naming the x axis
plt.xlabel('x - axis')
# naming the y axis
plt.ylabel('y - axis')

plt.legend() # Provides Legend
plt.title('Representation of functions $f(x) = x$, $g(x) = x^2$, $h(x) = x^3$, within range [0,4]  ')
plt.show() # Depicts graphic representation of the plot


import numpy as np
from scipy.stats import norm

def rnorm(n, mean=0, sd=1):
    """
    Random generation for the normal distribution with mean
    equal to mean and standard deviation equation to sd
    same functions as rnorm in r: ``rnorm(n, mean=0, sd=1)``

    :param n: the number of the observations
    :param mean: vector of means
    :param sd: vector of standard deviations
    :return: the vector of the random numbers

    :author: Wenqiang Feng
    :email:  von198@gmail.com
    """
    return norm.rvs(loc=mean, scale=sd, size=n)

def dnorm(x, mean=0, sd=1, log=False):
    """
    Density of the normal distribution with mean
    equal to mean and standard deviation equation to sd
    same functions as rnorm in r: ``dnorm(x, mean=0, sd=1, log=FALSE)``

    :param x: the vector od quantiles
    :param mean: vector of means
    :param sd: vector of standard deviations
    :return: the list of the density

    :author: Wenqiang Feng
    :email:  von198@gmail.com
    """
    if log:
        return np.log(norm.pdf(x=x, loc=mean, scale=sd))
    else:
        return norm.pdf(x=x, loc=mean, scale=sd)

def runif(n, min=0, max=1):
    """
    Random generation from the uniform distribution
    same functions as rnorm in r: ``runif(n, min=0, max=1)``

    :param n: the number of the observations
    :param min: the lower limit of the distribution
    :param max: the upper limit of the distribution
    :return: the list of n uniform random numers

    :author: Wenqiang Feng
    :email:  von198@gmail.com
    """
    return np.random.uniform(min, max, size=n)


"""
In the 5 by 5 matrix below, the minimal path sum from the top left to the
bottom right, by only moving to the right and down, is indicated in bold red
and is equal to 2427.

 A = np.array([  [131, 201, 630, 537, 805],
                 [673,  96, 803, 699, 732],
                 [234, 342, 746, 497, 524],
                 [103, 965, 422, 121,  37],
                 [18, 150, 111, 956, 331]  ]).T

Find the minimal path sum, in matrix.txt, a 31K text file containing a 80 by 80
matrix, from the top left to the bottom right by only moving right and down.

Solution comment: Solved using dynamic programming. If the shortest path from
your neighbors are known, then a cells shortest path is its own value added to
the minimum of its neighbors. Starting at the end of the path allows us to
build towards having the neighbors of the starting position solved. Iteration
is done diagonally, bottom left to top right.
"""

import numpy as np

def select_min(A, x, y):
    """Add to A[x, y] its minimum neighbor."""
    n, m = A.shape
    if x == n - 1:
        min_val = A[x, y+1]
    elif y == m - 1:
        min_val = A[x+1, y]
    else:
        min_val = min(A[x+1, y], A[x, y+1])
    A[x, y] += min_val

A = np.loadtxt('matrix.txt', dtype=int, delimiter=',')
print(A)
n, m = A.shape

for j in reversed(range(m-1)):
    for k in range(m-j):
        select_min(A, n-k-1, j+k)

for i in reversed(range(n-1)):
    for k in range(m-(n-i-1)):
        select_min(A, i-k, k)

print('Minimum path sum:', A[0,0])