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

# In[1]:

import numpy as np
import sympy
import matplotlib.pyplot as plt
from scipy import integrate
from scipy.special import legendre
from numpy.polynomial.legendre import Legendre
import matplotlib.ticker as mtick
import itertools

# Legendre polynomial
def leg(n, x):
    return Legendre(np.concatenate((np.zeros(n), np.array([1]))))(x)

def hbasis(i,x):
#  Evaluates the function Ni at x
    if i==0:
        Ni=0.5 *(1-x)
    elif i==1:
        Ni=0.5 *(1+x)
    else:
        Ni=(np.sqrt(1/(4*(i+1)-6)))*(leg(i,x)-leg(i-2,x))
    return Ni

def stifness_matrix(p):
#evaluates the elemental stifness matrix of size (p+1)x(p+1)
    K=np.zeros((p+1,p+1))
    K[0,0]=K[1,1]=0.5
    K[0,1]=K[1,0]=-0.5
    if p>=1:
        for i in range(2,p+1):
            K[i,i]=1
    return K

def stifness_matrix(p):
#evaluates the elemental stifness matrix of size (p+1)x(p+1)
    K=np.zeros((p+1,p+1))
    K[0,0]=K[1,1]=0.5
    K[0,1]=K[1,0]=-0.5
    if p>=1:
        for i in range(2,p+1):
            K[i,i]=1
    return K

def mass_matrix(p):
# Evaluates the elemental mass matrix of size (p+1)x(p+1)
    G=np.zeros((p+1,p+1))
    G[0,0]=G[1,1]=2/3
    G[0,1]=G[1,0]=1/3
    if p>=2:
        G[0,2]=G[1,2]=G[2,0]=G[2,1]=-1/np.sqrt(6)
        for i in range(2,p+1):
            G[i,i]=2/((2*(i+1)-1)*((2*(i+1)-5)))
    if p>=3:
        G[0,3]=G[3,0]=1/3*np.sqrt(10)
        G[1,3]=G[3,1]=-1/3*np.sqrt(10)
        for i in range(2,p+1):
            if i+2<p+1:
                G[i,i+2]=G[i+2,i]=(-1)/(((2*(i+1)-1)*np.sqrt(((2*(i+1)-3)* \
                ((2*(i+1)+1))))))
    return G

def load_vector(x_k,x_kk,p_k,f):
#Evaluates the elemental load vector
    vals=[]
    for i in range(p_k+1):
        g = lambda t : f((1-t)*x_k/2+(1+t)*x_kk/2)*hbasis(i,t)
        vals.append(integrate.quad(g,-1,1)[0])
    b=np.array(vals)
    return b

def el_stiff(x_k,x_kk,p_k):
    h_k=x_kk-x_k
    Kk=(2/h_k)*stifness_matrix(p_k)
    return Kk

def el_mass(x_k,x_kk,p_k):
    h_k=x_kk-x_k
    Gk=(h_k/2)*mass_matrix(p_k)
    return Gk

def pointer(M,p):
# [P] = pointer(M,p)
#
# Calculates the pointer matrix P, such that
# P(i,j)=k means that over the ith element
# the jth local basis function corresponds
# to the kth global basis function.
#
# M is the number of elements
# p is the degree vector of size M
# P is M by (max(p)+1)
#
    pmax=max(p)
    P=np.zeros((M,pmax+1))
    P=P.astype(int)
    for i in range(M):
        P[i,0] = i
        P[i,1] = i+1
    P[M-1,1]=0
    for i in range(M):
        for j in range(2,1+p[i]):
            P[i,j] = M
            M=M+1
    return P

def global_matrix(x,p):
    #Evaluates the elemental matrices, stiffnes(Kk) and mass(Gk) and then
    #the global matrix (G+K)
    P=pointer(len(x)-1,p)
    K=np.zeros((sum(p)-1,sum(p)-1))
    G=np.zeros((sum(p)-1,sum(p)-1))
    for k in range(len(x)-1):
        Kk=np.zeros((p[k]+1,p[k]+1))
        Gk=np.zeros((p[k]+1,p[k]+1))
        Kk=el_stiff(x[k],x[k+1],p[k])
        Gk=el_mass(x[k],x[k+1],p[k])
        for i in range(p[k]+1):
            l=P[k,i]
            for j in range(p[k]+1):
                m=P[k,j]
                if l!=0 and m!=0:
                    K[l-1,m-1]=K[l-1,m-1]+Kk[i,j]
                    G[l-1,m-1]=G[l-1,m-1]+Gk[i,j]
    GL=K+G
    return GL

def el_load(x_k,x_kk,p_k,f):
#evaluates the elemental load vector
    h_k=x_kk-x_k
    Fk=(h_k/2)*load_vector(x_k,x_kk,p_k,f)
    return Fk

def global_load_vector(x,p,f):
#Evaluates the global load vector
    P=pointer(len(x)-1,p)
    F=np.zeros((sum(p)-1))
    for k in range(len(x)-1):
        Fk=el_load(x[k],x[k+1],p[k],f)
        for i in range(p[k]+1):
            l=P[k,i]
            if l!=0:
                F[l-1]=F[l-1]+Fk[i]
    return F

def fem_solution(x,p,f):
# Evaluates the finite element method solution and the coifficients that
#generate the solution
    A=global_matrix(x,p)
    b=global_load_vector(x,p,f)
    a=np.linalg.solve(A,b)
    return a

def energy_norm(x,p,f,n):
# Evaluates the energy norm of the finite element method solution and it finds
# the percentage of the error
    a=fem_solution(x,p,f)
    DOF=len(a)
    enorm = np.dot(a, np.array(global_load_vector(x,p,f)))
    y = lambda z : (z**n-z)*f(z)
    enorm_uex=integrate.quad(y,0,1)[0]
    relE=100*np.sqrt(abs(enorm-enorm_uex)/abs(enorm_uex))
    return enorm,relE,DOF

def pairwise(iterable):
    a, b = itertools.tee(iterable)
    next(b, None)
    return zip(a,b)

def solid1d(y,x,M,p,f):
#Evaluates the finite element method solution for every y that belongs to
#[a,b] interval
    x_intervals = list(pairwise(x))
    k = [0]
    for point in y[1:]:
        for i, interval in enumerate(x_intervals[k[-1]:]):
            if point>=interval[0] and point<interval[1]:
                k.append(k[-1]+i)
                break
    k.append(len(x_intervals)-1)
    ksi=[]
    for k_val, y_i in zip(k,y):
        ksi.append((2*y_i-x_intervals[k_val][0]-x_intervals[k_val][1])/ \
                   (x_intervals[k_val][1]-x_intervals[k_val][0]))
    P=pointer(M,p)
    ufe=[]
    c=fem_solution(x,p,f)
    for k_val, ksi_i in zip(k,ksi):
        proxy=[]
        for i in range(1,p[k_val]+2):
            l=P[k_val,i-1]
            if l!=0:
                proxy.append(c[l-1]*hbasis(i-1,ksi_i))
            else:
                proxy.append(0)
        ufe.append(sum(proxy))
    return ufe

def h_unif(M,pmax,a,b):
#It creates a list of the uniform mesh and a list with the degrees of the
#polynomials which are constant.
    x=[]
    p=[]
    h=(b-a)/M
    for i in range(M+1):
        z=a+i*h
        x.append(z)
    for j in range(M):
        p.append(pmax)
    return x,p

def h_radical(M,pmax,s,a,b):
#It creates a list of the root-s mesh and a list with the degrees of the
#polynomials which are constant.
    p=[]
    x=[]
    for i in range(M+1):
        z=a+(b-a)*((i/M)**s)
        x.append(z)
    for j in range(M):
        p.append(pmax)
    return x,p

def p_unif(M,pmax,a,b):
#It creates a list of the uniform mesh which is constant and a list with
#the degrees of the polynomials that goes 1:pmax.
    p=[]
    x=[]
    h=(b-a)/M
    for i in range(M+1):
        z=a+i*h
        x.append(z)
    for j in range(1,pmax+1):
        p.append(j)
    return x,p

def p_geom(M,pmax,q,a,b):
#It creates a list of the geometric-q mesh which is constant and a list with
# the degrees of the polynomials that goes 1:pmax.
    p=[]
    x=[a]
    for i in range(2,M+2):
        z=a+(b-a)*(q**(M-i+1))
        x.append(z)
    for j in range(1,pmax+1):
        p.append(j)
    return x,p

def hp_geom(M,q,a,b):
#It creates a list of the geometric-q mesh and a list with the degrees of the
# polynomials that goes 1:M, M:=number of elements.
    x=[a]
    p=[]
    pmax=M
    for i in range(2,M+2):
        z=a+(b-a)*(q**(M-i+1))
        x.append(z)
    for j in range(1,pmax+1):
        p.append(j)
    return x,p

# In[2]:

#f(x)=x**n-x
#n=7.1,a=0,b=1
#Graphs the error against DOF  in logaritmic axes
#First it uses h fem with uniform mesh for polynomial degrees 1 and then 2
a=0
b=1
n=7.1
N=5
f = lambda x : (x**n)-x-n*(n-1)*(x**(n-2))
fig, axes = plt.subplots(1,4, figsize=(15,7))
fig.tight_layout()
fig.subplots_adjust(wspace=0.3)
for j in range(1,3):
    pmax=j
    errors=[]
    DOF=[]
    for i in range(1,N+1):
        M=2**i
        x=h_unif(M,pmax,a,b)[0]
        p=h_unif(M,pmax,a,b)[1]
        errors.append(energy_norm(x,p,f,n)[1])
        DOF.append(energy_norm(x,p,f,n)[2])
    axes[j-1].loglog(DOF,errors)
    axes[j-1].set_xlabel("Degrees of freedom")
    axes[j-1].set_ylabel("Error in energy norm(%)")
    axes[j-1].set_title(f"Error for l=7.1, h_unif with N=5,p={j}")
    print(f"the slope is:",(np.log(errors[-1])-np.log(errors[-2]))/ \
          (np.log(DOF[-1])-np.log(DOF[-2])))

#Second it uses p fem with uniform mesh with 1 element and then two elements
#for polynomial degrees 1,....,6
P_uni_list=[1,4]
for j,Mp in enumerate(P_uni_list):
    pmax=6
    errors=[]
    DOF=[]
    x=p_unif(Mp,pmax,a,b)[0]
    p=p_unif(Mp,pmax,a,b)[1]
    for i in p:
        p_list=[]
        for k in range(Mp):
            p_list.append(i)
        errors.append(energy_norm(x,p_list,f,n)[1])
        DOF.append(energy_norm(x,p_list,f,n)[2])
    axes[j+2].loglog(DOF,errors)
    axes[j+2].set_xlabel("Degrees of freedom")
    axes[j+2].set_ylabel("Error in energy norm(%)")
    axes[j+2].set_title(f"Error for l=7.1,p_unif with M={Mp},pmax=6")
    print(f"the slope is:",(np.log(errors[-4])-np.log(errors[-5]))/ \
          (np.log(DOF[-4])-np.log(DOF[-5])))

# In[3]:

#f(x)=x**n-x
#n=2.1,a=0,b=1
#Graphs the error against DOF  in logaritmic axes
#First it uses h fem with uniform mesh for polynomial degrees 1 and then 2
a=0
b=1
n=2.1
N=5
f = lambda x : (x**n)-x-n*(n-1)*(x**(n-2))
fig, axes2 = plt.subplots(1,3, figsize=(15,7))
fig.tight_layout()
fig.subplots_adjust(wspace=0.3)
for j in range(1,3):
    pmax=j
    errors=[]
    DOF=[]
    for i in range(1,N+1):
        M=2**i
        x=h_unif(M,pmax,a,b)[0]
        p=h_unif(M,pmax,a,b)[1]
        errors.append(energy_norm(x,p,f,n)[1])
        DOF.append(energy_norm(x,p,f,n)[2])
    axes2[j-1].loglog(DOF,errors)
    axes2[j-1].set_xlabel("Degrees if freedom")
    axes2[j-1].set_ylabel("Error in energy norm(%)")
    axes2[j-1].set_title(f"Error for l=2.1, h_unif with N=5,p={j}")
    print(f"the slope is:",(np.log(errors[-1])-np.log(errors[-2]))/ \
          (np.log(DOF[-1])-np.log(DOF[-2])))
#Second it uses p fem with uniform mesh with 1 element and then two elements
#for polynomial degrees 1,....,6
M=1
pmax=6
errors=[]
DOF=[]
x=p_unif(M,pmax,a,b)[0]
p=p_unif(M,pmax,a,b)[1]
for i in p:
    p_list=[]
    for k in range(M):
        p_list.append(i)
    errors.append(energy_norm(x,p_list,f,n)[1])
    DOF.append(energy_norm(x,p_list,f,n)[2])
axes2[2].loglog(DOF,errors)
axes2[2].set_xlabel("Degrees of freedom")
axes2[2].set_ylabel("Error in energy norm(%)")
axes2[2].set_title(f"Error for l=2.1,p_unif with M={M},pmax=6")
print(f"the slope is:",(np.log(errors[-1])-np.log(errors[-2]))/ \
      (np.log(DOF[-1])-np.log(DOF[-2])))

# In[4]:

#f(x)=x**n-x
#n=2.1,a=0,b=1
#Graphs the error against DOF  in logaritmic axes
a=0
b=1
n=2.1
f = lambda x : (x**n)-x-n*(n-1)*(x**(n-2))
fig, axes3 = plt.subplots(1,4, figsize=(15,7))
fig.tight_layout()
fig.subplots_adjust(wspace=0.3)
j=0

#It uses h radical fem for N=5, s=0.15 for polynomial degrees 2
N=5
s=0.15
pmax=2
errors=[]
DOF=[]
for i in range(1,N+1):
    M=2**i
    x=h_radical(M,pmax,s,a,b)[0]
    p=h_radical(M,pmax,s,a,b)[1]
    errors.append(energy_norm(x,p,f,n)[1])
    DOF.append(energy_norm(x,p,f,n)[2])
axes3[j].loglog(DOF,errors)
axes3[j].set_xlabel("Degrees of freedom")
axes3[j].set_ylabel("Error in energy norm(%)")
axes3[j].set_title(f"Error for l=2.1,h-radical with N=5,s=0.15,p=2")
print(f"the slope is:",(np.log(errors[-1])-np.log(errors[-2]))/ \
      (np.log(DOF[-1])-np.log(DOF[-2])))
#It uses p fem with geometric mesh for M=4, q=0.15 for polynomial degrees
#p=1,....,6
M=4
q=0.15
pmax=6
errors=[]
DOF=[]
x=p_geom(M,pmax,q,a,b)[0]
p=p_geom(M,pmax,q,a,b)[1]
for i in p:
    p_list=[]
    for k in range(M):
        p_list.append(i)
    errors.append(energy_norm(x,p_list,f,n)[1])
    DOF.append(energy_norm(x,p_list,f,n)[2])
axes3[j+1].loglog(DOF,errors)
axes3[j+1].set_xlabel("Degrees of freedom")
axes3[j+1].set_ylabel("Error in energy norm(%)")
axes3[j+1].set_title(f"Error for l=2.1,p-geom with M=4,q=0.15,p=6")
print(f"the slope is:",(np.log(errors[-5])-np.log(errors[-6]))/ \
      (np.log(DOF[-5])-np.log(DOF[-6])))
#It uses p fem with geometric mesh for M=4, q=0.15 for polynomial degrees 1,2
M=4
q=0.15
pmax=2
errors=[]
DOF=[]
x=p_geom(M,pmax,q,a,b)[0]
p=p_geom(M,pmax,q,a,b)[1]
for i in p:
    p_list=[]
    for k in range(M):
        p_list.append(i)
    errors.append(energy_norm(x,p_list,f,n)[1])
    DOF.append(energy_norm(x,p_list,f,n)[2])
axes3[j+2].loglog(DOF,errors)
axes3[j+2].set_xlabel("Degrees of freedom")
axes3[j+2].set_ylabel("Error in energy norm(%)")
axes3[j+2].set_title(f"Error for l=2.1,p-geom with M=4,q=0.15,p=2")
print(f"the slope is:",(np.log(errors[-1])-np.log(errors[-2]))/ \
      (np.log(DOF[-1])-np.log(DOF[-2])))

#It uses hp fem with geometric mesh for N=5, q=0.15 for polynomial degrees p=M
N=5
q=0.15
errors=[]
DOF=[]
for i in range(1,N+1):
    M=2**i
    x=hp_geom(M,q,a,b)[0]
    p=hp_geom(M,q,a,b)[1]
    for z in p:
        plist=[]
        for k in range(M):
            plist.append(z)
        errors.append(energy_norm(x,plist,f,n)[1])
        DOF.append(energy_norm(x,plist,f,n)[2])
axes3[j+3].semilogy(DOF,errors)
axes3[j+3].set_xlabel("Degrees of freedom")
axes3[j+3].set_ylabel("Error in energy norm(%)")
axes3[j+3].set_title(f"Error for l=2.1,hp-geom with N=6,q=0.15")
print(f"the slope is:",(np.log(errors[-4])-np.log(errors[-5]))/ \
      (np.log(DOF[-4])-np.log(DOF[-5])))

# In[5]:

import warnings
warnings.filterwarnings('ignore')

#n=2.1
#a=0,b=1
#Graphs the absolute error between the finite element solution(uFE)and the
#exact solution (uEX)

a = 0
b = 1
n = 2.1
M = 10
q = 0.15
f = lambda x : (x**n)-x-n*(n-1)*(x**(n-2))
y = np.linspace(0,1)
u_Ex_fun = lambda x : (x-x**n)

#First;y I use the h fem with uniform mesh for M=10,p=1
x=h_unif(M,1,a,b)[0]
p=h_unif(M,1,a,b)[1]
u_Fe = -np.array(solid1d(y,x,M,p,f))
u_Ex_values = np.array([u_Ex_fun(val) for val in y])
fig , axes4 =plt.subplots(figsize=(10,10))
axes4.plot(y,abs(u_Ex_values-u_Fe)/abs(u_Ex_values),color='black')
axes4.set_xlabel("x")
axes4.set_ylabel("y")
axes4.set_title(r'Error ${|u_{FE}-u_{EX}|/|u_{EX}|}$')
#Graphs the uFE against uEX
fig , axes5 =plt.subplots(figsize=(10,10))
axes5.plot(y,u_Fe, color="red")
axes5.plot(y,u_Ex_values, color="blue")
axes5.set_xlabel("x")
axes5.set_ylabel("y")
axes5.set_title(r'Plots of $u_{FE}$ and $u_{EX}$')
axes5.legend([r'$u_{FE}$','$u_{EX}$'])

#Secondly I use the h fem with uniform mesh for M=1,p=6
x=h_unif(1,6,a,b)[0]
p=h_unif(1,6,a,b)[1]
u_Fe = -np.array(solid1d(y,x,1,p,f))
u_Ex_values = np.array([u_Ex_fun(val) for val in y])
fig , axes6 =plt.subplots(figsize=(10,10))
axes6.plot(y,abs(u_Ex_values-u_Fe)/abs(u_Ex_values),color='black')
axes6.set_xlabel("x")
axes6.set_ylabel("y")
axes6.set_title(r'Error ${|u_{FE}-u_{EX}|/|u_{EX}|}$')
#Graphs the uFE against uEX
fig , axes7 =plt.subplots(figsize=(10,10))
axes7.plot(y,u_Fe, color="red")
axes7.plot(y,u_Ex_values, color="blue")
axes7.set_xlabel("x")
axes7.set_ylabel("y")
axes7.set_title(r'Plots of $u_{FE}$ and $u_{EX}$')
axes7.legend([r'$u_{FE}$','$u_{EX}$'])

#Thirdly I use the p fem with geometric mesh for M=10,p=2 for all elements
x=p_geom(M,2,q,a,b)[0]
p=[]
for r in range(M):
    p.append(2)
u_Fe = -np.array(solid1d(y,x,M,p,f))
u_Ex_values = np.array([u_Ex_fun(val) for val in y])
fig , axes8 =plt.subplots(figsize=(10,10))
axes8.plot(y,abs(u_Ex_values-u_Fe)/abs(u_Ex_values),color='black')
axes8.set_xlabel("x")
axes8.set_ylabel("y")
axes8.set_title(r'Error ${|u_{FE}-u_{EX}|/|u_{EX}|}$')
#Graphs the uFE against uEX
fig , axes9 =plt.subplots(figsize=(10,10))
axes9.plot(y,u_Fe, color="red")
axes9.plot(y,u_Ex_values, color="blue")
axes9.set_xlabel("x")
axes9.set_ylabel("y")
axes9.set_title(r'Plots of $u_{FE}$ and $u_{EX}$')
axes9.legend([r'$u_{FE}$','$u_{EX}$'])

#Fourthly I use the p fem with geometric mesh for M=10,p=4 for all elements
x=p_geom(M,4,q,a,b)[0]
p=[]
for r in range(M):
    p.append(4)
u_Fe = -np.array(solid1d(y,x,M,p,f))
u_Ex_values = np.array([u_Ex_fun(val) for val in y])
fig , axes10 =plt.subplots(figsize=(10,10))
axes10.plot(y,abs(u_Ex_values-u_Fe)/abs(u_Ex_values),color='black')
axes10.set_xlabel("x")
axes10.set_ylabel("y")
axes10.set_title(r'Error ${|u_{FE}-u_{EX}|/|u_{EX}|}$')
#Graphs the uFE against uEX
fig , axes11 =plt.subplots(figsize=(10,10))
axes11.plot(y,u_Fe, color="red")
axes11.plot(y,u_Ex_values, color="blue")
axes11.set_xlabel("x")
axes11.set_ylabel("y")
axes11.set_title(r'Plots of $u_{FE}$ and $u_{EX}$')
axes11.legend([r'$u_{FE}$','$u_{EX}$'])

import numpy,from numpy,from scipy,import sympy


import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib import animation
import numpy as np

fig, ax = plt.subplots(2, 2, figsize=(9, 8.5))

topend = 50
angle_list = []
int_angle = []
count = []
sidelengths = []
area_list = []

for x in range(3,topend):    #finds regular angle of an n-gon (where n = 3-topend)
    degrees = 180*(x-2)
    angle = degrees/x
    angle_list.append(angle)

    if angle.is_integer():    #checks if angle is an integer
        int_angle.append(angle)
        count.append(x)

    length = 2*np.cos(angle*np.pi/(180*2))
    height = np.sin(angle*np.pi/(180*2))
    sidelengths.append(length)
    area = 2*x*(1/2 * length/2 * height)
    area_list.append(area)

    if x == topend-1:    #approximates pi using the area of the largest n-gon
        print('pi is approx: ' + str(area))

#creates circle
circle = mpatches.Circle([0.5,0.5], 0.5,
                         ec="none",
                         facecolor = 'r',
                         alpha = 0.7
                         )
#creates polygon (triangle)
polygon = mpatches.RegularPolygon([0.5,0.5],3,0.5)

#Creates each frame for bottom right graph
def update_vector(i, polygon):
    ax[1,1].clear()
    polygon = mpatches.RegularPolygon([0.5,0.5],i+3,
                                      0.5,
                                      alpha=0.5
                                      )
    ax[1,1].add_patch(circle)
    ax[1,1].add_patch(polygon)

    return polygon,

#hides x,y axis ticks for bottom right graph
ax[1,1].axes.xaxis.set_visible(False)
ax[1,1].axes.yaxis.set_visible(False)

#animates bottom right frame
anim = animation.FuncAnimation(fig, update_vector,
                               fargs = (polygon,),
                               frames=20,    #sets max n-gon in animation
                               interval=650,
                               )

#creates each graph
ax[0,0].scatter(range(3,topend),angle_list)
ax[0,0].scatter(count,int_angle)
ax[0,0].set(xlabel='Number of Sides', ylabel= 'Degree of Angle')
ax[0,1].scatter(range(3,topend),area_list)
ax[0,1].set(xlabel='Number of Sides', ylabel= 'Area')
ax[1,0].scatter(range(3,topend),sidelengths)
ax[1,0].set(xlabel='Number of Sides', ylabel= 'Lenght of Side')
#sets title
fig.suptitle('Approaching the Area of a Circle', fontsize = 20)

plt.show()

# #**Uncomment to save file as mp4 or gif**
# import matplotlib
# matplotlib.use("Agg")
# # Set up formatting for the movie files
# Writer = animation.writers['ffmpeg']
# writer = Writer(fps=6, metadata=dict(artist='Me'), bitrate=1800)

# anim.save('app_circle_graphic.mp4', writer=writer)  #save as mp4, requires ffmpeg
# anim.save('app_circle_graphic.gif', writer='imagemagick', fps=6)  # save as gif requries, imagemagick and wand


import numpy as np

def two_f(A, a, b, c):
    import sys
    sys.stdout = open('2f.txt', 'w')

    def sat_equation(r, A, num_satallites=100):
        return A * num_satallites * (r / b) ** (a - 3) * np.exp(-(r / b) ** c)

    # Since we are integrating the sat_equation in 3D spherical integral, but only a dependence on r, integral
    # corresponds to sat_equation times dV, or the derivative of the volume of the sphere, so 4 * pi * r^2
    def three_d_integral(r, A, num_sats):
        volume_of_sphere = 4 * np.pi * r ** 2
        return volume_of_sphere * sat_equation(r, A, num_sats)

    def n(x):
        return three_d_integral(x, A, 100)

    def root_finder(bracket=[1e-8, 5], epsilon=0.001, max_iter=500, root_to_find=1 / 2):
        """
        First bracket the root, and see where the function changes sign -> that is the root

        Guaranteed to work (according to the slides), unlike secant method or others

        We are finding two roots, need to bracket on one side with bracket of [0,Nmax], other with [Nmax,5]

        Uses root_to_find by subtracting that value from the rest of the stuff, so that the root wanted is at 0

        :return:
        """

        for i in range(max_iter):
            mid_point = (bracket[0] + bracket[1]) / 2.
            # Now check to see which half the root is in
            # Is the midpoint the root?
            mid_value = n(mid_point) - root_to_find
            f_a = n(bracket[0]) - root_to_find
            if f_a * mid_value < 0:  # They have opposite signs
                bracket[1] = mid_point
            elif -1. * epsilon < mid_value < epsilon:  # This is the root within epsilon
                return mid_point
            else:
                # Other two have opposite signs
                bracket[0] = mid_point
            # Now check if it has converged, that is if the space between the brackets is within epsilon
        # If it gets to here, then no root found after the max iterations
        print("Root not found in {} iterations".format(max_iter))
        return (bracket[0] + bracket[1]) / 2.

    Nmax = n(np.arange(1e-4, 5, 0.001))
    max_index = list(Nmax).index(max(Nmax))
    max_val = np.arange(1e-4, 5, 0.001)[max_index]

    # There are two roots in this problem, as half the max happens on either side of the max
    # Starts at 1e-8 because of problems with divide by 0 otherwise
    lower_root = root_finder(bracket=[1e-8, max_val], root_to_find=n(max_val) / 2.)
    upper_root = root_finder(bracket=[max_val, 5], root_to_find=n(max_val) / 2.)
    print("Roots = {}, {}".format(lower_root, upper_root))


import math
import string
import sys

import numpy as np
from sympy import Matrix

def menu():
    while True:
        print("---- Hill Cipher ----\n")
        print("1) Encrypt a Message.")
        print("2) Decipher a Message.")
        print("3) Force a Ciphertext (Known Plaintext Attack).")
        print("4) Quit.\n")
        try:
            choice = int(input("Select a function to run: "))
            if 1 <= choice <= 4:
                return choice
            else:
                print("\nYou must enter a number from 1 to 4\n")
        except ValueError:
            print("\nYou must enter a number from 1 to 4\n")
        input("Press Enter to continue.\n")

# Create two dictionaries, english alphabet to numbers and numbers to english alphabet, and returns them
def get_alphabet():
    alphabet = {}
    for character in string.ascii_uppercase:
        alphabet[character] = string.ascii_uppercase.index(character)

    reverse_alphabet = {}
    for key, value in alphabet.items():
        reverse_alphabet[value] = key

    return alphabet, reverse_alphabet

# Get input from the user and checks if respects the alphabet
def get_text_input(message, alphabet):
    while True:
        text = input(message)
        text = text.upper()
        if all(keys in alphabet for keys in text):
            return text
        else:
            print("\nThe text must contain only characters from the english alphabet ([A to Z] or [a to z]).")

# Check if the key is a square in length
def is_square(key):
    key_length = len(key)
    if 2 <= key_length == int(math.sqrt(key_length)) ** 2:
        return True
    else:
        return False

# Create the matrix k for the key
def get_key_matrix(key, alphabet):
    k = list(key)
    m = int(math.sqrt(len(k)))
    for (i, character) in enumerate(k):
        k[i] = alphabet[character]

    return np.reshape(k, (m, m))

# Create the matrix of m-grams of a text, if needed, complete the last m-gram with the last letter of the alphabet
def get_text_matrix(text, m, alphabet):
    matrix = list(text)
    remainder = len(text) % m
    for (i, character) in enumerate(matrix):
        matrix[i] = alphabet[character]
    if remainder != 0:
        for i in range(m - remainder):
            matrix.append(25)

    return np.reshape(matrix, (int(len(matrix) / m), m)).transpose()

# Encrypt a Message and returns the ciphertext matrix
def encrypt(key, plaintext, alphabet):
    m = key.shape[0]
    m_grams = plaintext.shape[1]

    # Encrypt the plaintext with the key provided k, calculate matrix c of ciphertext
    ciphertext = np.zeros((m, m_grams)).astype(int)
    for i in range(m_grams):
        ciphertext[:, i] = np.reshape(np.dot(key, plaintext[:, i]) % len(alphabet), m)
    return ciphertext

# Transform a matrix to a text, according to the alphabet
def matrix_to_text(matrix, order, alphabet):
    if order == 't':
        text_array = np.ravel(matrix, order='F')
    else:
        text_array = np.ravel(matrix)
    text = ""
    for i in range(len(text_array)):
        text = text + alphabet[text_array[i]]
    return text

# Check if the key is invertible and in that case returns the inverse of the matrix
def get_inverse(matrix, alphabet):
    alphabet_len = len(alphabet)
    if math.gcd(int(round(np.linalg.det(matrix))), alphabet_len) == 1:
        matrix = Matrix(matrix)
        return np.matrix(matrix.inv_mod(alphabet_len))
    else:
        return None

# Decrypt a Message and returns the plaintext matrix
def decrypt(k_inverse, c, alphabet):
    return encrypt(k_inverse, c, alphabet)

def get_m():
    while True:
        try:
            m = int(input("Insert the length of the grams (m): "))
            if m >= 2:
                return m
            else:
                print("\nYou must enter a number m >= 2\n")
        except ValueError:
            print("\nYou must enter a number m >= 2\n")

# Force a Ciphertext (Known Plaintext Attack)
def plaintext_attack(c, p_inverse, alphabet):
    return encrypt(c, p_inverse, alphabet)

def main():
    while True:
        # Ask the user what function wants to run
        choice = menu()

        # Get two dictionaries, english alphabet to numbers and numbers to english alphabet
        alphabet, reverse_alphabet = get_alphabet()

        # Run the function selected by the user
        if choice == 1:
            # Asks the user the plaintext and the key for the encryption and checks the input
            plaintext = get_text_input("\nInsert the text to be encrypted: ", alphabet)
            key = get_text_input("Insert the key for encryption: ", alphabet)

            if is_square(key):
                # Get the key matrix k
                k = get_key_matrix(key, alphabet)
                print("\nKey Matrix:\n", k)

                # Get the m-grams matrix p of the plaintext
                p = get_text_matrix(plaintext, k.shape[0], alphabet)
                print("Plaintext Matrix:\n", p)

                input("\nPress Enter to begin te encryption.")
                # Encrypt the plaintext
                c = encrypt(k, p, alphabet)

                # Transform the ciphertext matrix to a text of the alphabet
                ciphertext = matrix_to_text(c, "t", reverse_alphabet)

                print("\nThe message has been encrypted.\n")
                print("Generated Ciphertext: ", ciphertext)
                print("Generated Ciphertext Matrix:\n", c, "\n")
            else:
                print("\nThe length of the key must be a square and >= 2.\n")

        elif choice == 2:
            # Asks the user the ciphertext and the key for the encryption and checks the input
            ciphertext = get_text_input("\nInsert the ciphertext to be decrypted: ", alphabet)
            key = get_text_input("Insert the key for decryption: ", alphabet)

            if is_square(key):
                # Get the key matrix k
                k = get_key_matrix(key, alphabet)

                # Check if the key is invertible and in that case returns the inverse of the matrix
                k_inverse = get_inverse(k, alphabet)

                if k_inverse is not None:
                    # Get the m-grams matrix c of the ciphertext
                    c = get_text_matrix(ciphertext, k_inverse.shape[0], alphabet)

                    print("\nKey Matrix:\n", k)
                    print("Ciphertext Matrix:\n", c)

                    input("\nPress Enter to begin the decryption.")

                    # Decrypt the ciphertext
                    p = decrypt(k_inverse, c, alphabet)

                    # Transform the ciphertext matrix to a text of the alphabet
                    plaintext = matrix_to_text(p, "t", reverse_alphabet)

                    print("\nThe message has been decrypted.\n")
                    print("Generated Plaintext: ", plaintext)
                    print("Generated Plaintext Matrix:\n", p, "\n")
                else:
                    print("\nThe matrix of the key provided is not invertible.\n")
            else:
                print("\nThe key must be a square and size >= 2.\n")

        elif choice == 3:
            # Asks the user the text and the ciphertext to use them for the plaintext attack
            plaintext = get_text_input("\nInsert the plaintext for the attack: ", alphabet)
            ciphertext = get_text_input("Insert the ciphertext of the plaintext for the attack: ", alphabet)

            # Asks the user the length of the grams
            m = get_m()

            if len(plaintext) / m >= m:
                # Get the m-grams matrix p of the plaintext and takes the firsts m
                p = get_text_matrix(plaintext, m, alphabet)
                p = p[:, 0:m]

                # Check if the matrix of the plaintext is invertible and in that case returns the inverse of the matrix
                p_inverse = get_inverse(p, alphabet)

                if p_inverse is not None:
                    # Get the m-grams matrix c of the ciphertext
                    c = get_text_matrix(ciphertext, m, alphabet)
                    c = c[:, 0:m]

                    if c.shape[1] == p.shape[0]:
                        print("\nCiphertext Matrix:\n", c)
                        print("Plaintext Matrix:\n", p)

                        input("\nPress Enter to begin the attack.")

                        # Force the ciphertext provided
                        k = plaintext_attack(c, p_inverse, alphabet)

                        # Transform the key matrix to a text of the alphabet
                        key = matrix_to_text(k, "k", reverse_alphabet)

                        print("\nThe key has been found.\n")
                        print("Generated Key: ", key)
                        print("Generated Key Matrix:\n", k, "\n")
                    else:
                        print("\nThe number of m-grams for plaintext and ciphertext are different.\n")
                else:
                    print("\nThe matrix of the plaintext provided is not invertible.\n")
            else:
                print("\nThe length of the plaintext must be compatible with the length of the grams (m).\n")
        elif choice == 4:
            sys.exit(0)
        input("Press Enter to continue.\n")

if __name__ == '__main__':
    main()


'''
autobins.py
automatically calculate bins
'''

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

def compute_histogram_bins(data, desired_bin_size):
    min_val = np.min(data)
    max_val = np.max(data)
    min_boundary = -1.0 * (min_val % desired_bin_size - min_val)
    max_boundary = max_val - max_val % desired_bin_size + desired_bin_size
    n_bins = int((max_boundary - min_boundary) / desired_bin_size) + 1
    bins = np.linspace(min_boundary, max_boundary, n_bins)
    return bins

# Creata a random data set

data = np.random.random_sample(100) * 200.02 - 100.01

# Call the bin computation routine

bins = compute_histogram_bins(data, 10.0)

# Show the results

print("Minimum value was", np.min(data))
print("Maximum value was", np.max(data))
print("The bins are set as follows...")
print(bins)

# And plot the data

plt.hist(data, bins=bins)
plt.xlabel('Value')
plt.ylabel('Counts')
plt.title('Automatic Bins Example')
plt.grid(True)
plt.show()


import numpy as np

def sor_solver(A, b, omega, initial_guess, convergence_criteria,T,k):
  count = 0
  phi = initial_guess[:]
  residual = np.linalg.norm(np.matmul(A, phi) - b) #Initial residual

  while residual > convergence_criteria and count<=k:
        for i in range(A.shape[0]):
          sigma = 0
          for j in range(A.shape[1]):
            if j != i:
              sigma += A[i][j] * phi[j]
              T[i][j] = sigma
          phi[i] = (1 - omega) * phi[i] + (omega / A[i][i]) * (b[i] - sigma)

        print('Iteración:{0}'.format(count))
        print(T)
        residual = np.linalg.norm(np.matmul(A, phi) - b)
        print('{0}'.format(phi))
        print('Error: {0:10.9g}\n'.format(residual))
        count+=1

  return phi

#An example case that mirrors the one in the Wikipedia article
residual_convergence = 10**-16
omega = 0.41#Relaxation factor

A = np.ones((3, 3))
A[0][0] = 1
A[0][1] = 3
A[0][2] = -1

A[1][0] = 4
A[1][1] = -1
A[1][2] = 1

A[2][0] = 1
A[2][1] = 1
A[2][2] = 7

b = np.ones(3)
b[0] = 18.0
b[1] = 27.34
b[2] = 16.2

initial_guess = np.zeros(3)
T = np.ones((3,3))
phi = sor_solver(A, b, omega, initial_guess, residual_convergence,T,1000)
print("Respuesta: ",phi)


import numpy as np

class LinearRegression:
    """
    A class that represents a Linear Regression Model

    Attributes
    ----------
    iterations : int
        number of training iterations
    learning_rate : float
        alpha multiplier for the strength of the gradient descent
    seed : int, list, etc. : Default - None
        random seed for reproducibility

    Methods
    -------
    train(X, y):
        Trains the model based on the data and labels

    predict(X):
        Predict the Labels for each provided X input data
    """
    def __init__(self, iterations=1000, learning_rate=0.01, seed=None):
        if seed is not None:
            np.random.seed(seed=seed)

        self.iterations = iterations
        self.learning_rate = learning_rate

    def _calculate_gradient(self, X, y):
        """
        Calculate the Gradient Vector for gradient descent
        """
        grad = []
        for i in range(X.shape[1]):
            grad.append(self._calculate_cost_der(X, y, i, X.shape[1]))
        return np.array(grad)

    def _calculate_cost(self, X, y, m):
        """
        Calculate the MSE / Cost Function based on the current theta values
        """
        result = (X@self.theta - y)@(X@self.theta - y)
        result *= (1/(2*m))
        return result

    def _calculate_cost_der(self, X, y, j, m):
        """
        Calculate the Partial Derivative of the Cost Function
        """
        result = (X@self.theta - y) @ X[:, j]
        result *= (1/m)
        return result

    def train(self, X, y):
        """
        Trains the model to learn the parameters
        """
        self.theta = np.random.rand(X.shape[1])

        for i in range(self.iterations):
            grad = self._calculate_gradient(X, y)
            self.theta -= self.learning_rate * grad

    def predict(self, X):
        """
        Predict the Labels for each provided X input data
        """
        preds = []
        for d in X:
            preds.append((self.theta @ d)[0])
        return np.array(preds)


import sys
import numpy as np

class Activation:
    def val(self, x): pass
    def deriv(self, x): pass

class Sigmoid(Activation):
    def val(self, x):
        return 1 / (1 + np.exp(-x))

    def deriv(x):
        return self.val(x) / (1 - self.val(x))

class Loss:
    @staticmethod
    def loss(y, y_hat): pass

    @staticmethod
    def gradient(X, y, y_hat): pass

class MeanSquaredError(Loss):
    @staticmethod
    def loss(y, y_hat):
        return np.square(y - y_hat).mean()

    @staticmethod
    def gradient(X, y, y_hat):
        return (-2 / X.shape[0]) * X.T.dot(y - y_hat)

class Normalization:
    def normalize(self, features): pass

class Rescale(Normalization):
    def normalize(self, features, min=-1, max=1):
        return min + (max - min) * (features - features.min(0)) / features.ptp(0)

class ZScoreNormalization(Normalization):
    @staticmethod
    def normalize(features):
        return (features - features.mean(0)) / features.std(0)

class Optimization:
    @staticmethod
    def optimize(): pass

class BatchGradientDescent(Optimization):
    @staticmethod
    def optimize(X, y, y_hat, params, learning_rate, loss_func):
        return params - loss_func.gradient(X, y, y_hat) * learning_rate

### Miscellaneous ###
def show_progress(iteration, max_iteration, loss, bar_size=40):
    prog = (iteration + 1) / max_iteration
    loss_str = 'Loss: ' + str(loss)
    sys.stdout.write('\r')
    sys.stdout.write(f"[{'=' * int(bar_size * prog):{bar_size}s}] {int(100 * prog)}%  {loss_str}")
    sys.stdout.flush()


# note: documentation not written yet

import numpy as np

import matplotlib.pyplot as plt
import seaborn as sns

sns.set_style("whitegrid")
prop_cycle = plt.rcParams["axes.prop_cycle"]
colors = prop_cycle.by_key()["color"]
import ipywidgets as widgets

def solow_equation(k, alpha, delta, s):

    saving = s * k ** alpha
    depreciation = delta * k
    k_plus = k + saving - depreciation
    return k_plus, saving, depreciation

def simulate_solow_model(k0, alpha, delta, s, T):

    k_path = [k0]
    saving_path = []
    depreciation_path = []

    for t in range(1, T):

        k_plus, saving, depreciation = solow_equation(k_path[t - 1], alpha, delta, s)

        k_path.append(k_plus)
        saving_path.append(saving)
        depreciation_path.append(depreciation)

    return k_path, saving_path, depreciation_path

def simulate(k_max=10, T=150):

    widgets.interact(
        simulate_,
        k0=widgets.FloatSlider(
            description="$k_0$", min=0, max=k_max, step=0.05, value=2
        ),
        alpha=widgets.FloatSlider(
            description="$\\alpha$", min=0.01, max=0.99, step=0.01, value=0.3
        ),
        delta=widgets.FloatSlider(
            description="$\\delta$", min=0.01, max=0.50, step=0.01, value=0.1
        ),
        s=widgets.FloatSlider(
            description="$s$", min=0.01, max=0.99, step=0.01, value=0.3
        ),
        T=widgets.fixed(T),
        k_max=widgets.fixed(k_max),
    )

def simulate_(k0, alpha, delta, s, T, k_max):

    k_path, saving_path, depreciation_path = simulate_solow_model(
        k0, alpha, delta, s, T * 5
    )

    fig = plt.figure(figsize=(16, 6), dpi=100)
    ax1 = fig.add_subplot(1, 2, 1)

    ax1.plot(k_path[:T], lw=2)
    ax1.set_title("Capital ($k_t$)")
    ax1.set_xlim([0, T])
    ax1.set_ylim([0, k_max])

    k_path = np.array(k_path)
    I = np.abs(np.log(k_path) - np.log(k_path[-1])) < 0.01
    t_converge = T * 5 - k_path[I].size
    ax1.plot(
        [t_converge, t_converge],
        [0, k_max],
        ls="--",
        color="black",
        label="$|\\log(k_t)-\\log(k^\\ast)| < 0.01$",
    )

    ax1.legend(loc="lower right")

    ax2 = fig.add_subplot(1, 2, 2)
    ax2.plot(saving_path[:T], label="saving")
    ax2.plot(depreciation_path[:T], label="depreciation")
    ax2.set_title("Saving ($sk^\\alpha$) and depreciation ($\\delta k_t$)")
    ax2.set_xlim([0, T])
    ax2.set_ylim([0, 0.5 * k_max])

    ax2.legend(loc="upper left")


from sympy import *

# A mailing service places a limit of 66 in. on the combined length and girth of
# (distance around) a package to be sent parcel post.
#
# What dimensions of a rectangular box with square cross-section will contain the largest
# volume that can be mailed? (Hint: There are two different girths.)

L = 42
x = symbols( 'x', positive = True )

V = x**2 * ( L - 4*x )
dV = diff( V, x )

max_x = solve( dV, x )[ 0 ]

print( 'Dim {0}, {1}'.format( max_x, max_x * 2 ) )


"""Build an Artificial Neural Network by implementing the Backpropagation algorithm and test the same using
appropriate data sets """

import numpy as np

X = np.array(([2, 9], [1, 5], [3, 6]), dtype=float)
y = np.array(([92], [86], [89]), dtype=float)
X = X / np.amax(X, axis=0)  # maximum of X array longitudinally
y = y / 100

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

# Derivative of Sigmoid Function
def derivatives_sigmoid(x):
    return x * (1 - x)

# Variable initialization
epoch = 7000  # Setting training iterations
lr = 0.1  # Setting learning rate
inputlayer_neurons = 2  # number of features in data set
hiddenlayer_neurons = 3  # number of hidden layers neurons
output_neurons = 1  # number of neurons at output layer
# weight and bias initialization
wh = np.random.uniform(size=(inputlayer_neurons, hiddenlayer_neurons))
bh = np.random.uniform(size=(1, hiddenlayer_neurons))
wout = np.random.uniform(size=(hiddenlayer_neurons, output_neurons))
bout = np.random.uniform(size=(1, output_neurons))
# draws a random range of numbers uniformly of dim x*y
for i in range(epoch):
    # Forward Propogation
    hinp1 = np.dot(X, wh)
    hinp = hinp1 + bh
    hlayer_act = sigmoid(hinp)
    outinp1 = np.dot(hlayer_act, wout)
    outinp = outinp1 + bout
    output = sigmoid(outinp)
    # Backpropagation
    EO = y - output
    outgrad = derivatives_sigmoid(output)
    d_output = EO * outgrad
    EH = d_output.dot(wout.T)
    hiddengrad = derivatives_sigmoid(hlayer_act)
    d_hiddenlayer = EH * hiddengrad
    wout += hlayer_act.T.dot(d_output) * lr
    # bout += np.sum(d_output, axis=0,keepdims=True) *lr
    wh += X.T.dot(d_hiddenlayer) * lr
    # bh += np.sum(d_hiddenlayer, axis=0,keepdims=True) *lr
print("Input: \n" + str(X))
print("Actual Output: \n" + str(y))
print("Predicted Output: \n", output)

########################################################################################################################
# OUTPUT:
# Ignore single quotes at beginning and end
########################################################################################################################

''''Input:
[[0.66666667 1.        ]
 [0.33333333 0.55555556]
 [1.         0.66666667]]
Actual Output:
[[0.92]
 [0.86]
 [0.89]]
Predicted Output:
 [[0.89613503]
 [0.87647952]
 [0.89699621]]
'''


# function
# Old version of buckling_load function

import sympy as sp
import numpy as np
from scipy import linalg
import matplotlib.pyplot as plt
from ._plot_surface_function import _plot_surface

def critical_buckling(a, b, D, method = 'sin_series', n = 3, shape_plot = False):
    '''
    ===========================================
    This is the old version of the buckling_load function.
    Both functions provide same results, but buckling_load is optmized.
    ===========================================
    '''

    #Creating shape function
    Nw = [[]]
    x = sp.symbols('x')
    y = sp.symbols('y')

    if method == 'sin_series':
        for i in range(n):
            for j in range(n):
                Nw[0].append(sp.sin((i+1)*sp.pi*x/a)*sp.sin((j+1)*sp.pi*y/b))

    if method =='polynomial':
        for i in range(n):
            for j in range(n):
                Nw[0].append(x**(i+1)*y**(j+1)*(x-a)*(y-b))

    #Shape function differentiation
    Nw_values = sp.Matrix(Nw)
    Nw_dx = sp.diff(Nw_values,x)
    Nw_dxx = sp.diff(Nw_values,x,x)
    Nw_dy = sp.diff(Nw_values,y)

    print('it may take a while...')

    #Matrices construction
    Ds = sp.Matrix(D)
    Nww_values = sp.Matrix([sp.diff(Nw_values,x,x),sp.diff(Nw_values,y,y),2*sp.diff(Nw_values,x,y)])
    print('Shape function ready!')
    KGx_values = np.array(sp.integrate(sp.integrate(sp.diff(Nw_values,x).T*sp.diff(Nw_values,x),(x,0,a)),(y,0,b)))
    print('[KGx] ready')
    #KGy_values = np.array(sp.integrate(sp.integrate(sp.diff(Nw_values,y).T*sp.diff(Nw_values,y),(x,0,a)),(y,0,b)))
    #KGxy_values = np.array(sp.integrate(sp.integrate(Nw_dx.T*Nw_dy + Nw_dy.T*Nw_dx,(x,0,a)),(y,0,b)))
    K_values = np.array(sp.integrate(sp.integrate(Nww_values.T*Ds*Nww_values,(x,0,a)),(y,0,b)))
    print('[K] ready')

    #eigenproblem
    A=np.array(K_values).astype('float')
    B=np.array(KGx_values).astype('float')
    eig_values, eig_vectors = linalg.eig(A,B)

    #finding the min eigenvalue
    min_eigen_value = min((eig_values.real**2)**(1/2))
    Nx = min_eigen_value*b
    print(f'buckling load is {Nx}')

    #Finding the corresponding C vector
    i = np.where(eig_values == min_eigen_value)
    C_vector = eig_vectors[::,i[0]].real

    #ploting function
    if shape_plot:
        _plot_surface(a, b, C_vector, Nw_values)

    return Nx


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

from sklearn.datasets import make_regression
# Regression Dataset
n = 100
X, y = make_regression(n_samples=n, n_features=1, noise=80)
X = X.reshape(n)
datadict = {'data': X, 'target': y}
data = pd.DataFrame(data=datadict)
plt.scatter(X,y)
plt.show()

X = data.iloc[:,[0]]
X.shape

y = data.target.values

# TRAIN TEST SPLIT
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)

X_train.shape

from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

plt.scatter(X,y)
plt.plot(X_test, y_pred, 'r')
plt.show()

from sklearn.metrics import mean_squared_error
mse = mean_squared_error(y_test, y_pred)
print(mse)

from sklearn.datasets import load_boston
boston_data = load_boston()
X = boston_data['data']
y = boston_data['target']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)

from sklearn.preprocessing import StandardScaler
scaleObj = StandardScaler()
X_train = scaleObj.fit_transform(X_train)
X_test = scaleObj.transform(X_test)

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

from sklearn.metrics import mean_squared_error
mse = mean_squared_error(y_test, y_pred)
print(mse)

for i in range(0, X.shape[1]):
  plt.scatter(X_train[:,[i]], y_train)
  plt.scatter(X_test[:,[i]], y_pred)
  plt.show()


import numpy as np

def multivariate_gaussian(x, mean, cov):
    '''Calculate the probability density of a multivariate
    gaussian distribution at x.
    '''
    n = x.shape[0]

    return 1 / (np.power(2 * np.pi, n / 2.0) * np.absolute(np.power(np.linalg.det(cov), 0.5))) \
            * np.exp(-0.5 * np.dot(x - mean, np.dot(np.linalg.inv(cov), x - mean)))

def gaussian(x, mean, std):
    '''Calculate the probability density of a gaussian distribution
    at x.
    '''
    return multivariate_gaussian(np.array([x]), np.array([mean]), np.array([[std]]))


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

class LogisticRegressionClassifier:
  def __init__(self, dims, learning_rate=0.1):
    self.w = np.zeros((dims,))
    self.b = np.zeros((1,))
    self.learning_rate = learning_rate

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

  def forward(self, X):
    return self.sigmoid(np.matmul(X, self.w) + self.b)

  def accuracy(self, y_pred, y_label):
    return 1 - np.mean(np.abs(np.round(y_pred) - y_label))

  def corss_entropy_loss(self, y_pred, y_label):
    return - np.dot(y_label, np.log(y_pred)) - np.dot(1-y_label, np.log(1-y_pred))

  def update_gradient(self, X, y_label, step=1):
    y_pred = self.forward(X)
    y_error = y_label - y_pred
    w_grad = -np.sum(y_error * X.T, 1)
    b_grad = -np.sum(y_error)
    self.w -= self.learning_rate/np.sqrt(step) * w_grad
    self.b -= self.learning_rate/np.sqrt(step) * b_grad

def shuffle(X, Y):
  randomsize = np.arange(X.shape[0])
  np.random.shuffle(randomsize)
  return X[randomsize], Y[randomsize]

if __name__ == "__main__":
  # parser args
  parser = argparse.ArgumentParser()
  parser.add_argument("X_train_path", metavar="X_train", help="X_train")
  parser.add_argument("Y_train_path", metavar="Y_train", help="Y_train")
  parser.add_argument("X_test_path", metavar="X_test", help="X_test")
  parser.add_argument("output_file", metavar="Output_test", help="Output")
  args = parser.parse_args()

  # load data
  with open(args.X_train_path, 'r') as f:
    next(f)
    X_train = np.array([line.strip('\n').split(',')[1:] for line in f], dtype='float')
  with open(args.Y_train_path, 'r') as f:
    next(f)
    Y_train = np.array([line.strip('\n').split(',')[1] for line in f], dtype='float')
  with open(args.X_test_path, 'r') as f:
    next(f)
    X_test = np.array([line.strip('\n').split(',')[1:] for line in f], dtype='float')
  print(X_train.shape, Y_train.shape, X_test.shape)

  # nomalize features
  X_mean = np.mean(X_train, axis=0).reshape(1, -1)
  X_std = np.std(X_train, axis=0).reshape(1, -1)
  X_train = (X_train - X_mean) / (X_std + 1e-8)
  X_test = (X_test - X_mean) / (X_std + 1e-8)
  print(X_train.shape, X_test.shape, X_mean.shape, X_std.shape)

  # split dev
  train_size = int(0.9 * X_train.shape[0])
  X_dev = X_train[train_size:]
  Y_dev= Y_train[train_size:]
  X_train = X_train[:train_size]
  Y_train = Y_train[:train_size]
  print('train {}; dev {}; test {}; dim {}'.format(
    X_train.shape[0], X_dev.shape[0], X_test.shape[0], X_train.shape[1]))

  # build model
  size, dims = X_train.shape
  lr_clf = LogisticRegressionClassifier(dims)

  # train model
  ## hyperparameters
  max_iter = 10
  batch_size = 32
  learning_rate = 0.2

  ## loss and acc
  train_loss, dev_loss, train_acc, dev_acc = [], [], [], []

  ## loop epoch
  step = 1
  for epoch in range(max_iter):
    X_train, Y_train = shuffle(X_train, Y_train)

    ### loop minibatch
    for idx in range(int(size/ batch_size)+1):
      X = X_train[idx*batch_size:(idx+1)*batch_size]
      Y = Y_train[idx*batch_size:(idx+1)*batch_size]

      #### update gradient with decay learning rate
      lr_clf.update_gradient(X, Y, step)
      step += 1

    ### compute loss and acc
    train_pred = lr_clf.forward(X_train)
    train_loss.append(lr_clf.corss_entropy_loss(train_pred, Y_train) / size)
    train_acc.append(lr_clf.accuracy(train_pred, Y_train))

    dev_pred = lr_clf.forward(X_dev)
    dev_loss.append(lr_clf.corss_entropy_loss(dev_pred, Y_dev) / X_dev.shape[0])
    dev_acc.append(lr_clf.accuracy(dev_pred, Y_dev))

  print('train loss {} and acc {}; dev loss {} and acc {}'.format(
    train_loss[-1], train_acc[-1], dev_loss[-1], dev_acc[-1]
  ))

  # plot loss and acc curve during training
  fig, axes = plt.subplots(1, 2, figsize=(12, 6))
  axes[0].plot(train_loss)
  axes[0].plot(dev_loss)
  axes[0].set_title('Loss')
  axes[0].legend(['train', 'dev'])
  axes[1].plot(train_acc)
  axes[1].plot(dev_acc)
  axes[1].set_title('Acc')
  axes[1].legend(['train', 'dev'])
  plt.savefig('train_loss_acc.png')

  # print most significant weights
  index = np.argsort(np.abs(lr_clf.w))[::-1]
  with open(args.X_test_path, 'r') as f:
    content = f.readline().strip('\n').split(',')
    features = np.array(content)
  for i in index[0:10]:
    print(features[i], lr_clf.w[i])

  # predict test
  test_pred = np.round(lr_clf.forward(X_test)).astype(np.int)
  with open(args.output_file, 'w') as f:
    f.write('id,label\n')
    for i,label in enumerate(test_pred):
      f.write('{},{}\n'.format(i, label))


import numpy as np
import sympy as sp
from sympy.core import S, Dummy
from sympy.functions.special.gamma_functions import gamma
from sympy.polys.orthopolys import (legendre_poly, laguerre_poly,
                                    hermite_poly, jacobi_poly)
from sympy.polys.rootoftools import RootOf

def gauss_gen_laguerre(n, alpha, n_digits):
    r"""
    Computes the generalized Gauss-Laguerre quadrature [1]_ points and weights.

    The generalized Gauss-Laguerre quadrature approximates the integral:

    .. math::
        \int_{0}^\infty x^{\alpha} e^{-x} f(x)\,dx \approx
            \sum_{i=1}^n w_i f(x_i)

    The nodes `x_i` of an order `n` quadrature rule are the roots of
    `L^{\alpha}_n` and the weights `w_i` are given by:

    .. math::
        w_i = \frac{\Gamma(\alpha+n)}
                {n \Gamma(n) L^{\alpha}_{n-1}(x_i) L^{\alpha+1}_{n-1}(x_i)}

    Parameters
    ==========

    n : the order of quadrature

    alpha : the exponent of the singularity, `\alpha > -1`

    n_digits : number of significant digits of the points and weights to return

    Returns
    =======

    (x, w) : the ``x`` and ``w`` are lists of points and weights as Floats.
             The points `x_i` and weights `w_i` are returned as ``(x, w)``
             tuple of lists.

    Examples
    ========

    >>> from sympy import S
    >>> from sympy.integrals.quadrature import gauss_gen_laguerre
    >>> x, w = gauss_gen_laguerre(3, -S.Half, 5)
    >>> x
    [0.19016, 1.7845, 5.5253]
    >>> w
    [1.4493, 0.31413, 0.00906]

    >>> x, w = gauss_gen_laguerre(4, 3*S.Half, 5)
    >>> x
    [0.97851, 2.9904, 6.3193, 11.712]
    >>> w
    [0.53087, 0.67721, 0.11895, 0.0023152]

    See Also
    ========

    gauss_legendre, gauss_laguerre, gauss_hermite, gauss_chebyshev_t, gauss_chebyshev_u, gauss_jacobi, gauss_lobatto

    References
    ==========

    .. [1] https://en.wikipedia.org/wiki/Gauss%E2%80%93Laguerre_quadrature
    .. [2] http://people.sc.fsu.edu/~jburkardt/cpp_src/gen_laguerre_rule/gen_laguerre_rule.html
    """
    x = Dummy("x")
    p = laguerre_poly(n, x, alpha=alpha, polys=True)
    p1 = laguerre_poly(n-1, x, alpha=alpha, polys=True)
    p2 = laguerre_poly(n-1, x, alpha=alpha+1, polys=True)
    xi = []
    w = []
    for r in p.real_roots():
        if isinstance(r, RootOf):
            r = r.eval_rational(S(1)/10**(n_digits+2))
        xi.append(r.n(n_digits))
        w.append((gamma(alpha+n) /
                 (n*gamma(n)*p1.subs(x, r)*p2.subs(x, r))).n(n_digits))
    return xi, w

alpha = 0
digits = 36
npoints = list(range(1,17))

for n in npoints:
    xi, wi = gauss_gen_laguerre(n, alpha, digits)

    print('\nN = %3d'%(n))
    for i in range(0,n):
        print('%3d %40.36f %40.36f'%(i+1,xi[i],wi[i]))


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

dataset = pd.read_csv("student_scores.csv")

# verisetinin boyutları
dataShape = dataset.shape
print(dataShape)

print(dataset.head(7)) # datasetin başını gösterir. default değer 5'tir.
print(dataset.tail(7)) # datasetin sonunu gösterir. default değer 5'tir.

# Veriseti hakkında ön bilgi için describe() kullanılabilir.
print(dataset.describe())
print(dataset.columns) # verisetindeki kolonların isimlerini verir.

X = dataset.iloc[:, :-1].values #Hours değerleri
y = dataset.iloc[:, 1].values #Scores değerleri

print(X)
print(y)

# Verisetini eğitim ve test olarak parçalar
# test_size=0.2 => verisetinin %80'i eğitim %20'si test için ayrılmasını sağlar
# random_state => fonksiyon her çalışmasında veriyi farklı bir sırayla çekmesinin önüne geçer
#   Böylece eğitim her zaman aynı sırayla yapılmış olur.
from sklearn.model_selection import train_test_split
Xtrain, Xtest, ytrain, ytest = train_test_split(X, y, test_size=0.2, random_state=0)

# Regresyon modelinin eklenmesi
from sklearn.linear_model import LinearRegression
reg = LinearRegression() # regresyon nesnesi tanımalama

reg.fit(Xtrain,ytrain) # hazırlanan veriler modele verilir ve eğitime başlanır.

print(reg.intercept_) # Çıkan sonucun yaklaşık 2.01816004143 değerde olması gerekiyor.

# x değişkenini bir birim artmasıyla y'de olan değişiklik
print(reg.coef_) # bu örnek için öğrenci bir saat fazla çalışarak çıkan sonuç kadar yüksek skor elde edebilir.

### Tahmin Yapma
yPred = reg.predict(Xtest) ## Xtest değerlerine göre yapılan tahminler
print(yPred)
# yapılan tahminlerin gerçek verilerle kıyaslanabilmesi için dataframe haline getirilir.
df = pd.DataFrame({"Gerçek Değer": ytest, "Tahmin Edilen değer": yPred})
print(df)

## Performans Değerlendirmesi
from sklearn import metrics

# Mean Absolute Error (MAE) (Ortalama Mutlak Hata) metriğine göre puanı
# Mutlak hata, tahmin edilen değerler ile gerçek değerler arasındaki farktır.
# gerçek değer ile tahmin arasındaki farkın mutlak değeridir.
maeScore = metrics.mean_absolute_error(ytest, yPred)
print("Ortalama Mutlak Hata = " + str(maeScore))

# Mean Squared Error (MSE) (Ortalama Kare Hatası) metriğine göre sonucu
# MSE her değer için gerçek değer ile tahmin arasındaki farkın kareleri toplamının
# aritmetik ortalamasıdır.
mseScore = metrics.mean_squared_error(ytest, yPred)
print("Ortalama Kare Hatası = " + str(mseScore))

# Root Mean Squared Error (RMSE) metriğine göre sonucu
rmseScore = np.sqrt(metrics.mean_squared_error(ytest, yPred))
print("RMSE = "+ str(rmseScore))

# Veri görselleştirme
dataset.plot(x="Hours", y="Scores", style="go")
random_x = [1.1, 5.01, 9.2]
plt.plot(random_x,
         reg.intercept_ + reg.coef_ * random_x,
         color='red',
         label='regresyon grafiği')
plt.title("Saatlere Göre Yüzdelik Skorlar")
plt.xlabel("Çalışma Saatleri")
plt.ylabel("Yüzdelik Skorlar")
plt.savefig("Grafik.jpg")
plt.show()

## Veri seti dışındaki veriler ile yapılan tahminler

testVeri = np.array([0.5, 1.0, 4.2, 6.7, 10.0]).reshape(-1,1)
pred = reg.predict(testVeri)

for i in range(len(testVeri)):
    print(str(testVeri[i]) + "=>" + str(pred[i]) )


#!/usr/bin/env python

from neml.math import tensors

import common

import unittest
import numpy as np
import numpy.linalg as la

class TestVector(unittest.TestCase):
  def setUp(self):
    self.a = np.array([2.2,-1.2,2.5])
    self.b = np.array([0.0,5.8,1.1])

    self.va = tensors.Vector(self.a)
    self.vb = tensors.Vector(self.b)

    self.s = 2.1

  def test_assign(self):
    self.vb = self.va
    self.assertTrue(np.allclose(self.vb.data, self.a))

  def test_norm(self):
    self.assertAlmostEqual(self.va.norm(), la.norm(self.a))

  def test_dot(self):
    self.assertAlmostEqual(np.dot(self.a,self.b), self.va.dot(self.vb))

  def test_smultiply(self):
    self.assertTrue(np.allclose(
      (self.s * self.va).data,
      self.s * self.a))
    self.assertTrue(np.allclose(
      (self.va * self.s).data,
      self.s * self.a))

  def test_sdivide(self):
    self.assertTrue(np.allclose(
      (self.va / self.s).data,
      self.a / self.s))

  def test_add(self):
    self.assertTrue(np.allclose(
      (self.va + self.vb).data,
      self.a + self.b))
    self.assertTrue(np.allclose(
      (self.vb + self.va).data,
      self.a + self.b))

  def test_subtract(self):
    self.assertTrue(np.allclose(
      (self.va - self.vb).data,
      self.a - self.b))
    self.assertTrue(np.allclose(
      (self.vb - self.va).data,
      self.b - self.a))

  def test_negate(self):
    self.assertTrue(np.allclose(
      (-self.va).data,
      -self.a))
    self.assertTrue(np.allclose(
      self.va.opposite().data,
      -self.a))

  def test_normalize(self):
    self.va.normalize()
    self.assertTrue(np.allclose(
      self.va.data, self.a / la.norm(self.a)))

  def test_cross(self):
    self.assertTrue(np.allclose(
      self.va.cross(self.vb).data,
      np.cross(self.a, self.b)))

  def test_equality(self):
    self.assertTrue(self.va == self.va)
    self.assertFalse(self.va == self.vb)

  def test_inequality(self):
    self.assertFalse(self.va != self.va)
    self.assertTrue(self.va != self.vb)

  def test_get(self):
    for i in range(3):
      self.assertTrue(np.isclose(self.va[i], self.a[i]))

  def test_set(self):
    for i in range(3):
      self.va[i] = 2.0
      self.a[i] = 2.0
      self.assertTrue(np.isclose(self.va[i], self.a[i]))

  def test_outer(self):
    self.assertEqual(self.va.outer(self.vb), tensors.outer(self.va, self.vb))
    self.assertEqual(tensors.RankTwo(np.outer(self.a, self.b)),
        tensors.outer(self.va, self.vb))

class TestRankTwo(unittest.TestCase):
  def setUp(self):
    self.A = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.TA = tensors.RankTwo(self.A)
    self.B = np.array([[10.2,-9.3,2.5],[0.1,3.1,2.8],[0.1,3.2,-6.1]])
    self.TB = tensors.RankTwo(self.B)

    self.a = np.array([2.2,-1.2,2.5])
    self.va = tensors.Vector(self.a)

    self.s = 2.1

  def test_norm(self):
    self.assertTrue(np.isclose(self.TA.norm(), np.sqrt(np.sum(self.A*self.A))))

  def test_equality(self):
    self.assertEqual(self.TA, self.TA)

  def test_inequality(self):
    self.assertNotEqual(self.TA, self.TB)

  def test_get(self):
    self.assertTrue(np.isclose(self.TA[0,0], self.A[0,0]))

  def test_set(self):
    self.A[0,0] = 1.5
    self.assertTrue(np.isclose(self.A[0,0], 1.5))

  def test_scalar_mult(self):
    self.assertEqual(tensors.RankTwo(self.s*self.A), self.s * self.TA)
    self.assertEqual(tensors.RankTwo(self.A / self.s), self.TA / self.s)

  def test_add(self):
    self.assertEqual(tensors.RankTwo(self.A + self.B), self.TA + self.TB)
    self.assertEqual(tensors.RankTwo(self.A - self.B), self.TA - self.TB)

  def test_matrix_vector(self):
    self.assertEqual(tensors.Vector(np.dot(self.A, self.a)), self.TA*self.va)
    self.assertEqual(tensors.Vector(np.dot(self.a, self.A)), self.va*self.TA)

  def test_matrix_matrix(self):
    self.assertEqual(tensors.RankTwo(np.dot(self.A, self.B)), self.TA*self.TB)

  def test_inverse(self):
    self.assertEqual(tensors.RankTwo(la.inv(self.A)), self.TA.inverse())

  def test_transpose(self):
    self.assertEqual(tensors.RankTwo(self.A.T), self.TA.transpose())

class TestSymmetric(unittest.TestCase):
  def setUp(self):
    self.A = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.A = 0.5*(self.A + self.A.T)
    self.TA = tensors.Symmetric(self.A)
    self.B = np.array([[10.2,-9.3,2.5],[0.1,3.1,2.8],[0.1,3.2,-6.1]])
    self.B = 0.5*(self.B + self.B.T)
    self.TB = tensors.Symmetric(self.B)

    self.a = np.array([2.2,-1.2,2.5])
    self.va = tensors.Vector(self.a)

    self.s = 2.1

  def test_norm(self):
    self.assertTrue(np.isclose(self.TA.norm(), np.sqrt(np.sum(self.A*self.A))))

  def test_equality(self):
    self.assertEqual(self.TA, self.TA)

  def test_inequality(self):
    self.assertNotEqual(self.TA, self.TB)

  def test_scalar_mult(self):
    self.assertEqual(tensors.Symmetric(self.s*self.A), self.s * self.TA)
    self.assertEqual(tensors.Symmetric(self.A / self.s), self.TA / self.s)

  def test_add(self):
    self.assertEqual(tensors.Symmetric(self.A + self.B), self.TA + self.TB)
    self.assertEqual(tensors.Symmetric(self.A - self.B), self.TA - self.TB)

  def test_matrix_vector(self):
    self.assertEqual(tensors.Vector(np.dot(self.A, self.a)), self.TA*self.va)
    self.assertEqual(tensors.Vector(np.dot(self.a, self.A)), self.va*self.TA)

  def test_matrix_matrix(self):
    self.assertEqual(tensors.Symmetric(np.dot(self.A, self.B)), self.TA*self.TB)

  def test_inverse(self):
    self.assertEqual(tensors.Symmetric(la.inv(self.A)), self.TA.inverse())

  def test_transpose(self):
    self.assertEqual(tensors.Symmetric(self.A.T), self.TA.transpose())

  def test_id(self):
    self.assertEqual(tensors.Symmetric.id(), tensors.Symmetric(np.eye(3)))

  def test_trace(self):
    self.assertTrue(np.isclose(self.TA.trace(), np.trace(self.A)))

  def test_dev(self):
    self.assertTrue(self.TA.dev(), tensors.Symmetric(
      self.A - np.trace(self.A)/3.0 * np.eye(3)))

class TestSkew(unittest.TestCase):
  def setUp(self):
    self.A = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.A = 0.5*(self.A - self.A.T)
    self.TA = tensors.Skew(self.A)
    self.B = np.array([[10.2,-9.3,2.5],[0.1,3.1,2.8],[0.1,3.2,-6.1]])
    self.B = 0.5*(self.B - self.B.T)
    self.TB = tensors.Skew(self.B)

    self.a = np.array([2.2,-1.2,2.5])
    self.va = tensors.Vector(self.a)

    self.s = 2.1

  def test_equality(self):
    self.assertEqual(self.TA, self.TA)

  def test_inequality(self):
    self.assertNotEqual(self.TA, self.TB)

  def test_scalar_mult(self):
    self.assertEqual(tensors.Skew(self.s*self.A), self.s * self.TA)
    self.assertEqual(tensors.Skew(self.A / self.s), self.TA / self.s)

  def test_add(self):
    self.assertEqual(tensors.Skew(self.A + self.B), self.TA + self.TB)
    self.assertEqual(tensors.Skew(self.A - self.B), self.TA - self.TB)

  def test_matrix_vector(self):
    self.assertEqual(tensors.Vector(np.dot(self.A, self.a)), self.TA*self.va)
    self.assertEqual(tensors.Vector(np.dot(self.a, self.A)), self.va*self.TA)

  def test_matrix_matrix(self):
    self.assertEqual(tensors.Skew(np.dot(self.A, self.B)), self.TA*self.TB)

  def test_transpose(self):
    self.assertEqual(tensors.Skew(self.A.T), self.TA.transpose())

# Test various multiplicative combinations of tensors
class TestComboTensorMultiply(unittest.TestCase):
  def setUp(self):
    self.S = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.S = 0.5*(self.S + self.S.T)
    self.TS = tensors.Symmetric(self.S)
    self.G = np.array([[10.2,-9.3,2.5],[0.1,3.1,2.8],[0.1,3.2,-6.1]])
    self.TG = tensors.RankTwo(self.G)
    self.W = np.array([[-5.0,7.1,1.0],[-0.2,0.25,1.2],[-0.4,0.4,-2]])
    self.W = 0.5*(self.W - self.W.T)
    self.TW = tensors.Skew(self.W)

  def test_sym_general(self):
    self.assertEqual(tensors.RankTwo(np.dot(self.S, self.G)), self.TS * self.TG)

  def test_general_sym(self):
    self.assertEqual(tensors.RankTwo(np.dot(self.G, self.S)), self.TG * self.TS)

  def test_skew_general(self):
    self.assertEqual(tensors.RankTwo(np.dot(self.W, self.G)), self.TW * self.TG)

  def test_general_skew(self):
    self.assertEqual(tensors.RankTwo(np.dot(self.G, self.W)), self.TG * self.TW)

  def test_skew_sym(self):
    self.assertEqual(tensors.RankTwo(np.dot(self.W, self.S)), self.TW * self.TS)

  def test_sym_skew(self):
    self.assertEqual(tensors.RankTwo(np.dot(self.S, self.W)), self.TS * self.TW)

  def test_contract_general_sym(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.G, self.S),
      self.TG.contract(self.TS)))

  def test_contract_sym_general(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.G, self.S),
      self.TS.contract(self.TG)))

  def test_contract_general_skew(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.G, self.W),
      self.TG.contract(self.TW)))

  def test_contract_skew_general(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.G, self.W),
      self.TW.contract(self.TG)))

  def test_contract_skew_sym(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.W, self.S),
      self.TW.contract(self.TS)))

  def test_contract_sym_skew(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.W, self.S),
      self.TS.contract(self.TW)))

  def test_contract_general_general(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.G, self.G),
      self.TG.contract(self.TG)))

  def test_contract_sym_sym(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.S, self.S),
      self.TS.contract(self.TS)))

  def test_contract_skew_skew(self):
    self.assertTrue(np.isclose(
      np.einsum('ij,ij', self.W, self.W),
      self.TW.contract(self.TW)))

class TestComboTensorAdd(unittest.TestCase):
  def setUp(self):
    self.S = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.S = 0.5*(self.S + self.S.T)
    self.TS = tensors.Symmetric(self.S)
    self.G = np.array([[10.2,-9.3,2.5],[0.1,3.1,2.8],[0.1,3.2,-6.1]])
    self.TG = tensors.RankTwo(self.G)
    self.W = np.array([[-5.0,7.1,1.0],[-0.2,0.25,1.2],[-0.4,0.4,-2]])
    self.W = 0.5*(self.W - self.W.T)
    self.TW = tensors.Skew(self.W)

  def test_add_sym_general(self):
    self.assertEqual(tensors.RankTwo(self.S + self.G), self.TS + self.TG)

  def test_add_general_sym(self):
    self.assertEqual(tensors.RankTwo(self.G + self.S), self.TG + self.TS)

  def test_add_skew_general(self):
    self.assertEqual(tensors.RankTwo(self.W + self.G), self.TW + self.TG)

  def test_add_general_skew(self):
    self.assertEqual(tensors.RankTwo(self.G + self.W), self.TG + self.TW)

  def test_sub_sym_general(self):
    self.assertEqual(tensors.RankTwo(self.S - self.G), self.TS - self.TG)

  def test_sub_general_sym(self):
    self.assertEqual(tensors.RankTwo(self.G - self.S), self.TG - self.TS)

  def test_sub_skew_general(self):
    self.assertEqual(tensors.RankTwo(self.W - self.G), self.TW - self.TG)

  def test_sub_general_skew(self):
    self.assertEqual(tensors.RankTwo(self.G - self.W), self.TG - self.TW)

class TestRankFour(unittest.TestCase):
  def setUp(self):
    self.R1 = np.array([[[[ 7.09627147,  9.22330744, -1.36602973],
             [-7.86118175, -1.6342633 , -5.75516189],
             [ 2.61734248,  6.40678382,  3.37981603]],
            [[ 5.65100254, -7.88797059,  7.31396665],
             [-6.35471595,  5.67698069, -8.18795178],
             [ 9.10447016,  8.91183436, -6.65254333]],
            [[ 3.20429862,  2.99308849,  4.0035241 ],
             [-4.02440197, -4.39975872, -4.33542791],
             [ 9.36746226, -2.91156335,  4.51572032]]],
           [[[-9.23675199,  8.63546962,  6.83448027],
             [ 4.35044123,  2.24508666,  9.80054664],
             [ 0.30835223, -4.05208575,  5.68966326]],
            [[ 6.40300092, -8.25998136,  5.63566553],
             [-5.02801101,  5.64005224, -7.39586166],
             [ 5.90893633,  6.02074669,  1.37112738]],
            [[-2.68485216, -4.67660156,  3.52618441],
             [-2.52484812, -0.08561168,  3.39072868],
             [ 9.11295675,  2.63102786, -4.82285415]]],
           [[[ 8.31973154,  4.76081593,  4.38377207],
             [ 6.22896742, -3.83995097,  5.37501029],
             [-0.16770967,  7.9453854 , -4.95548491]],
            [[-5.67884611, -8.44970885, -7.42037867],
             [-5.19908193, -7.87006493,  1.65949787],
             [-3.25934672,  6.27340198,  5.98643056]],
            [[-4.20166968, -2.38276224,  3.04551936],
             [ 3.68445989, -5.84357996,  3.61183543],
             [ 1.54886677,  3.3659842 ,  6.43067337]]]])
    self.TR1 = tensors.RankFour(self.R1)

    self.R2 = np.array([[[[-8.03675620e+00,  2.58575052e+00,  2.44069661e+00],
             [ 4.75021663e+00,  1.24463394e+00, -8.69751301e-01],
             [-1.46310894e+00, -1.15053235e+00, -3.75342982e+00]],
            [[-7.64033956e+00,  4.19956720e+00, -4.87644982e+00],
             [ 1.06577507e+00,  8.94272637e+00,  6.57264250e-01],
             [-4.22613258e+00, -5.08830314e+00,  1.57718186e+00]],
            [[-4.02243082e+00, -4.75463781e+00, -8.88662152e+00],
             [-1.30383950e+00, -1.98063574e+00, -3.18963544e+00],
             [-7.52071674e+00,  1.08931933e+00,  2.86988431e+00]]],
           [[[ 5.28621060e+00, -6.83799668e+00,  8.98005935e+00],
             [-7.92741122e+00,  5.75699425e-01,  1.66782544e+00],
             [ 2.60041984e+00, -1.04476986e-02, -6.12424787e+00]],
            [[-3.73727368e+00,  6.59764771e+00, -1.18045587e+00],
             [ 4.08567441e+00,  2.66148943e+00, -6.82495588e-01],
             [-1.64417262e+00,  5.33119298e+00,  8.11045988e-03]],
            [[-5.90193883e+00, -2.63316107e+00,  5.61381825e+00],
             [-6.08591194e+00,  8.77285539e+00, -7.15230533e+00],
             [ 3.15093096e+00,  1.41350149e+00,  1.11702016e+00]]],
           [[[-9.61472764e-01, -1.91492497e+00,  9.48275324e+00],
             [ 6.68841134e+00,  3.23412041e+00, -3.41944541e+00],
             [-9.80203467e+00,  6.58425335e+00, -2.16548636e+00]],
            [[ 6.63950740e+00,  3.91551441e+00, -8.98229111e+00],
             [ 9.84606756e+00, -8.16145090e+00,  8.41929062e-01],
             [-1.93839620e+00,  7.44485127e+00, -2.70832414e+00]],
            [[ 9.79265531e+00, -1.18212395e+00, -5.39433704e+00],
             [ 4.87152614e+00,  9.47287450e+00,  5.53838514e+00],
             [ 9.30443367e+00,  1.27090319e+00,  1.60409739e+00]]]])
    self.TR2 = tensors.RankFour(self.R2)

    self.SS = np.array([
      [ 5.99159801, -2.24342348,  0.26667281, -0.95466199,  3.98931478, -0.10846981],
      [ 1.86468226, -4.32391908, -7.82738638, -7.45008989,  5.89874777, 0.45820648],
      [-5.92565398,  2.4862829 , -6.02112389,  6.75455965,  4.65183463, 9.96900579],
      [ 0.60378883, -3.72189328, -7.63388446, -5.76559403, -0.3119789 , -1.1527258 ],
      [ 4.56813135, -6.06783828, -6.18341368,  8.06169686, -9.56928844, 9.08114655],
      [-8.25516614,  6.30663846,  7.2084381 , -7.38280703, -5.96279902, 8.9935982 ]])
    self.SS_full = common.ms2ts(self.SS)
    self.TSS = tensors.SymSymR4(self.SS)

    self.SW = np.array([
      [ 5.43434005, -6.55983214,  0.29737664],
      [-4.77472172, -8.51287287, -3.19380185],
      [ 4.43407952, -6.02555614,  5.87786914],
      [ 1.89488869, -5.65383917,  8.83717547],
      [-7.18030867,  1.56100537, -9.83238641],
      [-4.52369317, -3.07284914, -7.54966999]])
    self.SW_full = common.ws2ts(self.SW)
    self.TSW = tensors.SymSkewR4(self.SW)

    self.WS = np.array([
      [-8.3567359 , -5.39728818, -8.00844442, -8.33365112, -0.97903364, -8.23943149],
      [-6.97125417,  4.34802055,  7.06281056, -1.57511617,  7.83359933, -9.37625432],
      [-6.0799489 , -6.0309543 ,  3.68575895,  8.84296976,  6.55799427, -9.22029379]])
    self.WS_full = common.wws2ts(self.WS)
    self.TWS = tensors.SkewSymR4(self.WS)

    self.scalar = -2.2

    self.G = np.array([[ 9.50640677,  1.79084726, -2.8877036 ],
       [-1.63159958,  2.52866904, -8.71585042],
       [ 5.01859685, -8.7324075 , -0.42919134]])
    self.TG = tensors.RankTwo(self.G)

    self.S = np.array([[ 6.19999242, -6.95811611, -6.02901899],
           [ 8.38508084,  6.01607694,  6.79839425],
           [-4.4214246 , -2.36795313, -8.84070728]])
    self.S = 0.5*(self.S+self.S.T)
    self.TS = tensors.Symmetric(self.S)

    self.W = np.array([[-9.36416517,  2.95527444,  8.70983194],
           [-1.54693052,  8.7905658 , -5.10895168],
           [-8.52740468, -0.7741642 ,  2.89544992]])
    self.W = 0.5 * (self.W - self.W.T)
    self.TW = tensors.Skew(self.W)

  def test_add(self):
    self.assertEqual(tensors.RankFour(self.R1 + self.R2), self.TR2 + self.TR1)
    self.assertEqual(tensors.RankFour(self.R1 - self.R2), self.TR1 - self.TR2)

  def test_equality(self):
    self.assertEqual(self.TR1, self.TR1)

  def test_inequality(self):
    self.assertNotEqual(self.TR1, self.TR2)

  def test_negate(self):
    self.assertEqual(tensors.RankFour(-self.R1), -self.TR1)

  def test_scalar_mult(self):
    self.assertEqual(tensors.RankFour(self.scalar * self.R1), self.scalar * self.TR1)
    self.assertEqual(tensors.RankFour(self.scalar * self.R2), self.TR2 * self.scalar)
    self.assertEqual(tensors.RankFour(self.R1 / self.scalar), self.TR1 / self.scalar)

  def test_double_contraction(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.R1, self.R2)), self.TR1 * self.TR2)

  def test_sym_sym(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.R1, self.SS_full)), self.TR1 * self.TSS)

  def test_sym_skew(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.R1, self.SW_full)), self.TR1 * self.TSW)

  def test_skew_sym(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.R1, self.WS_full)), self.TR1 * self.TWS)

  def test_ranktwo(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.R1, self.G)), self.TR1 * self.TG)

  def test_symmetric(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.R1, self.S)), self.TR1 * self.TS)

  def test_skew(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.R1, self.W)), self.TR1 * self.TW)

  def test_get(self):
    self.assertTrue(np.isclose(self.R1[1,2,0,1], self.TR1[1,2,0,1]))

  def test_set(self):
    self.TR1[1,1,1,1] = 4.0
    self.assertTrue(np.isclose(self.TR1[1,1,1,1],4.0))

class TestSymSymR4(unittest.TestCase):
  def setUp(self):
    self.SS1 = np.array([
      [ 5.99159801, -2.24342348,  0.26667281, -0.95466199,  3.98931478, -0.10846981],
      [ 1.86468226, -4.32391908, -7.82738638, -7.45008989,  5.89874777, 0.45820648],
      [-5.92565398,  2.4862829 , -6.02112389,  6.75455965,  4.65183463, 9.96900579],
      [ 0.60378883, -3.72189328, -7.63388446, -5.76559403, -0.3119789 , -1.1527258 ],
      [ 4.56813135, -6.06783828, -6.18341368,  8.06169686, -9.56928844, 9.08114655],
      [-8.25516614,  6.30663846,  7.2084381 , -7.38280703, -5.96279902, 8.9935982 ]])
    self.SS1_full = common.ms2ts(self.SS1)
    self.TSS1 = tensors.SymSymR4(self.SS1)

    self.SS2 = np.array([
      [-3.83767383, -8.63726504, -4.52095938,  9.35252323,  2.12800902, 3.26478511],
      [ 0.41705962,  3.95885105, -4.21676978,  4.12817198,  7.38839962, 5.79308578],
      [ 6.09635931,  2.31981366, -4.40237946, -5.51856189,  5.63572381, -5.55192385],
      [-0.97547288, -6.35708101, -4.35087656, -2.56567326,  4.32627031, 5.99408963],
      [ 6.30359707,  5.72926973,  2.47121354, -7.26333416, -5.08412215, -9.21872687],
      [-6.10780884,  1.01881487, -1.93491321,  6.13272186, -8.8721007, -2.97045116]])
    self.TSS2 = tensors.SymSymR4(self.SS2)

    self.SW = np.array([
      [ 5.43434005, -6.55983214,  0.29737664],
      [-4.77472172, -8.51287287, -3.19380185],
      [ 4.43407952, -6.02555614,  5.87786914],
      [ 1.89488869, -5.65383917,  8.83717547],
      [-7.18030867,  1.56100537, -9.83238641],
      [-4.52369317, -3.07284914, -7.54966999]])
    self.SW_full = common.ws2ts(self.SW)
    self.TSW = tensors.SymSkewR4(self.SW)

    self.WS = np.array([
      [-8.3567359 , -5.39728818, -8.00844442, -8.33365112, -0.97903364, -8.23943149],
      [-6.97125417,  4.34802055,  7.06281056, -1.57511617,  7.83359933, -9.37625432],
      [-6.0799489 , -6.0309543 ,  3.68575895,  8.84296976,  6.55799427, -9.22029379]])
    self.WS_full = common.wws2ts(self.WS)
    self.TWS = tensors.SkewSymR4(self.WS)

    self.R = np.array([[[[-8.03675620e+00,  2.58575052e+00,  2.44069661e+00],
             [ 4.75021663e+00,  1.24463394e+00, -8.69751301e-01],
             [-1.46310894e+00, -1.15053235e+00, -3.75342982e+00]],
            [[-7.64033956e+00,  4.19956720e+00, -4.87644982e+00],
             [ 1.06577507e+00,  8.94272637e+00,  6.57264250e-01],
             [-4.22613258e+00, -5.08830314e+00,  1.57718186e+00]],
            [[-4.02243082e+00, -4.75463781e+00, -8.88662152e+00],
             [-1.30383950e+00, -1.98063574e+00, -3.18963544e+00],
             [-7.52071674e+00,  1.08931933e+00,  2.86988431e+00]]],
           [[[ 5.28621060e+00, -6.83799668e+00,  8.98005935e+00],
             [-7.92741122e+00,  5.75699425e-01,  1.66782544e+00],
             [ 2.60041984e+00, -1.04476986e-02, -6.12424787e+00]],
            [[-3.73727368e+00,  6.59764771e+00, -1.18045587e+00],
             [ 4.08567441e+00,  2.66148943e+00, -6.82495588e-01],
             [-1.64417262e+00,  5.33119298e+00,  8.11045988e-03]],
            [[-5.90193883e+00, -2.63316107e+00,  5.61381825e+00],
             [-6.08591194e+00,  8.77285539e+00, -7.15230533e+00],
             [ 3.15093096e+00,  1.41350149e+00,  1.11702016e+00]]],
           [[[-9.61472764e-01, -1.91492497e+00,  9.48275324e+00],
             [ 6.68841134e+00,  3.23412041e+00, -3.41944541e+00],
             [-9.80203467e+00,  6.58425335e+00, -2.16548636e+00]],
            [[ 6.63950740e+00,  3.91551441e+00, -8.98229111e+00],
             [ 9.84606756e+00, -8.16145090e+00,  8.41929062e-01],
             [-1.93839620e+00,  7.44485127e+00, -2.70832414e+00]],
            [[ 9.79265531e+00, -1.18212395e+00, -5.39433704e+00],
             [ 4.87152614e+00,  9.47287450e+00,  5.53838514e+00],
             [ 9.30443367e+00,  1.27090319e+00,  1.60409739e+00]]]])
    self.TR = tensors.RankFour(self.R)

    self.S = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.S = 0.5*(self.S + self.S.T)
    self.TS = tensors.Symmetric(self.S)

    self.S2 = np.array([[10.2,-9.3,2.5],[0.1,3.1,2.8],[0.1,3.2,-6.1]])
    self.S2 = 0.5*(self.S2 + self.S2.T)
    self.TS2 = tensors.Symmetric(self.S2)

    self.scalar = 5.2

    self.G = np.array([[ 9.50640677,  1.79084726, -2.8877036 ],
       [-1.63159958,  2.52866904, -8.71585042],
       [ 5.01859685, -8.7324075 , -0.42919134]])
    self.TG = tensors.RankTwo(self.G)

    self.W = np.array([[-9.36416517,  2.95527444,  8.70983194],
           [-1.54693052,  8.7905658 , -5.10895168],
           [-8.52740468, -0.7741642 ,  2.89544992]])
    self.W = 0.5 * (self.W - self.W.T)
    self.TW = tensors.Skew(self.W)

  def test_to_full(self):
    full_np = common.ms2ts(self.SS1)
    full_t = tensors.RankFour(full_np)
    full = self.TSS1.to_full()
    self.assertEqual(full_t, full)

  def test_from_full(self):
    full = self.TSS1.to_full()
    new = full.to_sym()
    self.assertEqual(self.TSS1, new)

  def test_add(self):
    self.assertEqual(tensors.SymSymR4(self.SS1 + self.SS2), self.TSS2 + self.TSS1)
    self.assertEqual(tensors.SymSymR4(self.SS1 - self.SS2), self.TSS1 - self.TSS2)

  def test_equality(self):
    self.assertEqual(self.TSS1, self.TSS1)

  def test_inequality(self):
    self.assertNotEqual(self.TSS1, self.TSS2)

  def test_negate(self):
    self.assertEqual(tensors.SymSymR4(-self.SS1), -self.TSS1)

  def test_scalar_mult(self):
    self.assertEqual(tensors.SymSymR4(self.scalar * self.SS1), self.scalar * self.TSS1)
    self.assertEqual(tensors.SymSymR4(self.scalar * self.SS2), self.TSS2 * self.scalar)
    self.assertEqual(tensors.SymSymR4(self.SS1 / self.scalar), self.TSS1 / self.scalar)

  def test_product_sym_sym(self):
    self.assertEqual(tensors.SymSymR4(np.dot(self.SS1, self.SS2)), self.TSS1 * self.TSS2)

  def test_product_sym_full(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.SS1_full, self.R)), self.TSS1 * self.TR)

  def test_product_sym_skew(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.SS1_full, self.SW_full)), self.TSS1 * self.TSW)

  def test_product_skew_sym(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.SS1_full, self.WS_full)), self.TSS1 * self.TWS)

  def test_product_symmetric(self):
    self.assertEqual(tensors.Symmetric(common.usym(np.dot(self.SS1, common.sym(self.S)))), self.TSS1 * self.TS)

  def test_product_general(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.SS1_full, self.G)), self.TSS1 * self.TG)

  def test_product_skew(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.SS1_full, self.W)), self.TSS1 * self.TW)

  def test_douter(self):
    self.assertEqual(tensors.SymSymR4(common.ts2ms(np.einsum('ij,kl', self.S, self.S2))), tensors.douter(self.TS, self.TS2))

  def test_id(self):
    id_t = tensors.SymSymR4(np.eye(6))
    self.assertEqual(id_t, tensors.SymSymR4.id())

  def test_id_dev(self):
    ot = np.zeros((6,6))
    ot[:3,:3] = 1.0/3.0
    id_t = tensors.SymSymR4(np.eye(6) - ot)
    self.assertEqual(id_t, tensors.SymSymR4.id_dev())

class TestSymSkewR4(unittest.TestCase):
  def setUp(self):
    self.SW1 = np.array([
      [ 5.43434005, -6.55983214,  0.29737664],
      [-4.77472172, -8.51287287, -3.19380185],
      [ 4.43407952, -6.02555614,  5.87786914],
      [ 1.89488869, -5.65383917,  8.83717547],
      [-7.18030867,  1.56100537, -9.83238641],
      [-4.52369317, -3.07284914, -7.54966999]])
    self.SW1_full = common.ws2ts(self.SW1)
    self.TSW1 = tensors.SymSkewR4(self.SW1)

    self.SW2 = np.array([
      [ 7.90885123, -1.89089468, -6.95528566],
      [-2.53495619,  9.47533071, -2.76302205],
      [-8.57887706,  4.21216331, -7.68619983],
      [-5.45955495,  2.0523769 , -9.71153458],
      [-5.61696943, -4.02142773, -6.41654212],
      [-8.76272792, -3.60354692,  2.7402794 ]])

    self.SW2_full = common.ws2ts(self.SW2)
    self.TSW2 = tensors.SymSkewR4(self.SW2)

    self.WS = np.array([
      [-8.3567359 , -5.39728818, -8.00844442, -8.33365112, -0.97903364, -8.23943149],
      [-6.97125417,  4.34802055,  7.06281056, -1.57511617,  7.83359933, -9.37625432],
      [-6.0799489 , -6.0309543 ,  3.68575895,  8.84296976,  6.55799427, -9.22029379]])
    self.WS_full = common.wws2ts(self.WS)
    self.TWS = tensors.SkewSymR4(self.WS)

    self.SS = np.array([
      [ 5.99159801, -2.24342348,  0.26667281, -0.95466199,  3.98931478, -0.10846981],
      [ 1.86468226, -4.32391908, -7.82738638, -7.45008989,  5.89874777, 0.45820648],
      [-5.92565398,  2.4862829 , -6.02112389,  6.75455965,  4.65183463, 9.96900579],
      [ 0.60378883, -3.72189328, -7.63388446, -5.76559403, -0.3119789 , -1.1527258 ],
      [ 4.56813135, -6.06783828, -6.18341368,  8.06169686, -9.56928844, 9.08114655],
      [-8.25516614,  6.30663846,  7.2084381 , -7.38280703, -5.96279902, 8.9935982 ]])
    self.SS_full = common.ms2ts(self.SS)
    self.TSS = tensors.SymSymR4(self.SS)

    self.R = np.array([[[[-8.03675620e+00,  2.58575052e+00,  2.44069661e+00],
             [ 4.75021663e+00,  1.24463394e+00, -8.69751301e-01],
             [-1.46310894e+00, -1.15053235e+00, -3.75342982e+00]],
            [[-7.64033956e+00,  4.19956720e+00, -4.87644982e+00],
             [ 1.06577507e+00,  8.94272637e+00,  6.57264250e-01],
             [-4.22613258e+00, -5.08830314e+00,  1.57718186e+00]],
            [[-4.02243082e+00, -4.75463781e+00, -8.88662152e+00],
             [-1.30383950e+00, -1.98063574e+00, -3.18963544e+00],
             [-7.52071674e+00,  1.08931933e+00,  2.86988431e+00]]],
           [[[ 5.28621060e+00, -6.83799668e+00,  8.98005935e+00],
             [-7.92741122e+00,  5.75699425e-01,  1.66782544e+00],
             [ 2.60041984e+00, -1.04476986e-02, -6.12424787e+00]],
            [[-3.73727368e+00,  6.59764771e+00, -1.18045587e+00],
             [ 4.08567441e+00,  2.66148943e+00, -6.82495588e-01],
             [-1.64417262e+00,  5.33119298e+00,  8.11045988e-03]],
            [[-5.90193883e+00, -2.63316107e+00,  5.61381825e+00],
             [-6.08591194e+00,  8.77285539e+00, -7.15230533e+00],
             [ 3.15093096e+00,  1.41350149e+00,  1.11702016e+00]]],
           [[[-9.61472764e-01, -1.91492497e+00,  9.48275324e+00],
             [ 6.68841134e+00,  3.23412041e+00, -3.41944541e+00],
             [-9.80203467e+00,  6.58425335e+00, -2.16548636e+00]],
            [[ 6.63950740e+00,  3.91551441e+00, -8.98229111e+00],
             [ 9.84606756e+00, -8.16145090e+00,  8.41929062e-01],
             [-1.93839620e+00,  7.44485127e+00, -2.70832414e+00]],
            [[ 9.79265531e+00, -1.18212395e+00, -5.39433704e+00],
             [ 4.87152614e+00,  9.47287450e+00,  5.53838514e+00],
             [ 9.30443367e+00,  1.27090319e+00,  1.60409739e+00]]]])
    self.TR = tensors.RankFour(self.R)

    self.S = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.S = 0.5*(self.S + self.S.T)
    self.TS = tensors.Symmetric(self.S)

    self.scalar = 5.2

    self.G = np.array([[ 9.50640677,  1.79084726, -2.8877036 ],
       [-1.63159958,  2.52866904, -8.71585042],
       [ 5.01859685, -8.7324075 , -0.42919134]])
    self.TG = tensors.RankTwo(self.G)

    self.W = np.array([[-9.36416517,  2.95527444,  8.70983194],
           [-1.54693052,  8.7905658 , -5.10895168],
           [-8.52740468, -0.7741642 ,  2.89544992]])
    self.W = 0.5 * (self.W - self.W.T)
    self.TW = tensors.Skew(self.W)

  def test_to_full(self):
    full_np = common.ws2ts(self.SW1)
    full_t = tensors.RankFour(full_np)
    full = self.TSW1.to_full()
    self.assertEqual(full_t, full)

  def test_from_full(self):
    full = self.TSW1.to_full()
    new = full.to_symskew()
    self.assertEqual(self.TSW1, new)

  def test_add(self):
    self.assertEqual(tensors.SymSkewR4(self.SW1 + self.SW2), self.TSW2 + self.TSW1)
    self.assertEqual(tensors.SymSkewR4(self.SW1 - self.SW2), self.TSW1 - self.TSW2)

  def test_equality(self):
    self.assertEqual(self.TSW1, self.TSW1)

  def test_inequality(self):
    self.assertNotEqual(self.TSW1, self.TSW2)

  def test_negate(self):
    self.assertEqual(tensors.SymSkewR4(-self.SW1), -self.TSW1)

  def test_scalar_mult(self):
    self.assertEqual(tensors.SymSkewR4(self.scalar * self.SW1), self.scalar * self.TSW1)
    self.assertEqual(tensors.SymSkewR4(self.scalar * self.SW2), self.TSW2 * self.scalar)
    self.assertEqual(tensors.SymSkewR4(self.SW1 / self.scalar), self.TSW1 / self.scalar)

  def test_product_sym_sym(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.SW1_full, self.SS_full)), self.TSW1 * self.TSS)

  def test_product_sym_full(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.SW1_full, self.R)), self.TSW1 * self.TR)

  def test_product_sym_skew(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.SW1_full, self.SW2_full)), self.TSW1 * self.TSW2)

  def test_product_skew_sym(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.SW1_full, self.WS_full)), self.TSW1 * self.TWS)

  def test_product_symmetric(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.SW1_full, self.S)), self.TSW1 * self.TS)

  def test_product_general(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.SW1_full, self.G)), self.TSW1 * self.TG)

  def test_product_skew(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.SW1_full, self.W)), self.TSW1 * self.TW)

class TestSkewSymR4(unittest.TestCase):
  def setUp(self):
    self.WS1 = np.array([
      [-8.3567359 , -5.39728818, -8.00844442, -8.33365112, -0.97903364, -8.23943149],
      [-6.97125417,  4.34802055,  7.06281056, -1.57511617,  7.83359933, -9.37625432],
      [-6.0799489 , -6.0309543 ,  3.68575895,  8.84296976,  6.55799427, -9.22029379]])
    self.WS1_full = common.wws2ts(self.WS1)
    self.TWS1 = tensors.SkewSymR4(self.WS1)

    self.WS2 = np.array([
      [-8.80662663,  0.46179936, -5.49454144,  7.91618428,  5.34053953, -6.68997405],
      [ 4.15874971, -4.59781751,  7.43746813,  8.99981425, -0.97692573, 2.5075246 ],
      [ 9.53201007, -8.03524224,  0.94329443, -6.44415877, -9.92911741, 3.51742689]])
    self.WS2_full = common.wws2ts(self.WS2)
    self.TWS2 = tensors.SkewSymR4(self.WS2)

    self.SW = np.array([
      [ 5.43434005, -6.55983214,  0.29737664],
      [-4.77472172, -8.51287287, -3.19380185],
      [ 4.43407952, -6.02555614,  5.87786914],
      [ 1.89488869, -5.65383917,  8.83717547],
      [-7.18030867,  1.56100537, -9.83238641],
      [-4.52369317, -3.07284914, -7.54966999]])
    self.SW_full = common.ws2ts(self.SW)
    self.TSW = tensors.SymSkewR4(self.SW)

    self.SS = np.array([
      [ 5.99159801, -2.24342348,  0.26667281, -0.95466199,  3.98931478, -0.10846981],
      [ 1.86468226, -4.32391908, -7.82738638, -7.45008989,  5.89874777, 0.45820648],
      [-5.92565398,  2.4862829 , -6.02112389,  6.75455965,  4.65183463, 9.96900579],
      [ 0.60378883, -3.72189328, -7.63388446, -5.76559403, -0.3119789 , -1.1527258 ],
      [ 4.56813135, -6.06783828, -6.18341368,  8.06169686, -9.56928844, 9.08114655],
      [-8.25516614,  6.30663846,  7.2084381 , -7.38280703, -5.96279902, 8.9935982 ]])
    self.SS_full = common.ms2ts(self.SS)
    self.TSS = tensors.SymSymR4(self.SS)

    self.R = np.array([[[[-8.03675620e+00,  2.58575052e+00,  2.44069661e+00],
             [ 4.75021663e+00,  1.24463394e+00, -8.69751301e-01],
             [-1.46310894e+00, -1.15053235e+00, -3.75342982e+00]],
            [[-7.64033956e+00,  4.19956720e+00, -4.87644982e+00],
             [ 1.06577507e+00,  8.94272637e+00,  6.57264250e-01],
             [-4.22613258e+00, -5.08830314e+00,  1.57718186e+00]],
            [[-4.02243082e+00, -4.75463781e+00, -8.88662152e+00],
             [-1.30383950e+00, -1.98063574e+00, -3.18963544e+00],
             [-7.52071674e+00,  1.08931933e+00,  2.86988431e+00]]],
           [[[ 5.28621060e+00, -6.83799668e+00,  8.98005935e+00],
             [-7.92741122e+00,  5.75699425e-01,  1.66782544e+00],
             [ 2.60041984e+00, -1.04476986e-02, -6.12424787e+00]],
            [[-3.73727368e+00,  6.59764771e+00, -1.18045587e+00],
             [ 4.08567441e+00,  2.66148943e+00, -6.82495588e-01],
             [-1.64417262e+00,  5.33119298e+00,  8.11045988e-03]],
            [[-5.90193883e+00, -2.63316107e+00,  5.61381825e+00],
             [-6.08591194e+00,  8.77285539e+00, -7.15230533e+00],
             [ 3.15093096e+00,  1.41350149e+00,  1.11702016e+00]]],
           [[[-9.61472764e-01, -1.91492497e+00,  9.48275324e+00],
             [ 6.68841134e+00,  3.23412041e+00, -3.41944541e+00],
             [-9.80203467e+00,  6.58425335e+00, -2.16548636e+00]],
            [[ 6.63950740e+00,  3.91551441e+00, -8.98229111e+00],
             [ 9.84606756e+00, -8.16145090e+00,  8.41929062e-01],
             [-1.93839620e+00,  7.44485127e+00, -2.70832414e+00]],
            [[ 9.79265531e+00, -1.18212395e+00, -5.39433704e+00],
             [ 4.87152614e+00,  9.47287450e+00,  5.53838514e+00],
             [ 9.30443367e+00,  1.27090319e+00,  1.60409739e+00]]]])
    self.TR = tensors.RankFour(self.R)

    self.S = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.S = 0.5*(self.S + self.S.T)
    self.TS = tensors.Symmetric(self.S)

    self.scalar = 5.2

    self.G = np.array([[ 9.50640677,  1.79084726, -2.8877036 ],
       [-1.63159958,  2.52866904, -8.71585042],
       [ 5.01859685, -8.7324075 , -0.42919134]])
    self.TG = tensors.RankTwo(self.G)

    self.W = np.array([[-9.36416517,  2.95527444,  8.70983194],
           [-1.54693052,  8.7905658 , -5.10895168],
           [-8.52740468, -0.7741642 ,  2.89544992]])
    self.W = 0.5 * (self.W - self.W.T)
    self.TW = tensors.Skew(self.W)

  def test_to_full(self):
    full_np = common.wws2ts(self.WS1)
    full_t = tensors.RankFour(full_np)
    full = self.TWS1.to_full()
    self.assertEqual(full_t, full)

  def test_from_full(self):
    full = self.TWS1.to_full()
    new = full.to_skewsym()
    self.assertEqual(self.TWS1, new)

  def test_add(self):
    self.assertEqual(tensors.SkewSymR4(self.WS1 + self.WS2), self.TWS2 + self.TWS1)
    self.assertEqual(tensors.SkewSymR4(self.WS1 - self.WS2), self.TWS1 - self.TWS2)

  def test_equality(self):
    self.assertEqual(self.TWS1, self.TWS1)

  def test_inequality(self):
    self.assertNotEqual(self.TWS1, self.TWS2)

  def test_negate(self):
    self.assertEqual(tensors.SkewSymR4(-self.WS1), -self.TWS1)

  def test_scalar_mult(self):
    self.assertEqual(tensors.SkewSymR4(self.scalar * self.WS1), self.scalar * self.TWS1)
    self.assertEqual(tensors.SkewSymR4(self.scalar * self.WS2), self.TWS2 * self.scalar)
    self.assertEqual(tensors.SkewSymR4(self.WS1 / self.scalar), self.TWS1 / self.scalar)

  def test_product_sym_sym(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.WS1_full, self.SS_full)), self.TWS1 * self.TSS)

  def test_product_sym_full(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.WS1_full, self.R)), self.TWS1 * self.TR)

  def test_product_sym_skew(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.WS1_full, self.SW_full)), self.TWS1 * self.TSW)

  def test_product_skew_sym(self):
    self.assertEqual(tensors.RankFour(np.einsum('ijkl,klmn', self.WS1_full, self.WS2_full)), self.TWS1 * self.TWS2)

  def test_product_symmetric(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.WS1_full, self.S)), self.TWS1 * self.TS)

  def test_product_general(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.WS1_full, self.G)), self.TWS1 * self.TG)

  def test_product_skew(self):
    self.assertEqual(tensors.RankTwo(np.einsum('ijkl,kl', self.WS1_full, self.W)), self.TWS1 * self.TW)

  def test_douter(self):
    self.assertEqual(tensors.SkewSymR4(common.ts2wws(np.einsum('ij,kl', self.W, self.S))), tensors.douter(self.TW, self.TS))

class TestCPSpeciality(unittest.TestCase):
  def setUp(self):
    self.SS = np.array([
      [ 5.99159801, -2.24342348,  0.26667281, -0.95466199,  3.98931478, -0.10846981],
      [ 1.86468226, -4.32391908, -7.82738638, -7.45008989,  5.89874777, 0.45820648],
      [-5.92565398,  2.4862829 , -6.02112389,  6.75455965,  4.65183463, 9.96900579],
      [ 0.60378883, -3.72189328, -7.63388446, -5.76559403, -0.3119789 , -1.1527258 ],
      [ 4.56813135, -6.06783828, -6.18341368,  8.06169686, -9.56928844, 9.08114655],
      [-8.25516614,  6.30663846,  7.2084381 , -7.38280703, -5.96279902, 8.9935982 ]])
    self.SS_full = common.ms2ts(self.SS)
    self.TSS = tensors.SymSymR4(self.SS)

    self.W = np.array([[-9.36416517,  2.95527444,  8.70983194],
           [-1.54693052,  8.7905658 , -5.10895168],
           [-8.52740468, -0.7741642 ,  2.89544992]])
    self.W = 0.5 * (self.W - self.W.T)
    self.TW = tensors.Skew(self.W)

    self.S = np.array([[4.1,2.8,-1.2],[3.1,7.1,0.2],[4,2,3]])
    self.S = 0.5*(self.S + self.S.T)
    self.TS = tensors.Symmetric(self.S)

    self.WS = np.array([
      [-8.3567359 , -5.39728818, -8.00844442, -8.33365112, -0.97903364, -8.23943149],
      [-6.97125417,  4.34802055,  7.06281056, -1.57511617,  7.83359933, -9.37625432],
      [-6.0799489 , -6.0309543 ,  3.68575895,  8.84296976,  6.55799427, -9.22029379]])
    self.WS_full = common.wws2ts(self.WS)
    self.TWS = tensors.SkewSymR4(self.WS)

  def test_symsymskew_skewsymsym(self):
    A1 = tensors.SymSymR4Skew_SkewSymR4SymR4(self.TSS, self.TW)

    A2_ten = np.einsum('kmst,ml', self.SS_full, self.W) - np.einsum('km,mlst',
        self.W, self.SS_full)
    A2 = tensors.SymSymR4(common.ts2ms(A2_ten))

    self.assertEqual(A1, A2)

  def test_symskewsym_skewsymsym(self):
    A1 = tensors.SymSkewR4Sym_SkewSymR4SymR4(self.TWS, self.TS)

    A2_ten = np.einsum('km,mlst', self.S, self.WS_full) - np.einsum('kmst,ml',
        self.WS_full, self.S)
    A2 = tensors.SymSymR4(common.ts2ms(A2_ten))

    self.assertEqual(A1, A2)

  def test_special(self):
    A1 = tensors.SpecialSymSymR4Sym(self.TSS, self.TS)
    A2_ten = np.einsum('ijkz,ky', self.SS_full, self.S) - np.einsum(
        'ijyl,zl', self.SS_full, self.S)
    A2 = tensors.SymSkewR4(common.ts2sww(A2_ten))

    self.assertEqual(A1, A2)


import numpy as np
import cv2.cv2 as cv2

from scipy import signal
from digital_image_processing.tools.logger_base import log as log_message
from digital_image_processing.algorithms.edge_detection_algorithms.others_algorithms import (
    handle_img_padding,
    zero_cross_detection
)

def log(img_to_log: np.ndarray) -> np.ndarray:
    """Runs the Laplacian of Gaussian algorithm

    Reference:
    S. R. Gunn, "Edge detection error in the discrete Laplacian of Gaussian," Proceedings 1998
    International Conference on Image Processing. ICIP98 (Cat. No.98CB36269), 1998, pp. 515-519 vol.2,
    doi: 10.1109/ICIP.1998.723491. Huertas, A., & Medioni, G. (1986).
    Comparison of Edge Detection Algorithms for Automated Radiographic
    Measurement of the Carrying Angle. Journal of Biomedical Engineering and Medical Imaging,
    2(6). https://doi.org/10.14738/jbemi.26.1753.

    :param img_to_log: The input image. Must be a gray scale image
    :type img_to_log: ndarray

    :return: The estimated local operator for each pixel
    :rtype: ndarray
    """

    log_message.info('========Laplacian of Gaussian==========')
    log_kernel = np.array([
        [0, 0, 1, 0, 0],
        [0, 1, 2, 1, 0],
        [1, 2, -16, 2, 1],
        [0, 1, 2, 1, 0],
        [0, 0, 1, 0, 0]
    ])
    sobelx = cv2.Sobel(img_to_log, cv2.CV_64F, 1, 0, ksize=3)
    sobely = cv2.Sobel(img_to_log, cv2.CV_64F, 0, 1, ksize=3)
    sobel_first_derivative = cv2.magnitude(sobelx, sobely)
    sobel_test = np.empty_like(sobel_first_derivative)
    sobel_test[:] = sobel_first_derivative
    sobel_test[sobel_test > 200] = 255
    sobel_test[sobel_test < 200] = 0
    ######################
    log_img = signal.convolve2d(img_to_log, log_kernel)
    zero_crossing_log = zero_cross_detection(log_img)
    zero_crossing_log = handle_img_padding(img_to_log, zero_crossing_log)
    log_img = cv2.bitwise_and(zero_crossing_log, sobel_test)
    log_img = (log_img > 0) * 255
    assert not np.logical_and(log_img > 0, log_img < 255).any(), 'Image LoG operator isn\'t monochrome'
    return log_img