import numpy as np

# import torch
from PIL import Image
import matplotlib.pyplot as plt

from functools import reduce

A = np.identity(4)
A

P = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
P
Q = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
Q

B = np.arange(16).reshape((4, 4))
B

B
np.dot(B, P)
np.dot(P, np.dot(B, P))

def blockMatrix(blocks):
    """creates a bloack 0-1 matrix.
    param blocks: list of non-negative integers which is the size of the blocks.
    a 0 block size corresponds to a 0 on the main diagonal.
    """
    blocks = np.array(blocks).astype("int64")
    f = lambda x: 1 if x == 0 else x
    n = np.sum([f(x) for x in blocks])
    n = int(n)
    A = np.zeros((n, n))
    pos = 0
    for i in range(len(blocks)):
        b = blocks[i]
        if b > 0:
            A[pos : pos + b, pos : pos + b] = np.ones((b, b))
        pos += f(b)
    return A

def permutationMatrix(ls):
    """returns a permutation matrix of size len(ls)^2.
    param ls: should be a reordering of range(len(ls)), which defines the
    permutation on the ROWS.
    returns a permutation matrix P.
    np.dot(P,A) should be rearrangement of the rows of A according to P.
    To permute the columns of a matrix A use:
    Q = np.transpose(P), then: np.dot(A,Q).
    """
    n = len(ls)
    P = np.zeros((n, n))
    for i in range(n):
        P[i, ls[i]] = 1
    return P

def shuffleCopyMatrix(lins, louts, msize):
    """Returns a matrix P that represents switch and copy operations
    on the rows of a matrix.
    param msize: the size (of the square matrix).
    param lins: row indices to be replaced.
    param louts: row that replace the ones listed in lins.
    lins and louts must be of the same length and contain indiced within
    range(msize).
    These operations are performed on the identity matrix, and the result
    is the return value P.
    """
    # P = np.zeros((msize,msize))
    P = np.identity(msize)
    I = np.identity(msize)
    if not len(lins) == len(louts):
        return P
    for i in range(len(lins)):
        P[lins[i]] = I[louts[i]]
    return P

def scoreMatrix(n):
    """The score function of the matrix. The assumption is that the true
    arrangement maximized the interaction close to the main diagonal.
    The total sum of the interaction is an invariant, preserved by permuations.
    param n: size of ca 2-d n over n array.
    returns the score matrix, which is used to calculate the score of any given
    n^2 matrix.
    """
    s = np.arange(n)
    s = np.exp(-s)
    S = np.zeros((n, n))
    for i in range(n):
        S[i][i:] = s[: n - i]
    return S

def score(A, S):
    return np.sum(A * S)

def reindexMatrix(iss, jss, A):
    """iss and jss are lists of indices of equal size, representing
    a permuation: iss[i] is replaced with jss[i]. all other indices which are
    not in the lists left unchanged.
    """
    n = len(A)
    B = np.zeros_like(A)
    tss = [i for i in range(n)]
    for i in range(len(iss)):
        tss[iss[i]] = jss[i]
    print(tss)
    for i in range(n):
        for j in range(n):
            B[i, j] = A[tss[i], tss[j]]
    return B

reindexMatrix([1, 5], [5, 1], np.arange(36).reshape((6, 6)))

X = permutationMatrix([0, 4, 3, 2, 5, 1])
Y = np.transpose(X)

S = shuffleCopyMatrix([1, 3], [0, 2], 4)
S

T = np.transpose(S)
T

R = shuffleCopyMatrix([0, 1, 2, 3], [2, 0, 3, 1], 4)
R

blockMatrix([2, 3])

blockMatrix([2, 0, 0, 3])

blocks = [1, 3, 0, 3]

np.random.shuffle(B)

Z = blockMatrix([10, 20, 0, 0, 10, 20, 30]).astype("int64")
Z
ZZ = 255.0 * Z

im = Image.fromarray(ZZ)

im.show()

plt.ion()
# plt.ioff()

plt.imshow(ZZ)

plt.imshow(im)

plt.matshow(ZZ)

plt.close()

# ls = [10,20,0,0,10,20,30]
l1 = [i for i in range(25)]
l2 = [i + 25 for i in range(27)]
l3 = [i + 25 + 27 for i in range(20)]
l4 = [i + 25 + 27 + 20 for i in range(20)]

l3b = l3.copy()
l3b.reverse()
l3b

ZZ.shape

# rows
PP1 = permutationMatrix(l3 + l1 + l2 + l4)
PP2 = permutationMatrix(l1 + l2 + l3b + l4)
PP3 = permutationMatrix(l1 + l3b + l2 + l4)
# columns
QQ1 = np.transpose(PP1)  # then: np.dot(A,QQ).
QQ2 = np.transpose(PP2)  # then: np.dot(A,QQ).
QQ3 = np.transpose(PP3)  # then: np.dot(A,QQ).
ZZZ1 = np.dot(np.dot(PP1, ZZ), QQ1)
ZZZ2 = np.dot(np.dot(PP2, ZZ), QQ2)
ZZZ3 = np.dot(np.dot(PP3, ZZ), QQ3)

# plt.imshow(ZZZ)
# plt.imshow(ZZ)

fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 4), sharex=True, sharey=True)

ax1.imshow(ZZ)
ax2.imshow(ZZZ)

fig, axs = plt.subplots(nrows=2, ncols=2)

fig.suptitle("original pattern and permutations")

axs[0, 0].imshow(ZZ)
axs[0, 0].set_title("original")

axs[0, 1].imshow(ZZZ1)
axs[0, 1].set_title("[52:73] moved to the start")

axs[1, 0].imshow(ZZZ2)
axs[1, 0].set_title("[52:73] reversed")

axs[1, 1].imshow(ZZZ3)
axs[1, 1].set_title("[52:73] moved to [25:52] and reversed")

plt.close()

sm = scoreMatrix(len(ZZ))
sm

score(ZZ, sm)
score(ZZZ1, sm)
score(ZZZ2, sm)
score(ZZZ3, sm)

def scorePair(iss, jss, refmat, scoremat):
    A = np.zeros_like(refmat)
    l = iss + jss
    n = len(l)
    for i in range(n):
        for j in range(n):
            A[i, j] = refmat[l[i], l[j]]
    return score(A, scoremat)

[scorePair(l2, l4, ZZ, sm)]

np.argmax([1, 9, 0, 3])

# reassembl
cs = [l2, l4, l1, l3]
cs

while len(cs) > 1:
    xs = cs.pop()
    l = np.argmax([scorePair(xs, y, ZZ, sm) for y in cs])
    sl = scorePair(xs, cs[l], ZZ, sm)
    r = np.argmax([scorePair(y, xs, ZZ, sm) for y in cs])
    sr = scorePair(cs[r], xs, ZZ, sm)
    if sl > sr:
        cs[l] = xs + cs[l]
    else:
        cs[r] = cs[r] + xs
    print(l, sl, r, sr)

test = cs[0]
test == l1 + l2 + l3 + l4

def scorePair2(iss, jss, refmat):
    s = 0
    temp = 0
    for i in range(len(iss)):
        for j in range(len(jss)):
            temp = np.exp(-np.abs(i - j))
            # we only care about interaction between the 2 segments and not
            # inside each one of them which wouldn't be affected by
            # rearrangement.
            s += refmat[iss[i], jss[j]] * temp
    return s

# reassembly 2
cs = [l2, l4, l1, l3]
cs

while len(cs) > 1:
    xs = cs.pop()
    l = np.argmax([scorePair2(xs, y, ZZ) for y in cs])
    sl = scorePair2(xs, cs[l], ZZ)
    r = np.argmax([scorePair2(y, xs, ZZ) for y in cs])
    sr = scorePair2(cs[r], xs, ZZ)
    if sl > sr:
        cs[l] = xs + cs[l]
    else:
        cs[r] = cs[r] + xs
    print(l, sl, r, sr)

test == l1 + l2 + l3 + l4

myblocks = [10, 15, 17, 19, 17, 15, 10]
mymatrix = blockMatrix(myblocks)

dmatrix = scoreMatrix(len(mymatrix))

dmatrix += np.transpose(dmatrix)

dmatrix -= np.identity(len(dmatrix))

plt.matshow(mymatrix)

plt.matshow(np.log10(dmatrix))

fig, axs = plt.subplots(nrows=1, ncols=2)
fig.suptitle("ideal distribution of 1s and 0s")
axs[0].imshow(dmatrix)
axs[0].set_title("original")
axs[1].imshow(np.log(dmatrix))
axs[1].set_title("log scale")

myblocks

# mysegments = [8, 19, 20, 21, 22, 13]
mysegments = [15, 29, 20, 21, 18]
np.cumsum(myblocks)
np.cumsum(mysegments)

# and the corrsponding indices are:
def articulate(l):
    """l is a list of positive integers.
    returns the implied articulation, meaning a list of lists (or 1d arrays)
    ls, such that ls[0] it the numbers 0 to l[0]-1, ls[1] is a list of the
    numbers ls[1] to ls[2]-1 etc.
    """
    # ls = [np.arange(l[0]).astype('uint64')]
    ls = []
    offsets = np.cumsum([0] + l)
    for i in range(0, len(l)):
        xs = np.arange(l[i]).astype("uint64") + offsets[i]
        ls.append(xs)
    return ls

# the blocks, explicitly indexed
articulate(myblocks)

temp = articulate(myblocks)

reduce(lambda x, y: x + list(y), temp, [])

# the original segments
mysegments
articulate(mysegments)

np.nancumsum(myblocks)
np.cumsum(mysegments)

# shuffle the order of the segments:
newOrder = np.random.permutation(len(mysegments))
newOrder

temp = articulate(mysegments)
temp

reindexlist = [temp[newOrder[i]] for i in newOrder]

reindexlist

reindexlist
# we shuffled the order, now lets reverse a few of the segments:
for i in [1, 4]:
    reindexlist[i] = np.flip(reindexlist[i])
reindexlist

reindexing = reduce(lambda x, y: x + list(y), reindexlist, [])
reindexing

# now lets see the original matrix and the matrix after the transformation:

newmatrix = np.zeros_like(mymatrix)
for i in range(len(mymatrix)):
    for j in range(len(mymatrix)):
        newmatrix[i, j] = mymatrix[reindexing[i], reindexing[j]]

fig, axs = plt.subplots(nrows=1, ncols=2)
fig.suptitle("original block matrix anb its transformation")
axs[0].imshow(mymatrix)
# axs[0].set_title('')
axs[1].imshow(newmatrix)
# axs[1].set_title('')

# what we have to work with is newmatrix, as well as a list of the segments
# in their shuffled order, not the orignal of course.
newsegments = [mysegments[newOrder[i]] for i in range(len(newOrder))]
newsegments

# so we need to reshuffle the segments
newsegments
# so that eventually they will be order like that (after re-indexing)
mysegments
# and some of the new segments we'll have to reverse as well
# can we do that?

def scorePair3(iss, jss, refmat, lreverse=False, rreverse=False):
    """iss, jss must be lists of segments of the index range of refmat,
    our reference matrix.
    reurns the interaction score of iss and jss as if reindexed the matrix so
    that they will be adjuscent to each other.
    """
    s = 0
    temp = 0
    for i in range(len(iss)):
        for j in range(len(jss)):
            x = i
            y = j
            if lreverse:
                x = iss[-1] - i
            if rreverse:
                y = jss[-1] - j
            # temp = np.exp(-np.abs(i-j))
            temp = np.exp(-np.abs(x - y))
            # we only care about interaction between the 2 segments and not
            # inside each one of them which wouldn't be affected by
            # rearrangement.
            s += refmat[iss[i], jss[j]] * temp
    return s

cs = articulate(newsegments)
cs = [list(x) for x in cs]
cs

xyz = np.zeros_like(newmatrix)
l = cs[0] + cs[1]
for i in l:
    for j in l:
        xyz[i, j] = newmatrix[i, j]
plt.imshow(xyz)

xyz = np.zeros_like(newmatrix)
l = cs[5]
for i in l:
    for j in l:
        xyz[i - l[0], j - l[0]] = newmatrix[i, j]
plt.imshow(xyz)

xyz = np.zeros_like(newmatrix)
l = cs[0] + cs[3]
# l = cs[0] + np.flip(cs[3])
for i in range(len(l)):
    for j in range(len(l)):
        xyz[i, j] = newmatrix[l[i], l[j]]
print(scorePair2(cs[0], cs[3], mymatrix))
print(scorePair2(cs[0], cs[3], newmatrix))
print(scorePair2(np.flip(cs[0]), cs[3], newmatrix))  # this is the problem?
plt.imshow(xyz)

plt.imshow(mymatrix)

for i in cs:
    for j in cs:
        # print(scorePair2(i,j, newmatrix))
        print(scorePair2(i, j, mymatrix))

reconstructionMatrix = np.zeros_like(newmatrix)

while len(cs) > 1:
    xs = cs.pop()
    print(xs)
    xsrev = xs.copy()
    xsrev.reverse()
    newmatrixrev = reindexMatrix(xs, xsrev, newmatrix)
    l = np.argmax([scorePair2(xs, y, newmatrix) for y in cs])
    sl = scorePair2(xs, cs[l], newmatrix)
    lrev = np.argmax(
        # [scorePair2(xsrev, y, newmatrix) for y in cs]
        [scorePair2(xs, y, newmatrixrev) for y in cs]
    )
    # slrev = scorePair2(xsrev, cs[l], newmatrix)
    slrev = scorePair2(xs, cs[lrev], newmatrixrev)
    r = np.argmax([scorePair2(y, xs, newmatrix) for y in cs])
    sr = scorePair2(cs[r], xs, newmatrix)
    rrev = np.argmax(
        # [scorePair2(y, xsrev, newmatrix) for y in cs]
        [scorePair2(y, xs, newmatrixrev) for y in cs]
    )
    # srrev = scorePair2(cs[r], xsrev,  newmatrix)
    srrev = scorePair2(cs[rrev], xs, newmatrixrev)
    iascores = [sl, slrev, sr, srrev]
    candidates = [cs[l], cs[lrev], cs[r], cs[rrev]]
    maxscore = np.max(iascores)
    if maxscore == sl:
        cs[l] = xs + cs[l]
    elif maxscore == sr:
        cs[r] = cs[r] + xs
    elif maxscore == lrev:
        cs[lrev] = xsrev + cs[lrev]
    else:
        cs[rrev] = cs[rrev] + xsrev

# reconstruction of the matrix
reconstructionMatrix = np.zeros_like(newmatrix)
myindices = cs[0]
myindices

n = len(newmatrix)
for i in range(n):
    for j in range(n):
        reconstructionMatrix[i, j] = newmatrix[myindices[i], myindices[j]]
        # reconstructionMatrix[myindices[i],myindices[j]] = newmatrix[i, j]

plt.imshow(newmatrix)

plt.imshow(reconstructionMatrix)

#### new try

reconstructionMatrix = np.zeros_like(newmatrix)
reconstructionMatrix = newmatrix.copy()

while len(cs) > 1:
    xs = cs.pop()
    print(xs)
    xsrev = xs.copy()
    xsrev.reverse()
    reconstructionMatrixrev = reindexMatrix(xs, xsrev, reconstructionMatrix)
    l = np.argmax([scorePair3(xs, y, reconstructionMatrix) for y in cs])
    sl = scorePair3(xs, cs[l], reconstructionMatrix)
    lrev = np.argmax(
        # [scorePair2(xsrev, y, reconstructionMatrix) for y in cs]
        # [scorePair2(xs, y, reconstructionMatrixrev) for y in cs]
        [scorePair3(xs, y, reconstructionMatrix, lreverse=True) for y in cs]
    )
    # slrev = scorePair2(xsrev, cs[l], reconstructionMatrix)
    slrev = scorePair3(xs, cs[lrev], reconstructionMatrix, lreverse=True)
    r = np.argmax([scorePair3(y, xs, reconstructionMatrix) for y in cs])
    sr = scorePair3(cs[r], xs, reconstructionMatrix)
    rrev = np.argmax(
        # [scorePair2(y, xsrev, reconstructionMatrix) for y in cs]
        [scorePair3(y, xs, reconstructionMatrix, rreverse=True) for y in cs]
    )
    # srrev = scorePair2(cs[r], xsrev,  reconstructionMatrix)
    srrev = scorePair3(cs[rrev], xs, reconstructionMatrix, rreverse=True)
    iascores = [sl, slrev, sr, srrev]
    candidates = [cs[l], cs[lrev], cs[r], cs[rrev]]
    maxscore = np.max(iascores)
    if maxscore == sl:
        cs[l] = xs + cs[l]
    elif maxscore == sr:
        cs[r] = cs[r] + xs
    elif maxscore == lrev:
        reconstructionMatrix = reindexMatrix(xs, xsrev, reconstructionMatrix)
        # cs[lrev] = xsrev + cs[lrev]
        cs[lrev] = xs + cs[lrev]
    else:
        reconstructionMatrix = reindexMatrix(xs, xsrev, reconstructionMatrix)
        # cs[rrev] = cs[rrev] + xsrev
        cs[rrev] = cs[rrev] + xs

n = len(newmatrix)
temp = np.zeros_like(newmatrix)
for i in range(n):
    for j in range(n):
        temp[i, j] = reconstructionMatrix[myindices[i], myindices[j]]
        # reconstructionMatrix[myindices[i],myindices[j]] = newmatrix[i, j]

######
plt.imshow(newmatrix)

cs = articulate(newsegments)
cs = [list(x) for x in cs]
cs
mysegments
newsegments

def improve(xss, yss, A):
    pass


# Authors: Federico Vaggi
# License: MIT
# Source: https://bitbucket.org/FedericoV/numpy-tip-complex-modeling/

import numpy as np
from math import *

def arc_distance_python_nested_for_loops(a, b):
    """
    Calculates the pairwise arc distance between all points in vector a and b.
    """
    a_nrows = a.shape[0]
    b_nrows = b.shape[0]

    distance_matrix = np.zeros([a_nrows, b_nrows])

    for i in range(a_nrows):
        theta_1 = a[i, 0]
        phi_1 = a[i, 1]
        for j in range(b_nrows):
            theta_2 = b[j, 0]
            phi_2 = b[j, 1]
            temp = (pow(sin((theta_2 - theta_1) / 2), 2)
                    +
                    cos(theta_1) * cos(theta_2)
                    * pow(sin((phi_2 - phi_1) / 2), 2))
            distance_matrix[i, j] = 2 * (atan2(sqrt(temp), sqrt(1 - temp)))
    return distance_matrix

def arc_distance_numpy_tile(a, b):
    """
    Calculates the pairwise arc distance between all points in vector a and b.
    """
    theta_1 = np.tile(a[:, 0], (b.shape[0], 1)).T
    phi_1 = np.tile(a[:, 1], (b.shape[0], 1)).T

    theta_2 = np.tile(b[:, 0], (a.shape[0], 1))
    phi_2 = np.tile(b[:, 1], (a.shape[0], 1))

    temp = (np.sin((theta_2 - theta_1) / 2)**2
            +
            np.cos(theta_1) * np.cos(theta_2)
            * np.sin((phi_2 - phi_1) / 2)**2)
    distance_matrix = 2 * (np.arctan2(np.sqrt(temp), np.sqrt(1 - temp)))

    return distance_matrix

def arc_distance_numpy_broadcast(a, b):
    """
    Calculates the pairwise arc distance between all points in vector a and b.
    """
    theta_1 = a[:, 0][:, None]
    theta_2 = b[:, 0][None, :]
    phi_1 = a[:, 1][:, None]
    phi_2 = b[:, 1][None, :]

    temp = (np.sin((theta_2 - theta_1) / 2)**2
            +
            np.cos(theta_1) * np.cos(theta_2)
            * np.sin((phi_2 - phi_1) / 2)**2)
    distance_matrix = 2 * (np.arctan2(np.sqrt(temp), np.sqrt(1 - temp)))
    return distance_matrix

benchmarks = (
    arc_distance_python_nested_for_loops,
    arc_distance_numpy_tile,
    arc_distance_numpy_broadcast,
)


import scipy.stats
import numpy as np

def hypergeometric(binary_classification,feature_matrix):

    X = feature_matrix
    y = binary_classification
    nF = feature_matrix.shape[1]

    pvals = np.ones(nF) #p-values

    M = feature_matrix.shape[0] #total number of objects
    n = sum(y) #number in class
    for feature_idx in range(nF):
        N = sum(X[:,feature_idx]) #number positive for this feature
        k = sum(np.multiply(X[:,feature_idx],y)) #number of positives within this set
        pvals[feature_idx] = scipy.stats.hypergeom.sf(k,M,n,N)

    return pvals

def FE(class_vector,feature_matrix):
    '''Calculate fold enrichment of class members in feature categories

    Args:
        class_vector: 1-by-nG array
        feature_matrix: nG-by-nF array
    Returns:
        1-by-nF array

    Example:
    >>> class_vector = np.array([0,1,1,0,0,0,0])
    >>> feature_matrix = np.array([[0,0],[1,0],[1,1],[1,0],[1,0],[0,1],[1,1]])
    >>> FE(class_vector,feature_matrix)
    array([ 1.4       ,  1.16666667])'''

    assert(all([x in [0,1] for x in class_vector])) #classification vector is binary

    nF = feature_matrix.shape[1]

    fold_enrich = np.ones(nF) #fold enrichment

    M = feature_matrix.shape[0] #total number of objects
    n = sum(class_vector) #number in class
    for feature_idx in range(nF):
        feature_vector = feature_matrix[:,feature_idx]
        N = sum(feature_vector) #number positive for this feature
        k = sum(np.multiply(feature_vector,class_vector)) #number of positives within this set
        try:
            fold_enrich[feature_idx] = (float(k)/n)/(float(N)/M)
        except ZeroDivisionError:
            fold_enrich[feature_idx] = 1.0

    return fold_enrich


import numpy as np

def compute_mse(img1, img2, **kwargs):
    """
    returns Mean Squared Error between two images
    :param img1:
    :param img2:
    :return:
    """
    return np.mean((img1 - img2) * (img1 - img2))

def compute_psnr(img1, img2, **kwargs):
    """
    Computes Peak Signal-to-Noise Ratio between two images.
    :param img1: image 1
    :param img2: image 2
    :param data_range: the maximum possible pixel value of the images.
                        For 8-bit images, range is 255.0; 1.0 for float images
    :return: psnr value
    """

    data_range = kwargs.pop("data_range", 1.0)

    mse = compute_mse(img1, img2)
    return 10.0 * np.log10(data_range * data_range / mse)


import math
import numpy as np
from enum import IntEnum

class Mode(IntEnum):
    CUSTOM          = 0
    EQUAL           = 1
    GAUSS           = 2
    GAUSS_SYM       = 3
    PYRAMID         = 4
    PYRAMID_SYM     = 5
    SIVEROO_1       = 6
    SIVEROO_2       = 7

#This function will return an list of value, like below:
# [0,1,2,3,...,n] -> [a,...,b]
def scaleRange(n: int, a: int, b: int):
    return [(x*(b-a)/(n-1))+a for x in range(0,n)]

def equal(n: int):
    return [1/n]*n

def gauss(n: int):
    r = range(n,0,-1)
    val = [math.exp(-(2.0*x/n)**2) for x in r]
    val = val/np.sum(val)
    return val

def gauss_sym(n: int):
    n = n/2
    r = range(int(n),-math.ceil(n),-1)
    val = ([math.exp(-(2.0*x/(n))**2) for x in r])
    val = val/np.sum(val)
    return val

def pyramid(n: int):
    r = range(1,n+1)
    val = [x/n for x in r]
    val = val/np.sum(val)
    return val

def pyramid_sym(n: int):
    r = range(0,n)
    val = [(n/2)-abs(x-(n-1)/2) for x in r]
    val = val/np.sum(val)
    return val

def siveroo1(n: int):
    r = scaleRange(n,-3,0.1)
    val = [math.floor(3*math.exp(-(x/1.9)**2))/3+0.1 for x in r]
    val = val/np.sum(val)
    return val

# this function will stretch the given array (w) to a specific length (n)
# example : n = 10, w = [1,2]
# result : val = [1,1,1,1,1,2,2,2,2,2] , flip it, and then normalize it so its sum is equal to 1
def stretch(n: int, w: int):
    r = scaleRange(n,0,len(w)-0.1)

    val = []
    idx = [math.floor(x) for x in r]
    for x in range(0,n):
        index = int(idx[x])
        val.append(w[index])
    val = val/np.sum(val)
    return val

def null(n: int):
    return [0]*n

def get_weight(mode: Mode, count: int):
    if count == 1:
        return [1.0]
    else:
        return {
            Mode.EQUAL          : equal(count),
            Mode.GAUSS          : gauss(count),
            Mode.GAUSS_SYM      : gauss_sym(count),
            Mode.PYRAMID        : pyramid(count),
            Mode.PYRAMID_SYM    : pyramid_sym(count),
            Mode.SIVEROO_1      : siveroo1(count),
            Mode.SIVEROO_2      : stretch(count,[1,3,3,2,2])
        }.get(mode, [1, 0]) # fallback to [1,0] if fucked up

def modeName(mode: Mode):
    return {
            Mode.EQUAL          : "[1] Equal",
            Mode.GAUSS          : "[2] Gaussian Asymmetric",
            Mode.GAUSS_SYM      : "[3] Gaussian Symmetric",
            Mode.PYRAMID        : "[4] Pyramid Asymmetric",
            Mode.PYRAMID_SYM    : "[5] Pyramid Symmetric",
            Mode.SIVEROO_1      : "[6] Siveroo's Preset I",
            Mode.SIVEROO_2      : "[7] Siveroo's Preset II"
        }[mode]


from numpy import loadtxt, zeros, ones, mean, std

def feature_normalize(input):
    mu = input.mean(axis=0)
    sd = input.std(axis=0)
    input_norm = (input - mu) / sd
    return input_norm

def cost_function(x, y, theta):
    m = x.shape[0]
    predictions = x.dot(theta)
    sqErrors = (predictions - y)
    J = (1.0 / (2 * m)) * sqErrors.T.dot(sqErrors)

    return J

def gradient_descent(x, y, theta):
    num_iters = 100
    alpha = 0.01
    theta_size = theta.size
    m = x.shape[0]

    J_history = zeros(shape=(num_iters, 1))

    for i in range(num_iters):
        predictions = x.dot(theta)

        for j in range(theta_size):
            temp = x[:, j]
            temp.shape = (m, 1)

            errors_x1 = (predictions - y) * temp

            theta[j][0] = theta[j][0] - alpha * (1.0 / m) * errors_x1.sum()

        J_history[i, 0] = cost_function(x, y, theta)

    return theta, J_history

data = loadtxt('ex1data2.txt', delimiter=',')
x = data[:, :-1]
y = data[:, -1]

m, n = x.shape
y.shape = (m, 1)

x_norm = feature_normalize(x)
#theta0, theta1, thetaM
x = ones(shape=(m, 3))
x[:, 1:3] = x_norm

y = feature_normalize(y)

theta = zeros(shape=(n+1, 1))

theta, j_history = gradient_descent(x, y, theta)
print theta, j_history


import numpy as np

def branin_hoo_factory(a, b, c, r, s, t):
    def branin_hoo(x):
        # f(x) = a(y - b*x^2 + c*x - r)^2 + s (1 - t) cos(x) + s
        return (
            a * (x[1] - b * x[0] ** 2 + c * x[0] - r) ** 2
            + s * (1 - t) * np.cos(x[0])
            + s
        )

    return branin_hoo

branin_hoo_params = dict(
    a=1, b=5.1 / (4 * np.pi ** 2), c=5 / np.pi, r=6, s=10, t=1 / (8 * np.pi)
)

def branin_hoo_fn(x):
    """The Branin-Hoo function is a popular benchmark for Bayesian optimization.
    """
    z = branin_hoo_factory(**branin_hoo_params)(x)
    return z

def sample_branin_hoo(n_samples, domain=[[-5, 15], [0, 10]]):
    """Take samples from the Branin-Hoo function.

    Args:
        n_samples (int): number of samples to draw

    Returns:
        np.array: 2d array of x, y z points
        np.array: 1d array of z points
    """
    [x_min, x_max], [y_min, y_max] = domain

    xy = np.random.uniform(
        low=[x_min, y_min], high=[x_max, y_max], size=(n_samples, 2)
    ).T
    z = branin_hoo_fn(xy)

    return xy, z


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

def compare_poission_exp():
    """
    This post explained the relation between these two distribution
      - https://stats.stackexchange.com/a/2094/134555
      - P(Xt <= x) = 1 - e^(-lambda * x)
    Now, I suppose lambda=1, just like this example(from wiki, Poisson_distribution):
      - On a particular river, overflow floods occur once every 100 years on average.
    :return:
    """
    x = np.arange(20)
    y1 = 1 - np.power(np.e, -x)  # lambda = 1
    y2 = 1 - np.power(np.e, -0.2*x)  # lambda = 0.2
    y3 = 1 - np.power(np.e, -5*x)  # lambda = 1.5
    print(y1)
    print(y2)
    print(y3)
    fig, ax = plt.subplots(1, 1)
    ax.plot(x, y1, 'r-', label='lambda=1')
    ax.plot(x, y2, 'g-', label='lambda=0.2')
    ax.plot(x, y3, 'b-', label='lambda=5')
    ax.legend(loc='best', frameon=False)
    plt.ylabel('Probability')
    plt.title('CDF of exponential distribution')
    plt.show()

compare_poission_exp()


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

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

class gradient_descent_algorithm:
    def __init__(self,x,y,alpha,total_iteration):
        self.x = x    # x represents input features of each training examples. x is a nxm matrix.
        self.y = y    # y represents outputs of each training examples. y is a real number.
        self.alpha = alpha
        self.total_iteration = total_iteration

    def get_cost_function(self,theta):
        m = self.x.shape[1]    # m represents number of training examples. Amount of columns in matrix x is passed to m.
        squared_error = 0
        squared_error_sum = 0
        for mm in range(0,m):
            hypothesis = theta.transpose() * self.x[:,mm]
            squared_error = (hypothesis[0,0] - self.y[0,mm]) ** 2
            squared_error_sum += squared_error
        cost_function = squared_error_sum / (2 * m)
        return cost_function

    def gradient_descent(self):
        n = self.x.shape[0]    # n represents number of features. Amount of rows in matrix x is passed to n.
        m = self.x.shape[1]    # m represents number of training examples. Amount of columns in matrix x is passed to m.
        iteration_count = 0
        cost_function   = 0
        theta = np.matrix([[None]]*n)    # theta represents parameters of x of each training examples. theta is a nx1 matrix.
        for nn in range(0,n):
            theta[nn,0] = 0
        cost_function_vs_iteration_count_plot = plt.subplot(212)
        cost_function_vs_iteration_count_plot.set_xlabel('Iterations of gradient descent')
        cost_function_vs_iteration_count_plot.set_ylabel(r'Cost function J($\Theta$)')
        for iteration in range(0,self.total_iteration):
            cost_function = self.get_cost_function(theta)
            partial_derivative_of_squared_error_sum = np.matrix([[None]]*n)
            for nn in range(0,n):
                partial_derivative_of_squared_error_sum[nn,0] = 0
            for mm in range(0,m):
                hypothesis = theta.transpose() * self.x[:,mm]
                cost = (hypothesis - self.y[0,mm])[0,0]
                partial_derivative_of_squared_error = self.x[:,mm] * cost * 2
                partial_derivative_of_squared_error_sum += partial_derivative_of_squared_error
            partial_derivative_of_cost_function = partial_derivative_of_squared_error_sum / (2 * m)
            theta = theta - partial_derivative_of_cost_function * self.alpha
            iteration_count += 1
            print '========================================'
            print 'iteration_count = ' , iteration_count
            print 'cost_function = '   , cost_function
            print 'theta = '           , theta
            cost_function_vs_iteration_count_plot.plot(iteration_count, cost_function, color='black', marker='o', markersize=2)
        print '========================================'
        print 'Learning rate = ', self.alpha
        print 'Final theta = ', theta
        print '========================================'
        return theta

class linear_random_number_generator:
    def __init__(self,number_of_training_examples,number_of_features):
        self.number_of_training_examples = number_of_training_examples
        self.number_of_features = number_of_features

    def linear_random_number_generator_method(self):
        x = np.matrix([[None]*self.number_of_training_examples]*self.number_of_features)
        y = np.matrix([[None]*self.number_of_training_examples])
        for training_example in range(0,self.number_of_training_examples):
            x[0,training_example] = 1
            x[1,training_example] = training_example
            y[0,training_example] = x[1,training_example] + random.uniform(0,1) * 10
        return x, y

class plot_hypothesis_and_linear_random_number:
    def __init__(self,x,y,theta):
        self.x = x
        self.y = y
        self.theta = theta

    def plot_hypothesis_and_linear_random_number_method(self):
        number_of_training_examples = self.x.shape[1]
        plot_generator = plt.subplot(211)
        plot_generator.set_xlabel('Input x of training examples')
        plot_generator.set_ylabel('Output y of training examples')
        for training_example in range(0,number_of_training_examples):
            plot_generator.plot(self.x[1,training_example], self.y[0,training_example], color='red', marker='o', markersize=6)
        theta_1 = self.theta[1,0]
        theta_0 = self.theta[0,0]
        x_axis = np.arange(0.,100.,0.2)
        hypothesis = theta_1 * x_axis + theta_0
        y_axis = hypothesis
        plot_generator.plot(x_axis, y_axis, color='black', linestyle='solid', linewidth=2)

# Main function
plt.figure()
L = linear_random_number_generator(number_of_training_examples=100, number_of_features=2)
input_x, output_y = L.linear_random_number_generator_method()
G = gradient_descent_algorithm(x=input_x, y=output_y, alpha=0.000001, total_iteration=4000)
final_theta = G.gradient_descent()
P = plot_hypothesis_and_linear_random_number(x=input_x, y=output_y, theta=final_theta)
P.plot_hypothesis_and_linear_random_number_method()
plt.show()


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

import numpy as np
import interpolL as fz
import matplotlib.pyplot as plt
import math
from funzioni_Interpolazione_Polinomiale import InterpL

def zeri_Cheb(a,b,n):
    t1=(a+b)/2
    t2=(b-a)/2
    x=np.zeros((n+1,))
    for k in range(n+1):
        x[k]=t1+t2*np.cos(((2*k+1)/(2*(n+1))*math.pi))

    return x

sceltaf=input("Scegli funzione ")

scelta_funzione = {
        '1': [lambda x: np.sin(x)-2*np.sin(2*x),-math.pi, math.pi],
        '2': [lambda x: np.sinh(x),-2,2],
        '3': [lambda x: np.abs(x), -1,1],
        '4': [lambda x: 1/(1+x**2), -5,5]
}
f,a,b=scelta_funzione.get(sceltaf)

sceltap= input("Scegli tipo punti : 1(equidistanti) 2(Chebishev)\n ")

n=int(input('Grado del polinomio '))
scelta_punti = {
        '1': np.linspace(a,b,n+1),
        '2': zeri_Cheb(a,b,n)
        }
x=scelta_punti.get(sceltap)
xx = np.linspace(a,b,200)
y = f(x)
yy = f(xx)
p = fz.interpolL(x, y, xx)

err = (np.abs(f(xx) - p) )
plt.plot(x, y, "ro", xx, p, "g", xx, f(xx), "b")
plt.legend(["Pti interpolazione", "Polinomio", "Funzione"])
plt.show()
plt.plot(xx, err)
plt.title("ErroreRel")
plt.show()


"""
This module implements a generic Runge-Kutta solver.
"""
import numpy as np

def runge_kutta(f, x0, t_start, t_end, delta_t):
    """
    The Runge-Kutta method for `n` dimensional (non-linear) systems.

    Parameters
    ----------
    f: function
        A list containing the entries of the vector-valued function `f`.
    x0: list
        Contains the initial values of the system.
    t_start: float
        The starting time.
    t_end: float
        The ending time.
    delta_t: float
        The time step used when solving the system.

    Return
    ------
    tuple
        The first entry is a list containing all the employed time steps.
        The second entry is a list, containing the corresponding at each time step.
    """
    t = []
    x = []
    t_last = t_start
    x_last = x0
    while t_last < t_end:

        k1 = f(x_last) * delta_t
        k2 = f(x_last + k1 * 0.5) * delta_t
        k3 = f(x_last + k2 * 0.5) * delta_t
        k4 = f(x_last + k3) * delta_t

        x_last = x_last + 1. / 6. * (k1 + k2 * 2. + k3 * 2 + k4)
        x.append(x_last)
        t_last = t_last + delta_t
        t.append(delta_t)

    return np.array(t), np.array(x)

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


# AUTORES:
# Ana Beatriz Popazoglo 9806892
# Lucas Viana Vilela 10748409
# Murilo Ramos Botto  9288561

from sympy import * # Biblioteca para trabalhar com variáveis simbólicas
from sympy.abc import _clash1 # Reconhecimento de 'x' como uma variável na função sympify()
from statistics import mean # Função que calcula a média entre uma lista de valores

expr_f = sympify("21*x**4 - 11*x**3 + 19*x**2 - 11*x - 2") # Expressão de f(x)
expr_df = diff(expr_f,'x') # Expressão de f'(x)
expr_psi = simplify(sympify('x') - (expr_f/expr_df)) # Expresão de psi(x) = x - f(x)/f'(x)

# Intervalos: [a[0], b[0]] = [-1, 0] e [a[1], b[1]] = [0, 1]
a = [-1, 0]
b = [0, 1]

sol = [-1/7, 2/3] # Raízes exatas nos respectivos intervalos

epsilon = 10**-6 # Precisão
MAXITER = 10**3 # Máximo de iterações

def f(value: float): # f(x) = expr_f - Função que vai avaliar f(x) em um determinado ponto
    variable = sympify('x')
    return (expr_f.subs(variable,value)).evalf()

def df(value: float): # f(x) = expr_f - Função que vai avaliar f(x) em um determinado ponto
    variable = sympify('x')
    return (expr_df.subs(variable,value)).evalf()

def psi(value: float): # f(x) = expr_f - Função que vai avaliar f(x) em um determinado ponto
    variable = sympify('x')
    return (expr_psi.subs(variable,value)).evalf()

for i in range(2): # Loop que vai fazer os cálculos para ambos os intervalos
    output = open("./newton_saída<%d>.txt" % i,"w") # Abre o arquivo de saída

    # Este e todos os outros output.write() vão escrever dados no arquivo de saída
    output.write("\n {:^15} {:^30} {:^30} {:^30} {:^30} \n".format("k", "x_k", "f(x_k)", "f'(x_k)", "e_k"))

    # Apenas dois valores x_k serão armazenados por vez, o atual e o anterior
    x = [b[i], 0]

    # Inicializa em 0 o índice auxiliar que será utilizado para acessar o valor atual de x
    j = 0

    output.write(" {:^15} {: ^ 30.8f} {: ^ 30.8e} {: ^ 30.8e} {: ^ 30.8e} \n".format("0", x[j], float(f(x[j])), float(df(x[j])), float(abs(x[j] - sol[i]))))

    # Loop pelo processo de cálculo até que o número máximo de iterações sera atingido
    for k in range(1,MAXITER+1):
        j += 1 if j < 1 else -j # j = 0 --> j = 1 --> j = 0 --> j = 1 ...

        x[j] = psi(x[j-1]) # Valor do x atual

        e = abs(x[j] - sol[i]) # Erro em relação à raiz exata

        output.write(" {:^15} {: ^ 30.8f} {: ^ 30.8e} {: ^ 30.8e} {: ^ 30.8e} \n".format(k, x[j], float(f(x[j])), float(df(x[j])), float(abs(x[j] - sol[i]))))

        # Checa se uma aproximação satisfatória foi encontrada
        isGoodApprox = ( f(x[j]) == 0 ) or ( abs(x[j] - x[j-1]) < epsilon * max(1,abs(x[j])) )
        if isGoodApprox: break # Se sim, encerra o cálculo neste intervalo

    output.write("\n")
    output.close() # Fecha o arquivo de saída

    # Se sair do 'for', significa que chegou-se a uma aproximação satisfatória ou que o número máximo de iterações foi atingido
    if isGoodApprox: continue
    else: raise Exception("The maximum number of iterations was met and no root was found.")


import numpy as np

def min_max_norm(y):
    """calculates the min max norm of an array

    Args:
        y(array-like): an array of int or floats

    Returns:
        out(array-like): a normalized array

    Raises:
        ValueError: if y is not array-like
    """
    try:
        _y = np.asarray(y)
    except BaseException as be:
        raise ValueError(
            "y must be scalar or array like :: {}".format(
                str(be)))
    return (_y - _y.min()) / (_y.max() - _y.min())

NORM_MAP = {
    "min_max": min_max_norm
}

def get_normalizer(normalizer: str):
    """returns a normalizer function using a string

    Args:
        normalizer(str): normalizer identifier key

    Returns:
        normalizer(Callable): a normalizing method

    Raises:
        KeyError: if normalizer is not available or key not found
    """
    if normalizer in NORM_MAP:
        return NORM_MAP[normalizer]
    else:
        raise KeyError("normalizer is not in available normalizers: [{}]".format(
            NORM_MAP.keys()
        ))


# Introdução à Computação Simbólica com _sympy_

## Motivação

- O valor de $\pi$ que você usa é finito...

```python
from math import pi
print(pi)
3.141592653589793
```
- E se pudéssemos usá-lo com precisão infinita?

- 3.141592653589793 é um valor razoavelmente aceitável

- Exemplo: a equipe de engenharia da NASA explica que, usando este valor para calcular o perímetro de uma circunferência com diâmetro igual a 25 bilhões de milhas, o erro de cálculo é próximo de 1,5 polegada [[NASA]](https://www.jpl.nasa.gov/edu/news/2016/3/16/how-many-decimals-of-pi-do-we-really-need/).

## O que é computação simbólica

> *Computação Simbólica* (CS) é uma subárea de estudo da matemática e da ciência da computação que se preocupa em resolver problemas usando objetos simbólicos representáveis em um computador.

## Para que serve CS?

- Base de vários *sistemas de computação algébrica* (SCAs).

- Álgebra computacional, projetos assistidos por computação (CAD)

- Raciocínio automatizado, gestão do conhecimento, lógica computacional, sistemas formais de verificação etc.

### Como a CS está integrada?

Fonte: [[RISC/JKU]](https://risc.jku.at/studying-symbolic-computation/)

## Principais SCAs

- Maple

- Mathematica

- MuPad

- Sagemath

- ...

## Por que *sympy*?

> O objetivo principal do *sympy* é ser uma biblioteca de manipulação simbólica para Python.

- 2006 em diante (2019, v. 1.5.1)

Principais características:

- é gratuito;

- é baseado inteiramente em Python;

- é leve e independente.

## Objetos numéricos x objetos simbólicos

Importaremos os módulos `math` e `sympy` para ver diferenças

import math as mt
import sympy as sy
sy.init_printing(pretty_print=True) # melhor impressão de símbolos

mt.pi # numérico

sy.pi # simbólico

Verifiquemos com `type`.

type(mt.pi)

type(sy.pi) # é um objeto simbólico

Vejamos mais um exemplo:

mt.sqrt(2)

sy.sqrt(2)

type(mt.sqrt(2))

type(sy.sqrt(2)) # é um objeto simbólico

### Função x método

A partir deste ponto, poderemos ver situações como as seguintes:

- `f(x)`: a função `f` é aplicada ao parâmetro `x`; ex. `print('a')`; `type('a')`

- `a.f()`: `f` é um método sem parâmetro do objeto `a`; ex. `z.conjugate()`

- `a.f(x)`: `f` é um método com parâmetro `x` do objeto `a`; ex. `mt.sqrt(2)`

A partir do último exemplo, podemos dizer que um método é, na verdade, uma função que pertence a um objeto.

### Atribuições com símbolos

Podemos atribuir símbolos a variáveis usando a função `symbols`.

x = sy.symbols('x')
y = sy.symbols('y')

`x` e `y` são símbolos sem valor definido.

x

y

Podemos operar aritmeticamente com símbolos e obter uma expressão simbólica como resultado.

z = sy.symbols('z')
x*y + z**2/3 + sy.sqrt(x*y - z)

**Exemplo**: escreva o produto notável $(x - y)^2$ como uma expressão simbólica.

x**2 - 2*x*y + y**2

Note que o nome da variável não tem a ver com o nome do símbolo. Poderíamos fazer o seguinte:

y = sy.symbols('x') # y é variável; x é símbolo
y

### Atribuição por desempacotamento

Também poderíamos realizar as atribuições anteriores da seguinte forma:

x, y, z = sy.symbols('x y z')

### Alfabeto de símbolos

O *sympy* dispõe de um submódulo chamado `abc` do qual podemos importar símbolos para letras latinas (maiúsculas e minúsculas) e gregas (minúsculas).

from sympy.abc import a,b,c,alpha,beta,gamma
(a + 2*b - 3*c)*(alpha/3 + beta/2 - gamma) # símbolico

from sympy.abc import D,G,psi,theta
D**a * G**b * psi**c * theta**2 # símbolico

**Nota**: algumas letras já são usadas como símbolos especiais, tais como `O`, que indica "ordem" e `I`, que é o complexo $i$. Neste caso, cuidado deve ser tomado com nomes de variáveis

sy.I # imaginário simbólico

type(sy.I)

### Símbolos com nomes genéricos

Para criar símbolos genéricos, temos de usar `symbols` ou `Symbol`.

sem_nocao = sy.symbols('nada')
sem_nocao

muito_louco = sy.Symbol('massa')
muito_louco

### Variáveis e símbolos

sem_medo = sem_nocao + 2
sem_medo

soma = muito_louco + 2
muito_louco = 3 # 'muito_louco' aqui não é o simbólico
soma

## Substituição

A operação de *substituição* permite que:

1. substituamos variáveis por valores numéricos para avaliar uma expressão ou calcular valores de uma função em um dado ponto.
2. substituamos uma subexpressão por outra.

Para tanto, procedemos da seguinte forma:

```python
expressao.subs(variavel,valor)
```

**Exemplo**: considere o polinômio $P(x) = 2x^3 - 4x -6$. Calcule o valor de $P(-1)$, $P(e/3)$, $P(\sqrt{3.2})$.

from sympy.abc import x
P = 2*x**3 - 4*x - 6
P1 = P.subs(x,-1)
Pe3 = P.subs(x,mt.e/3)
P32 = P.subs(x,mt.sqrt(3.2))
print(P1, Pe3, P32)

**Exemplo:** sejam $f(x) = 4^x$ e $g(x) = 2x - 1$. Compute o valor da função composta $f(g(x))$ em $x = 3$.

f = 4**x
fg = f.subs(x,2*x - 1)

fg.subs(x,3)

Poderíamos também fazer isso com um estilo "Pythônico":

fg = 4**x.subs(x,2*x - 1).subs(x,3)
fg

**Exemplo:** se $a(x) = 2^x$, $b(x) = 6^x$ e $c(x) = \cos(x)$, compute o valor de $a(x)b(c(x))$ em $x = 4$

a = 2**x
b = 6**x
c = sy.cos(x)
(a * b.subs(x,c)).subs(x,4)

Ou, de modo direto:

valor = ( 2**x * ( 6**x.subs(x,sy.cos(x))) ).subs(x,4)
valor

### Avaliação de expressão em ponto flutuante

Note que a expressão anterior não foi computada em valor numérico. Para obter seu valor numérico, podemos usar o método `evalf`.

valor.evalf()

#### Precisão arbitrária

`evalf` permite que escolhamos a precisão do cálculo impondo o número de dígitos de precisão. Por exemplo, a última expressão com 20 dígitos de precisão seria:

valor.evalf(20)

Com 55, seria:

valor.evalf(55)

E com 90 seria:

valor.evalf(90)

**Exemplo**: calcule o valor de $e$ com 200 dígitos de precisão.

sy.exp(1).evalf(200)

## Funções predefinidas x funções regulares

Apresentaremos 3 grupos de funções que podem ser criadas em Python

- **funções predefinidas** (*built-in functions*): funções já prontas que podemos usar (ex. `print()`, `type()` , `int()`, `float()`

- ** funções regulares**, ou *normais*, *definidas pelo usuário* (do inglês *user-defined functions*, ou simplesmente *UDF*): aquelas que você cria!

Podemos fazer isto de uma maneira usando uma "palavra-chave" (*keyword*) chamada `def` da seguinte forma:

```python
def f(x):
    (...)
    return y
```
- uma UDF **pode ter zero ou mais argumentos**, tantos quantos se queira;
- uma UDF **pode ou não ter valor de retorno**;

Vamos entender as UDFs com exemplos.

**Exemplo:** Suponha que você é um(a) analista de dados do mercado imobiliário e está estudando o impacto do repasse de comissões pagas a corretores mediante vendas de imóveis. Você, então, começa a raciocinar e cria um modelo matemático bastante simples que, antes de tudo, precisa calcular o valor do repasse a partir do preço de venda.

Se $c$ for o percentual de comissão, $V$ o valor da venda do imóvel e $r$ o valor a ser repassado para o corretor, então, a função a ser definida é

$$r(V) = c\, V,$$

assumindo que $c$ seja um valor fixo.

Digamos que $c$ corresponda a 1.03% do valor da venda do imóvel. Neste caso podemos criar uma UDF para calcular $r$ para nós da seguinte forma:

def repasse(V):
    r = 0.0103*V
    return r

Para $V = \, R\$ \, 332.130,00$:

repasse(332130)

O que é necessário observar:

- `def` seguido pelo *nome* da função
- argumentos enclausurados por parênteses
- os dois-pontos (`:`) são obrigatórios
- *escopo* da função, que deve ser escrito em uma ou mais linhas indentadas (pressione `TAB` para isso, ou use 4 espaços)
- o valor de retorno, se houver, é posto na última linha do escopo.

Podemos atribuir os valores do argumento e resultado a variáveis:

V = 332130
rep = repasse(V)
rep

Nomes iguais de variável e função são permissíveis.

repasse = repasse(V) # 'repasse' à esquerda é uma variável; à direita, função
print(repasse)

Todavia, isto pode ser confuso e é bom evitar.

O estilo "Pythônico" de escrever permite que o valor de retorno não seja explicitamente declarado. No escopo

```python
...
    r = 0.0103*V
    return r
```
 a variável `r` não é necessária.

Python é inteligente para permitir o seguinte:

def repasse(V):
    return 0.0103*V

# note que aqui não indentamos a linha.
# Logo esta instrução NÃO pertence ao escopo da função.
repasse(V)

Podemos criar uma função para diferentes valores de `c` e `V` usando *dois* argumentos:

def repasse_c(c,V): # esta função tem outro nome
    return c*V

c = 0.0234 # equivaleria a uma taxa de repasse de 2.34%
V = 197432 # o valor do imóvel agora é R$ 197.432,00
repasse_c(c,V)

A ordem dos argumentos importa:

V = 0.0234 # este deveria ser o valor de c
c = 197432 # este deveria ser o valor de V
repasse_c(c,V)

Por que o valor resultante é o mesmo? Porque a operação no escopo da função é uma multiplicação, `c*V`, que é comutativa independentemente do valor das variáveis. Porém, digamos que um segundo modelo tenha uma forma de cálculo distinta para a comissão dada por

$$r_2(V) = c^{3/5} \, V$$

Neste caso:

def repasse_2(c,V):
    return c**(3/5)*V

V = 197432
c = 0.0234

repasse_2(c,V)

Porém, se trocarmos o valor das variáveis, a função `repasse_2` calculará um valor distinto. Embora exista um produto também comutativo, o expoente `3/4` modifica apenas o valor de `c`.

# variáveis com valores trocados
c = 197432
V = 0.0234

repasse_2(c,V)

A ordem com que escrevemos os argumentos tem importância relativa aos valores que passamos e ao que definimos:

# variáveis com valores corretos
V = 197432
c = 0.0234

def repasse_2_trocada(V,c): # V vem antes de c
    return c**(3/5)*V

repasse_2_trocada(V,c)

Mas,

# os valores das variáveis estão corretos,
# mas foram passados para a função na ordem errada
repasse_2_trocada(c,V)

e

# a ordem dos argumentos está de acordo com a que foi definida
# mas os valores das variáveis foram trocados
V = 197432
c = 0.0234
repasse_2_trocada(c,V)

## Modelos matemáticos simbólicos

A partir do que aprendemos, podemos definir modelos matemáticos completamente simbólicos.

from sympy.abc import c,V

def repasse_2_simbolica(c,V):
    return c**(3/5)*V

Se chamarmos esta função, ela será um objeto simbólico.

repasse_2_simbolica(c,V)

Atribuindo em variável:

rep_simb = repasse_2_simbolica(c,V)

type(rep_simb) # é um objeto simbólico

**Exemplo:** Suponha, agora, que seu modelo matemático de repasse deva considerar não apenas um percentual $c$ pré-estabelecido, mas também um valor de "bônus" adicional concedido como prêmio pela venda do imóvel. Considere, então, que o valor deste bônus seja $b$. Diante disso, nosso novo modelo teria uma fórmula como a seguinte:

$$r_3(V) = c\,V + b$$

Simbolicamente:

# importaremos apenas o símbolo b,
# uma vez que c e V já foram importados
# como símbolos anteriormente
from sympy.abc import b

def r3(V):
    return c*V + b

rep_3 = r3(V)
rep_3

### Substituindo valores

Podemos usar a função `subs` para atribuir quaisquer valores para o modelo.

**Exemplo:** $c = 0.119$

rep_3.subs(c,0.119) # substituindo para c

**Exemplo:** $c = 0.222$

rep_3.subs(c,0.222) # substituindo para c

**Exemplo:** $c = 0.222$ e $b = 12.0$

rep_3.subs(c,0.222).subs(b,12.0) # substituindo para c, depois para b

### Substituição múltipla

O modo anterior de substituição não é "Pythônico". Para substituirmos mais de uma variável de uma vez, devemos usar *pares ordenados* separados por vírgula sequenciados entre colchetes como uma *lista*. Mais tarde, aprenderemos sobre pares ordenados e listas.

**Exemplo:** Modifique o modelo $r_3$ para que $c = 0.043$ e $b = 54.0$

# espaços foram adicionados para dar legibilidade
rep_3.subs( [ (c,0.043), (b,54.0) ] )

#### Pares ordenados

$$X \times Y = \{ (x,y) ; x \in X \text{ e } y \in Y \}$$

onde $X$ e $Y$ são conjuntos quaisquer e $x$ e $y$ são as *coordenadas*.

- ex. $X = Y = \mathbb{R}$; $(3,2)$, $(-1,3)$, $(\pi,2.18)$ etc.

- Este é o caso de $\mathbb{R} \times \mathbb{R} = \mathbb{R}^2$, que é exatamente o *plano cartesiano*.

A substituição múltipla com `subs` ocorre da seguinte forma;

- a primeira coordenada é o *símbolo*;

- a segunda coordenada é o *valor* que você quer dar para o símbolo.

**Exemplo:** Calcule $r_3(V)$ considerando $c = 0.021$, $b = 34.0$ e $V = 432.000$.

# armazenaremos o valor na variável 'valor'
valor = r3(V)

# subsituição
valor.subs( [ (c,0.021), (b,54.0) ] )

Com o estilo "Pythônico":

valor = r3(V).subs( [ (c,0.021), (b,54.0) ] ) #
valor

Podemos seguir esta regra de pares para substituir todos os valores de um modelo simbólico genérico não necessariamente definido através de uma função. Veja o exemplo aplicado a seguir.

## Exemplo de aplicação: o índice de caminhabilidade

O *índice de caminhabilidade* $W$ para uma vizinhança de casas é uma medida matemática que assume valores no intervalo $[0,1]$. A fórmula é definida por:

$$W(d) = e^{-5 \left( \dfrac{d}{M} \right)^5},$$

onde $d$ é a distância medida entre a vizinhança (0 metro) e um dado ponto de referência, e $M$ é a distância máxima de avaliação considerada a partir da qual a caminhabilidade é assumida como nula.

### Interpretação

- quando estamos na vizinhança, $d = 0$, $W = 1$ e a caminhabilidade é considerada ótima.

- à medida que nos afastamos da vizinhança em direção ao local da amenidade, $d$ aumenta e o valor $W$ decai vertiginosamente até atingir o valor limite $M$ a partir do qual $W = 0$ e a caminhabilidade é considerada "péssima".

- W é calculado com relação a um ponto de destino definido
- A distância deve levar em consideração as vias de circulação (ruas, rodovias etc) e não a distância mais curta (raio do perímetro).
- ex. Para $M = 500 \, m$ , um bar a 100 metros da vizinhança teria um índice de caminhabilidade maior do que o de uma farmácia localizada a 300 m e muito maior do que o de um shopping localizado a 800 m, ainda que muito famoso.

Fonte: *De Nadai, M. and Lepri, B. [[The economic value of neighborhoods: Predicting real estate prices from the urban environment]](https://arxiv.org/pdf/1808.02547.pdf)*.

### Modelo simbólico

Podemos modelar $W$ simbolicamente e calcular seu valor para diferentes valores de $d$ e $M$ usando a substituição múltipla.

from sympy.abc import d,M,W

W = sy.exp(-5*(d/M)**5) # função exponencial simbólica
W

**Exemplo:** A nossa corretora de imóveis gostaria de entender a relação de preços de imóveis para o Condomínio Pedras de Marfim. Considerando $M = 1 km$, calcule:

- o índice de caminhabilidade $W_1$ em relação à farmácia Dose Certa, localizada a 222 m do condomínio.

- o índice de caminhabilidade $W_2$ em relação ao restaurante Sabor da Arte, localizada a 628 m do condomínio.

- o índice de caminhabilidade $W_3$ em relação ao Centro Esportivo Physicalidade, localizada a 998 m do condomínio.

- o índice de caminhabilidade $W_4$ em relação à Padaria Dolce Panini, localizada a 1,5 km do condomínio.

# note que 1 km = 1000 m
W1 = W.subs([ (d,222), (M,1000) ])
W2 = W.subs([ (d,628), (M,1000) ])
W3 = W.subs([ (d,998), (M,1000) ])
W4 = W.subs([ (d,1500), (M,1000) ])

Perceba, entretanto, que os valores calculados ainda não são numéricos, como esperado.

W1

W2

W3

W4

Lembre-se que podemos usar `evalf` para calcular esses valores. Faremos isso considerando 3 casas decimais.

# reatribuindo todos os valores
W1n = W1.evalf(3)
W2n = W2.evalf(3)
W3n = W3.evalf(3)
W4n = W4.evalf(3)

print('W1 =', W1n, '; ' \
      'W2 =', W2n, '; ' \
      'W3 =', W3n, '; ' \
      'W4 =', W4n)

Como era de se esperar, os valores decaem de 0.997 a 3.24e-17, que é um valor considerado nulo em termos de aproximação numérica.

#### Quebrando instruções com `\`

A contra-barra `\` pode ser usada para quebrar instruções e continuá-las nas próximas linhas, porém não poderá haver nenhum caracter após ela, nem mesmo espaços. Caso contrário, um erro será lançado.

print('Continuando' \
      'na linha abaixo')

# neste exemplo, há um caracter de espaço após \
print('Continuando' \
      'na linha abaixo')

### O tipo `bool`

Em Python, temos mais um tipo de dado bastante útil, o `bool`, que é uma redução de "booleano". Objetos `bool`, que têm sua raiz na chamada Álgebra de Boole, são baseados nos conceitos *true* (verdadeiro) e *false*, ou *0* e *1* e são estudados em algumas disciplinas, tais como Circuitos Lógicos, Matemática Discreta, Lógica Aplicada, entre outras.

Aprenderemos sobre operadores lógicos mais à frente. Por enquanto, cabe mencionar as entidades fundamentais `True` e `False`.

True

False

type(True)

type(False)

Podemos realizar testes lógicos para concluir verdades ou falsidades quando temos dúvidas sobre objetos e relações entre eles. Por exemplo, retomemos os seguintes valores:

W1

W2

A princípio, é difícil determinar qual dos dois é o maior. Porém, podemos realizar "perguntas" lógicas para o interpretador Python com operadores lógicos. Mostraremos apenas dois exemplos com `>` e `<`.

W1 > W2 # isto quer dizer: "W1 é maior do que W2?"

O valor `True` confirma que o valor de `W1` é maior do que `W2`.

W4 < 0

> Note que, de acordo com nosso modelo de caminhabilidade, este valor deveria ser zero. Porém, numericamente, ele é uma aproximação para zero. Embora muito pequeno, não é exatamente zero! Por que isso ocorre? Porque o computador lida com uma matemática inexata e aproximada, mas com precisão satisfatória.


"""
  Name     : c12_26_simulation_link_2method_VaR.py
  Book     : Python for Finance (2nd ed.)
  Publisher: Packt Publishing Ltd.
  Author   : Yuxing Yan
  Date     : 6/6/2017
  email    : yany@canisius.edu
             paulyxy@hotmail.com
"""

import numpy as np
import scipy as sp
import pandas as pd
from scipy.stats import norm
#
position=1e6              # portfolio value
std=0.2                   # volatility
mean=0.08                 # mean return
confidence=0.99           # confidence level
nSimulations=50000        # number of simulations
# Method I
z=norm.ppf(1-confidence)
VaR=position*(mean+z*std)
print("Holding=",position, "VaR=", round(VaR,0), "tomorrow")
#
# Method II: Monte Carlo simulaiton
sp.random.seed(12345)
ret2=sp.random.normal(mean,std,nSimulations)
ret3=np.sort(ret2)
m=int(nSimulations*(1-confidence))
VaR2=position*(ret3[m])
print("Holding=",position, "VaR2=", round(VaR2,0), "tomorrow")


'''
Interpolate DFT to correct frequency/amplitude/phase
Take a cosine wave for example

XiaoCY 2021-02-23
'''

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

fs = 1000
N = 2000

t = np.arange(N)/fs
ax = 3.0
fx = 12.3
px = 0.5
x = ax*np.cos(2*np.pi*fx*t+px)

#%% uncorrect DFT
X = np.fft.fft(x,axis=0)
Nf = int(np.round(N/2.0+0.5))
f = np.arange(N)*fs/N
A = np.abs(X[:Nf])*2.0/N

k = np.argmax(A)
f1 = f[k]
a1 = A[k]
p1 = np.angle(X[k])

#%% ratio method: rectangular window
if A[k-1] > A[k+1]:
    dk = -A[k-1]/(A[k-1]+A[k])
else:
    dk = A[k+1]/(A[k+1]+A[k])

f2 = (k+dk)*fs/N
a2 = a1/np.sinc(dk)
p2 = p1-dk*np.pi

#%% ratio method: hanning window
win = np.hanning(N)
X3 = np.fft.fft(x*win,axis=0)
A3 = np.abs(X3[:Nf])*2.0/np.sum(win)

k3 = np.argmax(A3)
a3 = A3[k3]
p3 = np.angle(X3[k3])

if A3[k3-1] > A3[k3+1]:
    dk = (A3[k3]-2*A3[k3-1])/(A3[k3-1]+A3[k3])
else:
    dk = (2.0*A3[k3+1]-2*A3[k3])/(A3[k3+1]+A3[k3])

f3 = (k3+dk)*fs/N
a3 = a3*(1-dk**2)/np.sinc(dk)
p3 = p3-dk*np.pi

#%% results
print('%-18s%-12s%-12s%-12s%-12s\n' % ('','FFT','Rectwin','Hanning','True Value'))
print('%-18s%-12f%-12f%-12f%-12f\n' % ('Frequency (Hz)',f1,f2,f3,fx))
print('%-18s%-12f%-12f%-12f%-12f\n' % ('Amplitude',a1,a2,a3,ax))
print('%-18s%-12f%-12f%-12f%-12f\n' % ('Phase (rad)',p1,p2,p3,px))

xe1 = a1*np.cos(2*np.pi*f1*t+p1)-x
xe2 = a2*np.cos(2*np.pi*f2*t+p2)-x
xe3 = a3*np.cos(2*np.pi*f3*t+p3)-x

plt.figure()
plt.plot(t,xe1)
plt.plot(t,xe2)
plt.plot(t,xe3)
plt.grid()
plt.legend(('FFT','rect-correct','hann-corrcet'))
plt.xlabel('Time (s)')
plt.ylabel('Error')
plt.show()


# -*- coding: utf-8 -*-
"""
Ejercicio 1: Aproximación numérica de la derivada de la función f(x) = exp(x).

"""

import numpy as np
import matplotlib.pyplot as plt

# Función exponencial y su derivada f'
f =  lambda x:np.exp(x)            # función f
df = lambda x:np.exp(x)          # derivada exacta f'

#---------------------------------------------------------------
# h = 0.1
#---------------------------------------------------------------
h = 0.1     # paso

# Gráfica de la derivada
plt.close('all') # cerramos todas las ventanas

plt.figure(1)
xx = np.linspace(0,1)             # definimos un vector x para dibujar la derivada en [0,1]
plt.plot(xx,df(xx),'--')   # dibujamos la derivada
plt.grid()

# Diferencias progresivas, regresivas y centradas
a = 0               # extremo inferior del intervalo
b = 1               # extremo superior del intervalo
x = np.arange(a,b+h,h)    # vector con 1º coordenada a, última coordenanda b y diferencia entre coordenadas h
df_p = np.diff(f(x))/h   # vector que contiene los valores de las diferencias progresivas
df_r = df_p                     # vector que contiene los valores de las diferencias regresivas
df_c = (df_p[1:] + df_r[0:-1])/2 # vector que contiene los valores de las diferencias centradas

# Gráfica de f' y de las diferencia progresivas, regresivas y centradas
xp = x[0:-1]    # vector con las coordenadas x para las diferencias progresivas
xr = x[1:]    # vector con las coordenadas x para las diferencias regresivas
xc = x[1:-1]    # vector con las coordenadas x para las diferencias centradas
plt.plot(xp,df_p,xr,df_r,xc,df_c)  # dibujamos las diferencias progresivas, regresivas y centradas
plt.legend(["df","df_p","df_r","df_c"]) # leyenda
plt.title("Derivada de f(x) = e^x y sus aproximaciones (h=0.1)") # título de la gráfica
plt.show()

## Gráfica de los errores en cada punto
plt.figure(2)  # abrimos una nueva figura
plt.plot(xp,abs(df(xp)-df_p),'o',xr,abs(df(xr)-df_r),'o',xc,abs(df(xc)-df_c),'o')    # dibujamos los valores |f'(x_i)-df_aprox(x_i)| para las
# diferentes aproximaciones (progresiva, regresiva y centrada)
plt.grid()
plt.legend(["df_p","df_r","df_c"]) # leyenda
plt.title("Errores (h=0.1)")    # título
plt.show()

## Errores globales relativos
ErrorG_p = np.linalg.norm((df(xp)-df_p))/np.linalg.norm(df(xp)) # error global relativo para aprox. progresiva
ErrorG_r = np.linalg.norm((df(xr)-df_r))/np.linalg.norm(df(xr)) # error global relativo para aprox. regresiva
ErrorG_c = np.linalg.norm((df(xc)-df_c))/np.linalg.norm(df(xc)) # error global relativo para aprox. centrada
print("h = ",h)   # escribimos en pantalla los errores
print("ErrorG_p = ",ErrorG_p)
print("ErrorG_r = ",ErrorG_r)
print("ErrorG_c = ",ErrorG_c)

#---------------------------------------------------------------
# h = 0.01
#---------------------------------------------------------------

h = 0.01     # paso

# Gráfica de la derivada
plt.close('all') # cerramos todas las ventanas

plt.figure(1)
xx = np.linspace(0,1)             # definimos un vector x para dibujar la derivada en [0,1]
plt.plot(xx,df(xx),'--')   # dibujamos la derivada
plt.grid()

# Diferencias progresivas, regresivas y centradas
a = 0               # extremo inferior del intervalo
b = 1               # extremo superior del intervalo
x = np.arange(a,b+h,h)    # vector con 1º coordenada a, última coordenanda b y diferencia entre coordenadas h
df_p = np.diff(f(x))/h   # vector que contiene los valores de las diferencias progresivas
df_r = df_p                     # vector que contiene los valores de las diferencias regresivas
df_c = (df_p[1:] + df_r[0:-1])/2 # vector que contiene los valores de las diferencias centradas

# Gráfica de f' y de las diferencia progresivas, regresivas y centradas
xp = x[0:-1]    # vector con las coordenadas x para las diferencias progresivas
xr = x[1:]    # vector con las coordenadas x para las diferencias regresivas
xc = x[1:-1]    # vector con las coordenadas x para las diferencias centradas
plt.plot(xp,df_p,xr,df_r,xc,df_c)  # dibujamos las diferencias progresivas, regresivas y centradas
plt.legend(["df","df_p","df_r","df_c"]) # leyenda
plt.title("Derivada de f(x) = e^x y sus aproximaciones (h=0.1)") # título de la gráfica
plt.show()

## Gráfica de los errores en cada punto
plt.figure(2)  # abrimos una nueva figura
plt.plot(xp,abs(df(xp)-df_p),'o',xr,abs(df(xr)-df_r),'o',xc,abs(df(xc)-df_c),'o')    # dibujamos los valores |f'(x_i)-df_aprox(x_i)| para las
# diferentes aproximaciones (progresiva, regresiva y centrada)
plt.grid()
plt.legend(["df_p","df_r","df_c"]) # leyenda
plt.title("Errores (h=0.1)")    # título
plt.show()

## Errores globales relativos
ErrorG_p = np.linalg.norm((df(xp)-df_p))/np.linalg.norm(df(xp)) # error global relativo para aprox. progresiva
ErrorG_r = np.linalg.norm((df(xr)-df_r))/np.linalg.norm(df(xr)) # error global relativo para aprox. regresiva
ErrorG_c = np.linalg.norm((df(xc)-df_c))/np.linalg.norm(df(xc)) # error global relativo para aprox. centrada
print("h = ",h)   # escribimos en pantalla los errores
print("ErrorG_p = ",ErrorG_p)
print("ErrorG_r = ",ErrorG_r)
print("ErrorG_c = ",ErrorG_c)


# Author: Valeria Telles
# Date: 19 October 2020
# Course: PHYS3230 - Quantum Mechanics I

import numpy as np
import matplotlib.pyplot as plt

def trapz(X, Y):
    """
    Approximate the integral of f(x) from a to be by the trapezoid rule.

    Parameters
    ----------
    X : numbers - X values contained within the file 'wavedata.csv'
    Y : numbers - Y values contained within the file 'wavedata.csv'

    Returns
    -------
    float - approximation of the integral of f(x) using the trapezoid rule.
    """

    return 0.5*((X[1:]-X[:-1])*(Y[1:]+Y[:-1])).sum()

def coefficients(X, Y, n):
    """
    Calculate the coefficinents for the sine and cosine components of the trigonometric
    Fourier Series

    Parameters
    ----------
    X : numbers - X values contained within the file 'wavedata.csv'
    Y : numbers - Y values contained within the file 'wavedata.csv'
    n : numbers - 0, 1, 2...

    Returns
    -------
    list - coefficients of the trigonometric Fourier Series up to n = 5.
    """
    A = 0
    B = 0

    An = []
    Bn = []

    while (n <= 5):
        X1 = X
        Y1 = Y*np.cos(2*np.pi*X*n/10)
        Y2 = Y*np.sin(2*np.pi*X*n/10)

        A += trapz(X1, Y1)
        B += trapz(X1, Y2)

        A = A*2/10
        B = B*2/10

        An.append(A)
        Bn.append(B)
        n = n + 1

    return An, Bn

def fourier(x, n, An, Bn):
    """
    Reconstructing the function f(x) using a restricted number of Fourier modes.

    Parameters
    ----------
    x : numbers - X values contained within the file 'wavedata.csv'
    n : numbers - 0, 1, 2...
    An : list of values for the coefficient 'An' up to n = 5
    Bn : list of values for the coefficient 'Bn' up to n = 5

    Returns
    -------
    ndarray - reconstructed fourier series of order 'n'.
    """
    a0 = An[0]
    series = np.zeros(np.size(x))
    for i in range(1, n+1):
        series += ( An[i] * np.cos(2*np.pi*x*i/10) + Bn[i] * np.sin(2*np.pi*x*i/10) )
    return (a0/2) + series

def main():
    X, Y = np.loadtxt("wavedata.csv", delimiter=',', usecols=(0,1), unpack=True)
    k = np.arange(0, 6, 1)
    n = 0

    An, Bn = coefficients(X, Y, 0)

    print(fourier(X, 2, An, Bn))

    fig, (ax1, ax2) = plt.subplots(1, 2)
    fig.set_figheight(9)
    fig.set_figwidth(12)
    fig.tight_layout(pad=5.0)

    ax1.set_facecolor('#EAEAEA')
    ax1.bar(k, An, width=0.4, align='edge', label='An', color='#E40303')
    ax1.bar(k, Bn, width=-0.4, align='edge', label='Bn', color='#0402BD')
    ax1.legend(fontsize=12, shadow=True)
    ax1.set_xlabel('value of n', fontsize=12, fontweight='bold')
    ax1.set_ylabel('relative contribution', fontsize=12, fontweight='bold')
    ax1.set_title('Relative Contributions of An and Bn', fontsize=14, fontweight='bold')
    ax1.grid(color='#F6F6F6', linestyle='-')
    ax1.set_axisbelow(True)

    ax2.set_facecolor('#EAEAEA')
    ax2.plot(X, Y, label='original', color='#E40303')
    ax2.plot(X, fourier(X, 2, An, Bn), label='n = 2', color='#0402BD')
    ax2.plot(X, fourier(X, 3, An, Bn), label='n = 3', color='#38B748')
    ax2.plot(X, fourier(X, 5, An, Bn), label='n = 5', color='#F2AD12')
    ax2.legend(fontsize=12, shadow=True)
    ax2.set_xlabel('x', fontsize=12, fontweight='bold')
    ax2.set_ylabel('f(x)', fontsize=12, fontweight='bold')
    ax2.set_title('Fourier Reconstruction (n = modes)', fontsize=14, fontweight='bold')
    ax2.grid(color='#F6F6F6', linestyle='-')
    ax2.set_axisbelow(True)

    fig.savefig('FourierSeries.png', dpi = 900)

if __name__ == "__main__":
    main()


from scipy.interpolate import interp1d
import matplotlib.pyplot as plt
import numpy as np

t = np.linspace(0, 10, 10)
value = np.sin(2 * np.pi * t)
plt.plot(t, value, 'b^', ms=6, label='real value')

f1 = interp1d(t, value)  # 產生線性插值函數
x = np.linspace(0, 10, 50)  # 將間隔細分為50個區段
y = f1(x)  # 利用線性插值函數產生50個插值
plt.plot(x, y, "ro", label='linear interplot')

f1 = interp1d(t, value)  # 產生線性插值函數
x = np.linspace(0, 10, 50)  # 將間隔細分為50個區段
# y = interp1d(x)  # 利用線性插值函數產生50個插值
# plt.plot(x, y, label='linear interplot')

x = np.linspace(0, 10, 200)
f3 = interp1d(t, value, kind='cubic')  # 取得三次插值法函數
y = f3(x)  # 取得三次插值的值
plt.plot(x, y, label='cubic interpolate')
plt.show()

# import numpy as np
# from scipy.interpolate import interp1d

# x = np.linspace(0, 10, num=11, endpoint=True)
# y = np.cos(-x**2/9.0)
# f = interp1d(x, y)
# f2 = interp1d(x, y, kind='cubic')

# xnew = np.linspace(0, 10, num=41, endpoint=True)
# import matplotlib.pyplot as plt
# plt.plot(x, y, 'o', xnew, f(xnew), '-', xnew, f2(xnew), '--')
# plt.legend(['data', 'linear', 'cubic'], loc='best')
# plt.show()


#!/bin/python3

import numpy as np

def number_to_binary_array(n, r):
    """
    input integer
    output binary representation of integer as values in an array that can hold numbers less than 2^r
    """
    res = [int(i) for i in bin(n)[2:]]
    while len(res) < r:
        res = [0] + res
    return res

def make_identity_matrix(n):
    """
    input n
    output list of lists, identity matrix I_n
    """
    return [number_to_binary_array(2**i, n) for i in reversed(range(n))]

def make_extended_Golay_generator():
    """
    returns the generator for the extended Golay code
    """
    B = [[1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0]]
    for _ in range(10):
        next_line = B[-1][:]
        next_line = next_line + [next_line.pop(0)]
        B.append(next_line)
    j = [[1]*11]
    G_extended = np.concatenate((np.array(B), np.array(j)))
    j_0 = [[1]]*11 + [[0]]
    G_extended = np.concatenate((np.array(G_extended), np.array(j_0)), axis=1)
    Id_12 = make_identity_matrix(12)
    G_extended = np.concatenate((Id_12, np.array(G_extended)), axis=1)
    return G_extended

G_extended = np.array([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1],
                       [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1],
                       [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1],
                       [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1],
                       [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1],
                       [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1],
                       [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1],
                       [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1],
                       [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1],
                       [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1],
                       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1],
                       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]])

G = np.array([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0],
              [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1],
              [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1],
              [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0],
              [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1],
              [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1],
              [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1],
              [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0],
              [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0],
              [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0],
              [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1],
              [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])

def encode_extended_Golay(info):
    global G_extended

    if type(info) == str:
        info = [int(i) for i in info]
        res = info @ G_extended % 2
        return ''.join(str(i) for i in res)

    if type(info) == list:
        return list(info @ G_extended % 2)

    if type(info) == np.ndarray:
        return info @ G_extended % 2

def encode_Golay(info):
    global G

    if type(info) == str:
        info = [int(i) for i in info]
        res = info @ G % 2
        return ''.join(str(i) for i in res)

    if type(info) == list:
        return list(info @ G % 2)

    if type(info) == np.ndarray:
        return info @ G % 2


"""
Solves the second order ODE:

y'' + y = 0
y(0)=0; y'(0)=1

So the solution is y(x) = sin(x)
"""
from hermes1d import Node, Element, Mesh, DiscreteProblem

from math import pi
from numpy import zeros

def plot_Y(Y, a, b):
    # plot the result:
    from pylab import plot, legend, show, clf, axis
    #clf()
    # linearize the solution, divide each element into 5
    sln1, sln2 = d.linearize(Y, 5)
    x1, y1 = sln1
    x2, y2 = sln2
    plot(x1, y1, label="$u_1$")
    plot(x2, y2, label="$u_2$")
    axis([a, b, -1.5, 1.5])
    #legend()
    show()

# interval end points
a = 0.
b = pi*(1./4+1)

# number of elements:
N = 20

# x values of the nodes:
x_values =[(b-a)/N * i for i in range(N+1)]

# define nodes:
nodes = [Node(x) for x in x_values]

# define elements of the 1st mesh
elements = [Element(nodes[i], nodes[i+1], order=2) for i in range(N)]
m1 = Mesh(nodes, elements)
m1.set_bc(left=True, value=0)

# define elements of the 2nd mesh
elements = [Element(nodes[i], nodes[i+1], order=1) for i in range(N)]
m2 = Mesh(nodes, elements)
m2.set_bc(left=True, value=1)

# definition of the ODE system:
d = DiscreteProblem(meshes=[m1, m2])

# definition of the RHS:
def F(i, Y, t):
    if i == 0:
        return Y[1]
    elif i == 1:
        return -Y[0]
    raise ValueError("Wrong i (i=%d)." % (i))

# definition of the Jacobian matrix
def DFDY(i, j, Y, t):
    if i == 0 and j == 0:
        return 0.
    elif i == 0 and j == 1:
        return 1.
    elif i == 1 and j == 0:
        return -1.
    elif i == 1 and j == 1:
        return 0.
    raise ValueError("Wrong i, j (i=%d, j=%d)." % (i, j))

# assign both F and J to the discrete problem:
d.define_ode(F, DFDY)

# enumeration of unknowns:
d.assign_dofs()

# definition of the initial condition for the global Newton method:
Y = d.get_initial_condition_euler()
#plot_Y(Y, a, b)
#stop
#Y = zeros((d.ndofs,))

# Newton's iteration:
error = 1e10
i = 0
J = d.assemble_J(Y)
while error > 1e-5:
    F = d.assemble_F(Y)
    dY = d.solve(J, F)
    Y += dY
    #plot_Y(Y, a, b)
    error_dY = d.calculate_error_l2_norm(dY)
    error_F = d.calculate_error_l2_norm(d.assemble_F(Y))
    print "it=%d, l2_norm_dY=%e, l2_norm_F=%e" % (i, error_dY, error_F)
    error = max(error_dY, error_F)
    i += 1

plot_Y(Y, a, b)


"""
Random projection, Assignment 1c
"""
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
import random, mnist_dataloader
from scipy.spatial.distance import euclidean
import multiprocessing
import time

"""
load data sets
"""
def load_data_sets():
# load data set
    training_data, validation_data, test_data = mnist_dataloader.load_data()
    training_data_instances = training_data[0]
    training_data_labels = training_data[1]
    test_data_instances = test_data[0]
    test_data_labels = test_data[1]

    return training_data_instances, training_data_labels, test_data_instances, test_data_labels

"""
Generate random projection matrix R
@param: k, the reduced number of dimensions
@param: d, the original number of dimensions
@return: R, the generated random projection matrix, k * d size
"""

def generate_random_projection_matrix(k, d):
    R = np.zeros((k, d), dtype = np.float64)
    for r in np.nditer(R, op_flags=['readwrite']):
        r[...] = 2 * random.randint(0, 1) - 1
    # to be cautious, this is the point the exercise wants us to get
    R *= 1.0 / np.sqrt(k)

    return R

"""
random projection matrix P into R
@param R: random projection matrix
@param P: matrix to be reduced in dimension
@return: Q: projected matrix of P on R
"""
def random_projection(R, P):
    if R.shape[1] != P.shape[0]:
        return False

    print R.shape, P.shape

    return np.dot(R, P)

"""
plot distortion
@param training_data_instances: training data instances
"""
def plot_distortion(training_data_instances):
    # dimension of a training data instance
    d = training_data_instances.shape[1]
    # first m instances considered
    m = 20

    fig, axes = plt.subplots(1, 1)
    fig.suptitle("Distortion of random projection", fontsize = "x-large")

    for k in [50, 100, 500]:
        ## generate random projection matrix
        random_projection_matrix =  generate_random_projection_matrix(k, d)
        ## random projection
        m_instances = training_data_instances[0:m]
        projected_m_instances = np.dot(m_instances, np.transpose(random_projection_matrix))
        # print random_projected_matrix[0], random_projected_matrix.shape
        ## evaluate distortion - line chart
        m_instances_distortions = []
        for i in range(m):
            for j in range(i + 1, m):
                m_instances_distortions.append(euclidean(projected_m_instances[i], projected_m_instances[j]) / euclidean(m_instances[i], m_instances[j]))
        m_instances_distortions = np.array(m_instances_distortions)
        mean, std = np.mean(m_instances_distortions), np.std(m_instances_distortions)
        # line chart
        axes.plot(m_instances_distortions, label = "k=" + str(k))
        axes.plot([0, m_instances_distortions.size], [mean, mean], label = "k=" + str(k) + ", mean = " + str(round(mean, 4)))

        print "k = ", k, "distortion =", mean, "+-", std
    axes.set_xlabel("pairs of instances", fontsize = "large")
    axes.set_ylabel("distortion", fontsize = "large")
    axes.legend(loc = "center right", fontsize = "medium")
    plt.show()

"""
classify test_data
@param training_data_instances
@param training_data_labels
@param test_data_instances
@param test_instance_start_index
@param test_instance_end_index
@param classified_results: classified results, shared by different subprocesses
@return: None. Note, the updated classified results array.
"""
def find_nearest_instances_subprocess(training_data_instances, training_data_labels, test_data_instances, test_instance_start_index, test_instance_end_index,\
                                      classified_results):
    # print test_instance_start_index, test_instance_end_index
    for test_instance_index in range(test_instance_start_index, test_instance_end_index):
        test_instance = test_data_instances[test_instance_index]
        # find the nearest training instance with euclidean distance
        minimal_euclidean_distance = euclidean(test_instance, training_data_instances[0])
        minimal_euclidean_distance_index = 0
        for training_instance, training_instance_index in zip(training_data_instances, range(len(training_data_instances))):
            # compute the euclidean distance
            euclidean_distance = euclidean(test_instance, training_instance)
            if euclidean_distance < minimal_euclidean_distance:
                minimal_euclidean_distance = euclidean_distance
                minimal_euclidean_distance_index = training_instance_index
        classified_results[test_instance_index] =\
         training_data_labels[int(minimal_euclidean_distance_index)]

"""
find nearest neighbor algorithm without random projection
@param training_data_instances
@param training_data_labels
@param test_data_instances
@param test_data_labels
@return: classified_results, error_rate, and confusion_matrix
"""
def find_nearest_instances(training_data_instances, training_data_labels, test_data_instances, test_data_labels):
    start_time = time.time()
    # speed using multiple processes
    NUMBER_OF_PROCESSES = 4
    processes = []
    # shared by different processes, to be mentioned is that
    # global variable is only read within processes
    # the update of global variable within a process will not be submitted
    classified_results = multiprocessing.Array('i', len(test_data_instances), lock = False)
    test_data_subdivisions = range(0, len(test_data_instances) + 1,\
                                    int(len(test_data_instances) / NUMBER_OF_PROCESSES))
    test_data_subdivisions[-1] = len(test_data_instances)
    for process_index in range(NUMBER_OF_PROCESSES):
        process = multiprocessing.Process(target = find_nearest_instances_subprocess,
                                          args = (training_data_instances,
                                                  training_data_labels,
                                                  test_data_instances,
                                                  test_data_subdivisions[process_index],
                                                  test_data_subdivisions[process_index + 1],
                                                  classified_results))
        process.start()
        processes.append(process)

    print "Waiting..."
    # wait until all processes are finished
    for process in processes:
        process.join()
    print "Complete."
    print "--- %s seconds ---" % (time.time() - start_time)

    error_count = 0
    confusion_matrix = np.zeros((10, 10), dtype=np.int)
    for test_instance_index, classified_label in zip(range(len(test_data_instances)),\
                                                      classified_results):
        if test_data_labels[test_instance_index] != classified_label:
            error_count += 1
        confusion_matrix[test_data_labels[test_instance_index]][classified_label] += 1

    error_rate = 100.0 * error_count / len(test_data_instances)

    return classified_results, error_rate, confusion_matrix

if __name__ == '__main__':
    multiprocessing.freeze_support()
    training_data_instances, training_data_labels, test_data_instances, test_data_labels = load_data_sets()

    ## plot distortion
    # plot_distortion(training_data_instances)
    ## nearest neighbor without random projection
    print "Without random projection"
    classified_results, error_rate, confusion_matrix = find_nearest_instances(training_data_instances, training_data_labels, test_data_instances, test_data_labels)
    print "Error rate:", error_rate
    print "Confusion matrix:", confusion_matrix

    ## random projection
    # dimension of a training data instance
    d = training_data_instances.shape[1]
    for k in [50, 100, 500]:
        ## generate random projection matrix
        random_projection_matrix =  generate_random_projection_matrix(k, d)
        ## random projection
        projected_training_instances = np.zeros((len(training_data_instances), k), dtype = np.float64)
        for i in range(training_data_instances.shape[0]):
            for j in range(projected_training_instances.shape[1]):
                projected_training_instances[i][j] = np.dot(random_projection_matrix[j], training_data_instances[i])

        projected_test_instances = np.zeros((len(test_data_instances), k), dtype = np.float64)
        for i in range(test_data_instances.shape[0]):
            for j in range(projected_test_instances.shape[1]):
                projected_test_instances[i][j] = np.dot(random_projection_matrix[j], test_data_instances[i])

        classified_results, error_rate, confusion_matrix = find_nearest_instances(projected_training_instances, training_data_labels, projected_test_instances, test_data_labels)
        print "Error rate:", error_rate
        print "Confusion matrix:", confusion_matrix


"""
Monte Carlo methods, or Monte Carlo experiments, are a broad class of computational algorithms
that rely on repeated random sampling to obtain numerical results. The underlying concept is to
use randomness to solve problems that might be deterministic in principle. They are often used
in physical and mathematical problems and are most useful when it is difficult or impossible to
use other approaches. Monte Carlo methods are mainly used in three problem classes:[1] optimization,
numerical integration, and generating draws from a probability distribution.

Link wiki: https://en.wikipedia.org/wiki/Monte_Carlo_method
Link article: https://medium.com/cantors-paradise/estimating-%CF%80-using-monte-carlo-simulations-3459a84b5ef9

This code is an interactive application that estimates the value of pi
using the unit square unit circle method.

pi = 4 * (number of points in the circle)/(number of points in the square)

Author: Harivinay
Github: github.com/M87K452b
Date: Feb 2021

Note: The animate function is yet to be implemented.
"""

from tkinter import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np

root = Tk()
root.title("Estimate Pi")
root.geometry("900x600")

def make_points():
    num_pts = int(num_points.get())
    data = np.random.random((num_pts, 2))
    return data

def plot_points():

    global data
    data = make_points()
    figure = plt.figure(figsize=(6, 6), dpi=100)
    draw_circle = plt.Circle((0.5135, 0.4945), 0.3855, fill=False)
    figure.add_artist(draw_circle)
    figure.add_subplot(111).scatter(data[:, 0], data[:, 1], s=1)
    chart = FigureCanvasTkAgg(figure, frame_cv)
    chart.get_tk_widget().grid(row=0, column=0)

    plt.grid("off")
    axes = plt.axes()
    axes.set_xlim([0, 1])
    axes.set_ylim([0, 1])

def calc_pi():
    in_circle = 0
    pi = 0
    for i in range(len(data)):
        if np.sqrt(data[i, 0] ** 2 + data[i, 1] ** 2) <= 1:
            in_circle = in_circle + 1

    pi = 4 * (in_circle / len(data))

    num_in = "Points In = " + str(in_circle)
    num_out = "Points Out = " + str(len(data) - in_circle)

    Label(frame_btn, text=num_in, width=25, height=2, font=("Helvetica", 16)).grid(
        row=5, column=0
    )
    Label(frame_btn, text=num_out, width=25, height=2, font=("Helvetica", 16)).grid(
        row=6, column=0
    )

    pi_text = "Pi = " + str(round(pi, 6))
    estimated_pi = Label(
        frame_btn, text=pi_text, width=25, height=2, font=("Helvetica", 16)
    )
    estimated_pi.grid(row=7, column=0, padx=10, pady=5)

def animate():
    return

frame_cv = LabelFrame(root, padx=1, pady=1)
frame_cv.place(height=598, width=598, x=1, y=1)

figure = plt.figure(figsize=(6, 6), dpi=100)
figure.add_subplot(111)
draw_circle = plt.Circle((0.5135, 0.4945), 0.3855, fill=False)
figure.add_artist(draw_circle)
chart = FigureCanvasTkAgg(figure, frame_cv)
chart.get_tk_widget().grid(row=0, column=0)
plt.grid()
axes = plt.axes()
axes.set_xlim([0, 1])
axes.set_ylim([0, 1])

frame_btn = LabelFrame(root, padx=1, pady=20)
frame_btn.place(height=599, width=298, x=601, y=1)

Label(frame_btn, text="Number of points", font=("Helvetica", 16)).grid(row=0, column=0)

num_points = Scale(
    frame_btn, from_=0, to=100000, resolution=100, orient=HORIZONTAL, length=185
)
num_points.grid(row=1, column=0, padx=10, pady=10)

plot_btn = Button(
    frame_btn, text="Plot points", width=25, height=5, command=plot_points
)
plot_btn.grid(row=2, column=0, padx=10, pady=5)

pi_btn = Button(frame_btn, text="Estimate Pi", width=25, height=5, command=calc_pi)
pi_btn.grid(row=3, column=0, padx=10, pady=5)

animate = Button(
    frame_btn, text="Animate", width=25, height=5, command=animate, state=DISABLED
)
animate.grid(row=4, column=0, padx=5, pady=5)

num_in = "Points In = 0"
num_out = "Points Out = 0"
pi = "Pi = "

Label(frame_btn, text=num_in, width=25, height=2, font=("Helvetica", 16)).grid(
    row=5, column=0
)
Label(frame_btn, text=num_out, width=25, height=2, font=("Helvetica", 16)).grid(
    row=6, column=0
)
Label(frame_btn, text=pi, width=26, height=2, font=("Helvetica", 16)).grid(
    row=7, column=0
)

exit_btn = Button(frame_btn, text="Exit", width=25, height=5, command=root.quit)
exit_btn.grid(row=8, column=0, padx=5, pady=5)

root.mainloop()


"""
Please see
https://computationalmindset.com/en/neural-networks/ordinary-differential-equation-solvers.html#sys1
for details
"""

import numpy as np
import matplotlib.pyplot as plt

from scipy.integrate import solve_ivp

def ode_sys(t, XY):
	x=XY[0]
	y=XY[1]
	dx_dt= - x + y
	dy_dt= 4. * x - y
	return [dx_dt, dy_dt]

an_sol_x = lambda t : np.exp(t) + np.exp(-3. * t)
an_sol_y = lambda t : 2. * np.exp(t) - 2. * np.exp(-3. * t)

t_begin=0.
t_end=5.
t_nsamples=100
t_space = np.linspace(t_begin, t_end, t_nsamples)
x_init = 2.
y_init = 0.

x_an_sol = an_sol_x(t_space)
y_an_sol = an_sol_y(t_space)

method = 'RK45' #available methods: 'RK45', 'RK23', 'DOP853', 'Radau', 'BDF', 'LSODA'
num_sol = solve_ivp(ode_sys, [t_begin, t_end], [x_init, y_init], method=method, dense_output=True)
XY_num_sol = num_sol.sol(t_space)
x_num_sol = XY_num_sol[0].T
y_num_sol = XY_num_sol[1].T

plt.figure()
plt.plot(t_space, x_an_sol, '--', linewidth=2, label='analytical x')
plt.plot(t_space, y_an_sol, '--', linewidth=2, label='analytical y')
plt.plot(t_space, x_num_sol, linewidth=1, label='numerical x')
plt.plot(t_space, y_num_sol, linewidth=1, label='numerical y')
plt.title('System of 2 ODEs 1st order IVP solved by SciPy with method=' + method)
plt.xlabel('t')
plt.legend()
plt.show()


# Undergraduate Student: Arturo Burgos
# Professor: João Rodrigo Andrade
# Federal University of Uberlândia - UFU, Fluid Mechanics Laboratory - MFLab, Block 5P, Uberlândia, MG, Brazil

# Fourth exercise: Solving a Linear System --> ax = b

# Here I first set conditions

import numpy as np
from numpy import linalg as La
import matplotlib.pyplot as plt
import time
np.seterr(divide='ignore', invalid='ignore')

print('\n')

n = 9
K = np.sqrt(n)
k = int(K)

#################
# aX = b
#################

# Here I set the Matrix

a = np.zeros((n,n))

for i in range(n):
    for j in range(n):
        if i == j:
            a[i,j] = -4
        elif i == j-k or i ==j+k:
            a[i,j] = 1
        elif ((i+1) % k != 0 and i == j-1) or ((i+1) % k != 1 and i == j+1): # (i+1) because in Python we start from 0
            a[i,j] = 1

print('The coefficient Matrix is:')
print(a)
print('\n')

b = np.zeros(n)

for i in range(k):
    if i < k-1: # (k-1) because in Python we start from 0
        b[i] = -50
    else:
        b[i] = -150

for i in range(k,n-k):
    if (i+1)%k != 0: # (i+1) because in Python we start from 0
        b[i] = 0
    else:
        b[i] = -100

for i in range(n-k,n):
    if i < n-1: # (k-1) because in Python we start from 0
        b[i] = -50
    else:
        b[i] = -150

print('The result Matrix is:')
print(b)
print('\n')

def linearsystem(coeff,resul,size):

    # Initial x_k and x_k1 value

    x_k = np.zeros(size)
    x_k1 = np.ones(size)

    # Here I set the tolerance
    tolerance = 1e-9
    # Here I set the iterations
    ite = 0

    # Here I set the error based in the Infinite norm
    erro = La.norm((x_k1 - x_k),np.inf)
    #erro = (x_k1 - x_k)/x_k1

    while (erro > tolerance): #
        for i in range(0,size):

            x_k1[i] = resul[i]

            for j in range(0,size):
                if j!=i:
                    x_k1[i] =  x_k1[i] - coeff[i,j]*x_k[j]

            x_k1[i] =  x_k1[i]/ coeff[i,i]

        #erro = (x_k1 - x_k)/x_k1
        erro = La.norm((x_k1 - x_k),np.inf)

        x_k = x_k1.copy()
        #x_k[:] = x_k1[:] # -> the same as above

        ite = ite + 1

    print('The number of iterations is: ')
    print(ite)
    print('\n')

    print('Note that now the error is not an array anymore, but is normalized :')
    print(erro)
    print('\n')

    return x_k1

t_initial = time.time()
res = linearsystem(a,b,n)
t_final = time.time()

print('The solution is:')
print(res)
print('\n')

print("\n\n--- %s seconds ---\n" % (t_final - t_initial))

# PLOT OF THE MATRIX

def samplemat(dims,bb):

    aa = np.zeros(dims)

    #print(bb)

    aa = np.reshape(bb,(dims))

    return aa

# Display matrix
plt.matshow(samplemat((k, k),res))
plt.colorbar()

plt.show()


import numpy as np
import matplotlib.pyplot as plt

def f(x,lam):
    return lam*np.exp(-lam*x) if x>0 else 0

# This is the inverse of the cumulative distribution function of a random variable X we explicitly know
def F_inv(x,lam):
    return -1/lam * np.log(1-x)

def X(lam):
    U = np.random.uniform(0,1) # Generates a uniform random number between 0 and 1
    return F_inv(U,lam)
experiments = 10000
results = []
lam = 2.0
for _ in range(experiments):
    results.append(X(lam))

x = np.linspace(0.01,5,100)
plt.plot(x,[f(_,lam) for _ in x], color="red")
plt.hist(results, density=True, bins=20, color="blue")
plt.show()

def sample_circle():
    accept = False
    while not accept:
        [x,y] = np.random.uniform(-1,1,2)
        if (x**2 + y**2 < 1):
            accept = True
            return [x,y]

def rejection_sampling(experiments):
    accepted = []
    rejected = []
    for _ in range(experiments):
        [x,y] = np.random.uniform(-1,1,2)
        if (x*x + y*y < 1):
            accepted.append([x,y])
        else:
            rejected.append([x,y])
    plt.scatter([a[0] for a in accepted],[a[1] for a in accepted],color='green')
    plt.scatter([b[0] for b in rejected], [b[1] for b in rejected],color='red')
    plt.show()
    return [accepted,rejected]

#rejection_sampling(1000)

def beta_distribution(x,n,m):
    return x**n * (1-x)**m

def wiggly(x):
    return 2*np.exp(-x*x)*x*x / np.sqrt(np.pi)
def campana(x):
    return 0.7*np.exp(-x*x/4)

#x = np.linspace(-3,3,1000)
#plt.plot(x, [campana(_) for _ in x],color='blue')
#plt.plot(x,[wiggly(_) for _ in x],color='red')
#plt.show()

###

## NORMAL VARIABLE
#def n(x):
#    return np.exp(-x*x/2)/np.sqrt(2*np.pi)
#
#def f(x):
#    return 2*np.exp(-x**2/2)/np.sqrt(2*np.pi) if x>= 0 else 0
#
#def g(x):
#    return np.exp(-x) if x>=0 else 0
#
#def simulate_Y():
#    U = np.random.uniform(0,1)
#    return -np.log(U)
#
#def rejection_sampling():
#    while True:
#        U = np.random.uniform(0,1)
#        Y = simulate_Y()
#        if U <= np.exp(-(Y-1)**2 / 2):
#            S = 1 if np.random.uniform(0,1)>0.5 else -1
#            return S*Y
#
#samples = 100000
#res = []
#for _ in range(samples):
#    res.append(rejection_sampling())
#
#x = np.linspace(-5,5,1000)
#plt.plot(x,[n(_) for _ in x], color="red")
#plt.hist(res,density=True, bins = 30, color="blue")
#plt.show()


import numpy as np

def crab_fuel(n):
    return (n**2 + n) // 2

if __name__ == '__main__':
    with open('input.txt') as f:
        pin = np.array([int(x) for x in f.read().split(',')])

    distances = np.abs(pin[None, :] - np.arange(pin.max() + 1)[:, None])
    total_fuel = np.sum(distances, axis=1)
    print(f'Solution 1: {total_fuel.min()}')

    distances_v2 = crab_fuel(distances)
    total_fuel_v2 = np.sum(distances_v2, axis=1)
    print(f'Solution 2: {total_fuel_v2.min()}')


# %%
import numpy as np
import matplotlib.pyplot as plt
import gym
import matplotlib.pyplot as plt
import seaborn as sns

def policy_evaluation(P, nS, nA, policy, gamma=0.9, tol=1e-3):

    value_function = np.zeros(nS)

    loops = 0
    while True:
        loops += 1
        value_change = 0
        for state in range(nS):
            action = policy[state]
            old_value = value_function[state]
            new_value = 0
            for prob, next_state, reward, terminal in P[state][action]:
                new_value += prob * \
                    (reward + gamma * value_function[next_state])
            value_function[state] = new_value
            value_change = max(value_change, abs(old_value - new_value))
        if value_change < tol:
            break

    ############################
    return value_function, loops

def policy_improvement(P, nS, nA, value_from_policy, policy, gamma=0.9):

    new_policy = np.zeros(nS, dtype='int')

    for state in range(nS):
        new_action_value = -float('inf')
        new_action = None

        for action in range(nA):
            value = 0
            for prob, next_state, reward, terminal in P[state][action]:
                value += prob * (reward + gamma *
                                 value_from_policy[next_state])
            if value > new_action_value:
                new_action_value = value
                new_action = action

        assert new_action is not None
        new_policy[state] = new_action

    ############################
    return new_policy

def policy_iteration(P, nS, nA, gamma=0.9, tol=10e-3, printout=True):

    value_function = np.zeros(nS)
    policy = np.zeros(nS, dtype=int)

    policy_improv_steps = 0
    policy_eval_steps = 0
    while True:
        old_policy = policy.copy()
        value_function, add_policy_eval_steps = policy_evaluation(
            P, nS, nA, policy, gamma, tol)
        policy_eval_steps += add_policy_eval_steps
        policy = policy_improvement(P, nS, nA, value_function, policy, gamma)
        policy_improv_steps += 1
        if np.all(policy == old_policy):
            break

    if printout:
        print(f'{policy_improv_steps} policy improvement steps.')
        print(f'Total policy evaluation steps: {policy_eval_steps}')

    return value_function, policy

def heatmap_value_func(value_func):
    sns.set()
    sns.heatmap(value_func)
    plt.show()

if __name__ == '__main__':

    environment = "FrozenLake-v1"
    env = gym.make(environment)

    gamma = 0.9
    tolerance = 1e-3

    value_function, policy = policy_iteration(
        env.env.P, env.env.nS, env.env.nA, gamma, tolerance)

    print('optimal value function', value_function)
    print('optimal policy', policy)

    # change shape of value_function to 16,1
    value_function = np.reshape(value_function, (4, 4))

    heatmap_value_func(value_function)


import numpy as np

# Source: https://zpl.fi/aligning-point-patterns-with-kabsch-umeyama-algorithm/
def get_similarity_transform(A, B):
    """
    Compute the optimal similarity transform from B to A.
    Namely, compute rotation `R`, translation `t` and scale `c` that best align points `B` into
    reference points `A` in the least squares sense as `c * R @ b_i + t ≈ a_i` for all `i`.
    Or, in matrix form,
        (c*R[3,3] | t ) @ (B[3,N]) ≈ (A[3,N])
        (    0    | 1 ) @ (  1   )   (  1   )
    Uses the Kabsch-Umeyama algorithm.

    A, B
        np.ndarray, float32, (N, 3)

    return:
    R
        np.ndarray, float32, (3, 3)
    t
        np.ndarray, float32, (3,)
    c
        float
    """
    assert A.shape == B.shape
    n, m = A.shape

    EA = np.mean(A, axis=0)
    EB = np.mean(B, axis=0)
    VarA = np.mean(np.linalg.norm(A - EA, axis=1) ** 2)

    H = ((A - EA).T @ (B - EB)) / n
    U, D, VT = np.linalg.svd(H)
    d = np.sign(np.linalg.det(U) * np.linalg.det(VT))
    S = np.diag([1] * (m - 1) + [d]).astype(A.dtype)

    R = U @ S @ VT
    c = VarA / np.trace(np.diag(D) @ S)
    t = EA - c * R @ EB

    return R, t, c

if __name__ == '__main__':
    TARGET = np.float32([
        [-0.067039, 0.037248, 0.257207],
        [-0.021285, 0.123967, 0.23051],
        [-0.138212, 0.123967, 0.235361],
        [-0.077207, 0.310112, 0.13561],
        [-0.077207, -0.048883, 0.170014],
    ])

    SOURCE = np.float32([
        [-0.025177, 0.05771, 0.226592],
        [0.055307, 0.167654, 0.207354],
        [-0.111732, 0.160782, 0.215266],
        [-0.034637, 0.449523, 0.08648],
        [-0.021788, -0.052372, 0.10771],
    ])

    R, t, c = get_similarity_transform(TARGET, SOURCE)
    print(f"R:\n{R}")
    print(f"t:\n{t}")
    print(f"scale:\n{c}")


import numpy as np
import pandas as pd
import scipy
from pandas import Series

def ent(data: Series) -> float:
    p_data = data.value_counts() / len(data)  # calculates the probabilities
    entropy = scipy.stats.entropy(p_data, base=2)  # input probabilities to get the entropy
    # entropy = sc.stats.entropy(p_data)  # input probabilities to get the entropy
    return entropy

if __name__ == "__main__":
    input_data = pd.Series(np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]))
    print(ent(input_data))

    input_data = pd.Series(np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]))
    print(ent(input_data))