# Code by Desh Iyer

# ----------------------------------------------------------------

# PROBLEM STATEMENT

# Import the z table as a data structure in python. Create some 5 demo examples to find the area under the curve and report the answer from the z-table.

# ----------------------------------------------------------------

# SOLUTION

# Consider the situation:
# The scores - the number of which are determined by user input - on an exam are normally distributed with a mean of 65 and a std of 9
# Find the percentage of scores,
    # (i). less than 54
    # (ii). less than 20
    # (iii). at least 80
    # (iv). at least 90
    # (v.) between 40 and 50

# Import libs
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

# Function to plot the graphs of both generated lists of data
def pltGraphs(normallyDistributedData, zDistributedData):
    # Plot two graphs
    plt.title("Normal distribution w/ Mean: 65 & Std. Deviation: 9")
    plt.xlabel('x')
    plt.ylabel('Normal Distribution')
    plt.grid()
    plt.hist(normallyDistributedData)
    plt.show()

    plt.title("Z - Distribution for above graph")
    plt.xlabel('x')
    plt.ylabel('Standard Normal / Z - Distribution')
    plt.grid()
    plt.hist(zDistributedData)
    plt.show()

# Function to generate the lists of normalized and z-converted data
def generateNormalDataAndConvertToStdNormal():
    # Take user input for number of samples
    numberSamples = int(input("Enter number of samples: "))

    # Generate a normal distribution with arbitrary mean 65 and std 9
    normallyDistributedData = np.random.normal(65, 9, numberSamples)

    # Calculate summary stats
    normalMean = np.mean(normallyDistributedData)
    normalStd = np.std(normallyDistributedData)

    # Convert the above generated normally distributed data to Z-distributed data using list comprehension
    zDistributedData = [calcZScore(i, normalMean, normalStd) for i in normallyDistributedData]

    # Print the lists
    print("Normally Distributed Data:")
    print(normallyDistributedData)

    print("\nStandardized Normally Distributed Data (Z):")
    print(zDistributedData)

    # Plot lists using aforementioned function
    pltGraphs(normallyDistributedData, zDistributedData)

# Simple function to return the z-Score of a given value
def calcZScore(x, mean, std):
    return ((x - mean) / std)

# The main meat of the program
# A function to return the value in the z-Table provided the true value as per the question
# This function first converts the value into a z-Score and then finds the cell in the z-Table correspoinding to the z-Score calculated
def returnAreaUnderCurve(x, zTable):
    # Calc z-Score
    zScore = np.round(calcZScore(x, 65, 9), 2)
    displayZScore = np.round(calcZScore(x, 65, 9), 5)

    # Calculate z-Table indices
    rowIndex = np.round(zScore, 1)
    strRowIndex = str(rowIndex)

    colIndex = ((zScore * 100) % 10) / 100 if zScore > 0 else ((-zScore * 100) % 10) / 100
    strColIndex = str(colIndex)

    # Attain cell and store its value
    zTableValue = float(zTable.at[strRowIndex, strColIndex])

    # Print data
    print(f"Z-Score = {displayZScore}")
    print(f"Reading at ({rowIndex}, {colIndex}) = {zTableValue}")

    # Return the cell value
    return zTableValue

# Function that processes questions of "Score < " type
def calculateAndPrintLessThan(reqValue, zTable):
    reqArea = returnAreaUnderCurve(reqValue, zTable)

    print(f'The area under the graph = {np.round(reqArea * 100, 10)}%')

# Function that processes questions of "Score at least" type
def calculateAndPrintAtLeast(reqValue, zTable):
    lessThanArea = returnAreaUnderCurve(reqValue, zTable)
    reqArea = 1 - lessThanArea

    print(f'The required area = 1 - {lessThanArea} = {reqArea}\nThe area under the graph = {np.round(reqArea * 100, 10)}%')

# Function that processes questions of "Score in between" type
def calculateAndPrintBetween(firstReqValue, secondReqValue, zTable):
    firstReading = returnAreaUnderCurve(firstReqValue, zTable)
    secondReading = returnAreaUnderCurve(secondReqValue, zTable)

    inBetweenReading = abs(firstReading - secondReading)

    print(f'The required area in between {firstReqValue} and {secondReqValue} = {np.round(inBetweenReading * 100, 10)}%')

# Function to display all the sample questions, call the respective function to calculate the cell value and display results
def calculateCases(zTable):
    print("\n\nPROBLEM STATEMENT:\nConsider the situation:\nThe scores - the number of which are determined by user input - on an exam are normally distributed with a mean of 65 and a std of 9 Find the percentage of scores,\n(i). less than 54\n(ii). less than 20 \n(iii). at least 80\n(iv). at least 90\n(v.) between 40 and 50")

    # Case (i). -> % less than 54
    print("\n\nCase (i). -> percentage less than 54")
    calculateAndPrintLessThan(54, zTable)

    # Case (ii). -> % less than 20
    print("\n\nCase (ii). -> percentage less than 20")
    calculateAndPrintLessThan(20, zTable)

    # Case (iii). -> % at least 80 -> 1 - (% less than 80)
    print("\n\nCase (iii). -> % at least 80 -> 1 - (% less than 80)")
    calculateAndPrintAtLeast(80, zTable)

    # Case (iv). -> % at least 90 -> 1 - (% less than 90)
    print("\n\nCase (iv). -> % at least 90 -> 1 - (% less than 90)")
    calculateAndPrintAtLeast(90, zTable)

    # # Case (v). -> % between 40 and 50
    print("\n\nCase (v). -> between 40 and 50")
    calculateAndPrintBetween(40, 50, zTable)

# Main driver function
def main():
    # Read the zTable from the csv file containing details of the zTable in comma separated format & set the first column as the index col
    zTable = pd.read_csv(r"C:\Users\Volt\Code\B.Tech-CSE-Y2\applied-statistics\lab\experiment-5\zTable.txt", index_col=0)

    print("The generated z-Table is:\n", zTable)

    zTable = zTable.astype(str)
    zTable.index = zTable.index.map(str)

    generateNormalDataAndConvertToStdNormal()
    calculateCases(zTable)

# Invoke main()
if __name__ == "__main__":
    main()


# -*- coding: utf-8 -*-
"""
Created on Mon Feb 22 17:08:24 2021

@author: Chi Ding

Conjugate Gradient:
    Ax = b

    A: Hilbert matrix
    x: coefficients
    b: ones vector
"""

import numpy as np

# dimensionality for the system
D = 5,8,12,20

# stopping criterion
threshold = 1e-6

A = []

x = []

for n in D:

    k = 0

    A.append(
                np.column_stack([1/(i+j-1)
                         for i in range(1,n+1)
                         for j in range(1,n+1)]).reshape(n,n)
                )

    b = np.ones(n)

    # initial point
    x.append(np.zeros(n))

    # initial residual/gradient
    r = A[-1] @ x[-1] - b

    p = -r

    while True:
        """
            The conjugate gradient algorithm
        """
        r2 = r @ r
        alpha = r2 / (p @ A[-1] @ p)
        x[-1] = x[-1] + alpha*p
        r_ = r + alpha * A[-1] @ p
        beta = (r_ @ r_)/r2
        p = -r_ + beta * p
        r = r_.copy()
        k += 1

        if (r @ r)**0.5 < threshold:
            break

    print('dimension',n,'steps',k)


#!/usr/bin/env python
# Wenchang Yang (wenchang@princeton.edu)
# Mon Mar  7 17:12:07 EST 2022
if __name__ == '__main__':
    import sys
    from misc.timer import Timer
    tt = Timer('start ' + ' '.join(sys.argv))
import sys, os.path, os, glob, datetime
#import xarray as xr, numpy as np, pandas as pd, matplotlib.pyplot as plt
import numpy as np
from numpy import pi, sin, cos, arctan2
#more imports
#
if __name__ == '__main__':
    tt.check('end import')
#
#start from here
def gcdistance(lat1, lon1, lat2, lon2, units='m'):
    """
    From: https://stackoverflow.com/questions/19412462/getting-distance-between-two-points-based-on-latitude-longitude

    This uses the ‘haversine’ formula to calculate the great-circle distance between two points – that is,
    the shortest distance over the earth’s surface – giving an ‘as-the-crow-flies’ distance between the points
    (ignoring any hills they fly over, of course!).
    Haversine
    formula:    a = sin²(Δφ/2) + cos φ1 ⋅ cos φ2 ⋅ sin²(Δλ/2)
    c = 2 ⋅ atan2( √a, √(1−a) )
    d = R ⋅ c
    where   φ is latitude, λ is longitude, R is earth’s radius (mean radius = 6,371km);
    note that angles need to be in radians to pass to trig functions!
    """
    R = 6371.0088 * 1000
    #lat1,lon1,lat2,lon2 = map(np.radians, [lat1,lon1,lat2,lon2])
    lat1 = lat1*pi/180
    lon1 = lon1*pi/180
    lat2 = lat2*pi/180
    lon2 = lon2*pi/180

    dlat = lat2 - lat1
    dlon = lon2 - lon1
    a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2) **2
    c = 2 * arctan2(a**0.5, (1-a)**0.5)
    d = R * c

    if units == 'km':
        d = d/1000
    elif units == 'mile':
        d = d/1000*0.621371
    return d

def tc_distance(ds, lat0, lon0, units='m'):
    """use gcdistance to calculate great-circle distance from TC track points in ds to (lat0, lon0)"""
    return gcdistance(ds.lat, ds.lon, lat0, lon0, units=units)

if __name__ == '__main__':
    #from wyconfig import * #my plot settings
    # https://andrew.hedges.name/experiments/haversine/
    lat1, lon1, lat2, lon2 = 38.898556, -77.037852, 38.897147, -77.043934
    d = gcdistance(lat1, lon1, lat2, lon2)
    d_km = gcdistance(lat1, lon1, lat2, lon2, units='km')
    d_mile = gcdistance(lat1, lon1, lat2, lon2, units='mile')

    print(f'{d = } m: {lat1 = }, {lon1 = }, {lat2 = }, {lon2 = }')
    print(f'{d_km = } km: {lat1 = }, {lon1 = }, {lat2 = }, {lon2 = }')
    print(f'{d_mile = } mile: {lat1 = }, {lon1 = }, {lat2 = }, {lon2 = }')

    print()
    lat1, lon1, lat2, lon2 = 0, 0, np.array([0, 0, 0, 0, 90, 90, -90, -90]), np.array([90, 180, 270, 360, 0, 180, 0, 180])
    d = gcdistance(lat1, lon1, lat2, lon2, units='km')
    print(f'{d = } km: {lat1 = }, {lon1 = }, {lat2 = }, {lon2 = }')
    for ii,(lat_, lon_) in enumerate(zip(lat2, lon2)):
        print(f'{d[ii] = :,} km: {lat1 = }, {lon1 = }, {lat_ = }, {lon_ = }')

    #savefig
    if len(sys.argv)>1 and 'savefig' in sys.argv[1:]:
        figname = __file__.replace('.py', f'.png')
        if 'overwritefig' in sys.argv[1:]:
            wysavefig(figname, overwritefig=True)
        else:
            wysavefig(figname)
    tt.check(f'**Done**')
    print()
    #plt.show()


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

import numpy as np
import sympy as sp
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

__all__ = ['contour','contourf']

def contour ( expr , z , Ir , Ii , **kwargs):
  r"""
    function: `contour`
    brief: draw a contour of a complex expression
    params:
      - `expr`:`sympy.Expr` : complex expression to draw
      - `z`:`sympy.Symbol` : symbol on which the expression depends
      - `Ir`:`typle(3)` : real interval to draw
      - `Im`:`typle(3)` : imaginary interval to draw
      - `**kwargs`:`dict` : `pyplot.contour` optional arguments
  """
  import matplotlib.colors as mcolors # for mcolors.TABLEAU_COLORS
  palette = list(mcolors.TABLEAU_COLORS)

  x,y = sp.symbols("x y",real=True)
  f = sp.lambdify((x, y), expr.subs(z,x+sp.I*y),'numpy')

  a = np.linspace(*Ir)
  b = np.linspace(*Ii)
  X, Y = np.meshgrid(a, b)
  try:
    lab = kwargs.pop("label")
  except:
    lab = ""
  try:
    c = kwargs["colors"]
  except:
    c = palette[0]
  if len(lab) > 0 :
    plt.plot([0],[0],label=lab,color=c)
  return plt.contour(X,Y, np.real(f(X,Y)) ,**kwargs)

def contourf ( expr , z , Ir , Ii , **kwargs):
  r"""
    function: `contour`
    brief: draw a contour filled of a complex expression
    params:
      - `expr`:`sympy.Expr` : complex expression to draw
      - `z`:`sympy.Symbol` : symbol on which the expression depends
      - `Ir`:`typle(3)` : real interval to draw
      - `Im`:`typle(3)` : imaginary interval to draw
      - `**kwargs`:`dict` : `pyplot.contourf` optional arguments
  """
  import matplotlib.colors as mcolors # for mcolors.TABLEAU_COLORS
  palette = list(mcolors.TABLEAU_COLORS)

  x,y = sp.symbols("x y",real=True)
  f = sp.lambdify((x, y), expr.subs(z,x+sp.I*y),'numpy')

  a = np.linspace(*Ir)
  b = np.linspace(*Ii)
  X, Y = np.meshgrid(a, b)
  try:
    lab = kwargs.pop("label")
  except:
    lab = ""
  try:
    c = kwargs["colors"]
  except:
    c = palette[0]
  if len(lab) > 0 :
    plt.plot([0],[0],'s',markersize=1,label=lab,color=c)
  return plt.contourf(X,Y, np.real(f(X,Y)),**kwargs)

def reim(b):
  """
    function to split into 2 arrays real part and imaginary part of complex array
  """
  return ([z.real for z in b],[z.imag for z in b])

def plot(expr,x,I,*args,**kwargs):
  """
    just plot a SymPy expression `expr` of variable `x` on the interval `I`
  """
  X = np.linspace(I[0],I[1],500)
  F = sp.lambdify(x,expr,'numpy')
  plt.plot(X,F(X),*args,**kwargs)

def latex_aligned(list_eq):
  r"""
    function: `latex_aligned`
    brief: return a LaTeX string in `aligned` environnement of a list
     of equations
    params:
      - `list_eq`:`list(sympy.Eq)` : list of `sympy` equations
  """
  return r"\begin{aligned}" + r"\\".join([ sp.latex(eq.lhs) + r"&=" + sp.latex(eq.rhs) for eq in list_eq ]) + r"\end{aligned}"


"""
数値計算に使用する関数など。
"""
import numpy
import collections

def calc_divisor(a: int, include_one: bool) -> 'tuple[int]':
    """
    指定の値の約数を求める。
    自明な約数も含む。
    厳密な意味での約数とは異なるが、include_one=Trueの場合には1を含める。

    Parameters
    ----------
    a : int
        正の整数。
    should_include_one : bool
        約数の一覧に1を含めるかどうか。
        True:
            1を含める。
        False:
            1を含めない。

    Returns
    -------
    'tuple[int]'
        約数の一覧。
        降順に並ぶ。

    """
    div_set = set()
    div = 1 if include_one else 2
    div_max = numpy.sqrt(a)
    while div <= div_max:
       quot,mod = divmod(a,div)
       if mod == 0:
           div_set = div_set | {div,quot}
       div += 1
    return tuple(sorted(list(div_set),reverse=True))

def prime_factorize(n: int):
    """
    正の整数を素因数分解する。

    Parameters
    ----------
    n : int
        正の整数。

    Returns
    -------
    TYPE
        DESCRIPTION.

    """
    prime_list = []
    while (n % 2 == 0):
        prime_list.append(2)
        n //= 2
    f = 3
    while (f * f <= n):
        if (n % f == 0):
            prime_list.append(f)
            n //= f
        else:
            f += 2
    if n != 1: prime_list.append(n)
    return collections.Counter(prime_list)


# coding: utf-8

# # Numpy
#
# * Python extensions for manipulating large sets of objects organised in a grid-like fashion
#   * Vectors
#   * Matrices
#   * etc...
# * Normal Python datastructures too slow
#

# # Using numpy

# In[2]:

import numpy as np

# # Array objects
#
# * Homogeneous collection of large numbers of numbers
#   * Numbers of the same type
#   * Array objects must be full
#   * Size immutable
#   * Numbers can change
# * size = total number of elements in the array (Does not change)
# * shape = number of dimensions
#

# # Array objects
#
# * rank = len(shape)
# * typecode = single character identifying the element kind
#   * Number format (i, d etc)
#   * Character
#   * Python reference
# * itemsize = number 8-bit bytes representing a single element
#

# # Creating arrays from scratch

# Creating arrays with explicit type codes:

# In[4]:

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

# In[3]:

b = np.array([5, 4, 3], int)
print(b)

# When creating arrays without type codes, Python will figure out and select type from the provided data.

# In[4]:

c = np.array([1.0, 1.5, 3.0])
print(c)

# In[5]:

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

# # Multidimensional arrays

# In[6]:

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

# In[7]:

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

# The shape of the array can be queried using the shape attribute:

# In[8]:

print(a.shape)

# In[10]:

print(b.shape)
r, c = b.shape
print(r, c)

# # Reshaping arrays

# In[11]:

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

# In[12]:

a_flat = np.reshape(a, [4,])
print(a_flat)

# In[13]:

a_flat = np.reshape(a, [1,4])
print(a_flat)

# In[14]:

a_flat = np.reshape(a, [4,1])
print(a_flat)

# In[15]:

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

# In[16]:

b_shaped = np.reshape(b, [8,])
print(b_shaped)

# In[17]:

b_shaped = np.reshape(b, [4,2])
print(b_shaped)

# **Please note** this is not the same as:

# In[18]:

b_trans = np.transpose(b)
print(b_trans)

# # Growing an array

# In[19]:

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

# In[20]:

b = np.array([a,a])
print(b)

# In[21]:

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

# In[22]:

big = np.resize(base, [9,9])
print(big)

# In[23]:

big = np.resize(base, [4,4])
print(big)

# In[24]:

big = np.resize(base, [4,2])
print(big)

# # Arrays on the fly

# In[25]:

a = np.zeros([4,4])
print(a)

# In[26]:

b = np.ones([5,10], float)
print(b)

# In[27]:

a = np.arange(10)
print(a)

# In[28]:

b = np.reshape(np.arange(100), [10,10])
print(b)

# In[29]:

a = np.arange(0,10)
print(a)

# In[30]:

a = np.arange(-10,10)
print(a)

# In[31]:

a = np.arange(-10,10,2)
print(a)

# There are different ways of creating arrays with specific numbers. However, it is important to do it in the correct way. First the slow way:

# In[32]:

a = np.array([[42]*5]*5)
print(a)

# First creating a zero array and then adding 42 is much faster:

# In[35]:

a = np.zeros([5,5])+42
print(a)

# Identity arrays can also be created using the identity() function.

# In[38]:

i = np.identity(10)
print(i)

# # Creating arrays with a linear variation

# In[39]:

x = np.linspace(0,1.0,10)
print(x)

# In[40]:

x = np.linspace(0,1.0,20)
print(x)

# # Operating on arrays

# Most python operators can be used with arrays. The following examples shows how it can be used.

# In[41]:

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

# Element wise addition.

# In[42]:

print(a+3)

# Element wise multiplication

# In[43]:

print(a*3)

# It is also possible to use normal mathematical functions with arrays.

# In[44]:

print(np.sin(a))

# Element wise negation using the - operator.

# In[45]:

print(-a)

# Arrays of the same number of elements can also be added together.

# In[46]:

print(a+a)

# Adding differently sized arrays is also possible. Values from the smaller array is continously added to the larger array as shown in this example:

# In[47]:

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

# In[48]:

print(a+b)

# # Getting and setting values

# Retrieving a single value:

# In[49]:

a = np.arange(10)
print(a)

# In[52]:

print(a[0])

# Retrieving a range of values:

# In[53]:

print(a[1:5])

# Please note that the range index is defined as [start:stop] and does not include the *stop* index.

# You can get all values of a list except the last one by using -1 as the last index.

# In[58]:

print(a[:-1])

# Using other negative numbers will retrieve other ranges:

# In[59]:

print(a[:-2])

# It is also possible to use arange() and reshape to create arrays with increasing numbers:

# In[60]:

a = np.arange(16)+1
print(a)

# In[61]:

b = np.reshape(a, [4,4])
print(b)

# It is also possible to retrieve rows and columns:

# In[62]:

print(b[0]) # row 0 of b

# In[63]:

print(b[:,1]) # column 1 of b

# In[64]:

print(b[-1]) # last row of b

# In[65]:

print(b[:,-2]) # column 2 of b

# In[66]:

print(b)

# Values can be assigned using index notation as well:

# In[67]:

b[0,0] = 42
print(b)

# Lists and arrays can be intermixed and used in assignments. Assign a row using the following statement:

# In[68]:

b[1] = [42,42,42,42]
print(b)

# Columns can be assigned in the same fashion:

# In[69]:

b[:,2] = [42,42,42,42]
print(b)

# # Slicing

# In[70]:

a = np.reshape(np.arange(16)+1,[4,4])
print(a)

# In[71]:

print(a[:,:])

# In[72]:

print(a[:,1])

# In[73]:

print(a[1,:])

# # Ufuncs

# * Operates elementwise on arrays
# * Available as callable objects (functions)
# * Can operate on Python sequences
# * Can take output arguments
# * Have special methods

# In[74]:

a = np.arange(10)
print(a)

# In[ ]:

print(np.add(a,a))

# In[ ]:

print(a+a)

# In[ ]:

print(np.sin(a))

# In[ ]:

print(add(a, range(10)))

# Ufuncs are often faster, but source code will be harder to read. Sometimes using a ufunc can eliminate a unecessary copying of large arrays. The following example shows how a ufunc is used to avoid this.

# In[75]:

a = np.arange(10)
a = a * 10 # This will make a copy of a
print(a)

# Again with a ufunc.

# In[76]:

a = np.arange(10)
np.multiply(a,10,a)
print(a)

# ## The add.reduce function (A quicker sum)

# add.reduce() can be used to sum (reduce) an array.

# In[ ]:

a = np.arange(10)
print(a)

# In[ ]:

print(np.add.reduce(a))

# This is faster than:

# In[ ]:

print(a.sum())

# ## add.accumulate

# In[ ]:

a = np.arange(10)
print(a)

# In[ ]:

print(np.add.accumulate(a))

# ## transpose
#
# To do a matrix-transpos of an array, the transpose() function can be used.

# In[ ]:

a = np.reshape(np.arange(16), [4,4])
print(a)

# In[ ]:

print(np.transpose(a))

# ## diagonal() and trace() functions

# In[77]:

a = np.reshape(np.arange(16), [4,4])
print(a)

# In[78]:

print(np.diagonal(a))

# In[79]:

print(np.trace(a))

# It is also possible to do traces left and right of the diagonal, by adding a parameter to the trace() function.

# In[80]:

print(np.trace(a,1))

# In[81]:

print(np.trace(a,-1))

# ## Matrix multiplication
#
# The * operator on array is equivalent to an elementwise multiplication of matrices. If you want to do a matrix multiplication on arrays, .dot() method or the @-operator can be used.

# In[82]:

a = np.reshape(np.arange(16), [4,4])
print(a)

# In[83]:

print(a.dot(a))

# # Exempel 1

# In[84]:

import math
import numpy as np

x = np.linspace(0.0, 2.0*math.pi, 20)
y = np.sin(x)

print(x)
print(y)

# # Exempel 2

# In[85]:

import pylab as pl

# In[86]:

get_ipython().magic('matplotlib inline')

# In[88]:

x = np.linspace(0.0, 2.0*math.pi, 20)
y = np.sin(x)

pl.plot(x,y)

# # Matrices in Numpy
#
# Arrays in Numpy are a generic array storage type and in itself not aware of any matrix operations. If this is desired Numpy provides an array derived type matrix, which provides this functionality. Some examples are illustrated below:

# In[89]:

A = np.matrix( [[1,2,3],[11,12,13],[21,22,23]])
x = np.matrix( [[1],[2],[3]] )
y = np.matrix( [[1,2,3]] )
print(A)

# In[90]:

print(x)

# In[91]:

print(y)

# In[92]:

print(A.T) # Matrix transpose

# In[93]:

print(A*x) # Matrix mulitply A * x

# In[94]:

print(A.I) # Matrix inverse

# In[95]:

print(np.linalg.solve(A,x)) # Solve A * y = x

# In[ ]:


import matplotlib as mpl
import matplotlib.pyplot as plt

import numpy as np

import scipy.stats as stats
from scipy.optimize import root, brentq
from scipy.interpolate import interp1d

__all__ = ['estimate_quantile']

def estimate_quantile(data, q, alpha=0.05, method='exact', weights=None, nsamples=10000):
    """Calculate lower and upper CI of a given quantile using exact method,
        based on beta distribution

        Alan D. Hutson (1999) Calculating nonparametric confidence intervals
            for quantiles using fractional order statistics, Journal of
            Applied Statistics, 26:3, 343-353, DOI: 10.1080/02664769922458

        Wei L, Wang D, Hutson AD. An Investigation of Quantile Function
            Estimators Relative to Quantile Confidence Interval Coverage.
            Commun Stat Theory Methods. 2015;44(10):2107-2135.
            doi: 10.1080/03610926.2013.775304. PMID: 26924881;
            PMCID: PMC4768491.

    Parameters
    ----------
    data : np.array
        Data
    q : float, [0, 1]
        Quantile
    alpha : float
        Desired significance level
    method :str
        "exact" or "approximate"

    Returns
    -------
    Lower and upper bound of the quantile
    """
    def _est_bound(n, q, b):
        """Function to estimate the upper and lower bound
        b is targeted lower or upper CI bound"""
        return brentq(lambda x: stats.beta.cdf(q, (n+1)*x, (n+1)*(1-x)) - b, 1e-8, 1-1e-8)
    if not weights is None:
        if method != 'bootstrap':
            print('Using bootstrap method to accomodate weights!')
        method = 'bootstrap'
    n = len(data)
    if q > (1 - 1e-7):
        q = 1 - 1e-7
    if q < 1e-7:
        q = 1e-7
    if method == 'exact':
        lb = _est_bound(n, q, 1 - (alpha/2))
        ub = _est_bound(n, q, alpha/2)
        estx, lx, ux = np.quantile(data, [q, lb, ub], interpolation='linear')
    elif method == 'approximate':
        pn = (n+1) * q
        qn = (n+1) * (1-q)

        lb = stats.beta.ppf(alpha/2, pn, qn)
        ub = stats.beta.ppf(1 - alpha/2, pn, qn)
        estx, lx, ux = np.quantile(data, [q, lb, ub], interpolation='linear')
    elif method == 'bootstrap':
        bsamp = np.zeros(nsamples)
        ndata = len(data)
        if weights is None:
            for i in range(nsamples):
                bsamp[i] = np.quantile(np.random.choice(data, size=ndata, replace=True), q)
            estx = np.quantile(data, q, interpolation='linear')
        else:
            w = weights / weights.sum()
            for i in range(nsamples):
                rind = np.random.choice(np.arange(ndata), size=ndata, replace=True)
                bsamp[i] = weighted_quantile(data[rind], q, weights=w[rind])
            estx = weighted_quantile(data, q, weights=w)

        lx, ux = np.quantile(bsamp, [alpha/2, 1 - alpha/2])
        lb, ub = np.nan, np.nan

    return estx, lx, ux, q, lb, ub

def ecdf(x, weights=None, reverse=True, make_step=False):
    """
    For reverse = True:
    Y is proportion of samples >= X or Pr(X>=x)

    For reverse = False:
    Y is proportion of samples <= X or Pr(X<=x)
    """
    if weights is None:
        weights = np.zeros(len(x))

    x = np.array(x, copy=True)
    x.sort()
    if reverse:
        x = x[::-1]
    nobs = len(x)
    y = np.linspace(1./nobs, 1, nobs)

    if make_step:
        x = np.concatenate(([x[0]], np.repeat(x[1:].ravel(), 2)))
        y = np.repeat(y.ravel(), 2)[:-1]
    return x, y

def weighted_quantile(data, q, inverse=False, weights=None, reverse=False):
    """
    q : quantile in [0-1]!
    weights
    inverse : bool
        If True then q is treated as a new data point and its corresponding quantile will be returned.
    https://stackoverflow.com/questions/21844024/weighted-percentile-using-numpy
    """
    if weights is None:
        weights = np.ones(len(data))
    ix = np.argsort(data)
    if reverse:
        ix = ix[::-1]
    data = data[ix] # sort data
    weights = weights[ix] # sort weights
    cdf = (np.cumsum(weights) - 0.5 * weights) / np.sum(weights) # 'like' a CDF function
    if not inverse:
        out = np.interp(q, cdf, data)
    else:
        out = np.interp(q, data, cdf, left=np.min(cdf)/2, right=1 - np.min(cdf)/2)
    return out

def plot_recdfs(data, quantiles=None, keys=None, logscale=True, make_step=False, alpha=0.05, method='exact', palette=None):
    """
    SLOW for large datasets because it computes the CI at every data point.
    Could easily speed this up if needed.
    """
    if keys is None:
        keys = data.keys()
    if palette is None:
        palette = mpl.cm.Set3.colors

    figh = plt.figure(figsize=(9, 7))
    axh = figh.add_axes([0.1, 0.1, 0.7, 0.8], xscale='log' if logscale else 'linear')

    for k, color in zip(keys, palette):
        dat = data[k]
        dat = dat[~np.isnan(dat)]

        x, y = ecdf(dat)

        if quantiles is None:
            qvec = y
        else:
            qvec = quantiles
        n = len(qvec)

        estx = np.zeros(n)
        lq = np.zeros(n)
        uq = np.zeros(n)
        for yi, yy in enumerate(qvec):
            estx[yi], lx, ux, estq, lq[yi], uq[yi] = estimate_quantile(dat, 1 - yy, alpha=alpha, method=method)
        plt.fill_between(estx, y1=1 - lq, y2=1 - uq, color=color, alpha=0.3)
        plt.plot(x, y, '-', color=color, label=k)
        plt.ylabel('Pr(X\u2265x)')
        plt.ylim((0, 1))
        plt.yticks(np.arange(11)/10)
        plt.legend(loc='upper left', bbox_to_anchor=[1, 1])
    return figh

def test_plot(n1=20, n2=10000):
    data = {'A1':np.random.normal(40, 5, size=n1),
                        'A2':np.random.normal(40, 5, size=n2),
                        'B1':np.random.lognormal(0.5, 0, size=n1),
                        'B2':np.random.lognormal(0.5, 0, size=n2)}
    """Plot AVG of 10 ECDFs based on n1 and see if it looks like n2 ECDF to check for bias"""
    xmat = []
    for i in range(5000):
        x, y = ecdf(np.random.normal(40, 5, size=n1))
        xmat.append(x[:,None])
    x1 = np.mean(np.concatenate(xmat, axis=1), axis=1)
    plt.figure(figsize=(10,10))
    plt.plot(x1, y)
    x2, y2 = ecdf(np.random.normal(40, 5, size=n2))
    plt.plot(x2, y2, '-r')
    plt.grid('both')
    plt.yticks(np.arange(21)/20)

    y2i = np.interp(x1, x2[::-1], y2[::-1])
    plt.figure(figsize=(10,10))
    plt.plot(x1, y2i - y, '-r')

    figh = plot_recdfs(data, keys=['A1', 'A2'], logscale=False)
    plot_recdfs(data, keys=['B1', 'B2'], logscale=False)

def test():
    # parama, paramb = 0.5, 0
    # dist = stats.lognorm
    parama, paramb = 40, 5
    dist = stats.norm
    frozen = dist.freeze(parama, paramb)
    nsamp = 40
    ssamps = 50000
    bootsamps = 10000
    qvec = np.linspace(0.02, 0.2, 10)
    alpha = 0.05

    res = []
    for q in qvec:
        """Simulated CI"""
        qsamps = np.zeros(ssamps)
        for i in range(ssamps):
            # qsamps[i] = np.quantile(pop[np.random.permutation(npop)[:nsamp]], q)
            qsamps[i] = np.quantile(frozen.rvs(nsamp), q)
        sest, slci, suci = np.quantile(qsamps, [0.5, alpha/2, 1-alpha/2])
        res.append({'quantile':q,
                    'method':'simulated',
                    'est':sest,
                    'lci':slci,
                    'uci':suci})

        for repi in range(1000):
            data = frozen.rvs(size=nsamp)
            """Bootstrap CI"""
            """
            best, blci, buci, qest, lb, ub = estimate_quantile(data, q, alpha=0.05, method='bootstrap', nsamples=bootsamps)
            res.append({'quantile':q,
                        'method':'bootstrap',
                        'est':best,
                        'lci':blci,
                        'uci':buci})
            """

            """Exact CI"""
            #lbq, ubq = quantile_ci(q, nsamp, alpha, method='exact')
            #elb, eub = frozen.ppf([lbq, ubq])
            eest, elci, euci, qest, lb, ub = estimate_quantile(data, q, alpha=0.05, method='exact')

            res.append({'quantile':q,
                        'method':'exact',
                        'est':eest,
                        'lci':elci,
                        'uci':euci})

        """Approximate CI"""
        #albq, aubq = quantile_ci(q, nsamp, alpha, method='approximate')
        #alb, aub = frozen.ppf([albq, aubq])

        """print(f'Sampled:   [{slb:1.2f}, {sub:1.2f}]')
        print(f'Bootstrap: [{blb:1.2f}, {bub:1.2f}]')
        print(f'Exact:     [{elb:1.2f}, {eub:1.2f}]')
        print(f'Exact est: [{lci:1.2f}, {uci:1.2f}]')
        print(f'Approx:    [{alb:1.2f}, {aub:1.2f}]')"""

    resdf = pd.DataFrame(res)
    summ = resdf.groupby(['method', 'quantile']).agg(np.mean).reset_index()

    plt.figure(figsize=(10,10))
    for method, color in zip(['simulated', 'exact'], ['crimson', 'dodgerblue', 'green']):
        tmp = summ.loc[summ['method'] == method]
        plt.plot(tmp['quantile'], tmp['est'], '-', color=color, label=method)
        plt.plot(tmp['quantile'], tmp['lci'], '--', color=color)
        plt.plot(tmp['quantile'], tmp['uci'], '--', color=color)
    plt.xlabel('Quantile')
    plt.ylabel('X (normally distributed random variable)')
    plt.legend(loc='lower right')


"""Symbolic routines

Author: Dr.-Ing. Mauricio Fernández

This module offers a collection of tensor routines in sympy.
"""

# %% Import

import sympy as spy
import numpy as np
from itertools import permutations

import tensor_routines as tr

# %% Generate tensors

def t(n: int, s: str = "a", dims: list = None) -> spy.Array:
    """Generate tensor.

    Generate n-th-order tensor based on string `s` for the
    components and dimensions `dims`.

    Parameters
    ----------
    n : int
        Tensor order
    s : str, optional
        Base string for components, by default "a"
    dims : list, optional
        List of tensor dimensions, by default None.
        * For `dims == None`, `dims` is set to `[3]*n`

    Returns
    -------
    spy.Array
        Tensor
    """
    if dims is None:
        dims = [3]*n
    head = f"{s}:"
    dims_pattern = ":".join([str(d) for d in dims])
    symbol_pattern = head + dims_pattern
    symbols = spy.symbols(symbol_pattern, real=True)
    return spy.Array(symbols, shape=dims)

# %% Transform

def flatten(a: spy.Array) -> spy.Array:
    """Flatten tensor

    Parameters
    ----------
    a : spy.Array
        Tensor

    Returns
    -------
    spy.Array
        Flattened tensor (row vector)
    """
    return a.reshape(spy.prod(a.shape))

def vec(a: spy.Array) -> spy.Matrix:
    """Column vector representation of tensor

    Parameters
    ----------
    a : spy.Array
        Tensor

    Returns
    -------
    spy.Matrix
        Tensor as vector in R^{n x 1}
    """
    return a.reshape(spy.prod(a.shape), 1).tomatrix()

# %% Products

def sp(
    a: spy.Array,
    b: spy.Array
):
    """Scalar product.

    Compute the scalar product (full contraction) of
    tensors with equal shape.

    Parameters
    ----------
    a : spy.Array
        Tensor
    b : spy.Array
        Tensor with b.shape == a.shape

    Returns
    -------
    scalar
        a_ijkl... b_ijkl...

    Examples
    --------
    >>> a = t(3, "a", [2, 3, 4])
    ... b = t(3, "b", [2, 3, 4])
    ... c = sp(a, b) # c = a_ijk b_ijk
    """
    return vec(a).dot(vec(b))

def nf(a: spy.Array):
    """Frobenius norm.

    Frobenius norm of a real-valued tensor (square
    root of the sum of all squared tensor components).

    Parameters
    ----------
    a : spy.Array
        Tensor

    Returns
    -------
    scalar
        Frobenius norm
    """
    return vec(a).norm()

def td(
    a: spy.Array,
    b: spy.Array,
    n: int
) -> spy.Array:
    if a.shape == b.shape and n == a.rank():
        return sp(a, b)
    else:
        return spy.Array(np.tensordot(a, b, n))

def tp(*args):
    temp = args[0]
    for i in range(1, len(args)):
        temp = spy.tensorproduct(temp, args[i])
    return temp

def tc(a: spy.Array, i: list):
    return spy.tensorcontraction(a, i)

def lm(a: spy.Array, b: spy.Array):
    return td(a, b, b.rank())

def rp(q: spy.Array, a: spy.Array):
    ra = a.rank()
    if ra == 1:
        return spy.Array(q.tomatrix()*vec(a))
    else:
        con = (1, ra + 2 - 1)
        temp = tc(tp(q, a), con)
        for _ in range(ra - 1):
            temp = tc(tp(q, temp), con)
        return temp

# %% Linear algebra

def linsolve_t(eqs: spy.Array, vs=None) -> list:
    if vs is None:
        vs = eqs.free_symbols
    if eqs.is_scalar:
        eqs = [eqs]
    else:
        eqs = flatten(eqs)
    sol = list(spy.linsolve(eqs, *vs))[0]
    return [(v, s) for v, s in zip(vs, sol)]

def orthogonalize_basis(basis: spy.Array) -> spy.Array:
    n = basis.shape[0]
    if basis.rank() > 2:
        dims = basis.shape[1:]
    gs = spy.GramSchmidt([vec(b) for b in basis], orthonormal=True)
    gs = spy.Array([spy.Array(g) for g in gs])
    if basis.rank() == 2:
        return gs
    else:
        return gs.reshape(n, *dims)

# %% Transpositions

def tt(a: spy.Array, axes) -> spy.Array:
    return spy.permutedims(a, axes)

def tt_m(a: spy.Array) -> spy.Array:
    ra = a.rank()
    front = list(range(ra//2))
    back = list(range(ra//2, ra))
    return tt(a, back + front)

# %% Symmetrizations

def symmetrize(a: spy.Array) -> spy.Array:
    p = list(permutations(list(range(a.rank()))))
    for i in p:
        eqs = a - spy.permutedims(a, i)
        vs = eqs.free_symbols
        if len(vs) > 0:
            a = a.subs(linsolve_t(eqs))
    return a

def symmetrize_explicit(
    a: spy.Array,
    p: list
) -> spy.Array:
    eqs = a - spy.permutedims(a, p)
    vs = eqs.free_symbols
    if len(vs) > 0:
        a = a.subs(linsolve_t(eqs))
    return a

def symmetrize_l(a: spy.Array) -> spy.Array:
    return symmetrize_explicit(a, (1, 0, 2, 3))

def symmetrize_r(a: spy.Array) -> spy.Array:
    return symmetrize_explicit(a, (0, 1, 3, 2))

def symmetrize_m(a: spy.Array) -> spy.Array:
    return symmetrize_explicit(a, (2, 3, 0, 1))

def symmetrize_lr(a: spy.Array) -> spy.Array:
    return symmetrize_r(symmetrize_l(a))

def symmetrize_lrm(a: spy.Array) -> spy.Array:
    return symmetrize_m(symmetrize_lr(a))

def sym(a: spy.Array) -> spy.Array:
    out = 0*a
    p = list(permutations(list(range(a.rank()))))
    for i in p:
        out += spy.permutedims(a, i)
    return out/len(p)

def sym_r(a: spy.Array) -> spy.Array:
    return (a + tt(a, (0, 1, 3, 2)))/2

def sym_l(a: spy.Array) -> spy.Array:
    return (a + tt(a, (1, 0, 2, 3)))/2

def sym_lr(a: spy.Array) -> spy.Array:
    return sym_r(sym_l(a))

# %% Voigt notation (not normalized)

def vn(a: spy.Array) -> spy.Matrix:
    if a.shape == (3, 3):
        out = spy.Matrix([
            a[tr.VN_CONVENTION[0][0], tr.VN_CONVENTION[0][1]],
            a[tr.VN_CONVENTION[1][0], tr.VN_CONVENTION[1][1]],
            a[tr.VN_CONVENTION[2][0], tr.VN_CONVENTION[2][1]],
            a[tr.VN_CONVENTION[3][0], tr.VN_CONVENTION[3][1]],
            a[tr.VN_CONVENTION[4][0], tr.VN_CONVENTION[4][1]],
            a[tr.VN_CONVENTION[5][0], tr.VN_CONVENTION[5][1]]
        ])
    else:
        out = spy.eye(6)
        for i_1 in range(6):
            for i_2 in range(6):
                out[i_1, i_2] = a[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1]
                ]
    return out

# %% Normalized Voigt notation (NOT Voigt notation)

def nvn(a: spy.Array) -> spy.Matrix:
    """Normalized Voigt notation.

    Return the symmetric second- or minor symmetric fourth-order tensor
    `a` in normalized Voigt notation according to the convention defined
    by the module constant NVN_CONVENTION.

    The norm of the returned objects corresponds to the actual Frobenius
    norm of the original tensors. For fourth-order tensors, the inverse
    of the returned matrix corresponds to the `nvn` of the inverse of `a`
    (with respect to the linear map on symmetric second-order tensors).

    Parameters
    ----------
    a : spy.Array
        * Second order: a.shape == (3, 3) and symmetric
        * Fourth order: a.shape == (3, 3, 3, 3) and minor symmetric,
        i.e., a == sym_lr(a)

    Returns
    -------
    spy.Array
        * Second order: 6D vector representation
        * Fourth order: 6x6 matrix representation
    """
    if a.shape == (3, 3):
        out = spy.Matrix([
            a[tr.VN_CONVENTION[0][0], tr.VN_CONVENTION[0][1]],
            a[tr.VN_CONVENTION[1][0], tr.VN_CONVENTION[1][1]],
            a[tr.VN_CONVENTION[2][0], tr.VN_CONVENTION[2][1]],
            a[tr.VN_CONVENTION[3][0], tr.VN_CONVENTION[3][1]]*SR2,
            a[tr.VN_CONVENTION[4][0], tr.VN_CONVENTION[4][1]]*SR2,
            a[tr.VN_CONVENTION[5][0], tr.VN_CONVENTION[5][1]]*SR2
        ])
    else:
        out = spy.eye(6)
        for i_1 in range(6):
            for i_2 in range(6):
                out[i_1, i_2] = a[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1]
                ]
                if i_1 > 2:
                    out[i_1, i_2] *= SR2
                if i_2 > 2:
                    out[i_1, i_2] *= SR2
    return out

def nvn_inv(a: spy.Array) -> spy.Array:
    """Inverse of normalized Voigt notation.

    Reconstruct symmetric second- or minor symmetric fourth-order
    tensor from `nvn` representation.

    Parameters
    ----------
    a : spy.Array
        * 6D vector
        * 6x6 second-order tensor

    Returns
    -------
    spy.Array
        * symmetric 3x3 second-order tensor
        * minor symmetric 3x3x3x3x fourth-order tensor
    """
    if a.shape == (6, 1):
        out = spy.MutableDenseNDimArray(np.zeros([3]*2))
        for i in range(3):
            out[
                tr.VN_CONVENTION[i][0],
                tr.VN_CONVENTION[i][1]
            ] = a[i]
        for i in range(3, 6):
            out[
                tr.VN_CONVENTION[i][0],
                tr.VN_CONVENTION[i][1]
            ] = a[i]/SR2
            out[
                tr.VN_CONVENTION[i][1],
                tr.VN_CONVENTION[i][0]
            ] = a[i]/SR2
    else:
        out = spy.MutableDenseNDimArray(np.zeros([3]*4))
        for i_1 in range(6):
            for i_2 in range(6):
                temp = a[i_1, i_2]
                if i_1 > 2:
                    temp /= SR2
                if i_2 > 2:
                    temp /= SR2
                out[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_2][0],
                    tr.VN_CONVENTION[i_2][1],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_2][1],
                    tr.VN_CONVENTION[i_2][0],
                ] = temp
                out[
                    tr.VN_CONVENTION[i_1][1],
                    tr.VN_CONVENTION[i_1][0],
                    tr.VN_CONVENTION[i_2][1],
                    tr.VN_CONVENTION[i_2][0],
                ] = temp
    out = out.as_immutable()
    return out

def inv_nvn(a: spy.Array) -> spy.Array:
    """Compute inverse through nvn.

    Compute inverse of minor symmetric fourth-order tensor
    through `nvn`. The inverse applies only on symmetric
    second-order tensors.

    Parameters
    ----------
    a : spy.Array
        Fourth-order minor symmetrc tensor with a.shape == (3, 3, 3, 3)

    Returns
    -------
    spy.Array
        Inverse of `a` w.r.t. symmetric second-order tensors
    """
    a = nvn(a)
    if a == a.T:
        a = a.inv(method="CH")
    else:
        a = a.inv()
    return nvn_inv(a)

# %% Material tensors: stiffness

def stiffness_cub_l(l_1: float, l_2: float, l_3: float) -> spy.Array:
    return td(spy.Array([l_1, l_2, l_3]), P_CUB, 1)

def stiffness_cub_get_l(stiffness: spy.Array) -> spy.Array:
    return spy.Array([sp(stiffness, P)/sp(P, P) for P in P_CUB])

def stiffness_cub(
    c_1111: float,
    c_1122: float,
    c_2323: float
) -> spy.Array:
    l_1 = c_1111 + 2*c_1122
    l_2 = c_1111 - c_1122
    l_3 = 2*c_2323
    return stiffness_cub_l(l_1, l_2, l_3)

def stiffness_iso_l(l_1: float, l_2: float) -> spy.Array:
    return l_1*P_ISO_1 + l_2*P_ISO_2

def stiffness_iso(E: float, nu: float) -> spy.Array:
    l_1 = E/(1-2*nu)
    l_2 = E/(1+nu)
    return stiffness_iso_l(l_1, l_2)

# %% Rotations

def rotation_matrix(
    n: spy.Array,
    phi: float
) -> spy.Array:
    n = n/nf(n)
    out = (
        spy.cos(phi)*ID_2
        - spy.sin(phi)*lm(PT, n)
        + (1-spy.cos(phi))*tp(n, n)
    )
    return out

# %% Harmonic tensors

def harmonic(n: int, s: str = "h") -> spy.Array:
    if n == 0:
        return spy.symbols(s)
    elif n == 1:
        return t(1, s)
    else:
        a = symmetrize(t(n, s))
        return a.subs(linsolve_t(lm(a, ID_2)))

def harmonic_basis(n: int) -> spy.Array:
    h = harmonic(n)
    vs = list(h.free_symbols)
    return spy.Array([spy.diff(h, v) for v in vs])

def harmonic_onb(n: int) -> list:
    return orthogonalize_basis(harmonic_basis(n))

# %% Projections

def project_on_basis(
    a: spy.Array,
    basis: spy.Array
) -> spy.Array:
    cs = spy.Array(spy.symbols(f"c:{basis.shape[0]}"))
    lc = td(cs, basis, 1)
    return cs.subs(linsolve_t(a - lc, cs))

# %% Algebraic decompositions

def iso(a: spy.Array) -> spy.Array:
    if a.rank() == 2:
        return (a[0, 0] + a[1, 1] + a[2, 2])/3*ID_2
    elif a.rank() == 4:
        ls = [sp(a, P)/sp(P, P) for P in P_ISO]
        return ls[0]*P_ISO_1 + ls[1]*P_ISO_2 + ls[2]*P_ISO_3
    else:
        raise Exception("Not implemented")

def aniso(a: spy.Array) -> spy.Array:
    return a - iso(a)

def skw(a: spy.Array) -> spy.Array:
    return a - sym(a)

# %% Differentiation

def grad(a: spy.Array, x: spy.Array) -> spy.Array:
    temp = spy.derive_by_array(a, x)
    ra = a.rank()
    rx = x.rank()
    axes_x = list(range(rx))
    axes_a = list(range(rx, rx + ra))
    return tt(temp, axes_a + axes_x)

# %% Module constants

# Scalars
SR2 = spy.sqrt(2)

# Identity on vectors
ID_2 = spy.Array(spy.eye(3))

# Permutation tensor
PT = spy.MutableDenseNDimArray(
    np.zeros([3, 3, 3], dtype=int)
)
PT[0, 1, 2] = 1
PT[1, 2, 0] = 1
PT[2, 0, 1] = 1
PT[0, 2, 1] = -1
PT[2, 1, 0] = -1
PT[1, 0, 2] = -1

# Fourth-order
ITI = tp(ID_2, ID_2)
ID_4 = tt(ITI, (0, 2, 1, 3))
ID_S = sym_r(ID_4)
ID_A = ID_4 - ID_S
P_ISO_1 = ITI/3
P_ISO_2 = ID_S - P_ISO_1
P_ISO_3 = ID_A
P_ISO = [P_ISO_1, P_ISO_2, P_ISO_3]
P_CUB_1 = P_ISO_1
D_CUB = spy.MutableDenseNDimArray(
    np.zeros(shape=[3]*4, dtype=int)
)
for ii in range(3):
    D_CUB[ii, ii, ii, ii] = 1
P_CUB_2 = D_CUB - P_CUB_1
P_CUB_3 = ID_S - (P_CUB_1 + P_CUB_2)
P_CUB = spy.Array([P_CUB_1, P_CUB_2, P_CUB_3])


import numpy as np

def euclidian_dist(ax, ay, bx, by):
    """Return the Euclidian distance between two points a and b
    where a has coords ax, ay and b has coords bx, by
    Examples
    --------
    >>> euclidian_dist(0, 0, 1, 1)
    1.4142135623730951
    >>> euclidian_dist(0, 0, -1, -1)
    1.4142135623730951
    """
    return np.sqrt((bx - ax)**2 + (by - ay)**2)

def minkowski_dist(xs, ys, p=1):
    xs, ys = np.array(xs), np.array(ys)
    if np.isinf(p):
        return np.max(np.abs(xs - ys))
    elif np.isneginf(p):
        return np.min(np.abs(xs - ys))
    else:
        return np.sum(np.abs(xs - ys)**p)**(1/p)


import math
import numpy as np
import sympy as sp

def rotX(theta):
  return np.mat( [[1 , 0             , 0              , 0],
                  [0 , sp.cos(theta) , -sp.sin(theta) , 0],
                  [0 , sp.sin(theta) , sp.cos(theta)  , 0],
                  [0 , 0             , 0              , 1]])

def rotY(theta):
  return np.mat( [[sp.cos(theta)  , 0 , sp.sin(theta) , 0],
                  [0              , 1 , 0             , 0],
                  [-sp.sin(theta) , 0 , sp.cos(theta) , 0],
                  [0              , 0 , 0             , 1]])

def rotZ(theta):
  return np.mat( [[sp.cos(theta) , -sp.sin(theta) , 0 , 0],
                  [sp.sin(theta) , sp.cos(theta)  , 0 , 0],
                  [0             , 0              , 1 , 0],
                  [0             , 0              , 0 , 1]])

def trans(x, y, z):
  return  np.mat( [[1 , 0 , 0 , x ],
                   [0 , 1 , 0 , y ],
                   [0 , 0 , 1 , z ],
                   [0 , 0 , 0 , 1 ]])

def dhMatrix(theta, a, d, alpha):
  """ Calculate the transformation matrix with this dh-model """
  return rotZ(theta) * trans(a,0,d) * rotX(alpha)


import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from scipy.optimize import minimize
import pandas as pd

# generate data file from TCLab or get sample data file from:
#  http://apmonitor.com/pdc/index.php/Main/ArduinoEstimation2
# Import data file
# Column 1 = time (t)
# Column 2 = input (u)
# Column 3 = output (yp)
data = np.loadtxt('data.txt',delimiter=',',skiprows=1)
# extract data columns
t = data[:,0].T
Q1 = data[:,1].T
Q2 = data[:,2].T
T1meas = data[:,3].T
T2meas = data[:,4].T

# number of time points
ns = len(t)

# define energy balance model
def heat(x,t,Q1,Q2,p):
    # Optimized parameters
    U,alpha1,alpha2 = p

    # Parameters
    Ta = 23 + 273.15   # K
    m = 4.0/1000.0     # kg
    Cp = 0.5 * 1000.0  # J/kg-K
    A = 10.0 / 100.0**2 # Area in m^2
    As = 2.0 / 100.0**2 # Area in m^2
    eps = 0.9          # Emissivity
    sigma = 5.67e-8    # Stefan-Boltzman

    # Temperature States
    T1 = x[0] + 273.15
    T2 = x[1] + 273.15

    # Heat Transfer Exchange Between 1 and 2
    conv12 = U*As*(T2-T1)
    rad12  = eps*sigma*As * (T2**4 - T1**4)

    # Nonlinear Energy Balances
    dT1dt = (1.0/(m*Cp))*(U*A*(Ta-T1) \
            + eps * sigma * A * (Ta**4 - T1**4) \
            + conv12 + rad12 \
            + alpha1*Q1)
    dT2dt = (1.0/(m*Cp))*(U*A*(Ta-T2) \
            + eps * sigma * A * (Ta**4 - T2**4) \
            - conv12 - rad12 \
            + alpha2*Q2)

    return [dT1dt,dT2dt]

def simulate(p):
    T = np.zeros((len(t),2))
    T[0,0] = T1meas[0]
    T[0,1] = T2meas[0]
    T0 = T[0]
    for i in range(len(t)-1):
        ts = [t[i],t[i+1]]
        y = odeint(heat,T0,ts,args=(Q1[i],Q2[i],p))
        T0 = y[-1]
        T[i+1] = T0
    return T

# define objective
def objective(p):
    # simulate model
    Tp = simulate(p)
    # calculate objective
    obj = 0.0
    for i in range(len(t)):
        obj += ((Tp[i,0]-T1meas[i])/T1meas[i])**2 \
              +((Tp[i,1]-T2meas[i])/T2meas[i])**2
    # return result
    return obj

# Parameter initial guess
U = 10.0           # Heat transfer coefficient (W/m^2-K)
alpha1 = 0.0100    # Heat gain 1 (W/%)
alpha2 = 0.0075    # Heat gain 2 (W/%)
p0 = [U,alpha1,alpha2]

# show initial objective
print('Initial SSE Objective: ' + str(objective(p0)))

# optimize parameters
# bounds on variables
bnds = ((2.0, 20.0),(0.005,0.02),(0.002,0.015))
solution = minimize(objective,p0,method='SLSQP',bounds=bnds)
p = solution.x

# show final objective
print('Final SSE Objective: ' + str(objective(p)))

# optimized parameter values
U = p[0]
alpha1 = p[1]
alpha2 = p[2]
print('U: ' + str(U))
print('alpha1: ' + str(alpha1))
print('alpha2: ' + str(alpha2))

# calculate model with updated parameters
Ti  = simulate(p0)
Tp  = simulate(p)

# Plot results
plt.figure(1)

plt.subplot(3,1,1)
plt.plot(t/60.0,Ti[:,0],'y:',label=r'$T_1$ initial')
plt.plot(t/60.0,T1meas,'b-',label=r'$T_1$ measured')
plt.plot(t/60.0,Tp[:,0],'r--',label=r'$T_1$ optimized')
plt.ylabel('Temperature (degC)')
plt.legend(loc='best')

plt.subplot(3,1,2)
plt.plot(t/60.0,Ti[:,1],'y:',label=r'$T_2$ initial')
plt.plot(t/60.0,T2meas,'b-',label=r'$T_2$ measured')
plt.plot(t/60.0,Tp[:,1],'r--',label=r'$T_2$ optimized')
plt.ylabel('Temperature (degC)')
plt.legend(loc='best')

plt.subplot(3,1,3)
plt.plot(t/60.0,Q1,'g-',label=r'$Q_1$')
plt.plot(t/60.0,Q2,'k--',label=r'$Q_2$')
plt.ylabel('Heater Output')
plt.legend(loc='best')

plt.xlabel('Time (min)')
plt.show()


"""
Exercise 1
Verify the Continuity of a Function at a Point
Your challenge here is to write a program that will (1) accept a single-
variable function and a value as inputs and (2) check whether the input
function is continuous at the point where the variable assumes the value
input.
"""

from sympy import Limit, Symbol, sympify
from sympy.core.sympify import SympifyError

def isContinous(point, function, symbol):
    right_lim = Limit(function, symbol, point).doit()
    left_lim = Limit(function, symbol, point, dir="-").doit()

    if function.subs({symbol: point}) == left_lim == right_lim:
        return True
    return False

print("Enter a function in one variable: ")
function = input()

print("Enter the variable to differentiate with respect to: ")
symbol = input()

print("Enter the initial value of the variable: ")
initialValue = float(input())

try:
    function = sympify(function)

except SympifyError:
    print("Invalid function entered")

else:
    symbol = Symbol(symbol)
    if isContinous(initialValue, function, symbol):
        print("{0} is continuous at {1}".format(function, initialValue))
    else:
        print("{0} is not continuous at {1}".format(function, initialValue))


"""Surface plotting in spherical coordinates

spherical harmonic function is:
Y(l=2, m=0) = 3*cos(theta)**2 - 1
(red points are made NaN on purpose)
"""
from vtkplotter import *
import numpy as np

def rhofunc(theta, phi):
    if theta < 0.2:
        return np.nan # make some points invalid
    #return cos(theta)**2                       # Y(l=1 m=0)
    return (3*cos(theta)**2 - 1)**2             # Y(l=2 m=0)
    #return (5*cos(theta)**3 - 3*cos(theta))**2 # Y(l=3 m=0)

# Build the plot,
#  return an Assembly of 3 meshes, the unit
#  grid sphere, the surface rho(theta, phi) and
#  the red Points where rho is a complex number:
spl = plot(rhofunc, mode='spheric', cmap='viridis')

show(spl, Text2D(__doc__), axes=12, viewup='z')


import numpy as np
import sys
drift = {'S0': 0.03, 'S1': 0.035, 'S2': 0.025}
volatility = {'S0': 0.1, 'S1': 0.15, 'S2': 0.2}
stocks = drift.keys()

W = np.random.rand(len(stocks),len(stocks))
diag = np.diag(np.random.rand(len(stocks))) + np.diag([1]*len(stocks))
#this matrix is guaranteed to be pos. def by Gershgorin's circle theorem
A = np.transpose(W).dot(W) + diag
diag_A = np.diag(A)
B = np.diag(1.0/np.sqrt(diag_A))
correlation = B.dot(A).dot(B)
print('expected correlation: ')
print(correlation)

L = np.linalg.cholesky(correlation)
N = 10**6
normal_rvs = np.random.normal(size = (len(stocks),N))
correlated_normal_rvs = dict(zip(stocks, L.dot(normal_rvs)))

all_returns = []
for s in stocks:
    dt = 1
    mean = (drift[s] - 0.5 * volatility[s]**2) * dt
    ret = mean + volatility[s]*correlated_normal_rvs[s]
    rvs = np.exp(ret)
    if len(all_returns) != 0:
        all_returns = np.vstack((all_returns, ret))
    else:
        all_returns = ret
    print('%s mean: %g' % (s,np.mean(rvs)))
    print('%s var: %g' % (s,np.var(rvs)))
print('correlations:')
print(np.corrcoef(all_returns))


import numpy as np

from load_data import load_data

# Using the normal equation to solve for the global min theta parameters

# Load data

filename = 'housing_data.txt'
housing_data = load_data(filename)

# Insert intercept parameter

housing_data.insert(0, 'Ones', 1)

# Convert pandas DataFrame to numpy arrays

cols = housing_data.shape[1]

x_df = housing_data.iloc[:, 0:cols-1]
y_df = housing_data.iloc[:, cols-1:cols]

x_array = x_df.values
y_array = y_df.values

def normal_equation(x, y):
    """
        Computes the closed-form solution to multiple regression using the normal equation.

        Parameters
        ----------
        x : array_like
            Shape (m, n+1), where m is the number of examples, and n+1 the number of features
            including the vector of ones for the zeroth parameter.

        y : array_like
            Shape (m, 1), where m is the value of the function at each point.

        Returns
        -------
        normal_theta : array_like
            Shape (n+1, 1). Solved multiple regression parameters.
    """

    normal_theta = (np.linalg.pinv(x.T @ x) @ x.T) @ y
    return normal_theta

# Get theta parameters from the normal equation

theta = normal_equation(x_array, y_array)

# Predict house price with theta parameters

sq_feet = 1650
bedrooms = 3
new_house = np.array([1, sq_feet, bedrooms])
new_price = int(new_house @ theta)

print(f"The price for a {bedrooms} bedroom, {sq_feet} sq foot house is:\n\t${new_price}")


"""
    This function calculats the coefficients ai and bi of the non-complex
    Fourier series
    f(x)= a0/2 + sum(from i=0 to M of ai[i]*cos(2*pi*i*x/T) + bi[i]*sin(2*pi*i*x/T))
    with ai = <f(x),cos(2*pi*i*x/T)> and bi = <f(x),sin(2*pi*i*x/T)>.
    In a discret interval t the values of the above sum are calculated and
    returned as a result.
    Inputs:
        - f: function for Fourier series.
          syntax: f = lambda t: f(t)
        - T: float - period duration
        - M: int - maximal order of the Fourier series
        - dt: float - time-step of the calculation
    Used functions:
        - moca_int(f, N, a, b)
          Numerical integration using the monte carlo method
          (See 'lib/monte_carlo_integration.py')
"""

#imports
import numpy as np
from lib.monte_carlo_integration import *

#functions
def real_fourier(f, T, M, dt):
    nu = 1/T #frequency
    a0 = (2/T) * moca_int(f=f, N=1000, a=0, b=T) #coefficient a0
    an = [] #list of ai coefficients
    bn = [] #list of bi coefficients
    for i in range(1, M+1): #append the coefficients in the above lists
        ai_func = lambda x: f(x)*np.cos(2*np.pi*i*nu*x)
        bi_func = lambda x: f(x)*np.sin(2*np.pi*i*nu*x)
        an.append( (2/T) * moca_int(f=ai_func, N=1000, a=0, b=T) )
        bn.append( (2/T) * moca_int(f=bi_func, N=1000, a=0, b=T) )

    result = []

    t = np.arange(0, T, dt) #discret values for which the Fourier series is calculated

    for x in t:
        func = a0/2
        for i in range(1, M+1):
            func += an[i-1]*np.cos(2*np.pi*i*nu*x)+bn[i-1]*np.sin(2*np.pi*i*nu*x)
        result.append(func)

    return result


import numpy as np

from .base import Transformer

class Rigid(Transformer):
    """Use a rigid transformation function to align the pairs of cells.

    Rigid trasformations are constrained to the operations of rotation,
    reﬂection, translation, and combinations of these.
    """
    def __init__(self):
        super().__init__()

    def fit(self, A, B):
        # See http://nghiaho.com/?page_id=671
        # center
        A_centroid = np.mean(A, axis=0)
        B_centroid = np.mean(B, axis=0)

        H = np.dot((A - A_centroid).T, B - B_centroid)
        U, S, Vt = np.linalg.svd(H)
        R = np.dot(Vt.T, U.T)
        detR = np.linalg.det(R)
        x = np.identity(Vt.T.shape[1])
        x[x.shape[0]-1, x.shape[1]-1] = detR
        R = np.linalg.multi_dot([Vt.T, x, U.T])

        t = B_centroid.T - np.dot(R, A_centroid.T)

        model = {
            'R': R,
            't': t
        }
        return model

    def transform(self, model, A):
        return np.dot(model['R'], A.T).T + model['t']

    def chain(self, model, step_model, step_number):
        # For rigid transforms, since we can learn the final function simply
        # by learning the optimal rotation from the original A to the final A,
        # we don't need to maintain an overall model during fitting. See
        # finalize.
        return None

    def finalize(self, model, A_orig, A_final):
        return self.fit(A_orig, A_final)


import numpy as np

__all__ = ["find_nearest", "axis_angle_into_quaternion",
           "quaternion_into_axis_angle", "skew_matrix_from_array",
           "array_from_skew_matrix", "exp_matrix",
           "rotation_matrix_into_axis_angle", "axis_angle_into_rotation_matrix",
           "hamilton_product"]

def hamilton_product(q1, q2):
    """
Performs composition of two quaternions by Hamilton product. This is equivalent
of a rotation descried by quaternion_1 (q1), followed by quaternion_2 (q2).

https://en.wikipedia.org/wiki/Quaternion#Hamilton_product

    :param q1: 4-item iterable representing unit quaternion.
    :param q2: 4-item iterable representing unit quaternion.
    :return: Resulting quaternion.
    """
    a1 = q1[0]
    b1 = q1[1]
    c1 = q1[2]
    d1 = q1[3]

    a2 = q2[0]
    b2 = q2[1]
    c2 = q2[2]
    d2 = q2[3]

    return a1 * a2 - b1 * b2 - c1 * c2 - d1 * d2, \
           a1 * b2 + b1 * a2 + c1 * d2 - d1 * c2, \
           a1 * c2 - b1 * d2 + c1 * a2 + d1 * b2, \
           a1 * b2 + b1 * c2 - c1 * b2 + d1 * a2

def find_nearest(array_to_search, value):
    """
This function takes 1 array as first argument and a value to find the element
in array whose value is the closest. Returns the closest value element and its
index in the original array.

    :param array_to_search: Reference array.
    :param value: Value to find closest element.
    :return: Tuple (Element value, element index).
    """
    idx = (np.absolute(array_to_search - value)).argmin()
    return array_to_search[idx], idx

def axis_angle_into_quaternion(normalized_axis, angle):
    """
Takes an axis-angle rotation and converts into quaternion rotation.
https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles

    :param normalized_axis: Axis of rotation (3-element array).
    :param angle: Simple rotation angle (float or 1-element array).
    :return: 4-element array, containig quaternion (q0,q1,q2,q3).
    """
    # From axis-angle notation into quaternion notation.
    # https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
    quaternion_orientation_r = np.zeros((4,))
    quaternion_orientation_r[0] = np.cos(angle / 2)
    quaternion_orientation_r[1] = np.sin(angle / 2) * normalized_axis[0]
    quaternion_orientation_r[2] = np.sin(angle / 2) * normalized_axis[1]
    quaternion_orientation_r[3] = np.sin(angle / 2) * normalized_axis[2]

    return quaternion_orientation_r

def quaternion_into_axis_angle(quaternion):
    """
Takes an quaternion rotation and converts into axis-angle rotation.
https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles

    :param quaternion: 4-element tensor, containig quaternion (q0,q1,q2,q3).
    :return: (Axis of rotation (3-element tensor), Simple rotation angle (float or 1-element tensor))
    """
    # Simple rotation angle
    angle = np.nan_to_num(np.arccos(quaternion[0])) * 2

    # Avoids recalculating this sin.
    sin_angle_2 = np.sin(angle / 2)

    # Rotation axis
    normalized_axis = np.zeros((3,))
    normalized_axis[0] = quaternion[1] / sin_angle_2
    normalized_axis[1] = quaternion[2] / sin_angle_2
    normalized_axis[2] = quaternion[3] / sin_angle_2

    return normalized_axis, angle

def skew_matrix_from_array(x):
    """
Receives a 3-element array and return its respective skew matrix.

    :param x: 3-element array.
    :return: Respective skew-matrix (3x3)
    """
    return np.array([
        [0, -x[2], x[1]],
        [x[2], 0, -x[0]],
        [-x[1], x[0], 0],
    ])

def array_from_skew_matrix(x):
    """
Receives a skew matrix and returns its associated 3-element vector (array).

    :param x: Skew matrix (3x3)
    :return: Associated array (3-element).
    """
    return np.array([x[2][1], x[0][2], x[1][0]])

def exp_matrix(skew_matrix):
    norma = np.linalg.norm(skew_matrix)

    return np.eye(N=3, M=3) + \
           (np.sin(norma) / norma) * skew_matrix + \
           (1 - np.cos(norma)) / (norma ** 2) * np.matmul(skew_matrix, skew_matrix)

def rotation_matrix_into_axis_angle(r_matrix):
    """
Converts a 3x3 rotation matrix into equivalent axis-angle rotation.

    :param r_matrix: 3x3 rotation matrix (array).
    :return: Tuple -> (normalized_axis (3-element array), rotation angle)
    """
    # Converts R orientation matrix into equivalent skew matrix. SO(3) -> so(3)
    # phi is a simple rotation angle (the value in radians of the angle of rotation)
    if (np.trace(r_matrix) - 1) / 2 > 1 or (np.trace(r_matrix) - 1) / 2 < -1:
        print("valor fora do range: ", (np.trace(r_matrix) - 1) / 2)
    phi = np.nan_to_num(np.arccos((np.trace(r_matrix) - 1) / 2))

    # Skew "orientation" matrix into axis-angles tensor (3-element).
    # we do not multiply by phi, so we have a normalized rotation AXIS (in a SKEW matrix yet)
    # normalized because we didnt multiply the axis by the rotation angle (phi)
    return array_from_skew_matrix((r_matrix - r_matrix.T) / (2 * np.sin(phi))), phi

def axis_angle_into_rotation_matrix(normalized_axis, angle):
    return exp_matrix(skew_matrix_from_array(normalized_axis * angle))


 #!/usr/bin/env python3

# @file linearInterpolation.py
# @author Muhammad Mohsin Khan

# This script provides functions to interpolate
# 2-dimensional data linearly.

# Python import command
import numpy as np

__all__ = ['find_x_closeto_y', 'calc_setvalue']

def find_x_closeto_y(x, y, minMax="min"):

    """
    Returns indeces to the closest value in a list.

    Arguments
    ---------
    x : list of int or float
        Values to be searched
    y : list of int or float
        Array of values

    Returns
    -------
    pos : list of int or float
        Array of indeces corresponding to x searched in y

    Example
    -------
    >>> import linearInterpolation
    >>> values = [1,2,3,4,5]
    >>> pos = linearInterpolation.find_x_closeto_y([0.5,4.6], values)
    >>> pos
    array([0, 4])
    """

    pos = []

    for a in range(len(x)):
        if minMax.lower() == "min":
            pos.append(min(range(len(y)), key=lambda i: abs(y[i] - x[a])))

        else:
            pos.append(max(range(len(y)), key=lambda i: abs(y[i] - x[a])))

    pos = np.asarray(pos)

    return pos

def calc_setvalue(t, setTime, setvalue):

    """
    Returns a list of linearly interpolated values.

    Arguments
    ---------
    t        : list of int or float
        Time points for interpolation
    setTime  : list of int or float
        x axis of the data
    setvalue : list of int or float
        y axis of the data

    Returns
    -------
    val : list of int or float
        Interpolated values

    Example
    -------
    >>> import linearInterpolation
    >>> values = [0,1,2,3,4]
    >>> time = [1,2,3,4,5]
    >>> val = linearInterpolation.calc_setvalue([1.5,2.5,3.5], time, values)
    >>> val
    array([0.5, 1.5, 2.5])
    """

    val = []
    tTmpPos = find_x_closeto_y(t, setTime)

    # Linear interpolation between the two time points
    for a in range(len(tTmpPos)):
        if setTime[tTmpPos[a]] <= t[a] and setTime[-1] > t[a]:
            mTmp = (setvalue[tTmpPos[a] + 1] - setvalue[tTmpPos[a]])/(setTime[tTmpPos[a] + 1] - setTime[tTmpPos[a]])
        elif setTime[tTmpPos[a]] >= t[a]:
            mTmp = (setvalue[tTmpPos[a]] - setvalue[tTmpPos[a] - 1])/(setTime[tTmpPos[a]] - setTime[tTmpPos[a] - 1])
        elif t[a] > setTime[-1]:
            val.append(setvalue[-1])
            return val

        bTmp = setvalue[tTmpPos[a]] - mTmp*setTime[tTmpPos[a]]

        if mTmp*t[a] + bTmp == 0:
            val.append(1e-6)
        else:
            val.append(mTmp*t[a] + bTmp)

    val = np.asarray(val)

    return val


def cells():
    '''
    # 4/ Problem solutions
    '''

    '''
    '''

    # helper code needed for running in colab
    if 'google.colab' in str(get_ipython()):
        print('Downloading plot_helpers.py to util/ (only neded for colab')
        !mkdir util; wget https://raw.githubusercontent.com/minireference/noBSLAnotebooks/master/util/plot_helpers.py -P util

    '''
    '''

    # setup SymPy
    from sympy import *
    init_printing()

    # setup plotting
    %matplotlib inline
    import matplotlib.pyplot as mpl
    from util.plot_helpers import plot_plane, plot_line, plot_vec, plot_vecs

    # aliases
    Vector = Matrix  # define alias Vector so I don't have to explain this during video
    Point = Vector   # define alias Point for Vector since they're the same thing

    '''
    '''

    '''
    ## P4.2

    Find the lines of intersection between these pairs of planes:
    **a)** $P_1$: $3x-2y-z=2$ and $P_2$: $x+2y+z=0$,
    **b)** $P_3$: $2x+y-z=0$ and $P_4$: $x+2y+z=3$.

    <!--
    		\begin{answer}\textbf{a)}~;
    					\textbf{b)}~.\end{answer}
    		%	A = Matrix([[1,-2,-1,2],[1,2,1,0]])
    		%	A.rref()
    		%
    -->
    '''

    '''
    '''

    # a)         x   y   z  | c
    A = Matrix([[3, -2, -1,   2],
                [1,  2,  1,   0]])
    A.rref()

    '''
    '''

    '''
    So $z=s$ is a free variable, and the rest of the equation
    can be written as

    $$
      \begin{array}{rl}
      x                 &= \frac{1}{2}\\
      y + \frac{1}{2}s  &= -\frac{1}{4}\\
      z                 &= s
      \end{array}
    $$

    The answer is $(x,y,z) = (\frac{1}{2},-\frac{1}{4},0) + s(0,-\frac{1}{2},1),  \forall s \in \mathbb{R}$.
    '''

    '''
    '''

    # b)         x   y   z  | c
    B = Matrix([[2,  1, -1,   0],
                [1,  2,  1,   3]])
    B.rref()

    '''
    '''

    '''
    The free variable is $z=t$.
    The answer to b) is $\{ (-1,2,0) + t(1,-1,1), \forall t \in \mathbb{R}\}$.
    '''

    '''
    '''

    '''
    ## P4.11
    '''

    '''
    '''

    v = Vector([3, 4, 1])
    normal = Vector([2, -1, 4])

    vPperp = (normal.dot(v)/normal.norm()**2)*normal
    print('vPperp =', vPperp)
    vP = v - vPperp
    print('vP =', vP)

    plot_plane(normal, 0)            # plane P
    plot_vec(0.2*normal, color='r')  # its normal vec

    plot_vecs(v, vPperp, vP)

    '''
    '''

    v = Vector([3,   4,  1])
    normal = Vector([2,   -1,  4])
    D=4
    # point on P closest to the origin
    alpha = D/normal.norm()**2
    p_closest = alpha*normal
    # print('len normal', normal.norm())
    # print('p_closest', p_closest)
    assert p_closest.dot(normal) == 4

    vPperp = (normal.dot(v)/normal.norm()**2)*normal
    print('vPperp', vPperp)
    v_wrong = v - vPperp
    print('v_wrong', v_wrong)

    plot_plane(normal, D)                          # plane P
    plot_vec(0.2*normal, at=p_closest, color='r')  # its normal vec

    plot_vecs(v, vPperp, v_wrong)
    ax = mpl.gca()
    ax.grid(True,which='both')

    '''
    '''

    v = Vector([3,   4,  1])
    normal = Vector([2,   -1,  4])
    D = 4

    # some point on P
    p0 = Point([2,0,0])

    u = v - p0  # vector from p0 to tip of v

    uPperp = (normal.dot(u)/normal.norm()**2)*normal
    print('uPperp', uPperp)
    uInP = u - uPperp

    proj_v_on_P = p0 + uInP
    print('proj_v_on_P', proj_v_on_P)

    plot_plane(normal, D)                          # plane P
    plot_vec(0.2*normal, at=p_closest, color='r')  # its normal vec

    plot_vec(v)
    plot_vec(u, at=p0, color='r')
    plot_vec(uPperp, at=p0, color='b')
    plot_vec(uInP, at=p0, color='g')
    plot_vec(proj_v_on_P, color='y')

    ax = mpl.gca()
    ax.grid(True,which='both')

    '''
    '''

    '''
    '''


import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import warnings

class LR():
    def  __init__(self,X,y,alpha=0.01,lambd=0,polyfit=True,degree=3,FeatureNormalize=False):
        warnings.warn("Note: In current version, shape of X is (nf, n) and shape of y is (n,1), where nf is number of features and n is number of samples. These will be changed to conventional shapes (e.g. (n,nf), (n,)) of matrixs, in the later version.")
        self.alpha=alpha
        self.cost=[]
        self.itr=0
        self.lambd =lambd
        self.polyfit = polyfit
        self.degree = degree
        self.FeatureNormalize=FeatureNormalize
        if self.polyfit:
            self.X = self.PolyFeature(X)
        else:
            self.X = X

        self.means = np.mean(self.X,1).reshape(self.X.shape[0],-1)
        self.stds  = np.std(self.X,1).reshape(self.X.shape[0],-1)

        if FeatureNormalize:
            self.X = self.Normalize(self.X)

        self.y = y
        self.W =np.random.randn(1,self.X.shape[0])*np.sqrt(2.0/self.X.shape[0])
        self.b = 0

    def fit(self,X,y,itr=100,verbose=True):
        nf,N = X.shape
        if self.polyfit:
            X = self.PolyFeature(X)
        if self.FeatureNormalize:
            X = self.Normalize(X)
        #print(X.shape)
        for i in range(itr):
            self.itr +=1
            Z = np.dot(self.W,X) + self.b
            A = self.sigmoid(Z)
            err = A-y
            #J = np.sum(err**2)+self.lambd*np.sum(self.W**2)
            #J/=(2.0*A.shape[0])
            J = self.CostF(A,y)
            self.cost.append(J)
            if verbose:
                print('Epoc @ ',self.itr,' Cost ',J)
            dW = np.dot(X,err.T).T + self.lambd*self.W
            db = np.sum(err,axis=1,keepdims=True)+ self.lambd*self.b
            self.W -= self.alpha*dW
            self.b -= self.alpha*db
            #print(self.W,self.b)

    def PolyFeature(self,X):
        nf,N = X.shape
        Xnew = np.vstack([X,X**2,X[0,:]*X[1,:]])
        if self.degree>2:
            for d in range(3,self.degree+1):
                Xnew = np.vstack([Xnew,X**d])
        return Xnew

    def Normalize(self,X):
        #self.means = np.mean(X,1).reshape(X.shape[0],-1)
        #self.stds  = np.std(X,1).reshape(X.shape[0],-1)
        X1=(X-self.means)/self.stds
        return X1

    def CostF(self,A,y):
        J = -np.sum(y*np.log(A+1e-20)+(1-y)*np.log(1-A+1e-20))
        J = J/(2.0*A.shape[1])
        return J

    def sigmoid(self,z):
        return 1.0/(1+np.exp(-z))

    def predict(self,X):
        if self.polyfit:
            X = self.PolyFeature(X)
        if self.FeatureNormalize:
            X = self.Normalize(X)
        Z = np.dot(self.W,X) + self.b
        A = self.sigmoid(Z)
        return A,A>0.5

    def getWeights(self):
        return self.W,self.b

    def Bplot(self,ax=None,density =500,hardbound=False):
        if ax is None: fig, ax = plt.subplots(111)
        x1mn = np.min(self.X[0,:])
        x2mn = np.min(self.X[1,:])
        x1mx = np.max(self.X[0,:])
        x2mx = np.max(self.X[1,:])
        x1 = np.linspace(x1mn,x1mx,density)
        x2 = np.linspace(x2mn,x2mx,density)
        xv, yv = np.meshgrid(x1, x2)
        Xall = np.vstack([xv.flatten(),yv.flatten()])

        yp,yi = self.predict(Xall)
        yp = yp.reshape(density,density)
        self.yp=yp
        if hardbound:
            ax.imshow(yp>0.5, origin='lower', interpolation='bicubic',extent=(xv.min(), xv.max(), yv.min(), yv.max()))
        else:
            ax.imshow(yp, origin='lower', interpolation='bicubic',extent=(xv.min(), xv.max(), yv.min(), yv.max()))

        in0 = np.where(self.y==0)[1]
        in1 = np.where(self.y==1)[1]
        #print(in0,in1)
        ax.plot(self.X[0,in0],self.X[1,in0],'.b')
        ax.plot(self.X[0,in1],self.X[1,in1],'.r')
        ax.set_xlabel('X1')
        ax.set_ylabel('X2')
        ax.set_title('Decision Boundry')
        #plt.show()

    def LCurvePlot(self,ax=None):
        if ax is None: fig, ax = plt.subplots(111)
        ax.plot(range(self.itr),self.cost)
        ax.set_xlabel('iteration')
        ax.set_ylabel('Cost')
        ax.set_title('Learning Curve')
        ax.grid(True)
        #plt.show()

    def Wplot(self,ax=None):
        if ax is None: fig, ax = plt.subplots(111)
        W,b = self.getWeights()
        bW = np.hstack([b[0],W[0]])
        Wl = ['b']+['W'+str(i) for i in range(1,len(W[0])+1)]
        #ax.bar(np.arange(len(bW))*2,bW)
        ax.stem(np.arange(len(bW)),bW)
        ax.set_xticks(np.arange(len(bW)))
        ax.set_xticklabels(Wl)#np.arange(len(bW))
        ax.text(len(bW)*0.4,np.max(bW)*0.5,r'$\hat y = \sigma(Wx+b)$',fontsize=16,alpha=0.5)
        ax.grid(alpha=0.5)
        #ax.set_ylim([None,np.max(bW)*1.5])
        ax.set_ylabel('Weights')
        #plt.show()
    def Wplot2(self,ax=None):
        if ax is None: fig, ax = plt.subplots(111)
        W,b = self.getWeights()
        bW = np.hstack([b[0],W[0]])
        Wl = ['b']+['W'+str(i) for i in range(1,len(W[0])+1)]
        ax.matshow([bW],cmap="YlGn",origin='lower')
        ax.set_xticks(np.arange(len(bW)))
        ax.set_xticklabels(Wl)#np.arange(len(bW))
        ax.set_yticklabels([])
        ax.text(len(bW)*0.4,-1,r'$\hat y = \sigma(Wx+b)$',fontsize=16)
        for i in range(len(bW)):
            ax.text(i-0.2,0,str(np.around(bW[i],3)))


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Thu Aug  1 22:18:22 2019

@author: dileepn

Kernel Perceptron Algorithm: A Toy Example
"""
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# Data points
x = np.array([[5,2], [1,4], [2,0], [4,1], [3,3], [4,4], [1,1], [0,2], [5,5], [0,0]])
#x = np.array([[0,0], [0,2], [1,1], [2,0], [3,3], [1,4], [4,1], [4,4], [5,2], [5,5]])

# Labels
y = np.array([[1],[1],[-1],[1],[-1],[1],[-1],[-1],[1],[-1]])
#y = np.array([[-1],[-1],[-1],[-1],[-1],[1],[1],[1],[1],[1]])

# Number of examples
n = x.shape[0]

# Number of features
m = x.shape[1]

# No. of iterations
T = 100

# Initialize alphas to zero
alphas = np.zeros((n,1))
#alphas = np.array([[1],[31],[11],[65],[72],[21],[30],[4],[0],[15]])
theta0 = 0.0

# Tolerance for floating point errors
eps = 1e-8

# Kernel matrix: quadratic kernel, phi(x) = [x1^2, sqrt(2)*x1*x2, x2^2]
def kernel_matrix(x, y):
    """
    Returns kernel matrix (quadratic kernel, in this case) for input arrays, x
    and y: K(x,y) = phi(x).phi(y), which for a quadratic kernel is (x.y)^2
    """
    K = (x.dot(y.T))**2

    return K

def quad_kernel(x):
    """
    Returns the kernel representation (quadratic, in this case) of input array, x
    """
    try:
        k1 = x[:,0]**2
        k2 = x[:,1]**2
        k12 = np.sqrt(2)*x[:,0]*x[:,1]
        return np.vstack((k1,k12,k2))
    except IndexError:
        k1 = x[0]**2
        k2 = x[1]**2
        k12 = np.sqrt(2)*x[0]*x[1]
        return np.array([[k1],[k12],[k2]])

# Kernel matrix for our case
def shuffle(x, y, req='no'):
    """ Shuffles indices of data points to alter the order in which kernel
        perceptron algorithm runs through the data.
        Inputs:
            Data (x and y)
            req: if 'yes', indices of x and y are randomly permuted;
                 if 'no', x and y are returned as is.
        Returns: x and y
    """
    if req == 'yes':
        indices = np.random.permutation(len(x))
        x = x[indices,:]
        y = y[indices]
        return x, y
    else:
        return x, y

x, y = shuffle(x, y, 'no')
K = kernel_matrix(x,x)

# Start kernel perceptron loop
for t in range(T):
    counter = 0     # To check if all examples are classified correctly in loop
    for i in range(n):
        agreement = y[i]*(np.sum(alphas*y*(K[:,i].reshape(-1,1))) + theta0)
        if abs(agreement) < eps or agreement < 0.0:
            alphas[i] = alphas[i] + 1
            theta0 = theta0 + y[i]
        else:
            counter += 1

    # If all examples classified correctly, stop
    if counter == n:
        print("--------------------------------------------------------------")
        print("No. of iteration loops through the dataset:", t+1)
        print("--------------------------------------------------------------")
        break

# Initialize theta vector
theta = np.zeros((3,1))

# Calculate theta from calculated alphas
for i in range(n):
    theta = theta + alphas[i]*y[i]*quad_kernel(x[i,:])

print("theta0 =", theta0.item())
print("theta = [{:.2f}, {:.2f}, {:.2f}]".format(theta[0,0], theta[1,0], theta[2,0]))
print("----------------------------------------------------------------------")

print("=== Mistake Counts ===")
for i in range(n):
    print(x[i,:],":\t",alphas[i].item())

print("----------------------------------------------------------------------")
print("=== Classification Status ===")

for i in range(n):
    agreement = (y[i]*(theta.T.dot(quad_kernel(x[i,:])) + theta0)).item()
    if abs(agreement) < eps or agreement < 0.0:
        print("FAIL:\t", x[i,:])
    else:
        print("PASS:\t", x[i,:])

def decision_boundary(x, y, theta, theta0, space='orig'):
    """
    Computes the decision contour in original x-space.

    Inputs:
        x, y generated from np.meshgrid
        space in which to operate: original x-space ('orig') or feature space ('feature')
    Returns: Decision contour, theta.T.Phi(x) + theta0
    """
    if space == 'feature':
        return (-theta0 - theta[0]*x - theta[1]*y)/theta[2]
    else:
        return theta[0]*x**2 + theta[1]*np.sqrt(2)*x*y + theta[2]*y**2  + theta0

# Plot in feature space
def plot_feature_space(theta, theta0):
    """
    Plots decision boundary in the transformed feature space.
    Inputs:
        Trained parameter vector theta and offset theta0
    Returns: A 3D plot of the decision boundary along with the data points.
    """
    colors = ['r' if y == 1 else 'b' for y in y]

    fig = plt.figure(figsize=(8,8))
    ax = Axes3D(fig)
    pts = quad_kernel(x).T  # Coordinates in feature space
    ax.scatter3D(pts[:,0], pts[:,1], pts[:,2], s=30, c=colors)

    xx, yy = np.meshgrid(np.linspace(*ax.get_xlim()), np.linspace(*ax.get_ylim()))
    zz = decision_boundary(xx, yy, theta, theta0, 'feature')   # Linear decision boundary in feature space

    ax.plot_surface(xx, yy, zz, cmap='winter', alpha=0.2)
    ax.view_init(elev=10, azim=60)
    ax.set_xlabel(r'$\Phi_1 = x_1^2$', fontsize=12)
    ax.set_ylabel(r'$\Phi_2 = \sqrt{2}x_1x_2$', fontsize=12)
    #ax.zaxis.set_rotate_label(False)
    ax.set_zlabel(r'$\Phi_3 = x_2^2$', fontsize=12)
    ax.set_title("Separating hyperplane in feature ($\Phi$) space", fontsize=20)

# Plot decision boundary in original space
def plot_decision_boundary(theta, theta0, style='line'):
    """
    Plots decision boundary in the original x-space.
    Inputs:
        Trained parameter vector theta and offset theta0
        Contour style: 'line' or 'filled' (levels = -1, 0, 1)
    Returns: A plot of the decision boundary along with the data points.
    """

    colors = ['r' if y == 1 else 'b' for y in y]

    f, ax = plt.subplots(figsize=(8, 8))
    ax.scatter(x[:,0], x[:,1], s=40, c=colors)
    xx, yy = np.meshgrid(np.linspace(*ax.get_xlim()), np.linspace(*ax.get_ylim()))
    z = decision_boundary(xx, yy, theta, theta0)

    if style == 'filled':
        # Filled contour
        cs = ax.contourf(xx, yy, z, levels=[-10,-5,0,5,10],
                         colors = ['purple','cyan','orange','magenta'],
                         extend='both', alpha=0.2)
        cs.cmap.set_over('red')
        cs.cmap.set_under('blue')
        cs.clabel(cs.levels, inline=1, fontsize=10, colors='black', rightside_up=True)
        cs.changed()
        ax.set_title("Decision bounday in x-space (contour level = 0)", fontsize=20)
    else:
        # Contour line
        cs = ax.contour(xx, yy, z, levels=[-10,-5,0,5,10], cmap='winter',
                        alpha=0.5, linewidths=[1,1,2,1,1],
                        linestyles=['dashdot','dashed','solid','dashed','dashdot'])
        cs.clabel(cs.levels, inline=1, fontsize=10)
        ax.set_title("Decision bounday in x-space (solid line)", fontsize=20)

    ax.set_xlabel(r'$x_1$', fontsize=20)
    ax.set_ylabel(r'$x_2$', fontsize=20)

plot_decision_boundary(theta, theta0, 'line')
plot_feature_space(theta, theta0)


# we use the tplquad command to integrate f(x,y,z)=ysin(x)+zcos(x)
# over the region

# 0<=x<=π
# 0<=y<=1
# −1<=z<=1

from scipy.integrate import tplquad
import numpy as np

def integrand(z, y, x):
    return y * np.sin(x) + z * np.cos(x)

ans, err = tplquad(integrand,
                   0, np.pi,  # x limits
                   lambda x: 0,
                   lambda x: 1, # y limits
                   lambda x,y: -1,
                   lambda x,y: 1) # z limits

print ans


import numpy as np

def softmax(L):
    # Write a function that takes as input a list of numbers,
    # and returns the list of values given by the softmax function.
    probs = [];
    norm_fac = np.sum(np.exp(L));
    for l_k in L:
        p_k = np.exp(l_k) / norm_fac;
        probs.append(p_k);
    return probs;

# def softmax(L):
#     expL = np.exp(L);
#     sumExpL = np.sum(expL);
#     res = [];
#     for k in expL:
#         p_k = k * 1.0 / sumExpL;
#         res.append(p_k);
#     return res;


# Possible useful Functions
from numpy import exp

#1. : Trapezoid
def trapezoidal(f, a, b, n=1):

    if b < a : raise ValueError("a must be lower than b (received a=%f, b=%f)" %(a,b))
    if not isinstance(n,int): raise TypeError("n must be an integer. (received n={})".format(n))
    if n < 1 : raise ValueError("n must be greater than 1 (received n=%d)" % n)

    if n == 2: return (b - a) * 0.5 * (f(b) + f(a))

    dx  = (0.5 * (b - a)) / float(n)
    x   = a
    val = 0
    while x < b:
        val += f(x) * dx
        x   += dx
    return val

#2. : Simpson
def simpson(f, a, b, n=2):

    if b < a: raise ValueError("a must be lower than b (received a=%f, b=%f)" %(a,b))
    if not isinstance(n,int): raise TypeError("n must be an integer. (received n={})".format(n))
    if n < 1: raise ValueError("n must be greater than 1 (received n=%d)" % n)
    if n % 2: raise ValueError("n must be even (received n=%d)" % n)

    dx  = (b - a) / float(n)
    val = f(a) + f(b)

    for i in range(1, n,   2): val += 4 * f(a + i * dx)
    for i in range(2, n-1, 2): val += 2 * f(a + i * dx)

    return val * dx / float(3)

# 3. : Boole:

def boole(f, a, b, n=5):

    if b < a: raise ValueError("a must be lower than b (received a=%f, b=%f)" %(a,b))
    if not isinstance(n,int): raise TypeError("n must be an integer. (received n={})".format(n))
    if n < 1: raise ValueError("n must be greater than 1 (received n=%d)" % n)
    if n % 5: raise ValueError("n must be multiple of 5 (received n=%d)" % n)

    h   = (b - a) / float(n)
    val = 0
    x   = a
    while  x < b:
        bl = (7  * f(x) +
              32 * f(x + h) +
              12 * f(x + 2 * h) +
              32 * f(x + 3 * h) +
              7  * f(x + 4 * h)) * 2 * h / float(45)
        val += bl
        x   += h * 4
    return val

# Publication's profile, without normalization
def thun_prof(r, alpha=1.5, Rgap=0.55, DR=0.055):
    return r**(-alpha) / (1 + exp((Rgap - r) / DR))

def surf_prof(r, alpha=1.5, Rgap=0.55, DR=0.055):
    return r * thun_prof(r, alpha, Rgap, DR)


import numpy as np

# Write a function that takes as input a list of numbers, and returns
# the list of values given by the softmax function.
def softmax(L):
    exp_l = np.exp(L)
    sum_exp = sum(exp_l)
    result = []
    for i in exp_l:
        result.append(i*1.0/sum_exp) # e1/(e1+....+en)
    return result


# Given: Three positive integers k, m, and n, representing a population containing k+m+n organisms: k individuals are
#        homozygous dominant for a factor, m are heterozygous, and n are homozygous recessive.
#
# Return: The probability that two randomly selected mating organisms will produce an individual possessing a dominant
#         allele (and thus displaying the dominant phenotype). Assume that any two organisms can mate.
from scipy.special import comb

k, m, n = "25 22 20".split()
k = int(k)      # homozygous dominant
m = int(m)      # heterozygous
n = int(n)      # homozygous recessive

totalPop = k + m + n
totalCombos = comb(totalPop, 2)
validCombos = comb(k,2) + k*m + k*n + .5*m*n + .75*comb(m,2)
probability = validCombos/totalCombos

print("%.5f" % probability)

# Better solution
def firstLaw(k,m,n):
    N = float(k+m+n)
    return 1 - ( m*n + .25*m*(m-1) + n*(n-1) ) / ( N*(N-1) )

print("%.5f" % firstLaw(25,22,20))


import numpy as np

def diophantine_approx(x: float, Q: float):  # Find p/q with (p,q) \in Z**2 tq v-p/q < 1/Q
    """
    Find the rational approximation p/q of a real number x
    such that the error between x and v/q is less that 1/Q.
    This implementation uses the diophantine algorithm.

    Parameters
    ----------
    x: float
        Real number to approximate as a rational number.
    Q: float
        Definition of the

    Returns
    -------
    (p, q): Tuple[int]
        Tuple of two integers p and q approximating x
    """
    for q in range(1, Q + 1):
        q_v = q * x
        frac_q_v = q_v - np.floor(q_v)
        if frac_q_v <= Q**-1 or (1. - Q**-1) <= frac_q_v:
            p = round(q_v)
            return p, q


import numpy as np
import matplotlib.pyplot as plt

print("integrate pi")
# direct sampling
N = 1000
x = np.random.rand(N)
y = np.sqrt(1 - x**2)
I = 4 * 1 * np.mean(y)
I_err = 4 * 1 * np.std(y) / np.sqrt(N)
print(I, "+/-", I_err)

x = np.random.rand(N)
y = np.random.rand(N)
r = x**2 + y**2
inside = r < 1
pi_approx = 4 * np.mean(inside)
print("I =", pi_approx, "+/-", 1/np.sqrt(np.sum(inside)))
print("N_acc =", np.sum(inside))

plt.figure()
plt.plot(x[inside], y[inside], "gx", label="inside")
plt.plot(x[~inside], y[~inside], "rx", label="outside")
xs = np.linspace(0, 1, 300)
plt.plot(xs, np.sqrt(1 - xs**2), "k-", label="circle")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.title(r"$\pi \approx$ " + str(pi_approx))

print("importance sampling")
import sympy as sp
from sympy.abc import x, y
sp.init_printing()
x_prime = sp.symbols("x_prime")

x_min, x_max = 0, 10
N = 100000
f = x*sp.exp(-x)
g = 2/sp.E*sp.exp(-x/2)
G = g.integrate((x, x_min, x_prime)).subs(x_prime, x)
G_inv = sp.solve(sp.Eq(G, y), x)[0].subs(y, x)

print("f:", f)
print("g:", g)
print("G:", G)
print("G^-1:", G_inv)

f = sp.lambdify(x, f, "numpy")
g = sp.lambdify(x, g, "numpy")
G = sp.lambdify(x, G, "numpy")
G_inv = sp.lambdify(x, G_inv, "numpy")

A = (G(x_max) - G(x_min))
x_g_samples = G_inv(G(x_min) + np.random.rand(N)*A)
y_samples = np.random.uniform(0, g(x_g_samples))
N_acc = np.sum(y_samples < f(x_g_samples))

print("acceptence ratio:", N_acc / N)
print("integral:", N_acc / N * A, 1/np.sqrt(N))

from scipy.integrate import quad
print("check with quad from scipy:", quad(f, x_min, x_max))

plt.figure()
x = np.linspace(x_min, x_max, 400)
plt.plot(x, f(x), label="f")
plt.plot(x, g(x), label="g")
plt.hist(x_g_samples, density=True, label="samples", bins=30, histtype="step")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()

import numpy,from scipy,import sympy,from sympy


import matplotlib.pyplot as plt
import numpy as np

my_data = np.genfromtxt('headbrain.csv', delimiter=',') # read the data
X = my_data[:, 0].reshape(-1,1) # -1 tells numpy to figure out the dimension by itself
ones = np.ones([X.shape[0], 1]) # create a array containing only ones
X = np.concatenate([ones, X],1) # cocatenate the ones to X matrix
y = my_data[:, 1].reshape(-1,1) # create the y matrix

plt.scatter(my_data[:, 0].reshape(-1,1), y)

# notice small alpha value
alpha = 0.0001
iters = 1000

# theta is a row vector
theta = np.array([[1.0, 1.0]])

def computeCost(X, y, theta):
    inner = np.power(((X @ theta.T) - y), 2)
    # @ means matrix multiplication of arrays.
    # If we want to use * for multiplication
    # we will have to convert all arrays to matrices
    return np.sum(inner) / (2 * len(X))

print(computeCost(X, y, theta)) # outputs 319.40631589398157
def gradientDescent(X, y, theta, alpha, iters):
    for i in range(iters):
        theta = theta - (alpha/len(X)) * np.sum((X @ theta.T - y) * X, axis=0)
        cost = computeCost(X, y, theta)
        # if i % 10 == 0: # just look at cost every ten loops for debugging
        #     print(cost)
    return (theta, cost)

g, cost = gradientDescent(X, y, theta, alpha, iters)
print(g, cost)
g = np.array([[ 1.03533399,  1.45914293]])
cost = 56.041973777981703

plt.scatter(my_data[:, 0].reshape(-1,1), y)
axes = plt.gca()
x_vals = np.array(axes.get_xlim())
y_vals = g[0][0] + g[0][1]* x_vals #the line equation
plt.plot(x_vals, y_vals, '--')


import math
import numpy as np

class Utils():

    @staticmethod
    def get_traceback(e, msg):
        traceback_str = f'{msg}: {type(e).__name__} due to "{e}"\n'

        tb_curr = e.__traceback__
        while tb_curr != None:
            traceback_str += f'    File "{tb_curr.tb_frame.f_code.co_filename}", line {tb_curr.tb_lineno} in {tb_curr.tb_frame.f_code.co_name}\n'
            tb_curr = tb_curr.tb_next

        return traceback_str

class MathUtils():

    @staticmethod
    def normal_distr(x, avg, std):
        return 1/(std*((2*math.pi)**0.5))*math.exp(-0.5*((x - avg)/std)**2)

    @staticmethod
    def get_freq_hist(data):
        freq = np.zeros(data.shape[0])
        unique = np.unique(data)

        for val in unique:
            val_filter = (data == val)
            freq[val_filter] = np.arange(freq[val_filter].shape[0])

        return freq

    @staticmethod
    def r_squared(y_data, y_model):
        y_data_mean = np.mean(y_data)
        ss_res = np.sum((y_data - y_model)**2)
        ss_tot = np.sum((y_data - y_data_mean)**2)

        return 1 - (ss_res/ss_tot)

    @staticmethod
    def calc_err(x_data, y_data, r, t_min, y=0):
        curve_fit = MathUtils.softplus_func(x_data, r, t_min, y)
        return np.sum(np.abs(y_data - curve_fit))

    @staticmethod
    def softplus_func(t, r, t_min, y=0):
        lin = r*(t - t_min)
        lin[lin < 100] = np.log(np.exp(lin[lin < 100]) + np.exp(y))
        return lin

    @staticmethod
    def linear_regresion(x, y):
        x_avg = np.mean(x)
        y_avg = np.mean(y)

        ss_xx = np.sum((x - x_avg)**2)
        ss_xy = np.sum((x - x_avg)*(y - y_avg))

        b = ss_xy/ss_xx          # slope
        c = y_avg - (b * x_avg)  # y-intercept

        return b, c

    def exp_regresion(x, y):
        '''
        Thanks: https://math.stackexchange.com/a/2318659
        Fits y = a + be^(cx)
        '''
        if y.shape[0] < 4:
            return None, None, None

        if y.shape[0] != x.shape[0]:
            raise ValueError('x and y must have the same length')

        s = np.zeros(x.shape[0])
        for k in range(1, x.shape[0]):
            s[k] = s[k-1] + (x[k] - x[k-1])*(y[k] + y[k-1])/2

        mat_c0 = np.linalg.inv(np.asarray([
            [ np.sum((x - x[0])**2),  np.sum((x - x[0])*s) ],
            [ np.sum((x - x[0])*s),   np.sum(s**2)         ]
        ]))

        mat_c1 = np.asarray([
            [ np.sum((y - y[0])*(x - x[0])) ],
            [ np.sum((y - y[0])*s) ]
        ]),

        mat_dot = np.dot(mat_c0, mat_c1)
        c = mat_dot[1][0][0]

        mat_ab0 = np.linalg.inv(np.asarray([
            [ x.shape[0],          np.sum(np.exp(c*x))   ],
            [ np.sum(np.exp(c*x)), np.sum(np.exp(2*c*x)) ]
        ]))

        mat_ab1 = np.asarray([
            [ np.sum(y) ],
            [ np.sum(y*np.exp(c*x)) ]
        ])

        mat_dot = np.dot(mat_ab0, mat_ab1)
        a = mat_dot[0][0]
        b = mat_dot[1][0]

        return a, b, c

    @staticmethod
    def softplus_regression(x, y):
        a, b, c = MathUtils.exp_regresion(x, y)
        exp_model = lambda a, b, c, x: a + b*np.exp(c*x)

        exp_180 = exp_model(a, b, c, 180)
        exp_90  = exp_model(a, b, c, 90)
        exp_0   = exp_model(a, b, c, 0)

        a_ln  = np.exp(exp_0  - exp_180) - 1
        b0_ln = np.exp(exp_90 - exp_180) - 1
        b1_ln = np.exp(exp_0  - exp_90)  - 1
        b_ln  = (1/90)*np.log(b0_ln/b1_ln)

        return a_ln, b_ln, exp_180


import numpy as np
from .. import loadModel

def cost(features, labels, weights, reg=0):
    """
    Computes the squared error between the labels and predicted output with regularization for each weight.

    :param features: Numpy matrix of input data used to make prediction. Each row is a training example and each feature is a column.
    :param labels: Numpy array of the correct output for each training example.
    :param weights: Numpy array of the learned weights of the model.
    :param reg: Integer representing the regularization.

    :return: Cost of the linear regression model.
    """

    # Number of training set examples
    numEx = features.shape[0]

    # Compute the squared Error
    pred_labels= predict(features, weights)
    squaredError = (pred_labels - labels)**2
    cost = (1/(2 * numEx)) * squaredError.sum()

    # Add the Regularization for all weights except for the bias
    cost += (reg/(2*numEx)) * (weights[1:]**2).sum()

    return cost

def predict(features, weights):
    """
    Given the input data features and the weights computes the predictions from the linear regression model.

    Param: features - Numpy matrix of input data used to make prediction.
                      Each row is a training example and each feature is a column.
           weights - Numpy array of the learned weights of the model.
    Returns Numpy array of with a prediction for each example.
    """

    # Prepend a column of ones for bias feature
    numEx = features.shape[0]
    biasFeature = np.ones((numEx,1))
    features = np.concatenate((biasFeature, features), axis=1)

    prediction = np.dot(features, weights.T)

    return prediction

def gradient(features , labels, weights, reg=0):
    """
    Computes the gradient of the cost function For linear regression.

    :param features: Numpy matrix of input data used to make prediction. Each row is a training example and each feature is a column.
    :param labels: Numpy array of the correct output for each training example.
    :param weights: Numpy array of the learned weights of the model.

    :return: A numpy array containing the partial derivative of the cost function for each weight.
    """

    numEx = features.shape[0]
    prediction = predict(features, weights)

    # Prepend a column of ones for bias feature
    numEx = features.shape[0]
    biasFeature = np.ones((numEx,1))
    features = np.concatenate((biasFeature, features), axis=1)

    # Compute the gradient
    difference = (prediction - labels)
    grad = (1/numEx)*np.dot(features.T,difference.T)

    # Add the gradient of the regualrization term except for bias
    grad[1:] = grad[1:] + (reg/numEx)*weights[1:]

    return grad

def load(mlModel):
    """
    Load the linear regression model.
    """

    mlModel.cost = cost
    mlModel.gradient = gradient
    mlModel.predict = predict
    loadModel.load("linearRegression")


import math
layer_output = [4.8, 1.21, 2.385]

E = math.e

# The final value has to be a probability in a classification problem, the probability does not
# work well with negative values, so we need to way to preserve the information from negative values
# while not dealing with negative values, enter E raised to power x

exp_values = [E**value for value in layer_output]
print(exp_values)

# Now we need to normailise this, normalise is basically the value devided by sum of all other values
# so x/sum(of list of values) this gives us probability

sum_of_exp = sum(exp_values)
prob_values = [element/sum_of_exp for element in exp_values]
print(prob_values)
print("sum of total should be close to one " + str(sum(prob_values)))

# The above with numpy

import numpy as np

# we have a batch of output (for m training set)

layer_output = [[4.8, 1.21, 2.385],
                [4.8, 1.21, 2.385],
                [4.8, 1.21, 2.385]]
exp_values = np.exp(layer_output)
sum_of_exp = np.sum(exp_values, axis=1, keepdims=True)
prob_values = exp_values/sum_of_exp
print(prob_values)