from __future__ import annotations
import numpy as np
from numpy.linalg import inv, det, slogdet

# Implemented in Ex1

class UnivariateGaussian:
    """
    Class for univariate Gaussian Distribution Estimator
    """
    def __init__(self, biased_var: bool = False) -> UnivariateGaussian:
        """
        Estimator for univariate Gaussian mean and variance parameters

        Parameters
        ----------
        biased_var : bool, default=False
            Should fitted estimator of variance be a biased or unbiased estimator

        Attributes
        ----------
        fitted_ : bool
            Initialized as false indicating current estimator instance has not been fitted.
            To be set as True in `UnivariateGaussian.fit` function.

        mu_: float
            Estimated expectation initialized as None. To be set in `UnivariateGaussian.fit`
            function.

        var_: float
            Estimated variance initialized as None. To be set in `UnivariateGaussian.fit`
            function.
        """
        self.biased_ = biased_var
        self.fitted_, self.mu_, self.var_ = False, None, None

    def fit(self, X: np.ndarray) -> UnivariateGaussian:
        """
        Estimate Gaussian expectation and variance from given samples

        Parameters
        ----------
        X: ndarray of shape (n_samples, )
            Training data

        Returns
        -------
        self : returns an instance of self.

        Notes
        -----
        Sets `self.mu_`, `self.var_` attributes according to calculated estimation (where
        estimator is either biased or unbiased). Then sets `self.fitted_` attribute to `True`
        """

        self.mu_ = 1/len(X) * np.sum(X)
        if self.biased_ :
            self.var_ = 1/len(X) * np.sum( (X - self.mu_) **2 )
        else:
            self.var_ = 1/(len(X)-1) * np.sum( (X - self.mu_) **2 )

        self.fitted_ = True
        return self

    def pdf(self, X: np.ndarray) -> np.ndarray:
        """
        Calculate PDF of observations under Gaussian model with fitted estimators

        Parameters
        ----------
        X: ndarray of shape (n_samples, )
            Samples to calculate PDF for

        Returns
        -------
        pdfs: ndarray of shape (n_samples, )
            Calculated values of given samples for PDF function of N(mu_, var_)

        Raises
        ------
        ValueError: In case function was called prior fitting the model
        """
        if not self.fitted_:
            raise ValueError("Estimator must first be fitted before calling `pdf` function")
        stddiv = np.sqrt(self.var_)
        pdf = 1 / ( np.sqrt(2 * np.pi) * stddiv ) * \
                    np.exp( - ( X - self.mu_) ** 2 / (2 * stddiv ** 2) )

        return pdf

        # The full graph of a PDF:
        #sigma = np.sqrt(self.var_)
        #bins = round(np.sqrt(len(X)))
        #range_tuple = (self.mu_ - 5*sigma , self.mu_ + 5*sigma )
        #hist, bins_edges = np.histogram(X, bins=bins, range=range_tuple )
        #pdf = hist / len(X) * bins / (range_tuple[1] - range_tuple[0])
        #print("integral pdf samples "+str(sum(pdf) * (bins_edges[1]-bins_edges[0]) ) )

    @staticmethod
    def log_likelihood(mu: float, sigma: float, X: np.ndarray) -> float:
        """
        Calculate the log-likelihood of the data under a specified Gaussian model

        Parameters
        ----------
        mu : float
            Expectation of Gaussian
        sigma : float
            Variance of Gaussian
        X : ndarray of shape (n_samples, )
            Samples to calculate log-likelihood with

        Returns
        -------
        log_likelihood: float
            log-likelihood calculated
        """
        Xmm = X - mu
        m = len(X)
        loglike = m * ( - 0.5*np.log(2*np.pi) - np.log(sigma) ) - np.sum( Xmm * Xmm ) / 2*sigma**2

        return loglike

class MultivariateGaussian:
    """
    Class for multivariate Gaussian Distribution Estimator
    """
    def __init__(self):
        """
        Initialize an instance of multivariate Gaussian estimator

        Attributes
        ----------
        fitted_ : bool
            Initialized as false indicating current estimator instance has not been fitted.
            To be set as True in `MultivariateGaussian.fit` function.

        mu_: ndarray of shape (n_features,)
            Estimated expectation initialized as None. To be set in `MultivariateGaussian.fit`
            function.

        cov_: ndarray of shape (n_features, n_features)
            Estimated covariance initialized as None. To be set in `MultivariateGaussian.fit`
            function.
        """
        self.mu_, self.cov_ = None, None
        self.fitted_ = False

    def fit(self, X: np.ndarray) -> MultivariateGaussian:
        """
        Estimate Gaussian expectation and covariance from given samples

        Parameters
        ----------
        X: ndarray of shape (n_samples, n_features)
            Training data

        Returns
        -------
        self : returns an instance of self

        Notes
        -----
        Sets `self.mu_`, `self.cov_` attributes according to calculated estimation.
        Then sets `self.fitted_` attribute to `True`
        """

        self.mu_ =  1/np.size(X, axis=0) * np.sum(X, axis=0)

        # make a 2D array of mu (of each feature) per sample
        mu2d = [self.mu_] * np.size(X, axis=0)
        # X minus mu (expected values) to have only offsets from expected estimated value
        Xmm = X - mu2d
        # make 3D array having the new axis (num 2) in size of axis 1 (also size of mu)
        X3mm = np.repeat( Xmm[:,:,np.newaxis], np.size(X,1), axis=2 )
        X3mmT = np.transpose(X3mm, axes=(0,2,1))

        # The "in-place" X3mm * X3mmT will result in M[i,j,smp] = f_i*f_j of the sample
        # so averging along the 0 axis will give the Covariance of x_i with x_j
        self.cov_ = 1/(np.size(X,0)-1) * np.sum( X3mm * X3mmT  ,axis=0)

        self.fitted_ = True
        return self

    def pdf(self, X: np.ndarray):
        """
        Calculate PDF of observations under Gaussian model with fitted estimators

        Parameters
        ----------
        X: ndarray of shape (n_samples, n_features)
            Samples to calculate PDF for

        Returns
        -------
        pdfs: ndarray of shape (n_samples, )
            Calculated values of given samples for PDF function of N(mu_, cov_)

        Raises
        ------
        ValueError: In case function was called prior fitting the model
        """
        if not self.fitted_:
            raise ValueError("Estimator must first be fitted before calling `pdf` function")

        # make a 2D array of mu (of each feature) per sample
        mu2d = [self.mu_] * np.size(X, axis=0)
        # X minus mu (expected values) to have only offsets from expected estimated value
        Xmm = X - mu2d

        samples_pdf = []
        for smp in Xmm:
            pdf = 1 / ( np.sqrt(2 * np.pi) * np.linalg.det(self.cov_) ) * \
                        np.exp( -0.5 * smp @ self.cov_ @ smp.T )
            samples_pdf.append(pdf)

        return np.array(samples_pdf)

    @staticmethod
    def log_likelihood(mu: np.ndarray, cov: np.ndarray, X: np.ndarray) -> float:
        """
        Calculate the log-likelihood of the data under a specified Gaussian model

        Parameters
        ----------
        mu : ndarray of shape (n_features,)
            Expectation of Gaussian
        cov : ndarray of shape (n_features, n_features)
            covariance matrix of Gaussian
        X : ndarray of shape (n_samples, n_features)
            Samples to calculate log-likelihood with

        Returns
        -------
        log_likelihood: float
            log-likelihood calculated over all input data and under given parameters of Gaussian
        """

        # make a 2D array of mu (of each feature) per sample
        mu2d = [mu] * np.size(X, axis=0)
        # X minus mu (expected values) to have only offsets from expected estimated value
        Xmm = X - mu2d

        _, logdet_cov = np.linalg.slogdet(cov)
        loglike_coeff = - 0.5*np.log(2*np.pi) - logdet_cov
        accumulate = 0.0
        if False : # The clear but heavy computation method
            for smp in Xmm:
                loglike = loglike_coeff - 0.5 * smp @ np.linalg.inv(cov) @ smp.T
                accumulate = accumulate + loglike
        else: # cumputaion effective calculation
            # per sample feature set we have the multiple by the cov-matrix and then
            # each vector (alone) is multipled by the features vector again.
            accumulate =  len(X) * loglike_coeff - 0.5 * np.sum( (Xmm @ cov) * Xmm ) # like diag(Xmm @ cov @ Xmm)
            # Note the sum is both to complete the last inner-product (on axis 1)
            # and also summing along all samples (on axis 0)

        return accumulate


'''
08 - Calculations with .groupby()

The .groupby() method makes life  much  easier. In  this  exercise,
you'll perform the same calculations  as last time,  except  you'll
use the .groupby() method. You'll also perform calculations on data
grouped by two variables to see if sales differ by store type depending
on if it's a holiday week or not.

Instructions:

 - Group sales by "type", take the sum of "weekly_sales", and store as
   sales_by_type.

 - Calculate the proportion of sales at each store type by dividing by
   the sum of sales_by_type. Assign to sales_propn_by_type.

 - Group sales by "type" and "is_holiday", take the sum of weekly_sales,
   and store as sales_by_type_is_holiday.

-----------------------------------------------------------------------------------------------------------------
sales.head()

   store type  department       date  weekly_sales  is_holiday  temperature_c  fuel_price_usd_per_l  unemployment
0      1    A           1 2010-02-05      24924.50       False          5.728                 0.679         8.106
1      1    A           1 2010-03-05      21827.90       False          8.056                 0.693         8.106
2      1    A           1 2010-04-02      57258.43       False         16.817                 0.718         7.808
3      1    A           1 2010-05-07      17413.94       False         22.528                 0.749         7.808
4      1    A           1 2010-06-04      17558.09       False         27.050                 0.715         7.808
-----------------------------------------------------------------------------------------------------------------
'''
# Import pandas and numpy
import numpy as np
import pandas as pd

sales = pd.read_csv('content/wallmart_sales.csv')

# Group by type; calc total weekly sales
sales_by_type = sales.groupby("type")["weekly_sales"].sum()

# Get proportion for each type
sales_propn_by_type = sales_by_type / sum(sales["weekly_sales"])
print(sales_propn_by_type)

# Group by type and is_holiday; calc total weekly sales
sales_by_type_is_holiday = sales.groupby(['type', 'is_holiday'])[weekly_sales'].sum()
print(sales_by_type_is_holiday)

04_Data Manipulation with pandas/02_Aggregating Data/08_Calculations with .groupby().py


#numpy dizileri üzerinde vektörel işlemler  :işleme girerken karşılıklı elemanlar işleme giriyor
#her elemana aynı skaler işlemleri yap ak istersem np array ierisinde işlem yaptırmalıyım

#numpy mantığı:veri setini al sütunlar feature olsun satırlar item olsun bu şekilde analiz et. ####

import numpy as np
import matplotlib.pyplot as plt
#her matris
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
b = np.array([[10], [20], [30]])

c = a + b
print(c)

#matris çarpımı yapmak istersem dot fonksiyonunu kullanmalıyım
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
b = np.array([[1], [2], [3]])
c = np.dot(a, b)
print(c)

#broadcasting islemi ile satır satır toplama satır ve sütun uyumuna bakmadan işlem yapılabilir.satırlar işlem yaptırılıyor
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
b = np.array([10, 20, 30])
c = a + b
print(c)

#yukarıdaki örneğe benzemesine rağmen farklı bir output oluşturur:sütunlar işleme sokuluyor
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
b = np.array([[10], [20], [30]])
c = a + b
print(c)

#linspace fonksiyonu ile grafik çizdirme
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-6.28, 6.28, 100) #100'er tane x ve y noktası elde edip grafik çizdireceğim
y = np.sin(x)
plt.plot(x, y) #grafiği shown etme işlemi yapıyorum

#x**2 -4 parabolünü çizdirmek istersem
x = np.linspace(-10, 10, 100)
y = x ** 2 - 4
plt.plot(x, y)

#max fonksiyonu ile en büyük elemana erişmek istersem
import numpy as np
a = np.array([[23, 4, 5], [17, 7, 8], [9, 4, 4]])
result = np.max(a)
print(result) #23 ekran çıktısı oluşuyor

#bir nparrayde ve bir sutunda en büyük değeri bulmak istersem
a = np.array([[23, 4, 5], [17, 7, 8], [9, 15, 4]])
print(a)
print('-----------')
result = np.max(a, axis=0) #sütunlardaki en büyük değerleri buluyor
result2 = np.max(a, axis=1) #satırlardaki en büyük değeri bulmak istersem
print(result)
print(result2)

#sort fonksiyonu:view oluşturmaz
a = np.array([23, 4, 5, 17, 7, 8, 9, 4, 4])
b = np.sort(a) # a yı sort edip yeni bir b nesnesi oluşturuyor
print(b)
print('-' * 10)
a.sort() #a dizisini kendi icinde srt ediyor
print(a)

#satırları da kendi aralarında sort edebilirim
a = np.array([[23, 4, 5], [17, 7, 8], [9, 4, 4]])
b = np.sort(a, axis=1) #satır sorting islemi yaptım
print(b)

#argsort fonksiyonu:indisleri sıraya diziyor sortta ise elemanlar sıraya diziliyordu
a = np.array([23, 4, 5, 17])
b = np.argsort(a)
print(a)
print(b)

#argsort yardımıyla sorting yapma örneği:
a = np.array([23, 4, 5, 17])
b = np.argsort(a)
print(a)
print('-' * 10)
print(a[b])

#argmax fonksiyonu:
a = np.array([23, 4, 50, 17])
b = np.argmax(a)
print(a)
print(b)

#fromiter fonksiyonu:iterable bir nesneyi alıyor nparray elde ediyor np.array fonksiyonu ile  de bu işlem yapılabiliyordu fakat
#np.array fonksiyonuna sadece tuple,list gibi iterable nesneler veriliyor
b = np.fromiter((i * i for i in range(10)), dtype='float32')
print(b)

#asarray fonksiyonu:içeriği aynı kalsın dtype değişsin istersem kullanıyorum typecast işlemi yapmış gibi oluyorum
a = np.array([1, 2.9, 3.2, 4.3], dtype='float32')
b = np.asarray(a, dtype='int32')


import numpy as np
import math

import sys
from pathlib import Path

#############################################
root_path = Path(__file__).parents
sorting_  = str( root_path[1] ) + '/sorting/'

sys.path.append( sorting_ )
from quick_sort import *
#############################################

############################################################################################
# This is a class that collects statistical indexes such as mean, variance and permutations.
############################################################################################

class StatisticalIndexes(object):

	def __init__(self):
		super().__init__()

	def Ranker(self, x:list):

		x_ = quick_sort(x)
		n  = len(x)
		r  = []

		for i in range(n):
			for j in range(n):

				if x[i] == x_[j]:
					r.append(j + 1)
					break

		return r

	def Combination(self, n:int, k:int):

		num = math.factorial(n)
		den = math.factorial(k) * math.factorial(n - k)

		return num / den

	def Power(self, base:float, exp:int):

		'''
		...it would be easier to use the ** Python operator, but it would be less funny !
		'''

		if base == 0:
			return 0
		elif base == 1:
			return 1
		else:
			res = base
			for i in range(2, exp + 1):
				res *= base
			return res

	def Mean(self, x:list=None):

		return sum(x) / len(x)

	def SampleVariance(self, x:list=None):

		s_mean = self.Mean( x )
		num    = sum( [(j - s_mean)**2 for j in x] )
		den    = len(x) - 1

		return num / den

	def Variance(self, x:list=None):

		mean   = self.Mean( x )
		num    = sum( [(j - mean)**2 for j in x] )
		den    = len(x)

		return num / den

	def StandardDeviationSample(self, x:list):

		return self.SampleVariance(x) ** 0.5

	def StandardDeviation(self, x:list):

		return self.Variance(x) ** 0.5

	def Covariance(self, x:list, y:list):

		'''
		Attention: X and Y variables must have same array length.
		'''

		assert len(x) == len(y)

		mean_x = self.Mean(x)
		mean_y = self.Mean(y)

		den = len(x) - 1
		num = sum( [ (x[j] - mean_x) * (y[j] - mean_y) for j in range( den + 1 ) ] )

		return num / den


from numpy import argmax

# Works out multinomial coefficient nC(k1,k2,...,km)
def multiCoeff(n, kList):
	kMaxFirstPos = argmax(kList)
	kMax = kList[kMaxFirstPos]
	coefficient = 1
	i = kMax + 1
	# Perform repeated multiplication and division
	# for part of kList up to the maximum
	for k in kList[:kMaxFirstPos]:
		for j in range(1, k + 1):
			coefficient *= i
			coefficient //= j
			i += 1
	# Perform repeated multiplication and division
	# for part of kList after the maximum
	for k in kList[kMaxFirstPos + 1:]:
		for j in range(1, k + 1):
			coefficient *= i
			coefficient //= j
			i += 1
	return coefficient

# Works out coefficients of multinomial expansion
# given polynomial coefficents and power
def multiExpand(coeffsList, powersList, power):
	l = len(coeffsList)
	if l == 1:
		return [coeffsList[0] ** power], \
			[powersList[0] * power]
	if power == 0:
		return [1], [0]
	if power == 1:
		return coeffsList, powersList
	newCoeffs = []
	newPowers = []
	kList = [power]
	for i in range(l - 1):
		kList.append(0)
	newCoeff, newPower = multiCoeff(power, kList), 0
	for i in range(l):
		newCoeff *= (coeffsList[i] ** kList[i])
		newPower += (powersList[i] * kList[i])
	newCoeffs.append(newCoeff)
	newPowers.append(newPower)
	# kList should now look like [power, 0, ... 0]
	# (e.g. [5, 0, 0] for power = 5, l = 3)
	# Used to work out multinomial coefficients
	while kList[l-1] != power:
		# Update kList
		if kList[l-2] != 0:
			kList[l-2] -= 1
			kList[l-1] += 1
		else:
			i = l - 2
			while kList[i] == 0:
				i -= 1
			kList[i] -= 1
			kList[i+1] = kList[l-1] + 1
			kList[l-1] = 0
		# Update newCoeff, newPower
		newCoeff, newPower = \
			multiCoeff(power, kList), 0
		for i in range(l):
			newCoeff *= (coeffsList[i] ** kList[i])
			newPower += (powersList[i] * kList[i])
		ind = newPowers.index(newPower) \
			if newPower in newPowers else -1
		if ind > -1:
			newCoeffs[ind] += newCoeff
		else:
			newCoeffs.append(newCoeff)
			newPowers.append(newPower)
	# Remove terms with zero coefficients
	i = 0
	while i < len(newCoeffs):
		if newCoeffs[i] == 0:
			newCoeffs.pop(i)
			newPowers.pop(i)
		else:
			i += 1
	return newCoeffs, newPowers

# Works out Qsigma - Q^p
def QsMinQp(a, b, QpC, QpP, p):
	QpC.pop(0), QpP.pop(0)	   # First elements cancel
	minQpC = [-el for el in QpC]	# Take -Q^p coeffs
	# Merge middle term of Qsigma into QsigmaMinQp
	ind = QpP.index(p) if p in QpP else -1
	if ind > -1:
		minQpC[ind] += (a ** p)
	else:
		ind = 0
		while ind < len(QpP) and QpP[ind] > p:
			ind += 1
		if ind == len(QpP):
			minQpC.extend((a ** p, p))
		else:
			minQpC.insert(ind, a ** p)
			QpP.insert(ind, p)
	# Deal with last element of Qsigma
	ind = -1 if 0 in QpP else -2
	if ind > -2:
		minQpC[ind] += (b ** p)
	else:
		minQpC.extend((b ** p, 0))
	# Pop first terms if zero
	while minQpC[0] and minQpC[0] == 0:
		minQpC.pop(0), QpP.pop(0)
	# Pop last terms if zero
	while minQpC[-1] and QpP[-1] == 0:
		minQpC.pop(), QpP.pop()
	return minQpC, QpP

# Works out reduction of x^m in terms of powers of tau
# Coefficients containing powers of x capped at x^2
def xPowerExpand(a, b, power, rDict):
	p, r, tauCoeffs = power // 3, power % 3, []
	# No need to recalculate if this monomial
	# is already in the dictionary - just return
	if power in rDict:
		return rDict[power]
	# "Base case" of recursion
	elif p == 0:
		new = [0 for i in range(0, r)]
		new.append(1)
		tauCoeffs.append(new)
	else:
		# May be higher powers of x before reduction
		for i in range(p + 1):
			new = [0 for j in range(r)]
			tauCoeffs.append(new)
			for j in range(i + 1):
				# Coefficient of x^j on tau^(i-power)
				coeff = multiCoeff(p, [p-i, j, i-j]) \
					* ((-a)**j) * ((-b)**(i-j))
				tauCoeffs[i].append(coeff)
		# Working from coefficients on powers of tau
		# closest to zero (higher indices)
		i = p
		while len(tauCoeffs[i]) > 3 and i >= 0:
			# Working from coeffs on power of tau
			# that are higher powers of x
			j = i + r
			while j > 2:
				# Reduce this power of x by recursion
				nCoeffs = xPowerExpand(a, b, j, rDict)
				# Merge smaller expansion into main
				# expansion to progress reduction
				for k in range(len(nCoeffs)):
					for l in range(len(nCoeffs[k])):
						ind = i + k + 1 - len(nCoeffs)
						if len(tauCoeffs[ind]) > l:
							tauCoeffs[ind][l] += \
								tauCoeffs[i][j] * \
								nCoeffs[k][l]
						else:
							tauCoeffs[ind].append \
								(tauCoeffs[i][j] * \
								nCoeffs[k][l])
				# Remove high power of x from coeff
				# of tau term once reduction is done
				tauCoeffs[i].pop(j)
				j -= 1
			i -= 1
	rDict[power] = tauCoeffs
	return tauCoeffs

# Works out coefficients of polynomial in
# tau = y^-2 after reduction (x^3 = y^2 - ax - b)
def reducePoly(a, b, cList, pList, p, rDict):
	# Rewrite given polynomial in terms of tau
	# Reduce each power of x in turn and
	# combine into a single list of coefficients
	# Use first monomial's expansion as a base
	expn = xPowerExpand(a, b, pList[0], rDict)
	expnStr = expStr(expn, 0)
	print(f"Reduction of x^{pList[0]} = {expnStr}")
	# Make sure the correct multiple is used
	factor = cList[0]
	for i in range(0, len(expn)):
		expn[i] = [factor * el for el in expn[i]]
	# Then add multiples of other lists
	for i in range(1, len(pList)):
		factor = cList[i]
		tmp = xPowerExpand(a, b, pList[i], rDict)
		tmpStr = expStr(tmp, 0)
		print(f"Reduction of x^{pList[i]} = {tmpStr}")
		for j in range(len(tmp)):
			for k in range(len(tmp[j])):
				expn[j + len(expn) - len(tmp)][k] += \
					(tmp[j][k] * factor)
	return expn

def rXY(a, b, p):
	# reductionDict is a dictionary for tracking
	# monomials whose reductions are already computed
	reductionDict = {}
	# Arrays to represent the polynomial Q(x)
	coeffs, powers = [1, a, b], [3, 1, 0]
	# Step 1: find Qsigma - Q^p
	QpC, QpP = multiExpand(coeffs, powers, p)
	QsMinQpC, QsMinQpP = QsMinQp(a, b, QpC, QpP, p)
	QsMinQpStr = polyStr(QsMinQpC, QsMinQpP)
	print(f"(Qsigma - Q^p) = {QsMinQpStr}\n")
	# Step 2: rewrite Qsigma - Q^p in terms of tau
	expansion = reducePoly(a, b, \
		QsMinQpC, QsMinQpP, p, reductionDict)
	return expansion

# Coefficients on Q(x) = x^3 + ax + b; prime = p
a = int(input("Please enter a value for (a) in " + \
	"x^3 + ax + b. "))
b = int(input("Please also enter a value for (b). "))
p = int(input("Finally, please enter a prime p " + \
	"to determine the field. "))

# Set up arrays to represent Q as a polynomial
coeffs, powers = [1, a, b], [3, 1, 0]
print("\nThe polynomial entered was " + \
	f"{polyStr(coeffs, powers)}.\n")

# Output results
rXY_str = expStr(rXY(a, b, p), p)
print(f"\nR(x,y) = {rXY_str}")


# -*- coding: utf-8 -*-
"""
Created on Wed Mar 23 01:12:47 2016
@author: Wang Ziang
"""

"""
This function solves a system of first-order differential equations
(or A first-order differential equation, of course) of the form:

-----------------------------------------------

                X'[t] = F[t,X]

-----------------------------------------------

    where  X = (x1,x2,...,xn)T
    F:  X -> Y
        (x1,x2,...,xn)T -> (y1,y2,...,yn)T
        where:
        y1 = f1(t,x1,x2,...,xn),
        y1 = f1(t,x1,x2,...,xn),
        .
        .
        yn = f1(t,x1,x2,...,xn)
"""

'''
in the following method:
xi could be a scalar, or a vector
    if xi is a vector,
    it should be given by the data structure "array" defined in numpy package
    e.g. numpy.array([x1,x2,x3])
the f is the RHS-function in X'[t] == f(X,t)
    f: ti,xi -> xii
    f should be defined in the form
        define f(ti, Xi):
            return a vector with the same size as Xi
        or given by a equivalent lambda expression
with fixed step size h, and a given f,
the followingMethod is a iterative function
    followingMethod: state ->state
                     (ti,xi) -> (tii,xii)
'''
import numpy as np
array = np.array
from nest import nestList
from nest import nestUntilList
import math

"""
the constructor and selector of a State:(t,X)
"""
def makeState(t, x): return [t,x]
def getX(state): return state[1]
def getT(state): return state[0]

"""
four methed in Runge-Kutta's family
"""

def forwardEulerMethod(state, f, h):
    ti = getT(state)
    xi = getX(state)
    tii = ti + h  # tii = Subscibe[t,i+1]
    xii = xi + h * f(ti,xi)  # xii = Subscibe[x,i+1]
    return makeState(tii,xii)

def explicitTrapezoidalMethod(state, f, h):
    ti = getT(state)
    xi = getX(state)
    tii = ti + h
    xii = xi + h/2.0 *(f(ti,xi) + f(ti+h, xi+h*f(ti,xi)))
    return makeState(tii,xii)

def midPointMethod(state, f, h):
    ti = getT(state)
    xi = getX(state)
    h = float(h)
    tii = ti + h
    xii = xi + h * f(ti+h/2, xi+h/2*f(ti,xi))
    return makeState(tii,xii)

def rk4Method(state, f, h):
    '''
    Classic fourth-order Runge-Kutta method
    '''
    ti = getT(state)
    xi = getX(state)
    h = float(h)
    tii = ti + h
    s1 = f(ti,xi)
    s2 = f(ti+h/2, xi+h/2*s1)
    s3 = f(ti+h/2, xi+h/2*s2)
    s4 = f(ti+h, xi+h*s3)
    xii = xi + h/6 * (s1 + 2*s2 + 2*s3 +s4)
    return makeState(tii,xii)

__methodDictionary={\
    "ForwardEuler":forwardEulerMethod,\
    "ExplicitTrapezoidal":explicitTrapezoidalMethod,\
    "Midpoint":midPointMethod,\
    "RK4":rk4Method\
}

'''-------------------------------------------------------------------------------'''

def normalize(initX):
    """
    normalize X,
    if array([X__]) ,-> array([X])
    if X_Number ,-> array([X])
    used in dSolve

    """
    if type(initX) == np.ndarray:
        return initX
    else:
        return array([initX])

def dSolve(func, x0, tMax, stepSize, method = "RK4"):
    """
    dSolve solves a system of first-order differential equations
    (or A first-order differential equation, of course) of the form:

    -----------------------------------------------

                    X'[t] = F[t,X]

    -----------------------------------------------
    (

        where  X = (x1,x2,...,xn)T
        F:  X -> Y
            (x1,x2,...,xn)T -> (y1,y2,...,yn)T
            where:
            y1 = f1(t,x1,x2,...,xn),
            y1 = f1(t,x1,x2,...,xn),
            .
            .
            yn = f1(t,x1,x2,...,xn)
    )

    with initial value X[0] =x0,
    (
        !!! if xi is a vector,
        it should be given by the data structure "array" defined in numpy package
        e.g. numpy.array([x1,x2,x3])
    )

    from t = 0 to t = tMax,

    with fixed step size = stepSize,

    using method selected from __methodDictionary

    Output: [state0, state1,... ]
        where statei has the form [ti, xi] for a ODE,
            or [ti, array([x1i,x2i...])] for s system of ODEs

    """

    x0 = normalize(x0)
    times = int(math.ceil(float(tMax) / stepSize))
    methodFunc = __methodDictionary[method]
    toNext = lambda state: methodFunc(state, func, stepSize)
    return nestList(toNext, makeState(0, x0), times)

def dSolveUntil(func, x0, stepSize, stopQ, method = "RK4", tMax = 1000):
    '''
    stopQ is about a state
    '''
    x0 = normalize(x0)
    maxStep = int(math.ceil(float(tMax) / stepSize))
    methodFunc = __methodDictionary[method]
    toNext = lambda state: methodFunc(state, func, stepSize)
    return nestUntilList(toNext, makeState(0, x0), stopQ, maxStep)

'''-------------------------------------------------------------------------------'''

def getColumn(table,i):
    "get the ith column of the table\
    without security "
    return [elem[i] for elem in table]

def getTList(result):
    "get the time list T of the result given by function dSolve\
    without security "
    return getColumn(result, 0)

def getXiList(result, i = 1):
    "get the list of i-th component Xi of the result given by function dSolve\
    without security "

    return getColumn( getColumn(result,1), i-1)
    '''
    if type(result[0][1]) == np.ndarray:
        return getColumn( getColumn(result,2), i-1)
        #return [elem[1][i-1] for elem in result]
    else:
        return getColumn(result,2)
        #return [elem[1] for elem in result]
    '''
def reduceStateList(result, includeList):
    def reduceState(state):
       return makeState(getT(state), array([getX(state)[i] for i in includeList]))
    return map(reduceState, result)

'''-------------------------------------------------------------------------------'''


# Import the necessary modules
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pboc_utils as pboc

# In this script, we will look at the distribution of polymer ell_range under
# some different types of regulation. We will first think of some cytoskeletal
# polymer growing in an unlimited pool of available monomers (tubulin dimers)
# and the case in which the shortening of the polymer is dependent on the total
# length.

# To begin, we'll define a set of parameters. We'll assume that the production
# rate and the depoymerization rate are independent of length. Our master
# equation becomes
#
#  P(l, t + dt) = P(l, t) + r * dt * P(l-1, t) + y * dt * P(l+1, t) - r * dt * P(l, t + dt) - gamma * dt * P(l, t + dt)
#
# where l is the length of the polymer, t is the time, dt is the timestep, r
# is the growth rate of the polymer, and y is the depolymerization rate.
r = 5  # addition rate in monomers per time
gamma = 6  # Degradation rate in monomers per time
dt = 1 / 50 # Time step for spreading.
tot_length = 100  # Maximum polymer length
tot_time = 1500   # Time to integrate in time steps

# Now we are ready to perform integration as we typically do.

# Set up the two-dimensional matrix of probabilities.
prob = np.zeros((tot_length + 1, tot_time))

# Set our initial condition.
prob[0, 0] = 1.0

# Start the integration.
for t in range(1, tot_time):
    for ell in range(tot_length):
        # Evaluate the master equation.
        prob[ell, t] = prob[ell, t-1] - r * dt * prob[ell, t-1] +\
            gamma * dt * prob[ell+1, t-1]

        # Now we can work out the boundary condition when ell is 0.
        if ell > 0:
            prob[ell, t] = prob[ell, t] + r * dt * prob[ell-1, t-1] - \
                            gamma * dt * prob[ell, t-1]

# Now we'll show the plot in three dimensions.
pboc.bar3(prob, xlabel='time (steps)', ylabel='polymer length in monomers',
          zlabel='P(l, t)', bin_step=50)
plt.show()

# We can see that we are approaching steady state and the distribution appears
# to be exponential. What is the mean?  From our master equation, we can reach
# an equation that describes the mean polymer length at steady state,
#
#   <ell> = (r/y)  / (1 - (r/y))
#
# We can compute the mean length of our polymers and compare it to the
# prediction.
ell_range = np.arange(0, tot_length + 1, 1)
ell_probabilities = ell_range * prob[:, -1]
mean_length = np.sum(ell_probabilities)
predicted_length = (r / gamma) / (1 - (r/gamma))
print('Our predicted length is ' + str(predicted_length) + '. Our computed mean length is ' + str(mean_length) + '.')

# We are actually pretty close, but not exactly correct. This could be due to
# numerical precision or maybe we aren't quite at setady state yet. However,
# this is pretty close. Let's now see how well the entire distribution agrees
# with our numerical integration. Remember, the entire distribution can be
# described by
#
#  P(l) = (r / y)**l * (1 - (r / y)).
#
predicted_distribution = (r / gamma)**ell_range * (1 - (r / gamma))
plt.figure()
plt.bar(ell_range, prob[:,-1])
plt.plot(ell_range, predicted_distribution, 'r-')
plt.xlabel('length in momonmers')
plt.ylabel('$P(l)$')
plt.show()

# That agrees quite well!

# Now, let's look at the case in which gamma is dependent on the length of the
# polymer. In essence, this means that we now can redfine gamma as some
# constant times the length of the polymer.
gamma_ell = 1/2
r = 1

# We'll reset our probability matrix and redo the integration.
prob = np.zeros((tot_length + 1, tot_time))

# We'll set our initial condition this time with long filaments.
prob[70, 0] = 1.0

# Now we just integrate.
for t in range(1, tot_time):
    for ell in range(tot_length):
        # Evaluate the master equation.
        prob[ell, t] = prob[ell, t-1] - r * dt * prob[ell, t-1] +\
            gamma_ell * (ell + 1) * dt * prob[ell+1, t-1]

        # Now we can work out the boundary condition when ell is 0.
        if ell > 0:
            prob[ell, t] = prob[ell, t] + r * dt * prob[ell-1, t-1] - \
                            gamma_ell * ell * dt * prob[ell, t-1]

# Now we'll show the plot in three dimensions.
pboc.bar3(prob, xlabel='time (steps)', ylabel='polymer length in monomers',
          zlabel='P(l, t)', bin_step=50)
plt.show()

# We can see that it now decreases in length to teh steady state value. As
# we did before, we can compare our computed mean length to the predicted
# value. For this model, the predicted mean length is
#
# <ell> = (r / gamma_ell)
#
length_prob = ell_range * prob[:, -1]
mean_length = np.sum(length_prob)
predicted_mean =  r / gamma_ell
print('Our predicted length is ' + str(predicted_mean) + '. Our computed length is ' + str(mean_length) + '.')

# Again, that agrees quite well. We can solve for the steady state distribution
# of polymer ell_range as
#
# P(l) = (r / y')^l * (1 / l!) * e^(-(r/y'))
#
predicted_dist = []
for ell in ell_range:
    prediction = (r / gamma_ell)**ell * (1 / np.math.factorial(ell)) *\
                  np.exp(-r/gamma_ell)
    predicted_dist.append(prediction)

# Let's plot it.
plt.figure()
plt.bar(ell_range, prob[:, -1])
plt.plot(ell_range, predicted_dist, 'r-')
plt.xlabel('polymer length in monomers')
plt.ylabel('$P(\ell)$')
plt.show()

# Again, it looks like we've nailed it.


import numpy as np
import matplotlib.pyplot as plt
prop_cycle = plt.rcParams['axes.prop_cycle']
colors = prop_cycle.by_key()['color']

class ConsumerClass():

    def __init__(self,**kwargs):

        # a. baseline setup

        # utility
        self.utility = 'cobb_douglas'
        self.alpha = 1
        self.beta = 1

        # prices and income
        self.p1 = 1
        self.p2 = 1
        self.I = 5

        # figures
        self.x1label = '$x_1$'
        self.x2label = '$x_2$'
        self.N = 100

        # b. update
        for key,val in kwargs.items():
            setattr(self,key,val)

        # c. calculations
        self.calculations()

    def calculations(self):

        # a. utility function
        if self.utility == 'cobb_douglas':
            self.u = lambda x1,x2,alpha,beta: x1**alpha*x2**beta
            self.indiff_func_x1 = lambda u0,x1,alpha,beta: (u0/x1**alpha)**(1/beta)
            self.indiff_func_x2 = lambda u0,x2,alpha,beta: (u0/x2**beta)**(1/alpha)
        elif self.utility == 'quasi_log':
            self.u = lambda x1,x2,alpha,beta: alpha*np.log(x1)+x2*beta
            self.indiff_func_x1 = lambda u0,x1,alpha,beta: (u0-alpha*np.log(x1))/beta
            self.indiff_func_x2 = lambda u0,x2,alpha,beta: np.exp((u0-x2*beta)/alpha)
        elif self.utility == 'concave':
            self.u = lambda x1,x2,alpha,beta: alpha*x1**2+beta*x2**2
            self.indiff_func_x1 = lambda u0,x1,alpha,beta: np.sqrt((u0-alpha*x1**2)/beta)
            self.indiff_func_x2 = lambda u0,x2,alpha,beta: np.sqrt((u0-beta*x2**2)/alpha)
        else:
            raise ValueError('unknown utility function')

        # b. figures
        self.x1max = 1.2*self.I/self.p1
        self.x2max = 1.2*self.I/self.p2

    def figure(self):

        fig = plt.figure(frameon=False,figsize=(6,6),dpi=100)
        ax = fig.add_subplot(1,1,1)

        ax.set_xlim([0,self.x1max])
        ax.set_ylim([0,self.x2max])
        ax.set_xlabel(self.x1label)
        ax.set_ylabel(self.x2label)

        fig.tight_layout()

        return fig,ax

    def legend(self,ax,**kwargs):

        kwargs.setdefault('loc','upper right')
        kwargs.setdefault('frameon',True)

        legend = ax.legend(**kwargs)
        frame = legend.get_frame()
        frame.set_facecolor('white')

    ##########
    # choice #
    ##########

    def maximize_utility(self,**kwargs):

        # a. update
        for key,val in kwargs.items():
            setattr(self,key,val)

        # b. solve
        if self.utility == 'cobb_douglas':
            self.x1_ast = self.alpha/(self.alpha+self.beta)*self.I/self.p1
            self.x2_ast = self.alpha/(self.alpha+self.beta)*self.I/self.p2
        elif self.utility == 'quasi_log':
            self.x1_ast = (self.alpha*self.p2)/(self.beta*self.p1)
            self.x2_ast = self.I-self.p1*self.x1_ast
            if self.x2_ast < 0:
                self.x1_ast = self.I/self.p1
                self.x2_ast = 0
        elif self.utility == 'concave':
            self.x1_ast = self.I/self.p1
            self.x2_ast = self.I/self.p2
            if self.x1_ast**self.alpha < self.x2_ast**self.beta:
                self.x1_ast = 0
            else:
                self.x2_ast = 0
        else:
            raise ValueError('unknown utility function')

        # c. utility
        self.u_ast = self.u(self.x1_ast,self.x2_ast,self.alpha,self.beta)

        # d. return
        return np.array([self.x1_ast,self.x2_ast,self.u_ast])

    def plot_max(self,ax,**kwargs):

        kwargs.setdefault('ls','')
        kwargs.setdefault('marker','*')
        kwargs.setdefault('markersize',7)
        kwargs.setdefault('color','black')
        kwargs.setdefault('label',f'$u({self.x1_ast:.2f},{self.x2_ast:.2f}) = {self.u_ast:.2f}$')

        ax.plot(self.x1_ast,self.x2_ast,**kwargs)

    def minimize_cost(self,u,**kwargs):

        # a. update
        for key,val in kwargs.items():
            setattr(self,key,val)

        # b. solve
        if self.utility == 'cobb_douglas':
            self.h1_ast = (u*(self.p1/self.p2)**(-self.beta))**(1/(self.alpha+self.beta))
            self.h2_ast = (u*(self.p2/self.p1)**(-self.alpha))**(1/(self.alpha+self.beta))
        elif self.utility == 'quasi_log':
            self.h1_ast = (self.alpha*self.p2)/(self.beta*self.p1)
            self.h2_ast = (u-np.log(self.h1_ast))/self.beta
        else:
            raise ValueError('unknown utility function')

        # c. cost
        self.h_cost = self.p1*self.h1_ast+self.p2*self.h2_ast

        # d. return
        return np.array([self.h1_ast,self.h2_ast,self.h_cost])

    #############
    # budgetset #
    #############

    def plot_budgetline(self,ax,**kwargs):

        kwargs.setdefault('lw',2)

        x = [0,self.I/self.p1]
        y = [self.I/self.p2,0]

        ax.plot(x,y,**kwargs)

    #######################
    # indifference curves #
    #######################

    def find_indifference_curve(self,u0=None):

        if u0 == None:
            u0 = self.u_ast

        # a. fix x1
        x1_x1 = np.linspace(0,self.x1max,self.N)
        with np.errstate(divide='ignore',invalid='ignore'):
            x2_x1 = self.indiff_func_x1(u0,x1_x1,self.alpha,self.beta)

        # b. fix x2
        x2_x2 = np.linspace(0,self.x2max,self.N)
        with np.errstate(divide='ignore',invalid='ignore'):
            x1_x2 = self.indiff_func_x2(u0,x2_x2,self.alpha,self.beta)

        # c. combine
        x1 = np.hstack([x1_x1,x1_x2])
        x2 = np.hstack([x2_x1,x2_x2])

        # d. sort
        I = np.argsort(x1)
        x1 = x1[I]
        x2 = x2[I]

        return x1,x2

    def plot_indifference_curves(self,ax,u0s=[],do_label=True,**kwargs):

        # a. find utility levels
        if len(u0s ) == 0:
            self.maximize_utility()
            u0s = [self.u_ast,
                   self.u(0.8*self.x1_ast,0.8*self.x2_ast,self.alpha,self.beta),
                   self.u(1.2*self.x1_ast,1.2*self.x2_ast,self.alpha,self.beta)]

        # b. construct and plot indifference curves
        for u0 in u0s:

            x1,x2 = self.find_indifference_curve(u0)

            if do_label:
                ax.plot(x1,x2,label=f'$u = {u0:.2f}$',**kwargs)
            else:
                ax.plot(x1,x2,**kwargs)

    def plot_monotonicity_check(self,ax,x1=None,x2=None,text='north-east'):

        # a. value
        if x1 == None:
            x1 = self.x1_ast
        if x2 == None:
            x2 = self.x2_ast

        # b. plot
        polygon = plt.Polygon([[x1, x2],[self.x1max, x2],[self.x1max, self.x2max],[x1, self.x2max],[x1, x2]],
            color='black',alpha=0.10)
        ax.add_patch(polygon)
        ax.text(x1*1.05,0.95*self.x2max,text)

    def plot_convexity_check(self,ax,u=None,text='mix'):

        # a. value
        if u == None:
            u = self.u_ast

        # b. indifference curve
        x1, x2 = self.find_indifference_curve(u)

        # c. select
        I = (np.isnan(x1) == False) & (np.isnan(x2) == False)
        J = (x1[I] > 0) & (x2[I] > 0) & (x1[I] < self.x1max) & (x2[I] < self.x2max)

        x1 = x1[I][J]
        x2 = x2[I][J]
        N = x1.size
        i_low = np.int(N/3)
        i_high = np.int(N*2/3)

        x1_low = x1[i_low]
        x2_low = x2[i_low]

        x1_high = x1[i_high]
        x2_high = x2[i_high]

        # d. plot
        ax.plot([x1_low,x1_high],[x2_low,x2_high],ls='--',marker='o',
            markerfacecolor='none',color='black')

        x1 = 0.05*x1_low + 0.95*x1_high
        x2 = 0.05*x2_low + 0.95*x2_high
        ax.text(x1,x2*1.05,text)

    ###########
    # slutsky #
    ###########

    def plot_slutsky_endogenous(self,ax,p1_old,p1_new,p2,e1,e2,steps=4):

        I_old = p1_old*e1 + p2*e2
        I_new = p1_new*e1 + p2*e2

        # a. calculations
        A = self.maximize_utility(p1=p1_old,p2=p2,I=I_old)
        x1,x2,u_max = A

        C1 = self.maximize_utility(p1=p1_new,p2=p2,I=I_old)
        x1_fixI,x2_fixI,u_max_fixI = C1

        B = self.minimize_cost(u_max,p1=p1_new,p2=p2)
        h1, h2, h_cost = B

        C2 = self.maximize_utility(p1=p1_new,p2=p2,I=I_new)
        x1_new,x2_new,u_max_new = C2

        # b. plots
        self.p1,self.p2,self.I = p1_old,p2,I_old
        self.plot_budgetline(ax,ls='-',label='original',color=colors[0])
        if steps > 1:
            self.p1,self.p2,self.I = p1_new,p2,I_new
            self.plot_budgetline(ax,ls='-',label='final',color=colors[1])
        if steps > 2:
            self.p1,self.p2,self.I = p1_new,p2,h_cost
            self.plot_budgetline(ax,ls='-',alpha=0.50,label='compensated',color=colors[2])
        if steps > 3:
            self.p1,self.p2,self.I = p1_new,p2,I_old
            self.plot_budgetline(ax,ls='-',label='constant income',color=colors[3])
        ax.plot(e1,e2,'ro',color='black')
        ax.text(e1*1.03,e2*1.03,'$E$')

        # A
        ax.plot(x1,x2,'ro',color='black')
        ax.text(x1*1.03,x2*1.03,'$A$')
        self.plot_indifference_curves(ax,[u_max],do_label=False,ls='--',color=colors[0])

        # B
        if steps > 2:
            ax.plot(h1,h2,'ro',color='black')
            ax.text(h1*1.03,h2*1.03,'$B$')

        # C2
        if steps > 1:
            ax.plot(x1_new,x2_new,'ro',color='black')
            ax.text(x1_new*1.03,x2_new*1.03,'$C_2$')
            self.plot_indifference_curves(ax,[u_max_new],do_label=False,ls='--',color=colors[1])

        # C1
        if steps > 3:
            ax.plot(x1_fixI,x2_fixI,'ro',color='black')
            ax.text(x1_fixI*1.03,x2_fixI*1.03,f'$C_1$')
            self.plot_indifference_curves(ax,[u_max_fixI],do_label=False,ls='--',color=colors[3])

        if steps > 3:
            line = f'subtitution: $B-A$ = ({h1-x1:5.2f},{h2-x2:5.2f})\n'
            line += f'income: $C_1-B$ = ({x1_fixI-h1:5.2f},{x2_fixI-h2:5.2f})\n'
            line += f'wealth: $C_2-C_1$ = ({x1_new-x1_fixI:5.2f},{x2_new-x2_fixI:5.2f})'
            ax.text(0.55*self.x1max,0.87*self.x2max,line)


import numpy as np
import tensorflow as tf
import tensorflow.keras as keras
import matplotlib.pyplot as plt
print("using tensorflow version: " + tf.__version__)

def generate_time_series(batch_size, n_steps):
    freq1, freq2, offset1, offset2, = np.random.rand(4, batch_size, 1)
    time = np.linspace(0, 1, n_steps)
    series = 0.5 * np.sin((time - offset1) * (freq1 * 10 + 10))
    series += 0.2 * np.sin((time - offset2) * (freq2 * 20 + 20))
    series += 0.1 * (np.random.rand(batch_size, n_steps) - 0.5)
    return series[..., np.newaxis].astype(np.float32)

n_steps = 50
n_samples = 10000
series = generate_time_series(batch_size=n_samples, n_steps=n_steps + 1)
x_train, y_train = series[:7000, :n_steps], series[:7000, -1]
x_valid, y_valid = series[7000:9000, :n_steps], series[7000:9000, -1]
x_test, y_test = series[9000:, :n_steps], series[9000:, -1]

# Every sample is a serie of 50 values (x) with 1 prediction (y) at the end.
plt.plot(x_train[0])
plt.show()

def compile_and_fit(model, learning_rate=1e-3, epochs=20):
    optimizer = keras.optimizers.Adam(learning_rate=learning_rate)
    model.compile(loss="mse",
                  optimizer=optimizer, metrics=["mse"])
    model.fit(x_train, y_train, epochs=epochs)
    [loss, accuracy] = model.test_on_batch(x=x_test, y=y_test)
    print("loss=" + str(loss) + " accuracy=" + str(accuracy))
    return (loss, accuracy)

# Native forcasting with linear regression model.
model_linear_regression = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[50, 1]),
    keras.layers.Dense(1)
])
linear_loss, linear_accuracy = compile_and_fit(model_linear_regression)

f_x = np.expand_dims(x_test[0], axis=0)  # batch of 1 dimension
f_y = y_test[0]
f_pred = model_linear_regression.predict(f_x)
print((f_pred - f_y)*(f_pred - f_y))

model_single_rnn = keras.models.Sequential([
    keras.layers.SimpleRNN(1, input_shape=[None, 1]),
])
single_rnn_loss, single_rnn_accuracy = compile_and_fit(model_single_rnn)
# single rnn weights:
# single_rnn_layer = model_single_rnn.layers[0]
# single_rnn_layer.weights[0] => input matrix W_x
# single_rnn_layer.weights[1] => recurrent_matrix W_y
# single_rnn_layer.weights[2] => bias matrix

model_rnn = keras.models.Sequential([
    keras.layers.SimpleRNN(20, return_sequences=True, input_shape=[None, 1]),
    keras.layers.SimpleRNN(20, return_sequences=True),
    keras.layers.Dense(1),
])
rnn_loss, rnn_accuracy = compile_and_fit(model_rnn,
                                         learning_rate=1e-3,
                                         epochs=20)


#
#   House_Price_Prediction.py
#
#    This is a very simple prediction of house prices based on house size, implemented
#   in TensorFlow.  This code is part of Pluralsight's course "TensorFlow: Getting Started"
#

import tensorflow as tf
import numpy as np
import math
import matplotlib.pyplot as plt
import matplotlib.animation as animation # import animation support

#  generation some house sizes between 1000 and 3500 (typical sq ft of house)
num_house = 160
np.random.seed(42)
house_size = np.random.randint(low=1000, high=3500, size=num_house )

# Generate house prices from house size with a random noise added.
np.random.seed(42)
house_price = house_size * 100.0 + np.random.randint(low=20000, high=70000, size=num_house)

# Plot generated house and size
plt.plot(house_size, house_price, "bx")  # bx = blue x
plt.ylabel("Price")
plt.xlabel("Size")
plt.show()

# you need to normalize values to prevent under/overflows.
def normalize(array):
    return (array - array.mean()) / array.std()

# define number of training samples, 0.7 = 70%.  We can take the first 70% since the values are randomized
num_train_samples = math.floor(num_house * 0.7)

# define training data
train_house_size = np.asarray(house_size[:num_train_samples])
train_price = np.asanyarray(house_price[:num_train_samples:])

train_house_size_norm = normalize(train_house_size)
train_price_norm = normalize(train_price)

# define test data
test_house_size = np.array(house_size[num_train_samples:])
test_house_price = np.array(house_price[num_train_samples:])

test_house_size_norm = normalize(test_house_size)
test_house_price_norm = normalize(test_house_price)

#  Set up the TensorFlow placeholders that get updated as we descend down the gradient
tf_house_size = tf.placeholder("float", name="house_size")
tf_price = tf.placeholder("float", name="price")

# Define the variables holding the size_factor and price we set during training.
# We initialize them to some random values based on the normal distribution.
tf_size_factor = tf.Variable(np.random.randn(), name="size_factor")
tf_price_offset = tf.Variable(np.random.randn(), name="price_offset")

# 2. Define the operations for the predicting values - predicted price = (size_factor * house_size ) + price_offset
#  Notice, the use of the tensorflow add and multiply functions.  These add the operations to the computation graph,
#  AND the tensorflow methods understand how to deal with Tensors.  Therefore do not try to use numpy or other library
#  methods.
tf_price_pred = tf.add(tf.multiply(tf_size_factor, tf_house_size), tf_price_offset)

# 3. Define the Loss Function (how much error) - Mean squared error
tf_cost = tf.reduce_sum(tf.pow(tf_price_pred-tf_price, 2))/(2*num_train_samples)

# Optimizer learning rate.  The size of the steps down the gradient
learning_rate = 0.1

# 4. define a Gradient descent optimizer that will minimize the loss defined in the operation "cost".
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(tf_cost)

# Initializing the variables
init = tf.global_variables_initializer()

# Launch the graph in the session
with tf.Session() as sess:
    sess.run(init)

    # set how often to display training progress and number of training iterations
    display_every = 2
    num_training_iter = 50

    # calculate the number of lines to animation
    fit_num_plots = math.floor(num_training_iter/display_every)
    # add storage of factor and offset values from each epoch
    fit_size_factor = np.zeros(fit_num_plots)
    fit_price_offsets = np.zeros(fit_num_plots)
    fit_plot_idx = 0

   # keep iterating the training data
    for iteration in range(num_training_iter):

        # Fit all training data
        for (x, y) in zip(train_house_size_norm, train_price_norm):
            sess.run(optimizer, feed_dict={tf_house_size: x, tf_price: y})

        # Display current status
        if (iteration + 1) % display_every == 0:
            c = sess.run(tf_cost, feed_dict={tf_house_size: train_house_size_norm, tf_price:train_price_norm})
            print("iteration #:", '%04d' % (iteration + 1), "cost=", "{:.9f}".format(c), \
                "size_factor=", sess.run(tf_size_factor), "price_offset=", sess.run(tf_price_offset))
            # Save the fit size_factor and price_offset to allow animation of learning process
            fit_size_factor[fit_plot_idx] = sess.run(tf_size_factor)
            fit_price_offsets[fit_plot_idx] = sess.run(tf_price_offset)
            fit_plot_idx = fit_plot_idx + 1

    print("Optimization Finished!")
    training_cost = sess.run(tf_cost, feed_dict={tf_house_size: train_house_size_norm, tf_price: train_price_norm})
    print("Trained cost=", training_cost, "size_factor=", sess.run(tf_size_factor), "price_offset=", sess.run(tf_price_offset), '\n')

   # Plot of training and test data, and learned regression

    # get values used to normalized data so we can denormalize data back to its original scale
    train_house_size_mean = train_house_size.mean()
    train_house_size_std = train_house_size.std()

    train_price_mean = train_price.mean()
    train_price_std = train_price.std()

    # Plot the graph
    plt.rcParams["figure.figsize"] = (10,8)
    plt.figure()
    plt.ylabel("Price")
    plt.xlabel("Size (sq.ft)")
    plt.plot(train_house_size, train_price, 'go', label='Training data')
    plt.plot(test_house_size, test_house_price, 'mo', label='Testing data')
    plt.plot(train_house_size_norm * train_house_size_std + train_house_size_mean,
             (sess.run(tf_size_factor) * train_house_size_norm + sess.run(tf_price_offset)) * train_price_std + train_price_mean,
             label='Learned Regression')

    plt.legend(loc='upper left')
    plt.show()

    #
    # Plot another graph that animation of how Gradient Descent sequentually adjusted size_factor and price_offset to
    # find the values that returned the "best" fit line.
    fig, ax = plt.subplots()
    line, = ax.plot(house_size, house_price)

    plt.rcParams["figure.figsize"] = (10,8)
    plt.title("Gradient Descent Fitting Regression Line")
    plt.ylabel("Price")
    plt.xlabel("Size (sq.ft)")
    plt.plot(train_house_size, train_price, 'go', label='Training data')
    plt.plot(test_house_size, test_house_price, 'mo', label='Testing data')

    def animate(i):
        line.set_xdata(train_house_size_norm * train_house_size_std + train_house_size_mean)  # update the data
        line.set_ydata((fit_size_factor[i] * train_house_size_norm + fit_price_offsets[i]) * train_price_std + train_price_mean)  # update the data
        return line,

     # Init only required for blitting to give a clean slate.
    def initAnim():
        line.set_ydata(np.zeros(shape=house_price.shape[0])) # set y's to 0
        return line,

    ani = animation.FuncAnimation(fig, animate, frames=np.arange(0, fit_plot_idx), init_func=initAnim,
                                 interval=1000, blit=True)

    plt.show()

1. Simple Linear Price Prediction Model/House_Price_Prediction-with-anim.py


import numpy

def rand_walk(init_val, final_high, final_low, prob, N):

    nleft = 0
    nright = 0
    csum = 0

    left_end = final_low
    right_end = final_high
    p = prob
    n = N

    for i in range(0, n):

        run_val = init_val
        while(1):

            if((run_val == right_end) or (run_val == left_end)):
                break;

            u = numpy.random.uniform(0, 1)
            if(u < p):
                run_val = run_val + 1
            else:
                run_val = run_val - 1

        csum = csum + run_val

        if(run_val == right_end):
            nright = nright + 1
        else:
            nleft = nleft + 1

    num = float(n)
    nright = float(nright)
    a = nright / num

    nleft = float(nleft)
    b = nleft / num

    csum = float(csum)
    c = csum / num
    return a, b, c

[a, b, c] = rand_walk(100, 150, 0, 0.5, 10000)
print "TEST CASE 1 >> (answer for 6(a)) ", a, "\t(answer for 6(b)) ", b, "\t(answer for 6(c)) ", c
[a, b, c] = rand_walk(100, 200, 0, 0.52, 10000)
print "TEST CASE 2 >> (answer for 6(a)) ", a, "\t(answer for 6(b)) ", b, "\t(answer for 6(c)) ", c
[a, b, c] = rand_walk(200, 250, 0, 0.54, 10000)
print "TEST CASE 3 >> (answer for 6(a)) ", a, "\t(answer for 6(b)) ", b, "\t(answer for 6(c)) ", c


import numpy as np
import random
from sklearn.neighbors import NearestNeighbors
import matplotlib.pyplot as plt
import queue

training_files = ["bisecting.txt","blobs.txt","moons.txt"]
INPUT_FILE="blobs.txt"
ITERATIONS=50
#Define label for differnt point group
UNASSIGNED = 0
CORE_PT = -1
BORDER_PT = -2

dataset = []
noOfClusters = 0

def read_dataset(INPUT_FILE):
	"""
	Reading dataset
	"""
	global dataset
	f = open(INPUT_FILE, "r")
	lines = f.readlines()

	for i in range(len(lines)):
		data = lines[i].split()
		dataset.append(list(map(float, data)))

	print("Total dataset = {} points".format(len(dataset)))
	f.close()
	pass

def find_nearest_neighbour(k):
	"""
	Nearest neighbour
	"""
	global dataset
	nearest_neighbors = NearestNeighbors(n_neighbors=k)
	nearest_neighbors.fit(dataset)
	distances, indices = nearest_neighbors.kneighbors(dataset)
	distances = np.sort(distances, axis=0)[:, 1]
	# print(distances, indices)
	plt.grid()
	plt.plot(distances)
	# plt.savefig(INPUT_FILE+'_Nearest_Neighbour.png')
	plt.show()

def plotClusters(dataset, labels, noOfClusters, file):
	total_points = len(dataset)
	print("Plotting for {} points".format(total_points))
	plt.figure()

	# Color array for clusters
	scatterColors = ["blue","green","red","cyan","brown","indigo", "pink", "royalblue",
									"orange","yellow","black","olive", "gold", "orangered", "skyblue", "teal" ]

	for i in range(noOfClusters):
		if (i==0):
			#Plot all noise point as blue
			color='blue'
		else:
			color = scatterColors[i % len(scatterColors)]

		x = [];  y = []
		for j in range(total_points):
			if labels[j] == i:
				x.append(dataset[j][0])
				y.append(dataset[j][1])
		plt.scatter(x, y, c=color, alpha=1, marker='.')

	plt.grid()
	plt.savefig(file)
	# plt.show()

def euclidean_dist(point1, point2):
	"""
		Euclid distance function
	"""
	x1 = point1[0]
	x2 = point2[0]
	y1 = point1[1]
	y2 = point2[1]
	# create the points
	p1 = (x1 - x2)**2
	p2 = (y1 - y2)**2
	return np.sqrt(p1 + p2)

def neighbor_points(dataset, pointIdx, radius):
	'''
	find all neigbor points in radius from a given point.
	'''
	points = []
	for i in range(len(dataset)):
		# Calculating distance btn points
		if euclidean_dist(dataset[i], dataset[pointIdx]) <= radius:
			points.append(i)
	return points

def dbscan(data, Eps, MinPt):
	'''
	DBSCAN Algorithm
	'''
	global dataset, noOfClusters
	#initilize all pointlable to unassign
	pointlabel  = [UNASSIGNED] * len(data)
	neighbourhood_arr = []
	#initilize list for core/noncore point
	core_pts=[]
	non_core_pts=[]

	#Find all neigbor for all point
	for i in range(len(data)):
		neighbourhood_arr.append(neighbor_points(dataset,i,Eps))

	#Find all core point, edgepoint and noise
	for i in range(len(neighbourhood_arr)):
		# A point is a core point if it has more than a specified number of points (MinPts) within Eps
		if (len(neighbourhood_arr[i]) >= MinPt):
			pointlabel[i] = CORE_PT
			core_pts.append(i)
		else:
			non_core_pts.append(i)

	for i in non_core_pts:
		for j in neighbourhood_arr[i]:
			if j in core_pts:
				pointlabel[i] = BORDER_PT
				break

	#start assigning point to cluster
	cluster_no = 1

	# Put all neigbor core point in queue and find neigboir's neigbor
	for i in range(len(pointlabel)):
		q = queue.Queue()
		if (pointlabel[i] == CORE_PT):
			pointlabel[i] = cluster_no
			for j in neighbourhood_arr[i]:
				if(pointlabel[j] == CORE_PT):
					q.put(j)
					pointlabel[j]= cluster_no
				elif(pointlabel[j] == BORDER_PT):
					pointlabel[j] = cluster_no

			# checking queue
			while not q.empty():
				neighbors = neighbourhood_arr[q.get()]
				for n in neighbors:
					if (pointlabel[n] == CORE_PT):
						pointlabel[n]=cluster_no
						q.put(n)
					if (pointlabel[n] == BORDER_PT):
						pointlabel[n]=cluster_no

			cluster_no = cluster_no + 1

	noOfClusters = cluster_no
	return pointlabel

def DBSCAN_start(eps, minpts):
	"""
	docstring
	"""
	global dataset, noOfClusters

	print("Starting DBSCAN for EPS: {} | Minpts: {}".format(eps, minpts))
	labels = dbscan(dataset,eps,minpts)

	plotClusters(dataset, labels, noOfClusters, INPUT_FILE+'_DBSCAN.png')
	outliers  = labels.count(0)

	print("No. of Clusters: {}".format(noOfClusters-1))
	print("Outliers: {}".format(outliers))

	return noOfClusters - 1

def calc_distance(X1, X2):
	return (sum((X1 - X2)**2))**0.5

def assign_clusters(centroids, X):
	assigned_cluster = []
	for i in X:
		distance=[]
		for j in centroids:
			distance.append(calc_distance(i, j))
		# print(distance)
		# print(np.argmin(distance))
		# print("--------------------------------")
		assigned_cluster.append(np.argmin(distance)) # idx of minimum element
		# print(assigned_cluster)
	return assigned_cluster

def calc_centroids(clusters_lables, k):
	global dataset
	points_per_cluster = [[] for _ in range(k)]
	for i in range(len(clusters_lables)):
		points_per_cluster[clusters_lables[i]].append(dataset[i])

	centroids = []
	for i in range(k):
		centroids.append(np.mean(points_per_cluster[i], axis=0))

	return centroids

def match_centroids(c_new, c_old):
	return (np.array(c_new) == np.array(c_old)).all()

def k_means(k):
	"""
	K-Means clustering algorithm
	"""
	global dataset

	print("Running k-Means for {} clusters..".format(k))
	X = np.array(dataset)

	init_centroids = random.sample(range(0, len(dataset)), k)

	centroids, cluster_labels = [], []
	for i in init_centroids:
		centroids.append(dataset[i])

	# converting to 2D - array
	# centroids = np.array(centroids)
	# get_centroids = assign_clusters(centroids, X)

	prev_centroids = centroids.copy()
	for i in range(ITERATIONS):
		print("For iteration {}: ".format(i))
		prev_centroids = np.array(prev_centroids)
		cluster_labels = assign_clusters(prev_centroids, X)
		centroids = calc_centroids(cluster_labels, k)

		# print(prev_centroids)
		print(centroids)
		if match_centroids(centroids,prev_centroids):
			print("Converged ...")
			break
		else:
			prev_centroids = centroids.copy()

	plotClusters(dataset, cluster_labels, k, INPUT_FILE+'_k_means.png')

if __name__ == "__main__":

	print("Choose Training file...")
	for i, item in enumerate(training_files, start=1):
		print(i,item)
	choice = int(input())
	INPUT_FILE=training_files[choice-1]
	read_dataset(INPUT_FILE)
	print("1. Plot k Nearest Neighbours\n2. Run Clustering Algorithms")
	choice = int(input())
	if choice == 1:
		print("Enter the value of k:")
		k = int(input())
		find_nearest_neighbour(k)
	else:
		print("Enter EPS value:")
		eps = float(input())

		print("Enter Minpts value:")
		minpts = int(input())

		k = DBSCAN_start(eps, minpts)
		k_means(k)


# Para visualizar gráficos en la terminal interactiva
from IPython import get_ipython
get_ipython().run_line_magic('matplotlib', 'ipympl')

"""
# Actividad en clases: Diseño de filtros FIR

Objetivos:

- Aprender a utilizar scipy.signal.firwin para diseñar filtros FIR
- Filtrar señales de audio con filtros FIR

"""

import numpy as np
import scipy.fft
import scipy.signal
import matplotlib.pylab as plt
from IPython.display import display
import librosa
from utils import Audio

def listen_signal(signal: np.ndarray, Fs: int=44100, normalize:bool=True) -> None:
    if normalize:
        signal = signal.copy()/np.amax(np.absolute(signal))
    display(Audio(signal, rate=Fs))

"""

Sea la siguiente señal:

"""

data, Fs = librosa.load("nomekop.ogg", sr=8000)

Nw = 512
freq, ttime, Sxx = scipy.signal.spectrogram(data, fs=Fs, window='hamming', nperseg=Nw, noverlap=Nw//1.5)

fig, ax = plt.subplots(1, figsize=(8, 3), tight_layout=True)
ax.pcolormesh(ttime, freq, np.log10(Sxx+1e-5), shading='auto', cmap=plt.cm.magma);
ax.set_xlabel('Tiempo [s]')
ax.set_ylabel('Frecuencia [Hz]')
listen_signal(data, Fs=Fs)

"""

El siguiente código de ejemplo busca atenuar el primer personaje preservando lo más posible del segundo personaje

- Estudien atentamente la rutina y el resultado
- ¿Puedes mejorar el resultado modificando las frecuencias de corte?

"""

h = scipy.signal.firwin(numtaps=1000+1, cutoff=[50, 700, 1900, 2100], pass_zero=False, window='hamming', fs=Fs)
freq_response, response = scipy.signal.freqz(h, fs=Fs)
data_filt = scipy.signal.convolve(data, h)
freq, ttime, Sxx = scipy.signal.spectrogram(data_filt, fs=Fs, window='hamming', nperseg=Nw, noverlap=Nw//1.5)

# Resultados:
fig, ax = plt.subplots(1, 2, figsize=(7, 2), tight_layout=True)
ax[0].plot(h);
ax[1].semilogy(freq_response, np.abs(response))
fig, ax = plt.subplots(1, figsize=(8, 3), tight_layout=True)
ax.pcolormesh(ttime, freq, np.log10(Sxx+1e-5), shading='auto', cmap=plt.cm.magma);
ax.set_xlabel('Tiempo [s]')
ax.set_ylabel('Frecuencia [Hz]')
listen_signal(data_filt, Fs=Fs)

"""

- Modifique L y el tipo de ventana (truncamiento). Discuta sobre como cambia el resultado en términos visuales y acústicos
- Diseñe y pruebe un filtro FIR que atenue al segundo personaje y preserve al primer personaje

"""


# This script trains a MLP to approximate x^2
# Model source: https://sonnet.dev/
# Number of trainable source:
# https://stackoverflow.com/questions/38160940/how-to-count-total-number-of-trainable-parameters-in-a-tensorflow-model
import numpy as np
import sonnet as snt
import tensorflow as tf
import matplotlib.pyplot as plt

# %% Parameters.
# All parameters to be tweaked.
mlp_dimensions = [5, 5, 1]
num_training_iterations = 1000
learning_rate = 1e-3
range_tr = (-1, 1)
range_plt = (-2, 2)
iteration_print = 100

# Parameters depending on the tweaked ones.
x_plot = np.linspace(*range_plt, num=100)

# %% Data generator.
def get_data(low, high):
    x_np = np.random.uniform(low=low, high=high, size=(1, 1))
    y_np = np.square(x_np)
    x_tf = tf.convert_to_tensor(x_np, dtype=tf.float32)
    y_tf = tf.convert_to_tensor(y_np, dtype=tf.float32)
    return x_tf, y_tf

# %% Restart Training.
# Model.
model = snt.nets.MLP(mlp_dimensions)

# Optimizer.
optimizer = snt.optimizers.Adam(learning_rate)

# Starting values for training.
last_iteration = 0
loss_list = []

# %% Training (can be run multiple times for added num_training_iterations iterations).
gradients = None
for iteration in range(last_iteration, last_iteration + num_training_iterations):
    last_iteration = iteration
    x, y = get_data(*range_tr)
    with tf.GradientTape() as tape:
        y_pred = model(x)
        loss = tf.compat.v1.losses.mean_squared_error(y, y_pred)
    gradients = tape.gradient(loss, model.trainable_variables)
    loss_list.append(loss.numpy())
    if (iteration + 1) % iteration_print == 0:
        optimizer.apply(gradients, model.trainable_variables)
        print(f"Iteration: {iteration + 1}, Mean Loss: {np.mean(loss_list[-iteration_print:-1])}")

#%% Plot results.
num_trainable_variables = np.sum([np.prod(v.shape) for v in model.trainable_variables])
y_plot = np.array([model(tf.convert_to_tensor([[x_i]], dtype=tf.float32)).numpy().squeeze() for x_i in x_plot])
plt.plot(x_plot, np.square(x_plot), "k-")  # Ground truth
plt.plot(x_plot, y_plot, "k--")            # Model predicted
plt.legend(("Ground truth", "MLP predicted"), loc='upper center')
plt.title(rf"MLP tries to predict $x^2$ with {num_trainable_variables} trainable variables")
plt.show()

#%% Plot loss
start = 100
end = len(loss_list)
plt.plot(range(start, end), loss_list[start:end])
plt.ylabel("Loss (MSE)")
plt.xlabel("Training examples")
plt.show()

# %% Test
x, y = get_data(*range_tr)
y_pred = model(x)
print("x={:.2f}, y={:.3f}, y_pred={:.3f}, diff={:.3f}".format(*(i.numpy().squeeze() for i in (x, y, y_pred, y-y_pred))))


import numpy as np

from mvdr.linalg_utils import svd_wrapper

def get_principal_angles(A, B, n_components=None, is_ortho=True, deg=False):
    """
    Returns the vector of principal angles between two subspaces.

    Parameters
    ----------
    A: array-like, (n, p)
        Orthonormal basis for the first subspace.

    B: array-like, (n, q)
        Orthonormal basis for the second subspace.

    n_components: None, int
        Only compute the first few principal angles.

    is_ortho: bool
        Are A, B orthonormal? If they are not, we will orthonormalized them with a QR decomposition.

    deg: bool
        Return the vector of principal angles in degrees. If False, will use radians.

    Output
    ------
    theta: array-like, (n_components, )
        The principal angles in radians.
    """
    A = np.array(A)
    B = np.array(B)

    A_is_vec = False
    B_is_vec = False
    if A.ndim == 1:
        A = A.reshape(-1, 1)
        A_is_vec = True

    if B.ndim == 1:
        B = B.reshape(-1, 1)
        B_is_vec = True

    if not is_ortho:
        if A_is_vec:
            A /= np.linalg.norm(A)
        else:
            A = np.linalg.qr(A)[0]

        if A_is_vec:
            A /= np.linalg.norm(A)
        else:
            B = np.linalg.qr(B)[0]

    if A_is_vec and B_is_vec:
        cos_theta = A.T @ B
        cos_theta = cos_theta.reshape(-1)
    else:
        _, cos_theta, __ = svd_wrapper(A.T @ B, rank=n_components)

    cos_theta = np.clip(cos_theta, a_min=-1, a_max=1)
    theta = np.arccos(cos_theta)

    if deg:
        theta = np.rad2deg(theta)

    return theta

def subspace_dist(A, B, method='proj_f', is_ortho=True):
    """
    Computes  distance between two subspaces.

    See section 4.3 of (Edelman et al, 1998) for definitions
    http://www.cnbc.cmu.edu/cns/papers/edelman98.pdf

    Parameters
    ----------
    A, B: array-like, (n, p)
        Bases of the two subspace

    method: str
        Must be one of ['arc', 'fubini',
                        'proj_2', 'proj_f',
                        'chordal_2', 'chordal_f']

    is_ortho: bool
        Are A, B orthonormal? If they are not, we will orthonormalized them with a QR decomposition.
    """
    # TODO: check for edge cases e.g. n = 1 etc

    method = method.lower()
    if method not in ['arc_length', 'fubini',
                      'proj_2', 'proj_f',
                      'chordal_2', 'chordal_f']:
        raise ValueError(" method = {} is not currently an option".
                         format(method))

    A = np.array(A)
    B = np.array(B)

    if A.ndim == 1:
        A = A.reshape(-1, 1)

    if B.ndim == 1:
        B = B.reshape(-1, 1)

    if not is_ortho:
        A = np.linalg.qr(A)[0]
        B = np.linalg.qr(B)[0]

    if method in ['arc_length', 'fubini', 'chordal_2', 'chordal_f']:
        theta = get_principal_angles(A, B)

    if method == 'arc_length':
        return np.linalg.norm(theta)

    elif method == 'fubini':
        cos_theta = np.cos(theta)
        return np.arccos(np.product(cos_theta))

    elif method == 'chordal_2':
        return max(2 * np.sin(.5 * theta))

    elif method == 'chordal_f':
        return np.linalg.norm(2 * np.sin(.5 * theta))

    if method == 'proj_f':
        diff = A @ A.T - B @ B.T
        return np.sqrt(.5) * np.linalg.norm(diff, ord='fro')

    elif method == 'proj_2':
        diff = A @ A.T - B @ B.T
        return np.linalg.norm(diff, ord=2)


import gin
import jax.numpy as jnp

from typing import Optional, Union

from myriad.custom_types import Params
from myriad.systems import IndirectFHCS

@gin.configurable
class CancerTreatment(IndirectFHCS):
  """
      Taken from: Optimal Control Applied to Biological Models, Lenhart & Workman (Chapter 10, Lab 5)
      The model was originally described in K. Renee Fister and John Carl Panetta. Optimal control applied to
      competing chemotherapeutic cell-kill strategies. SIAM Journal of Applied Mathematics, 63(6):1954–71, 2003.

      The tumour is assumed to Gompertzian growth and the model follows a Skipper's log-kill hypothesis, that is, the
      cell-kill due to the chemotherapy treatment is proportional to the tumour population.

      This environment models the normalized density of a cancerous tumour undergoing chemotherapy.
      The state ( \\(x\\) ) is the
      normalized density of the tumour, while the control ( \\(u\\) ) is the strength of the drug used for chemotherapy.
      We are trying to minimize:

      An important note about this system: due to the log of the reciprocal of the state in the
      dynamics equation, special care must be taken to ensure that your numerical integration
      scheme doesn't take a step into the negative values for x. As such, it is recommended
      to either take small steps, or to always clip the state to [0., inf] during
      integration.

      .. math::

        \\begin{align}
        &\\min_u \\quad && \\int_0^T ax^2(t) + u^2(t) dt \\\\
        & \\; \\mathrm{s.t.}\\quad  &&x'(t) = rx(t)\\ln \\big( \\frac{1}{x(t)} \\big) - u(t)\\delta x(t) \\\\
        & && x(0)=x_0, \\; u(t) \\geq 0
        \\end{align}
      """

  def __init__(self, r=0.3, a=3., delta=0.45, x_0=0.975, T=20):
    super().__init__(
      x_0=jnp.array([x_0]),  # Starting state
      x_T=None,  # Terminal state, if any
      T=T,  # Duration of experiment
      bounds=jnp.array([  # Bounds over the states (x_0, x_1 ...) are given first,
        [1e-3, 1.],  # followed by bounds over controls (u_0, u_1,...)
        # [0., jnp.inf]       # Original bounds
        [0., 2.]  # Bounds based on optimal policy
      ]),
      terminal_cost=False,
      discrete=False,
    )

    self.adj_T = None  # Final condition over the adjoint, if any
    self.r = r
    """Growth rate of the tumour"""
    self.a = a
    """Positive weight parameter"""
    self.delta = delta
    """Magnitude of the dose administered"""

  def dynamics(self, x_t: jnp.ndarray, u_t: Union[float, jnp.ndarray],
               v_t: Optional[Union[float, jnp.ndarray]] = None, t: Optional[jnp.ndarray] = None) -> jnp.ndarray:
    d_x = self.r * x_t * jnp.log(1 / x_t) - u_t * self.delta * x_t
    return d_x

  def parametrized_dynamics(self, params: Params, x_t: jnp.ndarray, u_t: Union[float, jnp.ndarray],
                            v_t: Optional[Union[float, jnp.ndarray]] = None,
                            t: Optional[jnp.ndarray] = None) -> jnp.ndarray:
    r = params['r']
    delta = params['delta']
    d_x = r * x_t * jnp.log(1 / x_t) - u_t * delta * x_t
    return d_x

  def cost(self, x_t: jnp.ndarray, u_t: Union[float, jnp.ndarray], t: Optional[jnp.ndarray] = None) -> float:
    return self.a * x_t ** 2 + u_t ** 2

  def parametrized_cost(self, params: Params, x_t: jnp.ndarray, u_t: Union[float, jnp.ndarray],
                        t: Optional[jnp.ndarray] = None) -> float:
    # a = params['a']  # TODO: change back if we want to learn the cost also
    a = self.a
    return a * x_t ** 2 + u_t ** 2

  def adj_ODE(self, adj_t: jnp.ndarray, x_t: Optional[jnp.ndarray], u_t: Optional[jnp.ndarray],
              t: Optional[jnp.ndarray]) -> jnp.ndarray:
    return adj_t * (self.r + self.delta * u_t - self.r * jnp.log(1 / x_t)) - 2 * self.a * x_t

  def optim_characterization(self, adj_t: jnp.ndarray, x_t: Optional[jnp.ndarray],
                             t: Optional[jnp.ndarray]) -> jnp.ndarray:
    char = 0.5 * adj_t * self.delta * x_t
    return jnp.minimum(self.bounds[-1, 1], jnp.maximum(self.bounds[-1, 0], char))


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Jun 19 18:47:49 2018

@author: deepayanbhadra
"""

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import math
from hmwk1 import buildmat

#Q1(a) The function generates Nd data vectors, each containing Nf features
def create_classification_problem(Nd,Nf,kappa):
    D = buildmat(Nd,Nf,kappa)
    w = np.random.randn(Nf,1);
    c = np.sign(D.dot(w));
    flipper = np.ones((Nd,1)) - 2*(np.random.rand(Nd,1)>.9);
    c = c*flipper;
    return D,c

#Q1(b) The function evaluates the logistic regression objective function
def logreg_objective(x,D,c):
    z = np.diagflat(c).dot(D).dot(x)
    idxN, idxP = z<0, z>=0
    y1 = np.sum(-z[idxN]) + np.sum([math.log(np.exp(x)+1) for x in z[idxN]])
    y2 = np.sum([math.log(1+np.exp(-x)) for x in z[idxP]])
    y = y1+y2
    return y

#Q2(a) This function should load the mnist dataset
def load_mnist():
    import h5py
    f = h5py.File("mnist.h5")
    x_test = f["x_test"]
    x_train = f["x_train"]
    y_test = f["y_test"]
    y_train = f["y_train"]

    with x_test.astype("float"):
        x_test = x_test[:]
    with y_test.astype("float"):
        y_test = y_test[:]
    with x_train.astype("float"):
        x_train = x_train[:]
    with y_train.astype("float"):
        y_train = y_train[:]
    return x_test,x_train,y_test,y_train

#Q2(b) This function will implement the “SoftPlus” non-linearity which is a smoothed form of the RELU#

def softplus(X):
    y = np.zeros(X.shape)
    idxN, idxP = X<0, X>=0
    xn,xp = X[idxN],X[idxP]
    y[X<0] = [math.log(1+np.exp(x)) for x in xn]
    y[X>=0] = xp+[math.log(np.exp(-x)+1) for x in xp];
    return y

# Q2(c) This function will implement cross-entropy loss (measures the performance of a classification model)

def cross_entropy(X,Y):
    Xm = X - np.max(X,axis = 1, keepdims = True)
    softmax = np.divide(np.exp(Xm),np.sum(np.exp(Xm),axis = 1,keepdims = True))
    y = -math.log(np.mean(np.sum(softmax*Y,axis=1,keepdims = True)))
    return y

# Q2(d) This function will implement the loss of a neural network

def net_objective(W,D,L):
    temp = D
    for i in range(0,len(W)):
        temp = softplus(np.dot(temp,W[i]))
    y = cross_entropy(temp,L);
    return y

# Q3(a) This function will compute image gradient
def grad2d(X):

    # Computing the x-differences
    kernel = np.zeros(X.shape)
    kernel[0,0] = 1
    kernel[0,1] = -1
    Dx = np.fft.fft2(kernel)
    Gx = np.fft.ifft2(Dx*np.fft.fft2(X))

    #Computing the y-differences
    kernel = np.zeros(X.shape)
    kernel[0,0] = 1
    kernel[1,0] = -1
    Dy = np.fft.fft2(kernel)
    Gy = np.fft.ifft2(Dy*np.fft.fft2(X))

    return np.stack([Gx, Gy], axis=2)

# Q3(b) This function compute the divergence
def div2d(G):
    # Computing the x-differences
    #  extract the x- and y- derivatives
    Gx = G[:,:,0]
    Gy = G[:,:,1]

# We must convolve with the FLIPPED stencil to get the linear filtering we want
    kernel = np.zeros(Gx.shape)
    kernel[0,0]=1
    kernel[0][-1] =-1

#  create diagonal matrix in decomposition K=F'DF
    Dx = np.fft.fft2(kernel)

#  Use the eigen-decomposition to convolve the stencil with X, and get the differences in the horizontal direction.
    Divx = np.fft.ifft2(Dx*np.fft.fft2(Gx))

    kernel = np.zeros(Gy.shape)
    kernel[0,0]=1
    kernel[-1][0]=-1

    Dy = np.fft.fft2(kernel)

    Divy = np.fft.ifft2(Dy*np.fft.fft2(Gy))

    return Divx+Divy

if __name__ == "__main__":

    #Q1(a) to create a classification

    [ D,c ] = create_classification_problem(100, 2, 1)
    valid1 = D[np.where(c==1)[0]]
    valid2 = D[np.where(c==-1)[0]]
    plt.scatter(valid1[:, 0], valid1[:, 1])
    plt.scatter(valid2[:, 0], valid2[:, 1])
    plt.title("Scatter plot with condition no = 1")
    plt.show()

    [ D,c ] = create_classification_problem(100, 2, 10)
    valid1 = D[np.where(c==1)[0]]
    valid2 = D[np.where(c==-1)[0]]
    plt.scatter(valid1[:, 0], valid1[:, 1])
    plt.scatter(valid2[:, 0], valid2[:, 1])
    plt.title("Scatter plot with condition no = 10")
    plt.show()

    input("Press Enter to continue...")

    #Q1(b) to evaluate logistic regression

    Nd, Nf, kappa = 1000,100,10
    [D,c] = create_classification_problem(Nd,Nf,kappa)
    x = np.random.randn(Nf,1);

    print("The logistic regression objective is")
    y = logreg_objective(x,D,c)
    print(y)

    input("Press Enter to continue...")

    print("The logistic regression objective for a large x is");
    x = 10000*np.random.randn(Nf,1)
    #Calling on a large input to verify that we don't get any NaNs
    y = logreg_objective(x,D,c)
    print(y)

    input("Press Enter to continue...")

    #Q2(a) to load the dataset

    x_test,x_train,y_test,y_train = load_mnist()

    D = x_train
    W = [None] * 2
    W[0] = np.random.randn(784,100)
    W[1] = np.random.randn(100,10)
    y_train += np.ones((60000,))

    # One HOT Encoding
    s = pd.Series(y_train)
    L = pd.get_dummies(s).values

    print("""The objective value using the random weights and MNIST
          training data is {r:5.3f}""".format(r=net_objective(W,D,L)))

    input("Press Enter to continue...")

    # Q3(c) to verify adjointness using <x,Ay> = <A'x,y> where A = G or d

    print('\nTesting adjoints\n')
    # random inputs
    x = np.random.randn(100,100);
    y = np.random.randn(100,100,2)
    Ay = grad2d(y)
    x_Ay = np.sum(np.conj(x)*Ay)
    #  Compute Ax, where A is the gradient operator
    Ax = grad2d(x)
    #  Compute Aty, where A is the gradient operator
    Aty = div2d(y)

    left = np.inner(Ax.flatten(),y.flatten(1))
    right = np.inner(x.flatten(),Aty.flatten(1))
    print('first inner product = \t',left,'\n')
    print('second inner product = \t',right)

    # EOF


import numpy as np

from src.grid import Grid
from src.matrix import Matrix

def test_constructing_and_inspecting_4x4_matrix():
    M = Matrix.from_string(
        """
        | 1 | 2 | 3 | 4 |
        | 5.5 | 6.5 | 7.5 | 8.5 |
        | 9 | 10 | 11 | 12 |
        | 13.5 | 14.5 | 15.5 | 16.5 |
        """
    )

    assert M[0, 0] == 1
    assert M[0, 3] == 4
    assert M[1, 0] == 5.5
    assert M[1, 2] == 7.5
    assert M[2, 2] == 11
    assert M[3, 0] == 13.5
    assert M[3, 2] == 15.5

def test_2x2_matrix_ought_to_be_representable():
    M = Matrix.from_string(
        """
        | -3 | 5 |
        | 1 | -2 |
        """
    )

    assert M[0, 0] == -3
    assert M[0, 1] == 5
    assert M[1, 0] == 1
    assert M[1, 1] == -2

def test_3x3_matrix_ought_to_be_representable():
    M = Matrix.from_string(
        """
        | -3 | 5 | 0 |
        | 1 | -2 | -7 |
        | 0 | 1 | 1 |
        """
    )

    assert M[0, 0] == -3
    assert M[1, 1] == -2
    assert M[2, 2] == 1

def test_matrix_equality_with_identical_matrices():
    A = Matrix.from_string(
        """
        | 1 | 2 | 3 | 4 |
        | 5 | 6 | 7 | 8 |
        | 9 | 8 | 7 | 6 |
        | 5 | 4 | 3 | 2 |
        """
    )

    B = Matrix.from_string(
        """
        | 1 | 2 | 3 | 4 |
        | 5 | 6 | 7 | 8 |
        | 9 | 8 | 7 | 6 |
        | 5 | 4 | 3 | 2 |
        """
    )

    assert np.array_equal(A, B)

def test_matrix_equality_with_different_matrices():
    A = Matrix.from_string(
        """
        | 1 | 2 | 3 | 4 |
        | 5 | 6 | 7 | 8 |
        | 9 | 8 | 7 | 6 |
        | 5 | 4 | 3 | 2 |
        """
    )

    B = Matrix.from_string(
        """
        | 2 | 3 | 4 | 5 |
        | 6 | 7 | 8 | 9 |
        | 8 | 7 | 6 | 5 |
        | 4 | 3 | 2 | 1 |
        """
    )

    assert not np.array_equal(A, B)

def test_multiplying_two_matrices():
    A = Matrix.from_string(
        """
        | 1 | 2 | 3 | 4 |
        | 5 | 6 | 7 | 8 |
        | 9 | 8 | 7 | 6 |
        | 5 | 4 | 3 | 2 |
        """
    )

    B = Matrix.from_string(
        """
        | -2 | 1 | 2 | 3 |
        | 3 | 2 | 1 | -1 |
        | 4 | 3 | 6 | 5 |
        | 1 | 2 | 7 | 8 |
        """
    )

    expected = Matrix.from_string(
        """
        | 20| 22 | 50 | 48 |
        | 44| 54 | 114 | 108 |
        | 40| 58 | 110 | 102 |
        | 16| 26 | 46 | 42 |
        """
    )

    assert np.array_equal(A @ B, expected)

def test_matrix_multiplied_by_tuple():
    A = Matrix.from_string(
        """
        | 1 | 2 | 3 | 4 |
        | 2 | 4 | 4 | 2 |
        | 8 | 6 | 4 | 1 |
        | 0 | 0 | 0 | 1 |
        """
    )

    b = Grid(1, 2, 3, 1)

    assert A @ b == Grid(18, 24, 33, 1)

def test_multiplying_matrix_by_identity_matrix():
    A = Matrix.from_string(
        """
        | 0 | 1 | 2 | 4 |
        | 1 | 2 | 4 | 8 |
        | 2 | 4 | 8 | 16 |
        | 4 | 8 | 16 | 32 |
        """
    )

    assert np.array_equal(A @ np.eye(4), A)

def test_multiplying_identity_matrix_by_tuple():
    a = Grid(1, 2, 3, 4)
    m = Matrix(np.eye(4))
    assert m @ a == a

def test_transposing_matrix():
    A = Matrix.from_string(
        """
        | 0 | 9 | 3 | 0 |
        | 9 | 8 | 0 | 8 |
        | 1 | 8 | 5 | 3 |
        | 0 | 0 | 5 | 8 |
        """
    )

    expected = Matrix.from_string(
        """
        | 0 | 9 | 1 | 0 |
        | 9 | 8 | 8 | 0 |
        | 3 | 0 | 5 | 5 |
        | 0 | 8 | 3 | 8 |
        """
    )

    assert np.array_equal(A.T, expected)

def test_transposing_identity_matrix():
    A = np.eye(4).T
    assert np.array_equal(A, np.eye(4))

def test_calculating_inverse_of_another_matrix():
    A = Matrix.from_string(
        """
        | 8 | -5 | 9 | 2 |
        | 7 | 5 | 6 | 1 |
        | -6 | 0 | 9 | 6 |
        | -3 | 0 | -9 | -4 |
        """
    )

    expected = Matrix.from_string(
        """
        | -0.15385 | -0.15385 | -0.28205 | -0.53846 |
        | -0.07692 | 0.12308 | 0.02564 | 0.03077 |
        | 0.35897 | 0.35897 | 0.43590 | 0.92308 |
        | -0.69231 | -0.69231 | -0.76923 | -1.92308 |
        """
    )

    assert np.allclose(A.inv, expected, 1e-03, 1e-03)

def test_calculating_inverse_of_third_matrix():
    A = Matrix.from_string(
        """
        | 9 | 3 | 0 | 9 |
        | -5 | -2 | -6 | -3 |
        | -4 | 9 | 6 | 4 |
        | -7 | 6 | 6 | 2 |
        """
    )

    expected = Matrix.from_string(
        """
        | -0.04074 | -0.07778 | 0.14444 | -0.22222 |
        | -0.07778 | 0.03333 | 0.36667 | -0.33333 |
        | -0.02901 | -0.14630 | -0.10926 | 0.12963 |
        | 0.17778 | 0.06667 | -0.26667 | 0.33333 |
        """
    )

    assert np.allclose(A.inv, expected, 1e-03, 1e-03)

def test_multiplying_product_by_its_inverse():
    A = Matrix.from_string(
        """
        | 3 | -9 | 7 | 3 |
        | 3 | -8 | 2 | -9 |
        | -4 | 4 | 4 | 1 |
        | -6 | 5 | -1 | 1 |
        """
    )

    B = Matrix.from_string(
        """
        | 8 | 2 | 2 | 2 |
        | 3 | -1 | 7 | 0 |
        | 7 | 0 | 5 | 4 |
        | 6 | -2 | 0 | 5 |
        """
    )

    C = A @ B

    assert C @ B.inv == A


import random
import csv
import sys
import numpy as np

class LinearRegression:
    def __init__(self, x_train, y_train):

        self.theta = np.zeros(x_train.shape[1] + 1)
        self.x_train = np.ones((x_train.shape[0], x_train.shape[1] + 1))
        self.x_train[:, 1:] = x_train
        self.y_train = y_train
        self.N = x_train.shape[0]

    def h(self):
        return (self.theta * self.x_train).sum(axis=1)

    def cost(self):
        cost = ((self.h() - self.y_train) ** 2).sum()
        cost = cost / (2 * self.N)
        return cost

    def gradient(self):
        gradient = ((self.h() - self.y_train) * self.x_train.T).sum(axis=1)
        gradient = gradient / self.N
        return gradient

    def standardization(self):
        self.x_train[:, 1:] = (self.x_train[:, 1:] - self.x_train[:, 1:].mean(axis=0)) / self.x_train[:, 1:].std(axis=0)

    def fit(self, n, alpha):
        for i in range(n):
            self.theta -= alpha * self.gradient()  # descent direction

    def getParameters(self):
        return self.theta

string = sys.argv

name_input = string[1]
name_output = string[2]

with open(name_input, "r") as f:

    ff = csv.reader(f)
    data = []
    for row in ff:
        data.append(row)

data = np.array(data).astype(float)

x_train = data[:, :-1]
y_train = data[:, -1]

N = 100
alphas = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10]

with open(name_output, "w", newline="") as fp:

    for alpha in alphas:

        lr = LinearRegression(x_train, y_train)
        lr.standardization()
        lr.fit(N, alpha)
        params = lr.getParameters()

        fp.write(str(alpha) + "," + str(N) + "," + str(params[0]) + "," + str(params[1]) + "," + str(params[2]) + "\n")

    lr = LinearRegression(x_train, y_train)
    lr.standardization()
    lr.fit(10000, 0.01)
    params = lr.getParameters()
    fp.write(str(0.01) + "," + str(100000) + "," + str(params[0]) + "," + str(params[1]) + "," + str(params[2]))


# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/01_MovingAverage.ipynb (unless otherwise specified).

__all__ = ['simple', 'weighted', '__moving_average', 'SMA', 'WMA']

# Cell
import numpy as np
from numba import jit
import pandas as pd

from time_series_model_basics import simulate_data

# Cell
@jit(nopython=True)
def simple(
    ts,
    n=1,
):

    l = len(ts)
    if n > l:
        raise Exception('n should be less than the length of the time-series')

    f = np.full((l,), np.nan)

    for i in range(l - n + 1):
        f[n + i] = np.mean(ts[i:n + i])
    return f

# Cell
#@jit(nopython=True)
def weighted(
    ts,
    ws,
):
    if type(ws) is list:
        ws = np.array(ws, dtype=float)

    l = len(ts)
    n = len(ws)
    w = np.sum(ws)

    if n >= l:
        raise Exception('The length of ws should be less than the length of ts')

    f = np.full((l,), np.nan)
    #f[:n] = np.full(n, np.nan)

    for i in range(l - n ):
        f[n + i] = (1 / w) * np.dot(ws, ts[i:n + i])
    return f

# Cell
def __moving_average(
    *args,
    kind='simple',
    df=None,
    ts_col=None,
    **kwargs,
):
    """
    Forecasts  with a Moving Average

    -----
    Parameters
    -----

    kind : str , default 'simple'. It can be:

         'simple' : Simple Moving Average.
         'weighted' : Weighted Moving Average.
    df : DataFrame, default None. If df is None a dataframe
         with simulated data is generated.

    ts_col : str, default None. The column name with the
         time series. Set to 'time_series' when df is None.
    *args : int.  Each value represent a Moving
           Average Forecast and its corresponding
           window size
    **kwargs : passed to __pandas_time_series

    -------
    Returns
    -------

    tuple: First element is the Pandas Dataframe and the second
           is ploty's figure object
    """

    if len(args) == 0:
        raise Exception("list in *args should include at least one value")

    if kind not in ('simple', 'weighted'):
        raise Exception("Only supported methods are: 'simple', 'weighted'")

    if df is None:
        df = simulate_data.pandas_time_series(**kwargs)
        ts_col = 'time_series'

    if ts_col is None:
        raise Exception('When df is not None, ts_col should be specified')

    ts = df[ts_col].to_numpy()
    cols = [ts_col]
    for a in args:
        col = f'{kind}_{a}'
        cols.append(col)
        if kind == 'simple':
            df[col] = simple(ts=ts, n=a)
        elif kind == 'weighted':
            df[col] = weighted(ts=ts, ws=a)

    fig = df[cols].plot(
        backend='plotly',
        title=f'Moving Average: {kind}',
    )

    fig.update_layout(template="plotly_dark",)

    return df, fig

# Cell
def SMA(*args, df=None, ts_col=None, **kwargs):
    """
    Forecasts  with Simple Moving Average

    -----
    Parameters
    -----

    df : DataFrame, default None. If df is None a dataframe
         with simulated data is generated.
    ts_col : str, default None. The column name with the
         time series. Set to 'time_series' when df is None.
    *args : int.  Each value represent a Moving
           Average Forecast and its corresponding
           window size
    **kwargs : passed to __pandas_time_series

    -------
    Returns
    -------

    tuple: First element is the Pandas Dataframe and the second
           is ploty's fig object
    """

    return __moving_average(*args,
                            kind='simple',
                            df=df,
                            ts_col=ts_col,
                            **kwargs)

# Cell
def WMA(*args, df=None, ts_col='time_series', **kwargs):
    """
    Forecasts with Weighted Moving Average

    -----
    Parameters
    -----
    df : DataFrame, default None. If df is None a dataframe
         with simulated data is generated.
    ts_col : str, default 'time_series'. The column name with the
         time series. Ignored if df is None.
    *args : int.  Each value represent a Moving
           Average Forecast and its corresponding
           window size
    **kwargs : passed to __pandas_time_series
    -------
    Returns
    -------
    tuple: First element is the Pandas Dataframe and the second
           is ploty's fig object
    """

    return __moving_average(*args,
                            kind='weighted',
                            df=df,
                            ts_col=ts_col,
                            **kwargs)


# beta_plot.py

import numpy as np
from scipy.stats import beta
import matplotlib.pyplot as plt
import seaborn as sns

if __name__ == "__main__":
    sns.set_palette("deep", desat=.6)
    sns.set_context(rc={"figure.figsize": (8, 4)})
    x = np.linspace(0, 1, 100)
    params = [
        (0.5, 0.5),
        (1.0, 1.0),
        (4.0, 3.0),
        (2.0, 5.0),
        (6.0, 6.0)
    ]
    for p in params:
        y = beta.pdf(x, p[0], p[1])
        a = p[0]
        b = p[1]
        u = a/(a+b)
        s = ((a*b)/((a+b)**2*(a+b+1)))**0.5
        plt.plot(x, y, label="$\\alpha=%.4f$, $\\beta=%.4f$, $\\mu=%.4f$, $\\sigma=%.4f$" % (a,b,u,s))
    plt.xlabel("$\\theta$, Fairness")
    plt.ylabel("Density")
    plt.legend(title="Parameters")
    plt.show()


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

import numpy as np
import sympy as sym
import pytest

import src.fem_base.master.master_1D as M1D
import src.handlers.dof_handler_1D as DOFH
import src.msh.mesh_1D as MSH
import src.fem_maps.fem_map as MAP
import src.fem_base.compute_errors as ERRORS

def analytical_L2_norm(expr, Ω):
    """ symbolically compute L2 norm of expr
    @param expr  sympy expression using x as symbol
    @param Ω  tuple describing domain
    """
    x = sym.Symbol('x')
    integrand = sym.Abs(expr)**2
    integral = float(sym.integrate(integrand, (x, *Ω)))
    return np.sqrt(integral)

def computational_domain(p, n_elm, Ω=(0, 3)):
    """ generate a 1D computational domain """
    mesh = MSH.Mesh1D(P=np.linspace(*Ω, n_elm+1))
    master = M1D.Master1D(p=p)
    dofh = DOFH.DG_DOFHandler1D(mesh, master)
    mapdgn = np.zeros((dofh.dgnodes.shape[0], 1, dofh.dgnodes.shape[1]))
    mapdgn[:,0,:] = dofh.dgnodes
    _map = MAP.Isoparametric_Mapping(master=[master], dgnodes=[mapdgn], map_nodes='NODAL')
    return mesh, master, dofh, _map

@pytest.mark.parametrize("p", [1, 3, 10])
def test_compute_domain_volume(p):
    """ compute volume of computational domain """
    Ω = (-2, 3)
    mesh, master, dofh, _map = computational_domain(p=p, n_elm=4, Ω=Ω)
    u, detJ = np.ones_like(dofh.dgnodes), _map._detJ[0]
    domain_volume = np.sum(np.dot(master.M, detJ*u))
    assert np.isclose(domain_volume, 5.0)

# expected output generated with analytical_L2_norm above
@pytest.mark.parametrize("p, expected", [
        (1, 3.0),
        (2, 6.971370023173351),
        (5, 126.90261119170373),
        (10, 22318.42416672186)
])
def test_compute_polynomial_L2_domain_integral(p, expected):
    """ compute the L2 norm of x**p integrated over Ω """
    Ω = (0, 3)
    mesh, master, dofh, _map = computational_domain(p=p, n_elm=4, Ω=Ω)
    u_h = dofh.dgnodes ** p
    dom_integral = ERRORS._1D_L2_domain_integral(u_h, dofh, _map, master)
    assert np.isclose(dom_integral, expected)


# Importação das Livrarias
import numpy as np
import matplotlib.pyplot as plt

# TODO:
# Passo 0: Inicializar o vector de peso e o enviesamento com zeros (ou pequenos valores aleatórios).
# Passo 1: Calcular uma combinação linear das características e pesos de entrada.
# Passo 2: Aplicar a função sigmoide, que retorna valores binários
# Passo 3: Calcular actualizações de peso usando as regras do perceptron.
# Passo 4: Actualizar os pesos e o enviesamento.

class perceptron:
    """
    Perceptron: Nerónio artificial para classificação binária
    ...
    Parâmetros:

    n_interações -- número de épocas do algoritmo
    learning_rate -- taxa de aprendizagem
    min_error -- mínima alteração de erro

    """

    def __init__(self, n_iterations=100, learning_rate=0.01, min_error=0):
        # Iniciar pesos e enviesamento.
        self.iterations = n_iterations
        self.learning = learning_rate
        self.activation_function = self.sigmoid
        self.weights = None
        self.min_error = min_error
        # self.prob = 0.5

    def fit(self, X, y):
        # Função de Treino
        n_samples, n_features = X.shape
        self.weights = np.zeros(n_features)
        self.bias = 0.5
        y_real = np.array(y)

        count = 0
        sum_error = 0
        error_list = []
        error_dif = 0

        for _ in range(self.iterations):
            for index, xi in enumerate(X):
                predicted_value = self.predict(xi)
                update = self.learning * (y_real[index] - predicted_value)

                self.weights += update * xi
                self.bias += update

                error = y_real[index] - predicted_value
                sum_error += error ** 2

            error_list.append(sum_error)
            try:
                error_dif = abs(error_list[-2] - error_list[-1])
            except:
                error_dif = error_list[0]
                pass
            count += 1

            print(f"Iteração n. {count} com erro de {error_dif}")
            if error_dif <= self.min_error:
                break

    def predict(self, X):
        # Função de Previsão
        activation = np.dot(X, self.weights) + self.bias
        y_predicted = self.activation_function(activation)
        # y_predicted = np.where(self.activation_function(activation)>self.prob,1,0)
        return y_predicted

    def sigmoid(self, X):
        # Função de Ativação (0-1)
        return 1 / (1 + np.exp(-X))

    def scale(self, X, x_min, x_max):
        # Função de normalização dos dados.
        nom = (X - X.min(axis=0)) * (x_max - x_min)
        denom = X.max(axis=0) - X.min(axis=0)
        denom[denom == 0] = 1
        return x_min + nom / denom


#%%
"""
Created on July 12 2021
Monte Carlo paths for the Hull-White model

This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak
The course is based on the book “Mathematical Modeling and Computation
in Finance: With Exercises and Python and MATLAB Computer Codes”,
by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019.
@author: Lech A. Grzelak
"""
import numpy as np
import matplotlib.pyplot as plt

def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta):
    # time-step needed for differentiation
    dt = 0.0001
    f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt)

    # Initial interest rate is a forward rate at time t->0
    r0 = f0T(0.00001)
    theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + \
    eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t))

    Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps])
    W = np.zeros([NoOfPaths, NoOfSteps+1])
    R = np.zeros([NoOfPaths, NoOfSteps+1])
    R[:,0]=r0
    time = np.zeros([NoOfSteps+1])

    dt = T / float(NoOfSteps)
    for i in range(0,NoOfSteps):
        # making sure that samples from normal have mean 0 and variance 1
        if NoOfPaths > 1:
            Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i])
        W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i]
        R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i])
        time[i+1] = time[i] +dt

    # Outputs
    paths = {"time":time,"R":R}
    return paths

def mainCalculation():
    NoOfPaths = 1
    NoOfSteps = 5000
    T         = 50.0
    lambd     = 0.5
    eta       = 0.01

    # We define a ZCB curve (obtained from the market)
    P0T = lambda T: np.exp(-0.05*T)

    # Effect of mean reversion lambda
    plt.figure(1)
    legend = []
    lambdVec = [-0.01, 0.2, 5.0]
    for lambdTemp in lambdVec:
        np.random.seed(2)
        Paths = GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambdTemp, eta)
        legend.append('lambda={0}'.format(lambdTemp))
        timeGrid = Paths["time"]
        R = Paths["R"]
        plt.plot(timeGrid, np.transpose(R))
    plt.grid()
    plt.xlabel("time")
    plt.ylabel("R(t)")
    plt.legend(legend)

    # Effect of the volatility
    plt.figure(2)
    legend = []
    etaVec = [0.1, 0.2, 0.3]
    for etaTemp in etaVec:
        np.random.seed(2)
        Paths = GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, etaTemp)
        legend.append('eta={0}'.format(etaTemp))
        timeGrid = Paths["time"]
        R = Paths["R"]
        plt.plot(timeGrid, np.transpose(R))
    plt.grid()
    plt.xlabel("time")
    plt.ylabel("R(t)")
    plt.legend(legend)

mainCalculation()

Lecture 03-The HJM Framework/Materials/Hull-White-Paths.py