"""
  Name     : c6_20_myPackage.py
  Book     : Hands-on Data Science with Anaconda )
  Publisher: Packt Publishing Ltd.
  Author   : Yuxing Yan and James Yan
  Date     : 3/1/2018
  email    : yany@canisius.edu
             paulyxy@hotmail.com
"""

def bs_call(S,X,T,rf,sigma):
    """
       Objective: Black-Schole-Merton option model
       Format   : bs_call(S,X,T,r,sigma)
               S: current stock price
               X: exercise price
               T: maturity date in years
              rf: risk-free rate (continusouly compounded)
           sigma: volatiity of underlying security
       Example 1:
         >>>bs_call(40,40,1,0.1,0.2)
         5.3078706338643578
    """
    from scipy import log,exp,sqrt,stats
    d1=(log(S/X)+(rf+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    d2 = d1-sigma*sqrt(T)
    return S*stats.norm.cdf(d1)-X*exp(-rf*T)*stats.norm.cdf(d2)

def delta_call(S,X,T,rf,sigma):
    """
       Objective: detal based on the Black-Schole-Merton option model
       Format   : delta_call(S,X,T,r,sigma)
               S: current stock price
               X: exercise price
               T: maturity date in years
              rf: risk-free rate (continusouly compounded)
           sigma: volatiity of underlying security
       Example 1:
         >>>from p4f import *
         >>> round(delta_call(40,40,1,0.1,0.2),4)
        0.7257

    """
    from scipy import log,exp,sqrt,stats
    d1=(log(S/X)+(rf+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    return(stats.norm.cdf(d1))

def EAR_f(APR,m):
    return (1+APR/m)**m-1

def durationBond(rate,couponRate,nper,freq=1):
    """Objective : estimte the durtion for a given bond
       rate      : discount rate
       couponRate: coupon rate
         nper    : number of years

       Example 1: >>> discountRate=0.1
                  >>> couponRate=0.04
                  >>> n=4
                  >>>durationBond(rate,couponRate,n)
                      3.5616941835365492

       Example #2>>> durationBond(0.1,0.04,4)
                     3.7465335177625576
    """
    import scipy as sp
    d=0
    B=0
    n=0
    for i in sp.arange(nper):
        n=i+1
        B0=sp.pv(rate,n,0,-couponRate)
        d+=n*B0
        B+=B0
    #
    d+=n*sp.pv(rate,n,0,-1)
    B+=sp.pv(rate,n,0,-1)
    d=d/B
    return(d)

def pvValueNperiodModel(r,longTermGrowthRate,dividendNplus1):
    """Objective: estimate stock price based on an n-period model
                         r: discount rate
         LongTermGrowhRate: long term dividend growth rate
         dividendsNpus1   : a dividend vector n + 1

         PV          = d1/(1+R) + d2/(1+R)**2 + .... + dn/(1+R)**n + sellingPrice/(1+R)**n
         sellingPrice= d(n+1)/(r-g)
             where g is long term growth rate

    Example #1: >>> r=0.182
                >>> g=0.03
                >>> d=[1.8,2.07,2.277,2.48193,2.68,2.7877]
                >>> pvValueNperiodModel(r,g,d)
                   17.472364312825711
    """
    import scipy as sp
    d=dividendNplus1
    n=len(d)-1
    g=longTermGrowthRate
    pv=sp.npv(r,d[:-1])*(1+r)
    sellingPrice=d[n]/(r-g)
    pv+=sp.pv(r,n,0,-sellingPrice)
    return pv

def delta_put(S,X,T,rf,sigma):
    """
       Objective: detal based on the Black-Schole-Merton option model
       Format   : delta_put(S,X,T,r,sigma)
               S: current stock price
               X: exercise price
               T: maturity date in years
              rf: risk-free rate (continusouly compounded)
           sigma: volatiity of underlying security
       Example 1:

    """
    from scipy import log,exp,sqrt,stats
    d1=(log(S/X)+(rf+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    return(stats.norm.cdf(d1)-1)

def bs_put(S,X,T,rf,sigma):
    """
       Objective: Black-Schole-Merton option model
       Format   : bs_call(S,X,T,r,sigma)
               S: current stock price
               X: exercise price
               T: maturity date in years
              rf: risk-free rate (continusouly compounded)
           sigma: volatiity of underlying security
       Example 1:
       >>> put=bs_put(40,40,0.5,0.05,0.2)
       >>> round(put,2)
       1.77
    """
    from scipy import log,exp,sqrt,stats
    d1=(log(S/X)+(rf+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    d2 = d1-sigma*sqrt(T)
    return X*exp(-rf*T)*stats.norm.cdf(-d2)-S*stats.norm.cdf(-d1)

def binomial_grid(n):
    import networkx as nx
    import matplotlib.pyplot as plt
    G=nx.Graph()
    for i in range(0,n+1):
        for j in range(1,i+2):
            if i<n:
                G.add_edge((i,j),(i+1,j))
                G.add_edge((i,j),(i+1,j+1))
    posG={}    #dictionary with nodes position
    for node in G.nodes():
        posG[node]=(node[0],n+2+node[0]-2*node[1])
    nx.draw(G,pos=posG)

#from math import sqrt, log, pi,exp
#import re
#--------------------------------------------------------#
#--- Cumulative normal distribution        --------------#
#--------------------------------------------------------#
def CND(X):
    """ Cumulative standard normal distribution
            CND(x): x is a scale
            e.g.,
            >>> CND(0)
            0.5000000005248086
    """
    (a1,a2,a3,a4,a5)=(0.31938153,-0.356563782,1.781477937,-1.821255978,1.330274429)
    L = abs(X)
    K = 1.0 / (1.0 + 0.2316419 * L)
    w = 1.0 - 1.0 / sqrt(2*pi)*exp(-L*L/2.) * (a1*K + a2*K*K + a3*pow(K,3) +
    a4*pow(K,4) + a5*pow(K,5))
    if X<0:
        w = 1.0-w
    return w

#--------------------------------------------------------#
#--- Black-Scholes-Merton option model     --------------#
#--------------------------------------------------------#
def bs_call_old(S,X,T,r,v):
    """
       Objective: Black-Schole-Merton option model
         >>>bs_call(40,40,1,0.1,0.2)
         5.307874373215604
    """
    d1 = (log(S/X)+(r+v*v/2.)*T)/(v*sqrt(T))
    d2 = d1-v*sqrt(T)
    return S*CND(d1)-X*exp(-r*T)*CND(d2)

#--------------------------------------------------------#
#--- Present value of a perpetuity due     --------------#
#--------------------------------------------------------#
def pv_perpetuity_due(c,r):
    """Objective: estimate present value of a perpetuity due
                  i.e., 1st cash flow occurs today
          c: period payment
          r: discount rate
          e.g.,
          >>>pv_perpetuity_due(30000,0.08)
          405000.0
    """
    return(c/r*(1+r))

#--------------------------------------------------------#
#--------------------------------------------------------#
#--------------------------------------------------------#
def payback_(investment, cashflows):
    """Objective: estimate payback period: number of years requried to
                  recover the initial investment
       e.g.,
       >>>invest=200
       >>>future_cashflows=[60.0, 60.0, 70.0, 90.0]
       >>>payback_(invest,future_cashflows)
       or
       >>>payback_(200.0, [60.0, 60.0, 70.0, 90.0])
       3.1111111111111112
    """
    total, years, cumulative = 0.0, 0, []
    if not cashflows or (sum(cashflows) < investment):
        raise Exception("insufficient cashflows")
    for cashflow in cashflows:
        total += cashflow
        if total < investment:
            years += 1
        cumulative.append(total)
    A = years
    B = investment - cumulative[years-1]
    C = cumulative[years] - cumulative[years-1]
    return A + (B/C)

#--------------------------------------------------------#
#--------------------------------------------------------#
#--------------------------------------------------------#
def payback_period(cashflows):
    """Objective: estimate payback period, number of years needed to
                 recover the initial investment
       e.g.,
       >>>x=[-200.0, 60.0, 60.0, 70.0, 90.0]
       >>>payback_period(x)
       3.1111111111111112
    """
    investment, cashflows = cashflows[0], cashflows[1:]
    if investment < 0 : investment = -investment
    return payback_(investment, cashflows)

#--------------------------------------------------------#
#--------------------------------------------------------#
#--------------------------------------------------------#
def npv_f(rate, cashflows):
    """Objective  : estimate NPV value
         rate     : discount rate
         cashflows: cashflows
         e.g.
         >>>npv_f(0.1, [-100.0, 60.0, 60.0, 60.0])
         49.211119459053322
    """
    total = 0.0
    for i, cashflow in enumerate(cashflows):
        total += cashflow / (1 + rate)**i
    return total

#--------------------------------------------------------#
#--- IRR (Internal Rate of Return) ----------------------#
#--------------------------------------------------------#
def IRR_f(cashflows, iterations=100):
    """Objective: estimte IRR (Internal Rate of Return)
         e.g.,
         >>>x=[-100.0, 60.0, 60.0, 60.0]
         >>>IRR_f(x)
         0.36309653947517645
    """
    rate = 1.0
    investment = cashflows[0]
    for i in range(1, iterations+1):
        rate *= (1 - npv_f(rate, cashflows) / investment)
    return rate

#--------------------------------------------------------#
#----PV_f function --------------------------------------#
#--------------------------------------------------------#
def pv_f(fv,r,n):
    """Objective: estimate present value
              fv: fture value
              r : discount period rate
              n : number of periods
        formula : fv/(1+r)**n
           e.g.,
           >>>pv_f(100,0.1,1)
           90.9090909090909
           >>>pv_f(r=0.1,fv=100,n=1)
           90.9090909090909
           >>>pv_f(n=1,fv=100,r=0.1)
           90.9090909090909
    """
    return fv/(1+r)**n

#--------------------------------------------------------#
#--------------------------------------------------------#
#--------------------------------------------------------#
def bond_price(c,r,n,face_value):
  """Objective   : estimte price of a bond
       c         : coupon payment
       r         : discount rate
       n         : number of period
       face_value: principal
       formula   : c/r*(1-1/(1+r)**n) + face_value/(1+r)**n
       e.g.,
       >>>bond_price(10,0.08,10,1000)
       530.2943020740986
  """
  return  c/r*(1-1/(1+r)**n) + face_value/(1+r)**n

#----------------------------------------------------------#
#----Mimic PV() function in Excel -------------------------#
#----------------------------------------------------------#
def pv_excel(rate,nper,pmt=0,fv=0,type=0):
    """Objective: mimic Excel pv() function
           rate : period rate
           nper : number of periods
           pmt  : payment per period
           fv   : future value
           type : type=0, payments occur at the end of each period
                  type=1, payments occur at the begining of each perod
                  default value is type=0
           e.g.,
           >>>pv_excel(0.1,1,100)
           90.90909090909093
           >>>pv_excel(0.1,1,100,100)
           181.81818181818184
           >>>pv_excel(0.1,1,100,100,1)
           200.00000000000003
    """
    pv=pv_f(fv,rate,nper) +pv_annuity(nper,pmt,rate)
    if(type==1): pv=pv*(1+rate)
    return pv

#--------------------------------------------------------#
#--- Present value of a perpetuity         --------------#
#--------------------------------------------------------#
def pv_perpetuity(c,r):
    """
  Objective : estimate present value of a perpetuity
     c      : period payment
     r      : discount rate
  formula   : c/r
      e.g.,
      >>>pv_perpetuity(100,0.08)
      1250.0
    """
    return c/r

#--------------------------------------------------------#
#--- Present value of a growing perpetuity --------------#
#--------------------------------------------------------#
def pv_grow_perpetuity(c,r,q):
    """Objective : estimate present value of a growthing perpetuity
              r  : discount rate
              q  : growth rate of perpetuity
              c  : period payment
        formula  : c/(r-g)
        e.g.,
         >>>pv_grow_perpetuity(30000,0.08,0.04)
         750000.0
    """
    return(c/(r-q))

#--------------------------------------------------------#
#--- Present value of an annuity ------------------------#
#--------------------------------------------------------#
def pv_annuity(n,c,r):
    """Objective : estimate present value of an annuity
            n    : number of payments
            c    : payment amount
            r    : discount
       formula   : c/r*(1-1/(1+r)**n)
       e.g.,
       >>>pv_annuity(29,1,0.08)
       11.158406010577684
    """
    return c/r*(1-1/(1+r)**n)

#--------------------------------------------------------#
#--- Present value of an annuity with 1st payment        #
# -----------n-period from today ------------------------#
#--------------------------------------------------------#
def pv_annuity_k_period_from_today(n,c,r,k_period_from_today=0):
    """
  Objective            : estimate present value of an annuity
         n             : number of payments
         c             : payment amount
         r             : discount
    k_period_from_today: 1st payment is k periods from today
                         default is zero
         formula       : pv_annuity(n,c,r)/(1+r)**k_period_from_today
         e.g.,
         >>> pv_annuity_k_period_from_today(29,1,0.08,1)
         10.331857417201558
         >>> pv_annuity_k_period_from_today(29,1,0.08)
         11.158406010577684
    """
    return pv_annuity(n,c,r)/(1+r)**k_period_from_today

#--------------------------------------------------------#
#--- Estimate the number of periods for an annuity ------#
#--------------------------------------------------------#
def n_annuity(r,pv,pmt,fv):
    """
  Objective: esimate the number of period
        r  : discount rate
        fv : future value
        pv : present value
        pmt: payment per period
    formula: log((fv*r+pmt)/(pv*r+pmt))/log(1+r)
        e.g.,
        >>> n_annuity(0.0725,10050,5000,60000)
        6.999345696211027
    """
    return log((fv*r+pmt)/(pv*r+pmt))/log(1+r)

#--------------------------------------------------------#
#---fv_f  future value function -------------------------#
#--------------------------------------------------------#
def fv_f(pv,r,n):
    """
      Objective : estimate furuew value
             pv : present value
             r  : discount period rate
             n  : number of periods
       formula  : pv*(1+r)**n
        e.g.,
        >>>fv_f(100,0.1,1)
        110.00000000000001
    """
    return pv*(1+r)**n

#--------------------------------------------------------#
#--- Present value of an annuity ------------------------#
#--------------------------------------------------------#
def fv_annuity(n,c,r):
    """
     Objective: estimate future value of an annuity
       n      : number of payments
       c      : payment amount
       r      : discount
     formula  : c/r*((1+r)**n-1)
       e.g.,
       >>>fv_annuity(2,100,0.1)
       210.0000000000002
    """
    return c/r*((1+r)**n-1)

#--------------------------------------------------------#
#-- Present value of a growing annuity ------------------#
#--------------------------------------------------------#
def pv_growing_annuity(n,c,r,g):
    """
     Objective: estimate present value of a growting annuity
         c    : payment
         n    : number of payments
         r    : discount rate
         g    : growth rate  (g<r)
      formula : c/(r-g)*(1-((1+g)/(1+r))**n)
         e.g.
        >>>pv_growing_annuity(30,10000,0.1,0.05)
        150463.14700582038
    """
    tt=((1+g)/(1+r))**n
    return c/(r-g)*(1-tt)

#----------------------------------------------------------#
#--mean function  -----------------------------------------#
#----------------------------------------------------------#
def mean(numberList):
    """
     Objective: estimate mean of an input variable
     e.g.,
     >>>x=(1,2,3)
     >>>mean(x)
     2.0
     >>>x=[1,1,2]
     >>>mean(x)
     1.3333333333333333
    """
    if len(numberList) == 0:
        return float('nan')
    floatNums = [float(x) for x in numberList]
    return sum(floatNums) / len(numberList)

#----------------------------------------------------------#
#--sign function  -----------------------------------------#
#----------------------------------------------------------#
def sign(x):
    """
       Objective: take the sign of x
       e.g.,
          >>>sign(-1.2)
          -1
          >>>sign(5)
          1
    """
    if x>0: return 1
    elif x<0: return -1
    else: return 0

#----------------------------------------------------------#
#--IRR estimation -----------------------------------------#
#----------------------------------------------------------#
def IRRs_f(cash_flows):
    """
     Objective: find mutiple IRRs
       e.g,
        >>>x=[55,-50,-50,-50,100]
        >>>IRRs_f(x)
        [0.072, 0.337]
    """
    n=1000
    r=range(1,n)
    n_cash_flow=len(cash_flows)
    epsilon=abs(mean(cash_flows)*0.01)
    irr=[-99.00]
    j=1
    npv=[]
    for i in r: npv.append(0)
   #print("len(r)", len(r))
    #print("len(npv)",len(npv))
    lag_sign=sign(npv_f(float(r[0]*1.0/n*1.0),cash_flows))
    for i in range(1,n-1):
                  #print("r[i]",r[i])
                  interest=float(r[i]*1.0/n*1.0)
                  npv[i]=npv_f(interest,cash_flows)
                  s=sign(npv[i])
                  if s*lag_sign<0:
                      lag_sign=s
                      if j==1:
                          irr=[interest]
                          j=2
                      else:
                          irr.append(interest)
    return irr

#--------------------------------------------------------#
#--- Estimate Payment of an annuity ---------------------#
#--------------------------------------------------------#
def PMT(n,r,pv,fv):
    """
    Objective: estimate period payment (like Excel function)
       n    : number of periods
       r    : discount rate
       pv   : present value
       fv   : period payment
       e.g.,
         >>>PMT(10,0.08,100000,0)
         14902.948869707534
    """
    return (pv-fv/(1+r)**n)*r/(1-1/(1+r)**n)

#--------------------------------------------------------#
#--- Estimte the number of periods for an annuity -------#
#       Excel function                                   #
#--------------------------------------------------------#
def NPER(r,pv,pmt,fv):
    """
    Objective: esimate the number of periods
       r  : discount rate
       fv : future value
       pv : present value
       pmt: payment per period
       e.g.,
         >>>NPER(0.0725,10050,5000,60000)
         6.999345696211027
    """
    return log((fv*r+pmt)/(pv*r+pmt))/log(1+r)

#----------------------------------------------------------#
#----------------------------------------------------------#
#----------------------------------------------------------#
def r_continuous(r,m):
    """
      Objective : estimate a coutinusly compounded interest rate
              r : an annual rate (Annual Percentage Rate)
              m : compounding frequency per year
      formula   : m*log(1+r/m)
    """
    return  m*log(1+r/m)

#----------------------------------------------------------#
#----------------------------------------------------------#
#----------------------------------------------------------#
def Rc_f(r,m):
    """
     Objective: estimate a continously compounded rate
        r : norminal rate
        m : number of times compounded each year
        e.g.,
         >>>Rc_f(0.1,2)
         0.09758032833886408
    """
    return m*log(1+r/m)

#----------------------------------------------------------#
#----------------------------------------------------------#
#----------------------------------------------------------#
def Rm_f(Rc,m):
    """
      Get norminal rate for a given continuous compounded rate
       Rc : continuous compounded rate
       m  : number of desired times compounded each year
            e.g., Rm_f(0.08,4)= 0.08080536
    """
    return m*(exp(Rc/m)-1)


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

# def blueNoise(t, fs, fc, bd=32):
#    M = len(t)
#    x = np.random.uniform(-2 ** bd, 2 ** bd - 1, M)
#    X = np.fft.fft(x)
#    hp = fc / fs * M
#    Y = [X[i] if hp < i < M - hp else 0 for i in range(M)]
#    np.real(np.fft.ifft(Y))

def lp_filter(x, fs, fc, M=None, plot=False):
    if M is None:
        M = x.size
    Xf = scipy.fft.fft(x, n=M)
    Af = abs(Xf)

    # Convert cutoff frequencies into points on spectrum
    lp = fc / fs * M
    Y = [0 if lp < i < M - lp else Xf[i] for i in range(M)]

    y = scipy.fft.ifft(Y, n=M)

    if plot:

        plt.figure()
        plt.subplot(211)
        plt.plot(x)
        plt.plot(y)
        plt.grid(which='both')
        plt.minorticks_on()
        plt.subplot(212)
        plt.stem(Af[:len(Af)//2]*2.0/len(Af))
        plt.axis([0, fs / 2, None, None])
        plt.axvspan(0, lp, facecolor='g', alpha=0.5)
        plt.grid(which='both')
        plt.minorticks_on()
        plt.show()

    return y

def hp_filter(x, fs, fc, M=None, plot=False):
    if M is None:
        M = x.size
    Xf = scipy.fft.fft(x, n=M)
    Af = abs(Xf)

    # Convert cutoff frequencies into points on spectrum
    hp = fc / fs * M
    Y = [Xf[i] if hp < i < M - hp else 0 for i in range(M)]

    y = scipy.fft.ifft(Y, n=M)

    if plot:

        plt.figure()
        plt.subplot(211)
        plt.plot(x)
        plt.plot(y)
        plt.grid(which='both')
        plt.minorticks_on()
        plt.subplot(212)
        plt.stem(Af[:len(Af)//2]*2.0/len(Af))
        plt.axis([0, fs / 2, None, None])
        plt.axvspan(hp, fs/2, facecolor='g', alpha=0.5)
        plt.grid(which='both')
        plt.minorticks_on()
        plt.show()

    return y

def bp_filter(x, fs, fl, fh, M=None, plot=False):
    if M is None:
        M = x.size
    Xf = scipy.fft.fft(x, n=M)
    Af = abs(Xf)

    # Convert cutoff frequencies into points on spectrum
    hp = fh / fs * M
    lp = fl / fs * M
    Y = [Xf[i] if ((lp <= i <= hp) or (M-hp <= i <= M-lp) ) else 0 for i in range(M)]

    y = scipy.fft.ifft(Y, n=M)

    if plot:

        plt.figure()
        plt.subplot(211)
        plt.plot(x)
        plt.plot(y)
        plt.grid(which='both')
        plt.minorticks_on()
        plt.subplot(212)
        plt.stem(Af[:len(Af)//2]*2.0/len(Af))
        plt.axis([0, fs / 2, None, None])
        plt.axvspan(lp, hp, facecolor='g', alpha=0.5)
        plt.grid(which='both')
        plt.minorticks_on()
        plt.show()

    return y

def bs_filter(x, fs, fl, fh, M=None, plot=False):
    if M is None:
        M = x.size
    Xf = scipy.fft.fft(x, n=M)
    Af = abs(Xf)

    # Convert cutoff frequencies into points on spectrum
    hp = fh / fs * M
    lp = fl / fs * M
    Y = [0 if ((lp <= i <= hp) or (M-hp <= i <= M-lp) ) else Xf[i] for i in range(M)]

    y = scipy.fft.ifft(Y, n=M)

    if plot:

        plt.figure()
        plt.subplot(211)
        plt.plot(x)
        plt.plot(y)
        plt.grid(which='both')
        plt.minorticks_on()
        plt.subplot(212)
        plt.stem(Af[:len(Af)//2]*2.0/len(Af))
        plt.axis([0, fs / 2, None, None])
        plt.axvspan(0, lp, facecolor='g', alpha=0.5)
        plt.axvspan(hp, fs / 2, facecolor='g', alpha=0.5)
        plt.grid(which='both')
        plt.minorticks_on()
        plt.show()

    return y

if __name__ == "__main__":
    fs = 1000
    t = np.arange(0, 1, 1 / fs)
    Sines = [np.sin(2*np.pi*t*n)*(1-(n/500.0)) for n in [225.0, 187.5, 125.0, 62.5, 30.0, 7.5, 6.25]]
    x = np.sum(Sines, axis=0)

    #lp_filter(x, fs, 10.0, plot=True)
    #hp_filter(x, fs, 200.0, plot=True)
    #bp_filter(x, fs, 45.0, 80.0, plot=True)
    y1 = bs_filter(x, fs, 45.0, 200.0, plot=True)

    plt.figure()
    plt.magnitude_spectrum(y1, Fs=fs)
    plt.show()


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ch3Python.py. Evaluating the fit with the coefficient of variantion.
This is demonstration code, so it is transparent but inefficient.
"""
import numpy as np

x = [1.00,1.25,1.50,1.75,2.00,2.25,2.50,2.75,3.00,3.25,3.50,3.75,4.00]
y = [3.34,4.97,4.15,5.40,5.21,4.56,3.69,5.86,4.58,6.94,5.57,5.62,6.87]

# Convert data to vectors.
x = np.array(x)
y = np.array(y)
n = len(y)

# Find zero mean versions of x and y.
xzm = x-np.mean(x)
yzm = y-np.mean(y)
# Covariance between x and y.
covxy = np.sum(xzm * yzm)/n
# Variance of x.
varx = np.sum(xzm * xzm)/n
# Variance of y.
vary = np.sum(yzm * yzm)/n

# Find slope b1=0.764
b1 = covxy/varx

# Find intercept b0=3.225
b0 = np.mean(y) - b1*np.mean(x)

yhat = b1*x + b0 # 0.511
varyhat = np.var(yhat)

# Find coefficient of determination r2.
r2 = covxy * covxy / (varx * vary)
# OR equivalently ...
r2 = varyhat / vary # 0.466

print("Variance of x = %0.3f" %varx) # 0.875
print("Variance of y = %0.3f" %vary) # 1.095
print("Covariance of x and y = %0.3f" %covxy) # 0.669
print("Variance of yhat = %0.3f" %varyhat) # 0.511
print("Coefficient of variation = %0.3f" %r2) # 0.466
# END OF FILE.


import numpy as np

# Linear Regressions Exercises

def empirical_risk(theta, X, Y, loss):
    loss_sum = 0
    for i, feature_vector in enumerate(X):
        loss_sum += loss(Y[i] - np.dot(np.transpose(theta), feature_vector))
    return loss_sum/X.shape[0]

def hinge_loss(z):
    if z < 1:
        return 1 - z
    return 0

def lms_loss(z):
    return z**2/2

if __name__ == "__main__":
    # Defines the Feature Matrix and its Labels
    X = np.array([[1, 0, 1],
                  [1, 1, 1],
                  [1, 1, -1],
                  [-1, 1, 1]])

    Y = np.array([2,
                  2.7,
                  -0.7,
                  2])

    # Define the Parameters
    theta = np.array([0, 1, 2])

    # Calculate the Risk
    R_hinge = empirical_risk(theta, X, Y, hinge_loss)
    R_lms = empirical_risk(theta, X, Y, lms_loss)

    print('Hinge: {}'.format(R_hinge))
    print('LMS: {}'.format(R_lms))


import numpy as np
import matplotlib.pyplot as plt

DISPLAY_DATA = False

"""
Bikin data dummy dengan persamaan: y = 4 + 3x + Gaussian_noise
"""
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)

if DISPLAY_DATA is True:
  plt.figure(0)
  plt.scatter(X,y)
  plt.show()

"""
Solusi menggunakan persamaan normal.
Menggunakan fungsi inv() dari Linear Algebra module-nya Numpy ( np.linalg ) untuk menghitung inverse dari matrix,
dan fungsi dot() untuk perkalian matrix.
"""
X_b = np.c_[np.ones((100, 1)), X] # dibuat x0 = 1 supaya theta_0 tidak sama dengan 0
theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y)

print("Estimasi normal: ",theta_best[0], theta_best[1])

"""
Sekarang kita plot model-nya dengan dua end-point [0] dan [2]
"""
X_points = np.array([[0], [2]])
X_points_b = np.c_[np.ones((2, 1)), X_points] # dibuat x0 = 1 supaya theta_0 tidak 0
y_points = X_points_b.dot(theta_best)
#print(y_points)

"""
Solusi kedua menggunakan scikit-learn
"""
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)
print("Estimasi scikit: ", lin_reg.intercept_, lin_reg.coef_)
y_scikit = lin_reg.predict(X_points)

plt.figure(1)
plt.plot(X,y,"g.")
plt.plot(X_points, y_points, "r-")
plt.plot(X_points, y_scikit, "b-")
plt.axis([0,2,0,15])
plt.legend(["Data", "Model Normal", "Model Scikit"])
plt.title("Dengan Solusi Normal")
plt.show()


import numpy as np
from typing import List, Tuple
import torch
import pylab as plt

class PolicyEvaluation:

    def __init__(self, grid, policy, actions, small_change, gamma) -> None:
        self.grid = grid
        self.policy = policy
        self.actions = actions
        self.rows, self.cols = self.grid.grid_board.shape
        self.value_function = np.zeros((self.rows,self.cols))
        self.SMALL_CHANGE = small_change
        self.gamma = gamma
        self.actions.set_actions()

    def iterative_policy_evaluation(self):
        # First we do the policy evaluation for random policy.
        # For Random policy all states are equally likely so the probability would be
        # 1/ (No. of states in that policy)

        states = list(range(self.rows * self.cols))

        self.value_function = np.zeros((self.rows * self.cols))
        while True:
            # we iterate through all the states, for each state select action and sum there values.
            # this summation of values for each action multiplied by probability is known as expectation of that state
            # This expectation is then assigned to that particular state.
            # This process is continued untile we find very small change between two iterations for particular state.
            max_expected_value_change = 0
            for state in states:
                old_value_function = self.value_function[state]
                actions = self.actions.grid_actions[state]
                expected_value_sum = 0
                for action in actions:
                    expected_value_sum = expected_value_sum + (1/len(actions) * (self.actions.get_action_reward(state,action) + (self.gamma * self.value_function[self.actions.get_next_state_number(state,action)])))
                self.value_function[state] = expected_value_sum
                max_expected_value_change = max(max_expected_value_change, np.abs(expected_value_sum - old_value_function))
            if max_expected_value_change <= self.SMALL_CHANGE:
                break

        self.grid.print_values(self.value_function)

    def set_deterministic_policy(self):
        #Policy :
        # if we start from Start state we reach goal else we reach negative goal
        self.actions.grid_actions= self.policy.set_deterministic_policy(self.actions.grid_actions)

    def iterative_policy_evaluation_deterministic(self):
        self.gamma = 0.9
        self.set_deterministic_policy()
        self.grid.print_policy(self.actions.grid_actions)
        # Policy evaluation for deterministic policy
        # Probability for any action will be 1 as there is only one action
        states = list(range(self.rows * self.cols))

        self.value_function = np.zeros((self.rows * self.cols))
        while True:
            # holds the maximum change
            max_expected_value_change = 0
            for state in states:
                old_value_function = self.value_function[state]
                actions = self.actions.grid_actions[state]
                for action in actions:
                    # Not taking sum because there will be only one action
                    # multiplying by 1 because there will be only one action and probability of that action will be 1
                    self.value_function[state] =  (1 * (self.actions.get_action_reward(state,action)) + (self.gamma * self.value_function[self.actions.get_next_state_number(state,action)]))
                max_expected_value_change = max(max_expected_value_change, np.abs(self.value_function[state] - old_value_function))
            if max_expected_value_change < self.SMALL_CHANGE:
                break

        self.grid.print_values(self.value_function)


import numpy as np
import scipy as scp
def pca(X, frac, mask = None):
	"""
	function [y,n_dim,vecs,vals,mu] = pca(x,frac,mask)

	Principal Components Analysis
	 transforms input data x using PCA, reduces dimensionality
	 to capture fraction of magnitude of eigenvalues

	inputs:
	 x - input data M dimensions by N samples
	 frac - [0-1] fractional amount of total eigenvalue magnitude to retain, 1 = no dimensionality reduction
	 mask - binary mask of samples to include in covariance computation (use to mask off invalid pixels)
	         leave off the argument or use [] for no mask

	outputs:
	 y - PCA transformed dimensionality reduced data
	 n_dim - number of dimensions in the output data
	 vecs - full set of eigenvectors of covariance matrix (column vectors)
	 vals - eigenvalues of covariance matrix
	 mu - mean of input data, subtracted before rotating

	8/7/2012 - Taylor C. Glenn - tcg@cise.ufl.edu
	11/2018 - Python Implementation by Yutai Zhou
	"""
	n_dim, n_sample = X.shape
	if n_dim > 210:
		input('That is a lot of dimensions. It may crash. Press ctrl + c to stop execution, or enter to continue')

	mask = np.ones(n_sample, dtype=bool) if mask is None else mask
	mu = np.mean(X[:, mask == 1], 1)
	sigma = np.cov(X[:, mask == 1].T,  rowvar=False)

	z = X - mu[:, np.newaxis]
	U, S, V = scp.linalg.svd(sigma, lapack_driver = 'gesdd')

	evecs = U

	mag = np.sum(S)
	ind = np.where(np.cumsum(S) / mag >= frac)[0][0]
	n_dim = ind
	y = evecs[:, :ind + 1].T @ z # d x 72 * 72 x 4488
	return y, n_dim, evecs, S, mu


'''
The evaluation metric is weighted correlation as opposed to a weighted mean of correlation.

The metric is defined as follows, where 'a', 'b' and 'weights' are vectors of the same length.

'a' and 'b' are the expected and predicted targets, and ' weights' include the weight of each row,
            determined by its asset.
'''

import numpy as np

def weighted_correlation(a, b, weights):

  w = np.ravel(weights)
  a = np.ravel(a)
  b = np.ravel(b)

  sum_w = np.sum(w)
  mean_a = np.sum(a * w) / sum_w
  mean_b = np.sum(b * w) / sum_w
  var_a = np.sum(w * np.square(a - mean_a)) / sum_w
  var_b = np.sum(w * np.square(b - mean_b)) / sum_w

  cov = np.sum((a * b * w)) / np.sum(w) - mean_a * mean_b
  corr = cov / np.sqrt(var_a * var_b)

  return corr


"""
Nonlinearities and their derivatives

Each function returns the value and derivative of a nonlinearity. Given :math:`y = f(x)`, the function returns
:math:`y` and :math:`dy/dx`
"""
import numpy as np

def exp(x):
    """Exponential function"""

    # compute the exponential
    y = np.exp(x)

    # compute the first and second derivatives
    dydx = y
    dy2dx2 = y

    return y, dydx, dy2dx2

def softrect(x):
    """ Soft rectifying function

    .. math::
        y = \log(1+e^x)
    """

    # compute the soft rectifying nonlinearity
    x_exp = np.exp(x)
    y = np.log1p(x_exp)

    # compute the derivative
    dydx = x_exp / (1 + x_exp)

    # compute the second derivative
    dy2dx2 = x_exp / (1 + x_exp)**2

    return y, dydx, dy2dx2


# This program calculate moment of inertia of
# some 2D systems about origin and z axis
import numpy as np
import matplotlib.pyplot as plt

def disc():
    # generares disc (radius 1) as x, y pairs
    # returns : numpy arrays x and y
    x = np.linspace(-1, 1, 2000)
    y = np.linspace(-1, 1, 2000)

    u,v = np.meshgrid(x, y)

    cx = u[u**2+v**2<=1]
    cy = v[u**2+v**2<=1]

    return cx, cy

def disc_with_hole():
    # generares disc (radius 1) as x, y pairs
    # with a hole of radius 1/2
    # returns : numpy arrays x and y
    x = np.linspace(-1, 1, 2000)
    y = np.linspace(-1, 1, 2000)

    u,v = np.meshgrid(x, y)

    mask = np.logical_and(u**2+v**2<=1, (u-0.5)**2+v**2>=0.25)
    cx = u[mask]
    cy = v[mask]

    return cx, cy

# ############################################
# CALCULATION
x, y = disc_with_hole()
N = len(x)   # number of particles
M = 3.0/4.0  # mass
m = M / N   # masss of each particle
I = (m*(x**2+y**2)).sum()  # Moment of Inertia
print("M.I = ", round(I, 3))
print("k   = ", round(np.sqrt(I/M), 3))

# x, y = disc_with_hole()
# print(x.shape, y.shape)
fig, ax = plt.subplots()
ax.set_aspect("equal")
plt.scatter(x, y, s=0.5)
plt.show()


import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import scipy.optimize

Vdc = 3.293551538837322212
res = 3.3 / 1023
inputFile = 'DischargeProcessData.txt'

# define fitting function
def DischargeProcess(t, t0, tau):
    return Vdc * (np.exp(-(t-t0)/tau))

# read the experimental data from file
t, v = np.loadtxt(inputFile, delimiter = ' ', unpack = True)

# assign the experimental uncertainty on Voltage values
dv = np.ones(v.size) * res

# initial guesses for fitting parameters
t0_guess, tau_guess = 20, 1

# fitting procedure
nlfit, nlpcov = \
       scipy.optimize.curve_fit(DischargeProcess, t, v, \
                                p0 = [t0_guess, tau_guess], sigma = dv,\
                                bounds = (0, 100))

# obtaining parameters from the best fit procedure
t0, tau = nlfit

# obtaining uncertainties associated with fitting parameters
dt0, dtau = \
     [np.sqrt(nlpcov[j, j]) for j in range(nlfit.size)]

# create fitting function from fitted parameters
t_fit = np.linspace(t.min(), t.max(), 128)
v_fit = DischargeProcess(t_fit, t0, tau)

# residuals and reduced chi squared
resids = v - DischargeProcess(t, t0, tau)
redchisqr = ((resids/dv)**2).sum()/float(t.size-2)
ndf = t.size - 2
# where 2 is the number of free parameters

# create figure window to plot data
fig = plt.figure(1, figsize = (8,8))
gs = gridspec.GridSpec(2, 1, height_ratios = [6, 2])

# plotting data and fit
ax1 = fig.add_subplot(gs[0])
ax1.plot(t_fit, v_fit)
ax1.errorbar(t, v, yerr = dv, fmt = 'or', ecolor = 'black', markersize = 2)
ax1.set_xlabel(' time (s)')
ax1.set_ylabel('voltage $V_C$ (V)')
ax1.text(0.6, 0.80, r'$\tau$ = {0:6.4f}$\pm${1:0.4f}'.format(tau, dtau), transform = ax1.transAxes, fontsize = 14)
ax1.text(0.6, 0.70, r'$t_0$= {0:5.4f}$\pm${1:0.4f}'.format(t0, dt0), transform = ax1.transAxes, fontsize = 14)
ax1.text(0.6, 0.60, r'$\chi_r^2$ = {0:0.1f}, ndf = {1}'.format(redchisqr, ndf), transform = ax1.transAxes, fontsize = 14)

# plotting residuals
ax2 = fig.add_subplot(gs[1])
ax2.errorbar(t, resids, yerr = dv, ecolor = 'black', fmt ='ro', markersize = 2)
ax2.axhline(color = 'gray', zorder = -1)
ax2.set_xlabel('time (s)')
ax2.set_ylabel('residuals (V)')
plt.savefig('DischargingDataAndFit.pdf')
plt.show()


from typing import Union, Optional

import numpy as np

def gmean(
    a: np.ndarray,
    axis: Optional[int] = None,
    eps: Optional[float] = 1e-6):
    """Computes the geometric mean along the specified axis.
    Returns the geometric average of the array elements.
    That is:  n-th root of (x1 * x2 * ... * xn)

    Args:
        a: Input array
        axis: Axis along which the geometric mean is computed.
            If None, compute over the whole array `a`.
        eps: Small value for stability

    Returns:
        gmean: The geometric mean of `a`

    Examples:
    >>> gmean(np.array([1, 4]))
    2.0
    >>> gmean(np.array([1, 2, 3, 4, 5, 6, 7]))
    3.3800151591412964
    >>> import hdrpy
    >>> hdr = hdrpy.io.read("./data/CandleGlass.exr")
    >>> gmean(hdrpy.image.get_luminance(hdr))
    0.00040137774000534386
    """
    a_c = a.copy()
    if eps is not None:
        a_c[a_c <= 0] = eps
        a_c[np.isnan(a_c)] = eps
    log_a = np.log(a_c)
    return np.exp(log_a.mean(axis=axis))

def min_max_normalization(
    a: np.ndarray,
    min_: Optional[Union[float, int]] = None,
    max_: Optional[Union[float, int]] = None) -> np.ndarray:
    """Normalizes the given array `a`
    so that its range is in [0, 1]

    Args:
        a: Input array
        min: Minimum possible value of the array
        max: Minimum possible value of the array
    Returns:
        array in [0, 1] range

    Examples:
    >>> a = min_max_normalization(np.random.randn(100))
    >>> np.all(a <= 1)
    True
    >>> np.all(a >= 0)
    True
    """
    if min_ is None:
        min_ = float(np.amin(a))

    if max_ is None:
        max_ = float(np.amax(a))

    return (a - min_) / (max_ - min_)

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


# coding: utf-8

import numpy as np

def jacobi_method(A, b, eps=1e-5, max_steps=100, debug=False):
    """
    Solves a linear system Ax = b for x using the iterative jacobi method,
    to a requested error eps with at most max_steps steps.
    Turn on debug prints using debug=True.
    Returns the solution x. Raises a ValueError if the iteration doesn't converge.
    """

    cols = np.arange(A.shape[0])[None,:].repeat(A.shape[1], 0)
    rows = np.arange(A.shape[1])[:,None].repeat(A.shape[0], 1)

    L = (rows > cols) * A
    U = (rows < cols) * A
    D = np.diag(A)

    x = np.zeros_like(b)

    for i in range(max_steps):
        x =  (1.0 / D) * (b - (L + U) @ x)
        err = np.linalg.norm(A @ x - b)
        if debug:
            print("error:", err, "\nx:\n", x)
        if err <= eps:
            return x

    raise ValueError("Failed to converge.\neps = {}, max_steps = {}\nA:\n{}\nb:\n{}\nlast x:\n{}".format(eps, max_steps, A, b, x))

if __name__ == "__main__":

    A = np.array([[10., -1., 2., 0.],
                  [-1., 11., -1., 3.],
                  [2., -1., 10., -1.],
                  [0.0, 3., -1., 8.]])
    b = np.array([6., 25., -11., 15.])

    jacobi_x = jacobi_method(A, b, eps=0)
    numpy_x = np.linalg.solve(A, b)

    print("A:\n", A, "\nb:\n", b, "\nx using jacobi:\n", jacobi_x, "\nerror:", np.linalg.norm(jacobi_x - numpy_x))


import numpy as np
import random as rd
import matplotlib.pyplot as pl

# Load text file
file = open("list_of_engwords.txt", "r")
words = file.read().split('\n')

file.close()

# Task 1

# Manually add the two one-letter words
words.append("a")
words.append("i")

# Define information gained for each letter revealed as log_2 (N_b - N_m),
# where N_b and N_m are the number of words remaining before and after
# revealing the letter, given that all words are equally probable a priori.

def info_gain(word_pick):
    word_pick_list = list(word_pick)

    # Stores remaining words
    rem_words = words.copy()
    #rem_words.remove(word_pick)
    #print("Remaining num of words: ", len(rem_words))

    info_total = 0 # bits
    info_total_all = [0]

    for i in range(len(word_pick_list)):
        #print("Letter {0}: ".format(i+1), word_pick_list[i])

        no_rem_words_bef = len(rem_words)

        # Find remaning number of words after revealing letter i+1
        rem_word_ind = 0
        while rem_word_ind < len(rem_words):
            if i < len(list(rem_words[rem_word_ind])):
                # So that the word is longer than the one picked
                if list(rem_words[rem_word_ind])[i] == word_pick_list[i]:
                    # Same letter, remain
                    rem_word_ind += 1
                    continue
                else:
                    rem_words.remove(rem_words[rem_word_ind])
            else:
                rem_words.remove(rem_words[rem_word_ind])

        #print("Remaining num of words: ", len(rem_words))

        # Calculate info gained
        info_total += np.log2(no_rem_words_bef / len(rem_words))
        info_total_all.append(info_total)
        #print("Info gained from revealing letter {0}: ".format(i+1),
        #      np.log2(no_rem_words_bef / len(rem_words)), " bits")
        #print("Total amount of info gained after letter {0}: ".format(i+1),
        #      info_total, " bits")
        #print("Amount of info not revealed after letter {0}: ".format(i+1),
        #      np.log2(len(words)) - info_total, " bits")
    return info_total_all

# Part (d): Try wombat
#info_gain("wombat")

# Pick a word
#pick = rd.randint(0, len(words)-1)
#word_pick_rd = "versifying" # words[pick]
#info_gain(word_pick_rd)

# Part (e): Plot cumulative info gained
# Gather all words with 15 letters
fifteen_let = []
for word in words:
    if len(list(word)) == 15:
        fifteen_let.append(word)

pl.figure()
for wd in fifteen_let:
    cum_info = info_gain(wd)
    pl.plot(np.arange(0, 16), cum_info)

# Line of info gained revealing the word altogether, as comparison
pl.plot([0, 15.5], [np.log2(len(words)), np.log2(len(words))], "k--")
pl.xlabel("Letter revealed")
pl.ylabel("Cumulative information gained / bits")
pl.xticks(np.arange(0, 16))
pl.xlim(left=0)
pl.ylim(bottom=0)
pl.grid()


"""
Activation Functions
--------------------
"""

from abc import ABC, abstractmethod
import numpy as np

class Activator(ABC):
    """Activator interface (d = True for differentiation/backprop"""

    @abstractmethod
    def activation(self, x: np.ndarray, d: bool = False) -> np.ndarray:
        raise NotImplementedError("No activation method implemented.")

class Relu(Activator):
    """Compute rectified Linear Unit (relu) values for each sets of scores in x"""

    def activation(self, x: np.ndarray, d: bool = False) -> np.ndarray:
        if d:  # differentiation for backprop
            relu_x = np.heaviside(x, 1)
        else:
            relu_x = x
            relu_x[x < 0] = 0
        return relu_x

class Tanh(Activator):
    """Compute hyperbolic tan (tanh) values for each sets of scores in x"""

    def activation(self, x: np.ndarray, d: bool = False) -> np.ndarray:
        tanh_x = (2 / (1 + np.exp(-2 * x))) - 1
        if d:  # differentiation for backprop
            tanh_x = 1 - tanh_x ** 2
        return tanh_x

class Sigmoid(Activator):
    """Compute sigmoid values for each sets of scores in x"""

    def activation(self, x: np.ndarray, d: bool = False) -> np.ndarray:
        sigmoid_x = 1 / (1 + np.exp(-x))
        if d == True:
            sigmoid_x = sigmoid_x * (1 - sigmoid_x)
        return sigmoid_x

class Softmax(Activator):
    """Output function - Compute softmax values for each sets of scores in x"""

    def activation(self, x: np.ndarray, d: bool = False) -> np.ndarray:
        if d == True:
            raise NotImplementedError("Used for output layer only")
        sigmoid_x = np.exp(x)
        return sigmoid_x / sigmoid_x.sum()


import numpy as np

def calculate_distance(rA, rB):
    """
    calculate the distance between two points

    Parameters
    ----------
    rA, rB : np ndarray
        The coordinates of each point.

    Returns
    -------
    distance : float
        the distance of bla

    Examples
    --------
    >>> r1=np.array([0, 0, 0])
    >>> r2=np.array([3.0, 0, 0])
    >>> calculate_distance(r1, r2)
    3.0
    """

    # This function calculates the distance between two points given as numpy arrays.
    distance_vec=(rA-rB)
    dist=np.linalg.norm(distance_vec)
    return dist

def calculate_angle(rA, rB, rC, degrees=False):
    # Calculate the angle between three points. Answer is given in radians by default, but can be given in degrees
    # by setting degrees=True
    dist_AB = rB - rA
    dist_BC = rB - rC
    theta=np.arccos(np.dot(dist_AB, dist_BC)/(np.linalg.norm(dist_AB)*np.linalg.norm(dist_BC)))

    if degrees:
        return np.degrees(theta)
    else:
        return theta


# Here we will compute the Weighted Sum at Each Node (z) and Node Activation (a-sigmoid function)
# But will define small_network from Lab1_Artificial_Neural_Networks_Networks

import numpy as np

def intialize_network(n,n_hid,m,n_out):
    n_nodes_p=n
    network={}
    for layer in range(n_hid+1):
            if layer == n_hid:
                layer_name='output' # to define last layer as output layer
                n_nodes=n_out
            else:
                layer_name ='layer_{}'.format(layer+1) # to name each layer as layer 1, layer 2....
                n_nodes=m[layer]
            network[layer_name]={}
            for node in range(n_nodes):
                 node_name='node_{}'.format(node+1) # to define nodes for each layer
                 network[layer_name][node_name]={
                     'weights': np.around(np.random.uniform(size=n_nodes_p),decimals=2),
                     'bias': np.around(np.random.uniform(size=1),decimals=2) # define weights and bias for each node in each layer
                     }
            n_nodes_p=n_nodes
    return network

ns=5 #inputs
n_hids=3 #hidden layers
ms=[3,2,3] #nodes in each hidden layer
n_outs=1 #nodes in output layer

small_network=intialize_network(ns,n_hids,ms,n_outs)

## Computing weighted sum at each Node - we need inputs ,weights, biases

def compute_weighted_sum(inputs,weights,bias):
    return np.sum(inputs*weights)+bias

# Example print the weighted sum at layer 1 node 1 (first node at first hidden layer)

np.random.seed(12)
inputs = np.around(np.random.uniform(size=5), decimals=2) # random inputs

node_weights=small_network['layer_1']['node_1']['weights']
node_bias=small_network['layer_1']['node_1']['bias']

weighted_sum=compute_weighted_sum(inputs,node_weights,node_bias)

with open ("Lab1_ANN_Compute.txt",'a') as f:
    print('Inputs are: \n',inputs,file=f)
    print('The weighted sum at the first node in the hidden layer is {}'.format(np.around(weighted_sum[0], decimals=4)),file=f)

## Node activation (Sigmoid function)

def node_activation(weighted_sum):
    return 1.0/(1.0+np.exp(-1*weighted_sum))

# Example print the node activation at layer 1 node 1 (first node at first hidden layer)

node_a_output=node_activation(compute_weighted_sum(inputs,node_weights,node_bias))
with open ("Lab1_ANN_Compute.txt",'a') as f:
    print('The output at the first node in the hidden layer is {}'.format(np.around(node_a_output[0], decimals=4)),file=f)

## We define forward_propagate function to combine both calculations for outputs of all nodes of every hidden layer

def forward_propagate(network,inputs):
    layer_inputs=list(inputs)

    for layer in network:
        layer_data=network[layer]
        layer_outputs=[]

        for layer_node in layer_data:
            node_data=layer_data[layer_node]

            node_output = node_activation(compute_weighted_sum(layer_inputs, node_data['weights'], node_data['bias']))
            layer_outputs.append(np.around(node_output[0], decimals=4))

        if layer !='output':
            with open ("Lab1_ANN_Compute.txt",'a') as f:
                print('The outputs of the nodes in hidden layer number {} is {}'.format(layer.split('_')[1], layer_outputs),file=f)

        layer_inputs = layer_outputs

    network_predictions=layer_outputs
    return network_predictions

# Exercise - Print the predictions using forward_propagate function

predictions = forward_propagate(small_network, inputs)
with open ("Lab1_ANN_Compute.txt",'a') as f:
    print('The predicted value by the network for the given input is {}'.format(np.around(predictions[0], decimals=4)),file=f)

Introduction to Deep Learning & Neural Networks with Keras/Module1/Lab1_Artificial_Neural_Networks_Calculation.py


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Jun  1 23:14:54 2019

@author: fietekrutein

A script for solving a QCQP, using the log-barrier interior point method with backtracking line search
"""

import numpy as np
import cvxpy as cp
import math

# functions as per example problem

def obj(x,Q,p,r): # original objective
    return ((0.5*np.dot(np.dot(x,Q),x.T) + np.dot(p,x.T) + r)[0][0])

def grad(x,Q,p): # original gradient
    return (np.dot(x,Q).T + p)

def hess(Q): # original hessian
    return (Q)

def logbar(x,Q,p,r): # log barrier objective
    return (eta * obj(x,Q,p,r) - np.log(x[0][0] - x[0][1] + 2)
            - np.log(-x[0][0]-3*x[0][1]+5) - np.log(-x[0][0]**2 - x[0][1]**2 + 2*x[0][1] + 1))

def gradg3(x): # third gradient
    return (2*x - np.array([[0,2]]).T)

# Interior point method

def InteriorPoint(x_init, Q, p, r, alpha, beta, eta, nu, l, tol):
    # fixed constraint gradients
    gradg1 = np.array([[-1,1]])
    gradg2 = np.array([[1,3]])
    hessg3 = np.array([[2,0],
                       [0,2]])
    lIter = 0
    xCurrent = x_init
    while True:
        lIter += 1
        stop = 0
        nIter = 0

        while (stop == 0):
            nIter +=1
            g1 = -xCurrent[0][0] + xCurrent[0][1] - 2 # constraint 1
            g2 = xCurrent[0][0] + 3*xCurrent[0][1] - 5 #constraint 2
            g3 = xCurrent[0][0]**2 + xCurrent[0][1]**2 - 2*xCurrent[0][1] - 1 #constraint 3
            # gradient and hessian of P3(eta)
            gradient = eta * grad(xCurrent,Q,p.T) + (1/-g1)*gradg1.T + (1/-g2)*gradg2.T + (1/-g3)*gradg3(xCurrent.T)
            hessian = eta * hess(Q) + (1/(-g1)**2)*np.matmul(gradg1.T, gradg1)
            + (1/(-g2)**2)*np.dot(gradg2.T, gradg2)
            + (1/(-g3)**2)*np.dot(gradg3(xCurrent).T, gradg3(xCurrent))
            + (1/-g3)*hessg3

            t = 1
            d = np.dot(np.linalg.inv(hessian),gradient)
            xNext = xCurrent - t*d.T

            # check whether log goes to infinity at evaluated t
            while math.isnan(logbar(xNext,Q,p,r)):
                t = 0.98 * t
                xNext = xCurrent - t*d.T
            # implement backtracking line search
            while logbar(xNext,Q,p,r) > (logbar(xCurrent,Q,p,r) + alpha*t*(np.dot(gradient.T,d).item(0))):
                t = beta * t
                xNext = xCurrent - t*d.T
            print("IP Iteration {}, Newton Iteration {}: {}".format(lIter, nIter, xNext))

            if np.linalg.norm((xCurrent - xNext),2) <= tol:
                stop = 1
                xStar = xNext
                break
            else:
                xCurrent = xNext

        xCurrent = xStar
        if (l/eta <= tol):
            print("Optimal value found at IP iteration {}: {}".format(lIter, xStar))
            break
        else:
            eta = nu*eta

    return(xCurrent)

# initialize problem parameters
alpha = 0.01
beta = 0.7
eta = 1
nu = 2
l = 3
x_init = np.array([[1,1]])
Q = np.array([[1,0],[0,1]])
p = np.array([[-2,-2]])
r = 0

# execute

InteriorPoint(x_init, Q, p, r, alpha, beta, eta, nu, l, tol=10e-5)

# cvxpy script for comparison

# Define and solve
n = 2
x = cp.Variable(n)
objective = cp.Minimize(0.5*cp.quad_form(x, Q) + p*x + r)
constraints = [-x[0]+ x[1] <= 2,
               x[0] + 3*x[1] <= 5,
               x[0]**2 + x[1]**2 -2*x[1] <= 1]
prob = cp.Problem(objective, constraints)
prob.solve()

result = prob.solve()
# The optimal value for x is stored in `x.value`.
print("The optimum is reached at: ", result)
print("The optimal values for x are: ", x.value)


import numpy as np
import matplotlib.pyplot as plt

def interp(ni, nj, N):
    '''
    Returns an interpolation function from bin ni to bin nj (been N the total number of bins) in polar coordinates
    See https://calculus7.org/2013/04/20/peanut-allergy-and-polar-interpolation/
    '''
    n1 = min(ni, nj)
    n2 = max(ni, nj)
    Nm = int(N/2-0.0001)
    if (n2-n1 < N/2):
        r = ((Nm+1-n2+n1)/(Nm+1))**(-2)
        delta = np.pi/N*(n2-n1)
        theta0 = 2*n1*np.pi/N+delta
    elif (n2-n1 > N/2):
        r = ((Nm+1+n2-n1-N)/(Nm+1))**(-2)
        delta = np.pi/N*(N-n2+n1)
        theta0 = 2*n1*np.pi/N - delta
    else:
        r = 1e10 #yeah, I don't know why this works :O
        delta = np.pi/N*(n2-n1)
        theta0 = 2*n1*np.pi/N+delta

    return lambda x: (r + (1-r)*np.sin(x-theta0)**2/np.sin(delta)**2)**(-0.5)

def cov2corr(cov):
    '''
    Computes the correltion matrix from the covariance matrix
    '''
    return np.diag((np.diag(cov))**(-0.5)) @ cov @ np.diag((np.diag(cov))**(-0.5))

def circlecorr(cov, R=10, maxlw=5, labels=None):
    '''
    Circular plot for the correlation matrix.
    The bins represent the variance of each variable,
    and the lines the correlation coefficient:
    the width of the line corresponds to the absolute value of the correlation coefficient.
    Blue lines indicate negative correlation,
    Red lines indicate positive correlation.

    Function arguments:
    - cov: Covariance matrix.
    - R: Radius of the circle that contains the base of the bins.
    - maxlw: Linewidth of the lines of correlation = +-1
    - labels: list of labels for each bin
    '''
    N = cov.shape[0]
    corr = cov2corr(cov)

    theta = np.linspace(0, 2*np.pi, N, endpoint=False)
    width = 2*np.pi/(N*1.2)
    ax = plt.subplot(111, polar= True)
    ax.set_xticks(np.linspace(0, 2*np.pi, 6, endpoint=False))
    if labels is not None:
        ax.set_xticklabels(labels)
    ax.set_yticklabels([])
    bars = ax.bar(theta, np.diag(cov), width=width, bottom=R)
    for i in range(N):
        for j in range(i+1, N):
            if (j-i <= N/2):
                lin =np.linspace( 2*i*np.pi/N, 2*j*np.pi/N, 100 )
            else:
                lin =np.linspace( 2*j*np.pi/N, 2*(i+N)*np.pi/N, 100 )
            int1 = interp(i,j,N)
            if corr[i,j] > 0:
                c = 'red'
            else:
                c = 'blue'
            plt.plot(lin, R*int1(lin), c=c, lw=maxlw*abs(corr[i,j]))


###############################
######### Problem 2 ###########
###############################

""" Tools for calculating Gradient Descent for ||Ax-b||. """
import matplotlib.pyplot as plt
import numpy as np

def main():
    ################################################################################
    # Input Variables
    A = np.array([[15, 8], [6, 5]])  # do not change this until the last part
    b = np.array([4.5, 6])  # b in the equation ||Ax-b||
    initial_position = np.array([0, 0])  # position at iteration 0
    total_step_count = 200  # number of GD steps to take
    step_size = lambda i: 1/(1+i)  # step size at iteration i
    ################################################################################

    # computes desired number of steps of gradient descent
    positions = compute_updates(A, b, initial_position, total_step_count, step_size)

    # print out the values of the x_i
    print(positions)
    print(np.dot(np.linalg.inv(A), b))

    # plot the values of the x_i
    plt.scatter(positions[:, 0], positions[:, 1], c='blue')
    plt.scatter(np.dot(np.linalg.inv(A), b)[0],
                np.dot(np.linalg.inv(A), b)[1], c='red')
    plt.plot()
    plt.show()

def compute_gradient(A, b, x):
    """Computes the gradient of ||Ax-b|| with respect to x."""
    return np.dot(A.T, (np.dot(A, x) - b)) / np.linalg.norm(np.dot(A, x) - b)

def compute_update(A, b, x, step_count, step_size):
    """Computes the new point after the update at x."""
    return x - step_size(step_count) * compute_gradient(A, b, x)

def compute_updates(A, b, p, total_step_count, step_size):
    """Computes several updates towards the minimum of ||Ax-b|| from p.

    Params:
        b: in the equation ||Ax-b||
        p: initialization point
        total_step_count: number of iterations to calculate
        step_size: function for determining the step size at step i
    """
    positions = [np.array(p)]
    for k in range(total_step_count):
        positions.append(compute_update(A, b, positions[-1], k, step_size))
    return np.array(positions)

main()

###############################
######### Problem 4 ###########
###############################

from common import *

###############################
######### Part b ##############
###############################

########################################################################
#########  Gradient Computing and MLE ##################################
########################################################################
def compute_gradient_of_likelihood(single_obj_loc, sensor_loc, single_distance):
    """
    Compute the gradient of the loglikelihood function for part a.

    Input:
    single_obj_loc: 1 * d numpy array.
    Location of the single object.

    sensor_loc: k * d numpy array.
    Location of sensor.

    single_distance: k dimensional numpy array.
    Observed distance of the object.

    Output:
    grad: d-dimensional numpy array.

    """
    loc_diff = single_obj_loc - sensor_loc
    tmp = np.linalg.norm(loc_diff, axis=1)
    weight = (tmp - single_distance) / tmp
    grad = -np.sum(np.expand_dims(weight, axis=1) * loc_diff, axis=0)

    return grad

def find_mle_by_grad_descent_part_b(initial_obj_loc, sensor_loc, single_distance, lr=0.001, num_iters = 10000):
    """
    Compute the gradient of the loglikelihood function for part a.

    Input:
    initial_obj_loc: 1 * d numpy array.
    Initialized Location of the single object.

    sensor_loc: k * d numpy array. Location of sensor.

    single_distance: k dimensional numpy array.
    Observed distance of the object.

    Output:
    obj_loc: 1 * d numpy array. The mle for the location of the object.

    """
    obj_loc = initial_obj_loc
    for i in range(num_iters):
        obj_loc = obj_loc + lr * compute_gradient_of_likelihood(obj_loc, sensor_loc, single_distance)

    return obj_loc

if __name__ == "__main__":
    ########################################################################
    #########  MAIN ########################################################
    ########################################################################

    # Your code: set some appropriate learning rate here
    lr = 0.01

    np.random.seed(0)
    sensor_loc = generate_sensors()
    obj_loc, distance = generate_data(sensor_loc)
    single_distance = distance[0]
    print('The real object location is')
    print(obj_loc)
    # Initialized as [0,0]
    initial_obj_loc = np.array([[0.,0.]])
    estimated_obj_loc = find_mle_by_grad_descent_part_b(initial_obj_loc,
               sensor_loc, single_distance, lr=lr, num_iters = 10000)
    print('The estimated object location with zero initialization is')
    print(estimated_obj_loc)

    # Random initialization.
    initial_obj_loc = np.random.randn(1,2)
    estimated_obj_loc = find_mle_by_grad_descent_part_b(initial_obj_loc,
               sensor_loc, single_distance, lr=lr, num_iters = 10000)
    print('The estimated object location with random initialization is')
    print(estimated_obj_loc)

from common import *
#from part_b_starter import find_mle_by_grad_descent_part_b

#############################
######### Part c ############
#############################
def log_likelihood(obj_loc, sensor_loc, distance):
    """
    This function computes the log likelihood (as expressed in Part a).
    Input:
    obj_loc: shape [1,2]
    sensor_loc: shape [7,2]
    distance: shape [7]
    Output:
    The log likelihood function value.
    """
    diff = np.sqrt(np.sum((sensor_loc - obj_loc)**2, axis = 1)) - distance
    func_value = -sum((diff) ** 2) / 2
    return func_value

if __name__ == "__main__":
    ################################################################################
    ######### Compute the function value at local minimum for all experiments. #####
    ################################################################################
    num_sensors = 20

    np.random.seed(100)
    sensor_loc = generate_sensors(k=num_sensors)

    # num_data_replicates = 10
    num_gd_replicates = 100

    obj_locs = [[[i,i]] for i in np.arange(0,1000,100)]

    func_values = np.zeros((len(obj_locs),10, num_gd_replicates))
    # record sensor_loc, obj_loc, 100 found minimas
    minimas = np.zeros((len(obj_locs), 10, num_gd_replicates, 2))
    true_object_locs = np.zeros((len(obj_locs), 10, 2))

    for i, obj_loc in enumerate(obj_locs):
        for j in range(10):
            obj_loc, distance = generate_data_given_location(sensor_loc, obj_loc,
                                                           k = num_sensors, d = 2)
            true_object_locs[i, j, :] = np.array(obj_loc)

            for gd_replicate in range(num_gd_replicates):
                initial_obj_loc = np.random.randn(1,2) * (100 * i+1)
                obj_loc = find_mle_by_grad_descent_part_b(initial_obj_loc,
                         sensor_loc, distance[0], lr=0.1, num_iters = 1000)
                minimas[i, j, gd_replicate, :] = np.array(obj_loc)
                func_value = log_likelihood(obj_loc, sensor_loc, distance[0])
                func_values[i, j, gd_replicate] = func_value

    ########################################################################
    ######### Calculate the things to be plotted. ##########################
    ########################################################################
    local_mins = [[np.unique(func_values[i,j].round(decimals=2)) for j in range(10)] for i in range(10)]
    num_local_min = [[len(local_mins[i][j]) for j in range(10)] for i in range(10)]
    proportion_global = [[sum(func_values[i,j].round(decimals=2) == min(local_mins[i][j]))*1.0/100 \
                         for j in range(10)] for i in range(10)]

    num_local_min = np.array(num_local_min)
    num_local_min = np.mean(num_local_min, axis = 1)

    proportion_global = np.array(proportion_global)
    proportion_global = np.mean(proportion_global, axis = 1)

    ########################################################################
    ######### Plots. #######################################################
    ########################################################################
    fig, axes = plt.subplots(figsize=(8,6), nrows=2, ncols=1)
    fig.tight_layout()
    plt.subplot(211)

    plt.plot(np.arange(0,1000,100), num_local_min)
    plt.title('Number of local minimum found by 100 gradient descents.')
    plt.xlabel('Object Location')
    plt.ylabel('Number')
    #plt.show()
    #plt.savefig('num_obj.png')
    # Proportion of gradient descents that find the local minimum of minimum value.

    plt.subplot(212)
    plt.plot(np.arange(0,1000,100), proportion_global)
    plt.title('Proportion of GD that finds the global minimum among 100 gradient descents.')
    plt.xlabel('Object Location')
    plt.ylabel('Proportion')
    fig.tight_layout()
    #plt.show()
    #plt.savefig('prop_obj.png')

    ########################################################################
    ######### Plots of contours. ###########################################
    ########################################################################
    np.random.seed(0)
    # sensor_loc = np.random.randn(7,2) * 10
    x = np.arange(-10.0, 10.0, 0.1)
    y = np.arange(-10.0, 10.0, 0.1)
    X, Y = np.meshgrid(x, y)
    obj_loc = [[0,0]]
    obj_loc, distance = generate_data_given_location(sensor_loc,
                                                   obj_loc, k = num_sensors, d = 2)

    Z =  np.array([[log_likelihood((X[i,j],Y[i,j]),
                                 sensor_loc, distance[0]) for j in range(len(X))] \
                 for i in range(len(X))])

    plt.figure(figsize=(10,4))
    plt.subplot(121)
    CS = plt.contour(X, Y, Z)
    plt.clabel(CS, inline=1, fontsize=10)
    plt.title('With object at (0,0)')

    np.random.seed(0)
    # sensor_loc = np.random.randn(7,2) * 10
    x = np.arange(-400,400, 4)
    y = np.arange(-400,400, 4)
    X, Y = np.meshgrid(x, y)
    obj_loc = [[200,200]]
    obj_loc, distance = generate_data_given_location(sensor_loc,
                                                   obj_loc, k = num_sensors, d = 2)

    Z =  np.array([[log_likelihood((X[i,j],Y[i,j]),
                                 sensor_loc, distance[0]) for j in range(len(X))] \
                 for i in range(len(X))])

    # Create a simple contour plot with labels using default colors.  The
    # inline argument to clabel will control whether the labels are draw
    # over the line segments of the contour, removing the lines beneath
    # the label
    #plt.figure()
    plt.subplot(122)
    CS = plt.contour(X, Y, Z)
    plt.clabel(CS, inline=1, fontsize=10)
    plt.title('With object at (200,200)')
    #plt.savefig('likelihood_landscape.png')

    ########################################################################
    ######### Plots of Found local minimas. ###########################################
    ########################################################################
    #sensor_loc
    #minimas = np.zeros((len(obj_locs), 10, num_gd_replicates, 2))
    #true_object_locs = np.zeros((len(obj_locs), 10, 2))
    object_loc_i = 5
    trail = 0

    plt.figure()
    plt.plot(sensor_loc[:, 0], sensor_loc[:, 1], 'r+', label="sensors")
    plt.plot(minimas[object_loc_i, trail, :, 0], minimas[object_loc_i, trail, :, 1], 'g.', label="minimas")
    plt.plot(true_object_locs[object_loc_i, trail, 0], true_object_locs[object_loc_i, trail, 1], 'b*', label="object")
    plt.title('object at location (%d, %d), gradient descent recovered locations' % (object_loc_i*100, object_loc_i*100))
    plt.legend()
    plt.show()

###############################
######### Part f ##############
###############################

########################################################################
#########  Gradient Computing and MLE ##################################
########################################################################
def compute_grad_likelihood(sensor_loc, obj_loc, distance):
    """
    Compute the gradient of the loglikelihood function for part f.

    Input:
    sensor_loc: k * d numpy array.
    Location of sensors.

    obj_loc: n * d numpy array.
    Location of the objects.

    distance: n * k dimensional numpy array.
    Observed distance of the object.

    Output:
    grad: k * d numpy array.
    """
    grad = np.zeros(sensor_loc.shape)
    ### finish the grad loglike ###
    for i, j in enumerate(sensor_loc):
        d = distance[:, i]
        grad[i] = compute_gradient_of_likelihood(j, obj_loc, d)

    return grad

def find_mle_by_grad_descent(initial_sensor_loc,
           obj_loc, distance, lr=0.001, num_iters = 1000):
    """
    Compute the gradient of the loglikelihood function for part f.

    Input:
    initial_sensor_loc: k * d numpy array.
    Initialized Location of the sensors.

    obj_loc: n * d numpy array. Location of the n objects.

    distance: n * k dimensional numpy array.
    Observed distance of the n object.

    Output:
    sensor_loc: k * d numpy array. The mle for the location of the object.

    """
    sensor_loc = initial_sensor_loc
    ### finish the gradient descent ###
    for i in range(num_iters):
        sensor_loc = sensor_loc + lr * compute_grad_likelihood(sensor_loc, obj_loc, distance)

    return sensor_loc
########################################################################
#########  Gradient Computing and MLE ##################################
########################################################################

np.random.seed(0)
sensor_loc = generate_sensors()
obj_loc, distance = generate_data(sensor_loc, n = 100)
print('The real sensor locations are')
print(sensor_loc)
# Initialized as zeros.
initial_sensor_loc = np.zeros((7,2)) #np.random.randn(7,2)
estimated_sensor_loc = find_mle_by_grad_descent(initial_sensor_loc,
                                                obj_loc, distance, lr=0.001, num_iters = 1000)
print('The predicted sensor locations are')
print(estimated_sensor_loc)

########################################################################
#########  Estimate distance given estimated sensor locations. #########
########################################################################

def compute_distance_with_sensor_and_obj_loc(sensor_loc, obj_loc):
    """
    stimate distance given estimated sensor locations.

    Input:
    sensor_loc: k * d numpy array.
    Location of the sensors.

    obj_loc: n * d numpy array. Location of the n objects.

    Output:
    distance: n * k dimensional numpy array.
    """
    estimated_distance = scipy.spatial.distance.cdist(obj_loc, sensor_loc, metric='euclidean')
    return estimated_distance

########################################################################
#########  MAIN  #######################################################
########################################################################
np.random.seed(100)
########################################################################
#########  Case 1. #####################################################
########################################################################
mse =0

for i in range(100):
    obj_loc, distance = generate_data(sensor_loc, k = 7, d = 2, n = 1, original_dist = True)
    #obj_loc, distance = generate_data_given_location(estimated_sensor_loc, obj_loc, k = 7, d = 2)
    l = float('-inf')
    ### compute the mse for this case ###
    estimate = compute_distance_with_sensor_and_obj_loc(estimated_sensor_loc, obj_loc)
    mse = mse + np.mean(np.sum(estimate, axis = 1))

mse = mse / 100

print('The MSE for Case 1 is {}'.format(mse))

########################################################################
#########  Case 2. #####################################################
########################################################################
mse = 0

for i in range(100):
    obj_loc, distance = generate_data(sensor_loc, k = 7, d = 2, n = 1, original_dist = False)
    #obj_loc, distance = generate_data_given_location(estimated_sensor_loc, obj_loc, k = 7, d = 2)
    l = float('-inf')
    ### compute the mse for this case ###
    estimate = compute_distance_with_sensor_and_obj_loc(estimated_sensor_loc, obj_loc)
    mse = mse + np.mean(np.sum(estimate, axis = 1))

mse = mse / 100

print('The MSE for Case 2 is {}'.format(mse))

########################################################################
#########  Case 3. #####################################################
########################################################################
mse =0

for i in range(100):
    obj_loc, distance = generate_data(sensor_loc, k = 7, d = 2, n = 1, original_dist = False)
    obj_loc, distance = generate_data_given_location(estimated_sensor_loc, obj_loc, k = 7, d = 2)
    l = float('-inf')
    # Your code: compute the mse for this case
    estimate = compute_distance_with_sensor_and_obj_loc(estimated_sensor_loc, obj_loc)
    mse = mse + np.mean(np.sum(estimate, axis = 1))

mse = mse / 100

print('The MSE for Case 2 (if we knew mu is [300,300]) is {}'.format(mse))

###############################
######### Problem 5 ###########
###############################

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

# Gradient descent optimization
# The learning rate is specified by eta
class GDOptimizer(object):
    def __init__(self, eta):
        self.eta = eta

    def initialize(self, layers):
        pass

    # This function performs one gradient descent step
    # layers is a list of dense layers in the network
    # g is a list of gradients going into each layer before the nonlinear activation
    # a is a list of of the activations of each node in the previous layer going
    #
    def update(self, layers, g, a):
        m = a[0].shape[1]
        for layer, curGrad, curA in zip(layers, g, a):

            ############################
            ########## PART F ##########
            ############################

            ########################################################################################
            # Compute the gradients for layer.W and layer.b using the gradient for the output of the
            # layer curA and the gradient of the output curGrad
            # Use the gradients to update the weight and the bias for the layer
            #
            # Normalize the learning rate by m (defined above), the number of training examples input
            # (in parallel) to the network.
            #
            # It may help to think about how you would calculate the update if we input just one
            # training example at a time; then compute a mean over these individual update values.
            # ######################################################################################

            WeightUpdate = curGrad @ curA.T
            BiasUpdate = np.sum(curGrad, axis=1).reshape(layer.b.shape)
            layer.updateWeights(-self.eta/m * WeightUpdate)
            layer.updateBias(-self.eta/m * BiasUpdate)

# Cost function used to compute prediction errors
class QuadraticCost(object):

    # Compute the squared error between the prediction yp and the observation y
    # This method should compute the cost per element such that the output is the
    # same shape as y and yp
    @staticmethod
    def fx(y,yp):

        ############################
        ########## PART B ##########
        ############################

        return 0.5 * (y - yp) ** 2

    # Derivative of the cost function with respect to yp
    @staticmethod
    def dx(y,yp):

        ############################
        ########## PART B ##########
        ############################

        return yp - y

# Sigmoid function fully implemented as an example
class SigmoidActivation(object):
    @staticmethod
    def fx(z):
        return 1 / (1 + np.exp(-z))

    @staticmethod
    def dx(z):
        return SigmoidActivation.fx(z) * (1 - SigmoidActivation.fx(z))

# Hyperbolic tangent function
class TanhActivation(object):

    # Compute tanh for each element in the input z
    @staticmethod
    def fx(z):

        ############################
        ########## PART C ##########
        ############################

        return np.tanh(z)

    # Compute the derivative of the tanh function with respect to z
    @staticmethod
    def dx(z):

        ############################
        ########## PART C ##########
        ############################

        return 1 - np.tanh(z) ** 2

# Rectified linear unit
class ReLUActivation(object):
    @staticmethod
    def fx(z):

        ############################
        ########## PART C ##########
        ############################

        return np.where(z < 0, 0, z)

    @staticmethod
    def dx(z):

        ############################
        ########## PART C ##########
        ############################

        return np.where(z <= 0, 0, 1)

# Linear activation
class LinearActivation(object):
    @staticmethod
    def fx(z):

        ############################
        ########## PART C ##########
        ############################

        return z

    @staticmethod
    def dx(z):

        ############################
        ########## PART C ##########
        ############################

        return np.ones(z.shape)

# This class represents a single hidden or output layer in the neural network
class DenseLayer(object):

    # numNodes: number of hidden units in the layer
    # activation: the activation function to use in this layer
    def __init__(self, numNodes, activation):
        self.numNodes = numNodes
        self.activation = activation

    def getNumNodes(self):
        return self.numNodes

    # Initialize the weight matrix of this layer based on the size of the matrix W
    def initialize(self, fanIn, scale=1.0):
        s = scale * np.sqrt(6.0 / (self.numNodes + fanIn))
        self.W = np.random.normal(0, s,
                                   (self.numNodes,fanIn))
        self.b = np.random.uniform(-1,1,(self.numNodes,1))

    # Apply the activation function of the layer on the input z
    def a(self, z):
        return self.activation.fx(z)

    # Compute the linear part of the layer
    # The input a is an n x k matrix where n is the number of samples
    # and k is the dimension of the previous layer (or the input to the network)
    def z(self, a):
        return self.W.dot(a) + self.b # Note, this is implemented where we assume a is k x n

    # Compute the derivative of the layer's activation function with respect to z
    # where z is the output of the above function.
    # This derivative does not contain the derivative of the matrix multiplication
    # in the layer.  That part is computed below in the model class.
    def dx(self, z):
        return self.activation.dx(z)

    # Update the weights of the layer by adding dW to the weights
    def updateWeights(self, dW):
        self.W = self.W + dW

    # Update the bias of the layer by adding db to the bias
    def updateBias(self, db):
        self.b = self.b + db

# This class handles stacking layers together to form the completed neural network
class Model(object):

    # inputSize: the dimension of the inputs that go into the network
    def __init__(self, inputSize):
        self.layers = []
        self.inputSize = inputSize

    # Add a layer to the end of the network
    def addLayer(self, layer):
        self.layers.append(layer)

    # Get the output size of the layer at the given index
    def getLayerSize(self, index):
        if index >= len(self.layers):
            return self.layers[-1].getNumNodes()
        elif index < 0:
            return self.inputSize
        else:
            return self.layers[index].getNumNodes()

    # Initialize the weights of all of the layers in the network and set the cost
    # function to use for optimization
    def initialize(self, cost, initializeLayers=True):
        self.cost = cost
        if initializeLayers:
            for i in range(0,len(self.layers)):
                if i == len(self.layers) - 1:
                    self.layers[i].initialize(self.getLayerSize(i-1))
                else:
                    self.layers[i].initialize(self.getLayerSize(i-1))

    # Compute the output of the network given some input a
    # The matrix a has shape n x k where n is the number of samples and
    # k is the dimension
    # This function returns
    # yp - the output of the network
    # a - a list of inputs for each layer of the newtork where
    #     a[i] is the input to layer i
    #     (note this does not include the network output!)
    # z - a list of values for each layer after evaluating layer.z(a) but
    #     before evaluating the nonlinear function for the layer
    def evaluate(self, x):
        curA = x.T
        a = [curA]
        z = []
        for layer in self.layers:
            z.append(layer.z(curA))
            curA = layer.a(z[-1])
            a.append(curA)
        yp = a.pop()
        return yp, a, z

    # Compute the output of the network given some input a
    # The matrix a has shape n x k where n is the number of samples and
    # k is the dimension
    def predict(self, a):
        a,_,_ = self.evaluate(a)
        return a.T

    # Computes the gradients at each layer. y is the true labels, yp is the
    # predicted labels, and z is a list of the intermediate values in each
    # layer. Returns the gradients and the forward pass outputs (per layer).
    #
    # In particular, we compute dMSE/dz_i. The reasoning behind this is that
    # in the update function for the optimizer, we do not give it the z values
    # we compute from evaluating the network.
    def compute_grad(self, x, y):
        # Feed forward, computing outputs of each layer and
        # intermediate outputs before the non-linearities
        yp, a, z = self.evaluate(x)

        # d represents (dMSE / da_i) that you derive in part (e);
        #   it is inialized here to be (dMSE / dyp)
        d = self.cost.dx(y.T, yp)
        grad = []

        # Backpropogate the error
        for layer, curZ in zip(reversed(self.layers),reversed(z)):

            ############################
            ########## PART D ##########
            ############################

            # Compute the gradient of the output of each layer with respect to the error
            # grad[i] should correspond with the gradient of the output of layer i
            #   before the activation is applied (dMSE / dz_i); be sure values are stored
            #   in the correct ordering!
            grad.insert(0, d * layer.dx(curZ))
            d = layer.W.T @ grad[0]

        return grad, a

    # Computes the gradients at each layer. y is the true labels, yp is the
    # predicted labels, and z is a list of the intermediate values in each
    # layer. Uses numerical derivatives to solve rather than symbolic derivatives.
    # Returns the gradients and the forward pass outputs (per layer).
    #
    # In particular, we compute dMSE/dz_i. The reasoning behind this is that
    # in the update function for the optimizer, we do not give it the z values
    # we compute from evaluating the network.
    def numerical_grad(self, x, y, delta=1e-4):

        # computes the loss function output when starting from the ith layer
        # and inputting z_i
        def compute_cost_from_layer(layer_i, z_i):
            cost = self.layers[layer_i].a(z_i)
            for layer in self.layers[layer_i+1:]:
                cost = layer.a(layer.z(cost))
            return self.cost.fx(y.T, cost)

        # numerically computes the gradient of the error with respect to z_i
        def compute_grad_from_layer(layer_i, inp):
            mask = np.zeros(self.layers[layer_i].b.shape)
            grad_z = []
            # iterate to compute gradient of each variable in z_i, one at a time
            for i in range(mask.shape[0]):
                mask[i] = 1
                delta_p_output = compute_cost_from_layer(layer_i, inp+mask*delta)
                delta_n_output = compute_cost_from_layer(layer_i, inp-mask*delta)
                grad_z.append((delta_p_output - delta_n_output) / (2 * delta))
                mask[i] = 0;

            return np.vstack(grad_z)

        _, a, _ = self.evaluate(x)

        grad = []
        i = 0
        curA = x.T
        for layer in self.layers:
            curA = layer.z(curA)
            grad.append(compute_grad_from_layer(i, curA))
            curA = layer.a(curA)
            i += 1

        return grad, a

    # Train the network given the inputs x and the corresponding observations y
    # The network should be trained for numEpochs iterations using the supplied
    # optimizer
    def train(self, x, y, numEpochs, optimizer):

        # Initialize some stuff
        n = x.shape[0]
        x = x.copy()
        y = y.copy()
        hist = []
        optimizer.initialize(self.layers)

        # Run for the specified number of epochs
        for epoch in range(0,numEpochs):

            # Compute the gradients
            grad, a = self.compute_grad(x, y)

            # Update the network weights
            optimizer.update(self.layers, grad, a)

            # Compute the error at the end of the epoch
            yh = self.predict(x)
            C = self.cost.fx(y, yh)
            C = np.mean(C)
            hist.append(C)
        return hist

    ############################
    ########## PART J ##########
    ############################

    def trainBatch(self, x, y, batchSize, numEpochs, optimizer):
        hist = []
        for epoch in np.arange(0,numEpochs):
            e = []
            for i in range(0, x.shape[0], batchSize):
                end = min(i+batchSize, x.shape[0])
                batchx = x[i:end, :]
                batchy = y[i:end, :]
                e = e + self.train(batchx, batchy, 1, optimizer)
            hist.append(np.mean(e))
        return hist

if __name__ == '__main__':
    # switch these statements to True to run the code for the corresponding parts
    # PART E
    DEBUG_MODEL = False
    # Part G
    BASE_MODEL = False
    # Part H
    DIFF_SIZES = False
    # Part I
    RIDGE = False
    # Part J
    SGD = False
    # Part K
    PARTk = False

    # Generate the training set
    np.random.seed(9001)
    x=np.random.uniform(-np.pi,np.pi,(1000,1))
    y=np.sin(x)
    xLin=np.linspace(-np.pi,np.pi,250).reshape((-1,1))
    yHats = {}

    activations = dict(ReLU=ReLUActivation,
                       tanh=TanhActivation,
                       linear=LinearActivation)
    lr = dict(ReLU=0.02,tanh=0.02,linear=0.005)
    names = ['ReLU','linear','tanh']

    #### PART F ####
    if DEBUG_MODEL:
        print('Debugging gradients..')
        # Build the model
        activation = activations["ReLU"]
        model = Model(x.shape[1])
        model.addLayer(DenseLayer(10,activation()))
        model.addLayer(DenseLayer(10,activation()))
        model.addLayer(DenseLayer(1,LinearActivation()))
        model.initialize(QuadraticCost())

        grad, _ = model.compute_grad(x, y)
        n_grad, _ = model.numerical_grad(x, y)
        for i in range(len(grad)):
            print('squared difference of layer %d:' % i, np.linalg.norm(grad[i] - n_grad[i]))

    #### PART G ####
    if BASE_MODEL:
        print('\n----------------------------------------\n')
        print('Standard fully connected network')
        for key in names:
            # Build the model
            activation = activations[key]
            model = Model(x.shape[1])
            model.addLayer(DenseLayer(100,activation()))
            model.addLayer(DenseLayer(100,activation()))
            model.addLayer(DenseLayer(1,LinearActivation()))
            model.initialize(QuadraticCost())

            # Train the model and display the results
            hist = model.train(x,y,500,GDOptimizer(eta=lr[key]))
            yHat = model.predict(x)
            yHats[key] = model.predict(xLin)
            error = np.mean(np.square(yHat - y))/2
            print(key+' MSE: '+str(error))
            plt.plot(hist)
            plt.title(key+' Learning curve')
            plt.show()

        # Plot the approximations
        font = {'family' : 'DejaVu Sans',
            'weight' : 'bold',
                'size'   : 12}
        matplotlib.rc('font', **font)
        y = np.sin(xLin)
        for key in activations:
            plt.plot(xLin,y)
            plt.plot(xLin,yHats[key])
            plt.title(key+' approximation')
            plt.savefig(key+'-approx.png')
            plt.show()

    # Train with different sized networks
    #### PART H ####
    if DIFF_SIZES:
        print('\n----------------------------------------\n')
        print('Training with various sized network')
        names = ['ReLU', 'tanh']
        sizes = [5,10,25,50]
        widths = [1,2,3]
        errors = {}
        y = np.sin(x)
        for key in names:
            error = []
            for width in widths:
                for size in sizes:
                    activation = activations[key]
                    model = Model(x.shape[1])
                    for _ in range(width):
                        model.addLayer(DenseLayer(size,activation()))
                    model.addLayer(DenseLayer(1,LinearActivation()))
                    model.initialize(QuadraticCost())
                    hist = model.train(x,y,500,GDOptimizer(eta=lr[key]))
                    yHat = model.predict(x)
                    yHats[key] = model.predict(xLin)
                    e = np.mean(np.square(yHat - y))/2
                    error.append(e)
            errors[key] = np.asarray(error).reshape((len(widths),len(sizes)))

        # Print the results
        for key in names:
            error = errors[key]
            print(key+' MSE Error')
            header = '{:^8}'
            for _ in range(len(sizes)):
                header += ' {:^8}'
            headerText = ['Layers'] + [str(s)+' nodes' for s in sizes]
            print(header.format(*headerText))
            for width,row in zip(widths,error):
                text = '{:>8}'
                for _ in range(len(row)):
                    text += ' {:<8}'
                rowText = [str(width)] + ['{0:.5f}'.format(r) for r in row]
                print(text.format(*rowText))

    # Perform ridge regression on the last layer of the network
    #### PART I ####
    if RIDGE:
        print('\n----------------------------------------\n')
        print('Running ridge regression on last layer')
        from sklearn.linear_model import Ridge
        errors = {}
        for key in names:
            error = []
            sizes = [5,10,25,50]
            widths = [1,2,3]
            for width in widths:
                for size in sizes:
                    activation = activations[key]
                    model = Model(x.shape[1])
                    for _ in range(width):
                        model.addLayer(DenseLayer(size,activation()))
                    model.initialize(QuadraticCost())
                    ridge = Ridge(alpha=0.1)
                    X = model.predict(x)
                    ridge.fit(X,y)
                    yHat = ridge.predict(X)
                    e = np.mean(np.square(yHat - y))/2
                    error.append(e)
            errors[key] = np.asarray(error).reshape((len(widths),len(sizes)))

        # Print the results
        for key in names:
            error = errors[key]
            print(key+' MSE Error')
            header = '{:^8}'
            for _ in range(len(sizes)):
                header += ' {:^8}'
            headerText = ['Layers'] + [str(s)+' nodes' for s in sizes]
            print(header.format(*headerText))
            for width,row in zip(widths,error):
                text = '{:>8}'
                for _ in range(len(row)):
                    text += ' {:<8}'
                rowText = [str(width)] + ['{0:.5f}'.format(r) for r in row]
                print(text.format(*rowText))

        # Plot the results
        for key in names:
            for width,row in zip(widths,errors[key]):
                layer = ' layers'
                if width == 1:
                    layer = ' layer'
                plt.semilogy(row,label=str(width)+layer)
            plt.title('MSE for ridge regression with '+key+' activation')
            plt.xticks(range(len(sizes)),sizes)
            plt.xlabel('Layer size')
            plt.ylabel('MSE')
            plt.legend()
            plt.savefig(key+'-ridge.png')
            plt.show()

    ############################
    ########## PART J ##########
    ############################

    if SGD:
        print('\n----------------------------------------\n')
        print('Running SGD')
        batchSizes = [1, 10, 20, 50, 100]
        for key in names:
            for batchSize in batchSizes:
                activation = activation[key]
                model = Model(x.shape[1])
                model.addLayer(DenseLayer(100,activation()))
                model.addLayer(DenseLayer(100,activation()))
                model.addLayer(DenseLayer(1,LinearActivation()))
                model.initialize(QuadraticCost())
                epoch = 25 * batchSize
                hist = model.trainBatch(x, y, batchSize, epoch, GDOptimizer(eta=lr[key]))
                yHat = model.predict(x)
                yHats[key] = model.predict(xLin)
                err = np.mean(np.square(yHat - y))/2
                print(key, 'batchsize:', batchSize, 'MSE:', err)

    ############################
    ########## PART K ##########
    ############################

    if PARTk:
        print('\n----------------------------------------\n')
        print('Running Bonus Question')
        # we only use ReLU in this question
        activation = activations['ReLU']
        model = Model(x.shape[1])
        model.addLayer(DenseLayer(100,activation()))
        model.addLayer(DenseLayer(100,activation()))
        model.addLayer(DenseLayer(1,LinearActivation()))
        model.initialize(QuadraticCost())

        # Train the model and display the results
        hist = model.train(x,y,100,GDOptimizer(eta=0.01))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('100 max epochs, 0.01 LR'+' MSE: '+str(error))

        hist = model.train(x,y,100,GDOptimizer(eta=0.05))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('100 max epochs, 0.05 LR'+' MSE: '+str(error))

        hist = model.train(x,y,100,GDOptimizer(eta=0.1))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('100 max epochs, 0.1 LR'+' MSE: '+str(error))

        hist = model.train(x,y,200,GDOptimizer(eta=0.01))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('200 max epochs, 0.01 LR'+' MSE: '+str(error))

        hist = model.train(x,y,200,GDOptimizer(eta=0.05))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('200 max epochs, 0.05 LR'+' MSE: '+str(error))

        hist = model.train(x,y,200,GDOptimizer(eta=0.1))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('200 max epochs, 0.1 LR'+' MSE: '+str(error))

        hist = model.train(x,y,500,GDOptimizer(eta=0.01))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('500 max epochs, 0.01 LR'+' MSE: '+str(error))

        hist = model.train(x,y,500,GDOptimizer(eta=0.05))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('500 max epochs, 0.05 LR'+' MSE: '+str(error))

        hist = model.train(x,y,100,GDOptimizer(eta=0.1))
        yHat = model.predict(x)
        error = np.mean(np.square(yHat - y))/2
        print('500 max epochs, 0.1 LR'+' MSE: '+str(error))