import numpy as np
import matplotlib.pyplot as plt

## Interpolare spline liniară

# Definesc funcția
def f(x):
    return x ** 2

# def f(x):
#     return np.cos(2 * x) - 2 * np.sin(3 * x)

# Definesc domeniul pe care fac interpolarea
left = -np.pi
right = +np.pi

# Numărul de funcții folosite în interpolare
N = 4

# Iau N + 1 puncte egal depărtate
x = np.linspace(left, right, N + 1)
# Eșantionez funcția
y = f(x)

def construieste_functie_spline_liniara(i):
    a = y[i]
    b = (y[i + 1] - y[i])/(x[i + 1] - x[i])

    def spline(t):
        return a + b * (t - x[i])

    return spline

x_grafic = np.linspace(left, right, 100)

y_grafic = f(x_grafic)

y_aproximat = np.piecewise(
    x_grafic,
    [
        (x[i] <= x_grafic) & (x_grafic < x[i + 1])
        for i in range(N - 1)
    ],
    [
        construieste_functie_spline_liniara(i)
        for i in range(N)
    ]
)

plt.plot(x_grafic, y_grafic, label='funcția')
plt.plot(x_grafic, y_aproximat, label='funcția aproximată')
plt.legend()
plt.show()


#! /usr/bin/python3

import sys
import pennylane as qml
import numpy as np

def gradient_200(weights, dev):
    r"""This function must compute the gradient *and* the Hessian of the variational
    circuit using the parameter-shift rule, using exactly 51 device executions.
    The code you write for this challenge should be completely contained within
    this function between the # QHACK # comment markers.

    Args:
        weights (array): An array of floating-point numbers with size (5,).
        dev (Device): a PennyLane device for quantum circuit execution.

    Returns:
        tuple[array, array]: This function returns a tuple (gradient, hessian).

            * gradient is a real NumPy array of size (5,).

            * hessian is a real NumPy array of size (5, 5).
    """

    @qml.qnode(dev, interface=None)
    def circuit(w):
        for i in range(3):
            qml.RX(w[i], wires=i)

        qml.CNOT(wires=[0, 1])
        qml.CNOT(wires=[1, 2])
        qml.CNOT(wires=[2, 0])

        qml.RY(w[3], wires=1)

        qml.CNOT(wires=[0, 1])
        qml.CNOT(wires=[1, 2])
        qml.CNOT(wires=[2, 0])

        qml.RX(w[4], wires=2)

        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(2))

    gradient = np.zeros([5], dtype=np.float64)
    hessian = np.zeros([5, 5], dtype=np.float64)

    # QHACK #
    s = np.pi/2
    default = circuit(weights)
    weight_copy = np.copy(weights)
    for i in range(len(weights)):
        weight_copy[i] += s
        plus = circuit(weight_copy)
        weight_copy[i] -= (2 * s)
        minus = circuit(weight_copy)
        gradient[i] = (plus - minus)/(2 * np.sin(s))
        hessian[i][i] = (plus - 2 * default + minus) / 2
        weight_copy[i] = weights[i]

    weight_copy = np.copy(weights)
    for i in range(len(hessian)):
        for j in range(i, len(hessian[i])):
            if i == j:
                continue
            else:
                weight_copy[i] += s
                weight_copy[j] += s
                plus = circuit(weight_copy)
                weight_copy[i] -= 2 * s
                minus_1 = circuit(weight_copy)
                weight_copy[i] += 2 * s
                weight_copy[j] -= 2 * s
                minus_2 = circuit(weight_copy)
                weight_copy[i] -= 2 * s
                minus_3 = circuit(weight_copy)
                hessian[i][j] = (plus - minus_1 - minus_2 + minus_3) / (2 * np.sin(s))**2
                weight_copy[i] = weights[i]
                weight_copy[j] = weights[j]

    for i in range(len(hessian)):
        for j in range(0, i):
            hessian[i][j] = hessian[j][i]

    # QHACK #

    return gradient, hessian, circuit.diff_options["method"]

if __name__ == "__main__":
    # DO NOT MODIFY anything in this code block
    weights = sys.stdin.read()
    weights = weights.split(",")
    weights = np.array(weights, float)

    dev = qml.device("default.qubit", wires=3)
    gradient, hessian, diff_method = gradient_200(weights, dev)

    print(
        *np.round(gradient, 10),
        *np.round(hessian.flatten(), 10),
        dev.num_executions,
        diff_method,
        sep=","
    )


import theano
import theano.tensor as T
import numpy as np

x = T.dscalar('x')
m = T.dscalar('mu')
s = T.dscalar('s')

sigma = T.exp(s)
first = 1 / (sigma * T.sqrt(2 * np.pi))
second = T.exp(-T.sqr(x-m)/(2 * T.sqr(sigma)))

f0 = first * second
f_x0 = T.grad(f0, x)
f_m0 = T.grad(f0, m)
f_s0 = T.grad(f0, s)

# compilation
f_noraml = theano.function([x,m,s], f0)
fprime_noraml_x = theano.function([x,m,s], f_x0)
fprime_normal_m = theano.function([x,m,s], f_m0)
fprime_normal_s = theano.function([x,m,s], f_s0)


from sympy import Eq, Matrix, cancel, expand, fraction, gcd_list, lcm_list, poly, solve, symbols

def sphere(P1, P2, P3):
    # return F(x, y, z) such that F(x, y, z) = 0 is the sphere's equation
    g, h, j, k, x, y, z = symbols('g, h, j, k, x, y, z')
    sphere_eq = Eq(x**2 + y**2 + z**2 + g*x + h*y + k, 0)
    sphere_eqs = []
    sphere_eqs.append(sphere_eq.subs(x, P1[0]).subs(y, P1[1]).subs(z, P1[2]))
    sphere_eqs.append(sphere_eq.subs(x, P2[0]).subs(y, P2[1]).subs(z, P2[2]))
    sphere_eqs.append(sphere_eq.subs(x, P3[0]).subs(y, P3[1]).subs(z, P3[2]))
    s = solve(sphere_eqs, (g, h, k))
    return fraction(cancel(x**2 + y**2 + z**2 + s[g]*x + s[h]*y + s[k]))[0]

def coplanar(P1, P2, P3, P4):
    mat = []
    mat.append([P1[0], P1[1], P1[2], 1])
    mat.append([P2[0], P2[1], P2[2], 1])
    mat.append([P3[0], P3[1], P3[2], 1])
    mat.append([P4[0], P4[1], P4[2], 1])
    return Eq(fraction(cancel(Matrix(mat).det()))[0], 0)

def plane(P1, P2, P3):
    x, y, z = symbols('x, y, z')
    return coplanar((x, y, z), P1, P2, P3)

def intersect(P, S, sphere_l):
    # return the intersection of PS and sphere_l(x, y, z) = 0:
    x, y, z, t = symbols('x, y, z, t')
    xt, yt, zt = P[0] + (S[0] - P[0])*t, P[1] + (S[1] - P[1])*t, P[2] + (S[2] - P[2])*t
    t0 = solve(Eq(sphere_l.subs(x, xt).subs(y, yt).subs(z, zt)/t, 0), t)[0]
    return xt.subs(t, t0), yt.subs(t, t0), zt.subs(t, t0)

def multiplied(x, y, z, w):
    x1, y1, z1, w1 = fraction(cancel(x)), fraction(cancel(y)), fraction(cancel(z)), fraction(cancel(w))
    lcd = lcm_list([x1[1], y1[1], z1[1], w1[1]])
    return x1[0]*cancel(lcd/x1[1]), y1[0]*cancel(lcd/y1[1]), z1[0]*cancel(lcd/z1[1]), w1[0]*cancel(lcd/w1[1])

def to_homogeneous(P):
    return multiplied(P[0], P[1], P[2], 1)

def reduced(x, y, z, w):
    gcd = gcd_list([x, y, z, w])
    if gcd == 0:
        return 0, 0, 0, 1
    return cancel(x/gcd), cancel(y/gcd), cancel(z/gcd), cancel(w/gcd)

def cross(P1, P2, P3):
    x10, x11, x12, x13 = P1[0], P1[1], P1[2], P1[3]
    x20, x21, x22, x23 = P2[0], P2[1], P2[2], P2[3]
    x30, x31, x32, x33 = P3[0], P3[1], P3[2], P3[3]
    # generated by cross-3d.py
    x = -x11*x22*x33 + x11*x23*x32 + x12*x21*x33 - x12*x23*x31 - x13*x21*x32 + x13*x22*x31
    y = x10*x22*x33 - x10*x23*x32 - x12*x20*x33 + x12*x23*x30 + x13*x20*x32 - x13*x22*x30
    z = -x10*x21*x33 + x10*x23*x31 + x11*x20*x33 - x11*x23*x30 - x13*x20*x31 + x13*x21*x30
    w = x10*x21*x32 - x10*x22*x31 - x11*x20*x32 + x11*x22*x30 + x12*x20*x31 - x12*x21*x30
    return reduced(x, y, z, w)

def on_sphere(sphere_h, P):
    x, y, z, w = symbols('x, y, z, w')
    return expand(sphere_h.subs(x, P[0]).subs(y, P[1]).subs(z, P[2]).subs(w, P[3])) == 0

def tangent_plane(sphere_poly, P):
    # Tangent plane to point (x0, y0, z0, w0) and quadratic curve plane
    # /x\T/a b d g\/x\        /a b d g\/x0\
    # |y| |b c e h||y| = 0 is |b c e h||y0|
    # |z| |d e f j||z|        |d e f j||z0|
    # \w/ \g h j k/\w/        \g h j k/\w0/
    # For a sphere with equator on xy:
    # a = c = f
    # b = d = e = j = 0
    x, y, z, w = symbols('x, y, z, w')
    coeff_a = sphere_poly.coeff_monomial(x**2)
    coeff_g = sphere_poly.coeff_monomial(x*w)/2
    coeff_h = sphere_poly.coeff_monomial(y*w)/2
    coeff_k = sphere_poly.coeff_monomial(w**2)
    x0 = coeff_a*P[0] + coeff_g*P[3]
    y0 = coeff_a*P[1] + coeff_h*P[3]
    z0 = coeff_a*P[2]
    w0 = coeff_g*P[0] + coeff_h*P[1] + coeff_k*P[3]
    return reduced(x0, y0, z0, w0)

def dist2(P1, P2):
    return cancel((P1[0]/P1[3] - P2[0]/P2[3])**2 + (P1[1]/P1[3] - P2[1]/P2[3])**2 + (P1[2]/P1[3] - P2[2]/P2[3])**2)

def main():
    # https://imomath.com/index.cgi?page=inversion (Problem 12)
    a, b, c, d, e, f, x, y, z, w = symbols('a, b, c, d, e, f, x, y, z, w')
    A, B, C, S = (a, 0, 0), (0, b, 0), (c, 0, 0), (d, e, f)
    sphere_l = sphere(A, B, C)
    print('Sphere Equation:', sphere_l, '= 0')
    A1, B1, C1 = intersect(A, S, sphere_l), intersect(B, S, sphere_l), intersect(C, S, sphere_l)
    sphere_poly = poly(sphere_l, (x, y, z)).homogenize(w)
    sphere_h = sphere_poly.expr
    print('Sphere Equation in Homogeneous:', sphere_h, '= 0')
    S, A, B, C = to_homogeneous(S), to_homogeneous(A), to_homogeneous(B), to_homogeneous(C)
    A1, B1, C1 = to_homogeneous(A1), to_homogeneous(B1), to_homogeneous(C1)
    print('A1:', A1)
    print('B1:', B1)
    print('C1:', C1)
    print('Is A on Sphere?', on_sphere(sphere_h, A))
    print('Is B on Sphere?', on_sphere(sphere_h, B))
    print('Is C on Sphere?', on_sphere(sphere_h, C))
    print('Is A1 on Sphere?', on_sphere(sphere_h, A1))
    print('Is B1 on Sphere?', on_sphere(sphere_h, B1))
    print('Is C1 on Sphere?', on_sphere(sphere_h, C1))
    plane_A1 = tangent_plane(sphere_poly, A1)
    plane_B1 = tangent_plane(sphere_poly, B1)
    plane_C1 = tangent_plane(sphere_poly, C1)
    print('Tangent plane to A1:', plane_A1)
    print('Tangent plane to B1:', plane_B1)
    print('Tangent plane to C1:', plane_C1)
    O = cross(plane_A1, plane_B1, plane_C1)
    print('O:', O)
    OS2 = dist2(O, S)
    print('OS**2 =', OS2)
    print('OS**2 - OA1**2 =', OS2 - dist2(O, A1))
    print('OS**2 - OB1**2 =', OS2 - dist2(O, B1))
    print('OS**2 - OC1**2 =', OS2 - dist2(O, C1))

if __name__ == '__main__':
    main()


"""
measure.py

This module is for which performs measurements

"""
import numpy as np

def calculate_distance(r_a, r_b):
    """
    Calculate the distance between two points.

    Parameters
    ----------
    r_a, r_b : np.ndarray
	The coordinates of each point.

    Return
    ------
    distance : float
        The distance between the two points.

    Example
    --------
    >>> r1 = nparray([0, 0, 0])
    >>> r2 = nparray([0, 0.1, 0])
    >>> calculate_distance(r1, r2)
    0.1
    """
    # This function calculates the distance between two points given as numpy arrays.
    if not isinstance(r_a, np.ndarray) or not isinstance(r_b, np.ndarray):
        raise TypeError("Input are np array not list")

    distance_vector = (r_a - r_b)
    distance = np.linalg.norm(distance_vector)
    return distance

def calculate_angle(r_a, r_b, r_c, degrees = False):
    # Calculate the angle between three points. Answer is given in radians by default, but can be given in degrees
    # by setting degrees=True
    ab = r_b - r_a
    bc = r_b - r_c
    theta = np.arccos(np.dot(ab, bc) / (np.linalg.norm(ab)*np.linalg.norm(bc)))

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


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

"""
最小二乘法线性回归
math
"""

import numpy as np
import matplotlib.pyplot as plt

def lstsq(X,Y):
    """
    最小二乘法线性回归
    """
    n = len(X)
    sumX,sumY,sumXY,sumXX =0,0,0,0
    for i in range(0,n):
        sumX  += X[i]
        sumY  += Y[i]
        sumXX += X[i]*X[i]
        sumXY += X[i]*Y[i]
    a = (n*sumXY -sumX*sumY)/(n*sumXX -sumX*sumX)
    b = (sumXX*sumY - sumX*sumXY)/(n*sumXX-sumX*sumX)
    return a,b,

if __name__ == '__main__':
    # 数据准备
    X = [1,2,3,4,5,6,7,8,9,10]
    Y = [10,11.5,12,13,14.5,15.5,16.8,17.3,18,18.7]

    #进行拟合
    a,b=lstsq(X,Y)
    print("y = %10.5fx + %10.5f" %(a,b))

    #展示拟合结果
    X1 = np.linspace(0,10)
    Y1 = a * X1 + b
    plt.plot(X1,Y1)
    plt.scatter(X,Y)
    plt.show()


#!/usr/bin/env python3
# coding: utf8

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

class Perceptron(object):

	def __init__(self):

		self.l_rate = 0.01
		self.n_epoch = 5
		self.bias = 0
		self.input_data = [[0.2, 0.7], [15.25, 14.37], [0.02, 0.68], [14.55, 16.36], [0.55, 0.36], [0.45, 0.16], [0.45, 0.26], [11.54, 17.226], [12.58, 17.36], [13.95, 15.26]]
		self.expected = [0, 1, 0, 1, 0, 0, 0, 1, 1, 1]

		#~ x = [i[0] for i in self.input_data]
		#~ y = [i[1] for i in self.input_data]
		#~ plt.scatter(x, y)
		#~ plt.show()

	def predict(self, row, weights):

		# Activation threshold function
		activation = 0
		for i in range(len(row) - 1):
			activation += weights[i] * row[i]
		activation += self.bias
		# Return class
		if activation >= 0.0:
			return 1.0
		else:
			return 0.0

	# Estimate Perceptron weights using stochastic gradient descent
	def train_weights(self):

		# Let's initiate weights with small values
		weights = list(np.random.uniform(low = 0, high = 0.1, size = 2))
		global_errors = []
		for epoch in range(self.n_epoch):
			# Keep track of errors for plotting
			epoch_errors = 0.0
			for row, expected in zip(self.input_data, self.expected):
				prediction = self.predict(row, weights)
				error = expected - prediction
				# We keep a track of global errors for plotting
				global_errors.append(abs(error))
				epoch_errors += abs(error)
				# If predicted and expected are different, update weights and bias
				if expected != prediction:
					self.bias = self.bias + self.l_rate * error
					for i in range(len(row)-1):
						weights[i] = weights[i] + self.l_rate * error * row[i]
			print('epoch: {}, lrate: {}, errors: {}'.format(epoch, self.l_rate, epoch_errors))

		plt.plot(global_errors)
		plt.ylim(-1, 2)
		plt.show()

		return weights

if __name__ == '__main__':
	# Calculate weights

	neuron = Perceptron()

	weights = neuron.train_weights()

	print(neuron.predict([8.23, 9.45], weights))
	print(neuron.predict([0.23, 1.45], weights))


##========================================================================================
import numpy as np
from scipy import linalg

""" --------------------------------------------------------------------------------------
fit h0 and w based on Expectation Reflection
input: features x[l,n], target: y[l]
 output: h0, w[n]
"""
def fit(x,y,niter_max=500,regu=0.1):
    #print(niter_max)

    n = x.shape[1]

    x_av = np.mean(x,axis=0)
    dx = x - x_av
    c = np.cov(dx,rowvar=False,bias=True)
    c += regu*np.identity(n)
    c_inv = linalg.inv(c)

    # initial values
    h0 = 0.
    w = np.random.normal(0.0,1./np.sqrt(n),size=(n))

    cost = np.full(niter_max,100.)
    for iloop in range(niter_max):
        h = h0 + x.dot(w)
        y_model = np.tanh(h)

        # stopping criterion
        cost[iloop] = ((y[:]-y_model[:])**2).mean()
        if iloop>0 and cost[iloop] >= cost[iloop-1]: break

        # update local field
        t = h!=0
        h[t] *= y[t]/y_model[t]
        h[~t] = y[~t]

        # find w from h
        h_av = h.mean()
        dh = h - h_av
        dhdx = dh[:,np.newaxis]*dx[:,:]

        dhdx_av = dhdx.mean(axis=0)
        w = c_inv.dot(dhdx_av)
        h0 = h_av - x_av.dot(w)

    return h0,w

""" --------------------------------------------------------------------------------------
calculate probability p based on x,h0, and w
input: x[l,n], w[n], h0
output: p[l]
"""
def predict(x,h0,w):
    #h = h0 + x.dot(w)
    #p = 1/(1+np.exp(-2.*h))
    #return p
    return np.sign(h0 + x.dot(w))


# -*- coding: UTF-8 -*-

from matplotlib import pyplot as plt
import numpy as np

################################################################################

def logistic(x, r):
    """Logistic map, with parameter r."""
    return r * x * (1 - x)

def bif_plot(x0, rs, axes):
    """Bifurcation plot for the logistic map.
    Inputs: x0 - initial condition.
            rs - Numpy array with values for
                 parameter r.
            axes - matplotib axes object."""
    y = np.zeros([2, 50])
    for r in rs:
        x = np.arange(x0, 800)
        for i in xrange(len(x)-1):
            x[i+1] = logistic(x[i], r)

        y[0, :] = r
        y[1, :] = x[-50:]
        axes.plot(y[0, :], y[1, :], 'b.')

################################################################################

if __name__=='__main__':
    # parameter definitions
    rs = np.linspace(0.001, 4, 2000) # values for the parameter r
    x0 = .5 #initial condition for x

    fig = plt.figure()
    ax = fig.add_subplot(111)
    bif_plot(x0, rs, ax)
    plt.show(fig)


# -*- coding: utf-8 -*-
# @Time    : 7/28/2018 5:58 PM
# @Author  : DonaldY

import numpy as np

# 1. 定义数据集
# 针对原 x 数据添加截距项系数 1
x = np.matrix([[1, 56], [1, 72], [1, 69], [1, 88], [1, 102], [1, 86], [1, 76], [1, 79], [1, 94], [1, 74]])
y = np.matrix([92, 102, 86, 110, 130, 99, 96, 102, 105, 92])

# 2. 定义 求解函数
def w_matrix(x, y):
    w = (x.T * x).I * x.T * y
    return w

w_matrix(x, y.reshape(10, 1))


import math
import numpy as np
import collections
import scipy.stats as sst
import matplotlib.pyplot as plt

def plot(*args, **kwargs):
    plt.plot(*args, **kwargs)
    plt.show()

def linregshow(x, y, col: str="r"):
    linregresult = sst.linregress(list(zip(x, y)))
    plot(x, y, col, x, [(val * linregresult.slope) + linregresult.intercept for val in x])
    return linregresult

def list_or_starargs(func):
    """This is a decorator to specify that a function either takes iterable input in the form of an iterable or a list of passed arguments.
    If other arguments are needed, the function will need to use kwargs.
    This passes the list as the first argument."""
    def decorated(*args, **kwargs):
        if isinstance(args[0], collections.Iterable):
            data = args[0]
            # We make generators into lists
            data = [val for val in data]
        else:
            data = args
        return func(data, **kwargs)

    return decorated

@list_or_starargs
def spridning(data):
    """Returns the size of the range of values in the data."""
    return max(data) - min(data)

@list_or_starargs
def medel(data):
    """Returns the arithmetic mean."""
    return sum(data) / len(data)

@list_or_starargs
def median(data):
    """Returns the median."""
    # We sort the data
    data = sorted(data)
    length = len(data)
    if length % 2 == 0:
        return medel(data[length // 2], data[(length // 2) - 1])
    else:
        return data[int(length // 2)]

@list_or_starargs
def kvartiler(data):
    """Returns the three quartiles of the data in order: lower, median, higher."""
    # We sort the data
    data = sorted(data)
    # We divide the data into two lists
    length = len(data)
    if length % 2 == 1:
        low_list = data[:(length // 2)]
        high_list = data[((length // 2) + 1):]
    else:
        low_list = data[:int(length / 2)]
        high_list = data[int(length / 2):]

    # We return the three quartiles
    return median(low_list), median(data), median(high_list)

def standardav(data, stick=False):
    """Returns the standard deviation of the input data, which has to be an iterable. stick specifies if it should be treated like
    non-total set of values (divide by n-1 instead of n)."""
    div_by = len(data) if (not stick) else (len(data) - 1)
    medelv = medel(data)
    return math.sqrt(sum([(val-medelv)**2 for val in data]) / div_by)

def normal_d(x, u, o):
    """Returns the value of a normal/standard distribution at the value x. u is Mu, and o is the standard deviation."""
    return (1 / (o * math.sqrt(2*math.pi))) * (math.e ** (-(((x-u)**2) / (2 * (o**2)))))


from sympy.core.expr import Expr
from sympy.vector import Laplacian
from symplyphysics import (
    symbols, Eq, solve, pretty, Function, Quantity, units, simplify, S,
    validate_input, expr_to_quantity, convert_to
)

# Description
## The diffusion equation, based on Fick's law, provides an analytical solution of spatial neutron flux
## distribution in the multiplying system.

## Law: -D * Δ(Ф(x)) + Σa * Ф(x) = (1 / k) * v * Σf * Ф(x)
## Where:
## D - diffusion coefficient.
##   See [diffusion coefficient](./neutron_diffusion_coefficient_from_scattering_cross_section.py) implementation.
## Δ - Laplacian operator.
## Ф(x) (neutron flux density) - number of neutrons crossing through some arbitrary cross-sectional unit area in all
##   directions per unit time.
## Σf - macroscopic fission cross-section of the fuel.
##   See [macroscopic cross-section](./macroscopic_cross_section_from_free_mean_path.py) implementation.
## Σa - macroscopic absorption cross-section of the fuel.
## k - effective multiplication factor.
##   See [effective multiplication factor](./effective_multiplication_factor.py)
## v - average number of neutrons produced per fission.
## x - coordinates for the neutron flux density.

diffusion_coefficient = symbols('diffusion_coefficient')
macroscopic_absorption_cross_section = symbols('macroscopic_absorption_cross_section')
macroscopic_fission_cross_section = symbols('macroscopic_fission_cross_section')
neutron_flux_function = symbols('neutron_flux_function', cls = Function)
effective_multiplication_factor = symbols('effective_multiplication_factor')
neutrons_per_fission = symbols('neutrons_per_fission')
flux_position = symbols('flux_position')

law = Eq(
    -1 * diffusion_coefficient * Laplacian(neutron_flux_function(flux_position)) +
    macroscopic_absorption_cross_section * neutron_flux_function(flux_position),
    (1 / effective_multiplication_factor) * neutrons_per_fission * macroscopic_fission_cross_section *
    neutron_flux_function(flux_position))

# rearrange the law and check it is the same as original
rearranged_law = Eq(
    -1 * Laplacian(neutron_flux_function(flux_position)) / neutron_flux_function(flux_position),
    ((1 / effective_multiplication_factor) * neutrons_per_fission * macroscopic_fission_cross_section - macroscopic_absorption_cross_section) /
    diffusion_coefficient)

assert solve(law, neutron_flux_function(flux_position))[0] == solve(rearranged_law, neutron_flux_function(flux_position))[0]

def print():
    return pretty(law, use_unicode=False)

# neutron_flux_function_ should be a function on CoordSys3D
def apply_neutron_flux_function(neutron_flux_function_: Function) -> Expr:
    applied_law = law.subs(neutron_flux_function(flux_position), neutron_flux_function_)
    return simplify(applied_law.doit())

# neutron_flux_function_ should be a function on CoordSys3D
# neutron_flux_function_ geometry should be defined with Quantity, eg width.dimension == units.length
@validate_input(
    macroscopic_fission_cross_section_=(1 / units.length),
    macroscopic_absorption_cross_section_=(1 / units.length),
    diffusion_coefficient_=units.length)
def calculate_multiplication_factor(
    neutron_flux_function_: Function,
    neutrons_per_fission_: float,
    macroscopic_fission_cross_section_: Quantity,
    macroscopic_absorption_cross_section_: Quantity,
    diffusion_coefficient_: Quantity) -> float:

    applied_law = apply_neutron_flux_function(neutron_flux_function_)
    result_expr = applied_law.subs({
        neutrons_per_fission: neutrons_per_fission_,
        macroscopic_fission_cross_section: macroscopic_fission_cross_section_,
        macroscopic_absorption_cross_section: macroscopic_absorption_cross_section_,
        diffusion_coefficient: diffusion_coefficient_})
    result_factor_expr = solve(result_expr, effective_multiplication_factor, dict=True)[0][effective_multiplication_factor]
    result_factor = expr_to_quantity(result_factor_expr, 'eff_multiplication_factor')
    return convert_to(result_factor, S.One).n()


#ZADANIE 2
#Wykorzystaj metode iteracyjna Gaussa-Seidela do
#znalezienia rozwiazan ukladow rownan a) – d). Wykonaj
#obliczenia w przy uzyciu arkusza kalkulacyjnego oraz programu
#napisanego w Python.

import numpy as np;

def diag_dom_test(a):
    test = []
    for i in range(len(a)):
        s = sum([np.abs(a[i,j]) for j in range(len(a)) if i!=j])
        if np.abs(a[i,i])>=s:
            test.append(1)
        else:
            test.append(0)
    return test

def Gauss_Seidl_iter(a, b, max_iter):
    x = np.zeros_like(b)
    x_k = np.zeros_like(x)

    l_iter = 0
    for k in range(max_iter):
        l_iter = l_iter + 1
        for i in range(a.shape[0]):
            s1 = sum([a[i, j] * x_k[j] for j in range(i)])
            s2 = sum([a[i, j] * x[j] for j in range(i + 1, a.shape[0])])
            x_k[i] = (b[i] - s1 - s2) / a[i, i]
        if np.allclose(x, x_k, rtol = 1e-10):
            break
        x = np.copy(x_k)
    print("Rozwiazanie:")
    print(x)
    error = np.dot(a, x) - b
    print("Blad:")
    print(error)
    print("Liczba iteracji:")
    print(l_iter)

a1 = np.array([[3.0, 1.0, -1.0], [1.0, 2.0, -1.0], [1.0, 1.0, 5.0]])
b1 = np.array([181.05, 108.35, 142.55])
print(30*'-')
print("Przyklad A\n")
print("Sprawdzenie czy macierz a jest diagonalnie dominujaca. Jesli zwrocony zostal wektor jedynek to jest diagonalnie dominujaca.")
print(diag_dom_test(a1))
print("Jesli macierz a nie jest diagonalnie dominujaca to wyniki sa niepoprawne.\n")
Gauss_Seidl_iter(a1, b1, 100)
print(30*'-')

a2 = np.array([[1.0, -1.0, 2.0], [3.0, 2.0, 1.0], [2.0, -3.0, -2.0]])
b2 = np.array([5.0, 10.0, -10.0])
print(30*'-')
print("Przyklad B\n")
print("Sprawdzenie czy macierz a jest diagonalnie dominujaca. Jesli zwrocony zostal wektor jedynek to jest diagonalnie dominujaca.")
print(diag_dom_test(a2))
print("Jesli macierz a nie jest diagonalnie dominujaca to wyniki sa niepoprawne.\n")
Gauss_Seidl_iter(a2, b2, 100)
print(30*'-')

a3 = np.array([[5.0, 1.0, 1.0, 1.0], [2.0, -1.0, -1.0, 1.0], [3.0, -1.0, 2.0, -2.0], [5.0, -4.0, 3.0, -2.0]])
b3 = np.array([685.0, 165.0, 256.0, 361.0])
print(30*'-')
print("Przyklad C\n")
print("Sprawdzenie czy macierz a jest diagonalnie dominujaca. Jesli zwrocony zostal wektor jedynek to jest diagonalnie dominujaca.")
print(diag_dom_test(a3))
print("Jesli macierz a nie jest diagonalnie dominujaca to wyniki sa niepoprawne.\n")
Gauss_Seidl_iter(a3, b3, 100)
print(30*'-')

a4 = np.array([[1.0, 3.0, 5.0], [2.0, 5.0, 1.0], [2.0, 3.0, 8.0]])
b4 = np.array([10.0, 8.0, 3.0])
print(30*'-')
print("Przyklad D\n")
print("Sprawdzenie czy macierz a jest diagonalnie dominujaca. Jesli zwrocony zostal wektor jedynek to jest diagonalnie dominujaca.")
print(diag_dom_test(a4))
print("Jesli macierz a nie jest diagonalnie dominujaca to wyniki sa niepoprawne.\n")
Gauss_Seidl_iter(a4, b4, 100)
print(30*'-')

#Metoda iteracyjna Gaussa-Seidela moze byc uzyta tylko wtedy gdy:
#- macierz a jest diagonalnie dominujaca
#- lub gdy macierz a jest macierza symetryczna
#- lub gdy macierz a jest dodatnio okreslona
#Tylko pierwsza z macierzy a podanych w przykladach powyzej jest diagonalnie dominujaca.
#Pozostale trzy macierze a nie sa diagonalnie dominujace.
#Wyniki jakie zwraca metoda iteracyjna Gaussa-Seidela dla ostatnich trzech przykladow nie sa poprawne.


# -*- coding: utf-8 -*-
"""
Exercise 03: Chapter 03, Kinder & Nelson

A common way to determine the value of a function is to sum over a series.
For example, the Maclaurin series for sin(x) is

    sin(x) = x - x**3/3! + x**5/5! - x**7/7! + ...

Perform a series expansion to derive the equation above. Next, write down
a general expression for the sum of the series that is valid between n = 0
and n = N, where N ≥ 0. This will serve as your algorithm for summing
the series.

One problem with the algorithm is that we do not know which value
of N is suitable when calcualting the series. Instead of guessing, have
your code proceed with the summation until the Nth term contributes a
negligible amount to the final summation, say 1 part in 10**8. We call
this _numerical convergence_.

Before writing any lines of code, discuss an approach with your neighbor
and write out on paper how your code should proceed. Code up your approach
in Spyder once you're done.

Here are your tasks:

   1. Perform a Maclaurin series expansion of the function sin(_x_) to
      derive the equation above.
   2. Derive a generalized, finite summation form for the series based
      on your Maclaurin series expansion.
   3. Discuss with your neighbor about how to approach coding the problem
      and write out on paper how you code should proceed.
   4. Code your approach in Spyder once you are finished.
   5. Show that, for small values of _x_, the algorithm converges and that
      it converges to the correct value by comparing your results to the
      value determined using NumPy's sine function.
   6. Which value for _N_ was required to reach the desired precision
      to obtain numerical convergence for small values of _x_?
   7. Steadily increase _x_ and write down the relative error between your
      calculated value for sin(_x_) and the NumPy function's value.
   8. What do you notice about the relative error?
   9. Will there be a time when the series does not numerically converge?
      Make a figure or two from the data you generate to support your
      conclusion.
  10. _Challenge_ How can you modify your algorithm to be valid for any
      value of _x_?

Created on Tue Aug 20 11:02:00 2019

@author: gafeiden
"""

import numpy as np
import matplotlib.pyplot as plt

x_values = np.arange(0.001,30,0.1)
def my_fact(n):
    return 1 if (n==1 or n==0) else n*my_fact(n-1)
actual_array = []
approx_array = []
err_array = []
summation = 0
#Create loop to access each x-value to approx.
for numm in x_values:
    #1. Calculate actual value
    actual_value = np.sin(numm)
    print('X value:',numm)
    print('Actual sin(x) value:', actual_value)

    #Run approx for each x value
    for each_value in range(50):
        #2. Calculate the factorial for n
        a = (2*each_value)+1
        factorial = my_fact(a)
        #3.calculate interations
        iteration = (((-1)**each_value)*(numm**a))/factorial
        #4.Iteration update
        summation = summation + iteration
        if abs(iteration/summation) <= 1e-8:
            break
    error = np.abs(actual_value - summation)/numm
    actual_array.append(actual_value)
    approx_array.append(summation)
    err_array.append(error)
    print('Approx value:', summation)
    print('Number of iterations:', each_value)
    print("Error:", error)

plt.scatter(x_values, actual_array, s=2)
plt.title('X values versus Actual sinx values')
plt.show()
plt.scatter(x_values, approx_array, s=2)
plt.title('X values versus Approx sinx values')
plt.show()
plt.scatter(x_values,err_array, s=2)
plt.title('X values versus Relative Error')
plt.show()

# Trying to add in the Phase folding.....
x_values = np.arange(0.001,30,0.1)
def my_fact(n):
    return 1 if (n==1 or n==0) else n*my_fact(n-1)
actual_array = []
approx_array = []
err_array = []
#Create loop to access each x-value to approx.
for numm in x_values:
    #1. Calculate actual value
    actual_value = np.sin(numm)
    new_x = 0
    #Phase folding
    if np.sign(numm) == 1:
        m = numm//2*np.pi
        new_x = numm-(2*np.pi*m)
        actual_value = np.sin(new_x)
    elif np.sign(numm) == -1:
        m = numm//2*np.pi
        new_x = numm+(2*np.pi*m)
        actual_value = np.sin(new_x)
    print(new_x)
    print('X value:',numm)
    print('Actual sin(x) value:', actual_value)

    #Run approx for each x value
    summation = 0.0
    for each_value in range(50):
        #2. Calculate the factorial for n
        a = (2*each_value)+1
        factorial = my_fact(a)
        #3.calculate interations
        iteration = (((-1)**each_value)*(new_x**a))/factorial
        #4.Iteration update
        summation = summation + iteration
        if abs(iteration/summation) <= 1e-8:
            print(abs(iteration/summation))
            break
    error = np.abs(actual_value - summation)/numm
    actual_array.append(actual_value)
    approx_array.append(summation)
    err_array.append(error)
    print('Approx value:', summation)
    print('Number of iterations:', each_value)
    print("Error:", error)

plt.scatter(x_values, actual_array, s=2)
plt.title('X values versus Actual sinx values')
plt.show()
plt.scatter(x_values, approx_array, s=2)
plt.title('X values versus Approx sinx values')
plt.show()
plt.scatter(x_values,err_array, s=2)
plt.title('X values versus Relative Error')
plt.show()


# Illustrate benefits of centering data
# for reducing correlation between slope and intercept for 1d regression

# Based on
#https://github.com/aloctavodia/BAP/blob/master/code/Chp3/03_Modeling%20with%20Linear%20Regressions.ipynb

import superimport

import pymc3 as pm
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import arviz as az
import pyprobml_utils as pml
import os

np.random.seed(1)
N = 100
alpha_real = 2.5
beta_real = 0.9
noiseSD = 0.5
eps_real = np.random.normal(0, noiseSD, size=N)

x = np.random.normal(10, 1, N) # centered on 10
y_real = alpha_real + beta_real * x
y = y_real + eps_real

# save untransformed data for later
x_orig = x
y_orig = y

_, ax = plt.subplots(1, 2, figsize=(8, 4))
ax[0].plot(x, y, 'C0.')
ax[0].set_xlabel('x')
ax[0].set_ylabel('y', rotation=0)
ax[0].plot(x, y_real, 'k')
az.plot_kde(y, ax=ax[1])
ax[1].set_xlabel('y')
plt.tight_layout()

# Fit posterior with MCMC instead of analytically (for simplicity and flexibility)
# This is the same as BAP code, except we fix the noise variance to a constant.

with pm.Model() as model_g:
    w0 = pm.Normal('w0', mu=0, sd=10)
    w1 = pm.Normal('w1', mu=0, sd=1)
    #ϵ = pm.HalfCauchy('ϵ', 5)
    mu = pm.Deterministic('mu', w0 + w1 * x)
    #y_pred = pm.Normal('y_pred', mu=μ, sd=ϵ, observed=y)
    y_pred = pm.Normal('y_pred', mu=mu, sd=noiseSD, observed=y)
    trace_g = pm.sample(1000, cores=1, chains=2)

az.plot_trace(trace_g, var_names=['w0', 'w1'])

az.plot_pair(trace_g,var_names=['w0', 'w1'], plot_kwargs={'alpha': 0.1});
pml.savefig('linreg_2d_bayes_post_noncentered_data.pdf')
plt.show()

# To reduce the correlation between alpha and beta, we can center the data
x = x_orig - x_orig.mean()

# or standardize the data
#x = (x - x.mean())/x.std()
#y = (y - y.mean())/y.std()

with pm.Model() as model_g_centered:
    w0 = pm.Normal('w0', mu=0, sd=10)
    w1 = pm.Normal('w1', mu=0, sd=1)
    #ϵ = pm.HalfCauchy('ϵ', 5)
    mu = pm.Deterministic('mu', w0 + w1 * x)
    #y_pred = pm.Normal('y_pred', mu=μ, sd=ϵ, observed=y)
    y_pred = pm.Normal('y_pred', mu=mu, sd=noiseSD, observed=y)
    trace_g_centered = pm.sample(1000, cores=1, chains=2)

az.plot_pair(trace_g_centered, var_names=['w0', 'w1'], plot_kwargs={'alpha': 0.1});
pml.savefig('linreg_2d_bayes_post_centered_data.pdf')
plt.show()


"""
 Function:  x, info  =  quasi_newton(fx, gradf, parameter)
 Purpose:   Implementation of the BFGS algorithm.
 Parameter: x0         - Initial estimate.
            maxit      - Maximum number of iterations.
            tolx       - Error toleration for stopping condition.
            Lips       - Lipschitz constant for gradient.
            strcnvx    - Strong convexity parameter of f(x).
            kappa      - Line search parameter

 Author: Cijo Jose.  [josevancijo@gmail.com]

"""
import time

import numpy as np

def quasi_newton(fx, gradf, parameter, verbose=0, xtrue=None):
    # print '%s\n', repmat('*', 1, 68);
    # Initialize x1.
    x = parameter["x0"]
    d = len(x)
    grad_next = gradf(x)
    B = 1.0 * np.eye(d)
    kappa = parameter["kappa"]
    info = {"itertime": [], "fx": [], "iter": None, "time": None, "totaltime": "None"}
    # Set the clock.
    timestart = time.time()
    # Main loop.
    for iter in range(int(parameter["maxit"])):
        # Compute error and save data to be plotted later on.
        info["itertime"].append(time.time() - timestart)
        fc = fx(x)
        info["fx"].append(fc)
        # Print the information.
        if verbose:
            print("Iter = %4d, f(x) = %5.3e" % (iter, info["fx"][iter]))
        # Start the clock.
        timestart = time.time()
        gd = grad_next
        p = np.dot(B, -gd)
        ddg = np.dot(p.T, gd)
        # Update the next iteration.
        alpha = 1.0
        while True:
            x_next = x + alpha * p
            if fx(x_next) <= fc + kappa * alpha * ddg:
                break
            alpha *= 0.5
        if np.sum((alpha * p) ** 2) <= parameter["tolx"]:
            break
        grad_next = gradf(x_next)
        v = grad_next - gd
        bv = np.dot(B, v)
        qbv = np.sum(v * bv)
        B += -np.outer(bv, bv.T) / qbv + alpha * np.outer(p, p.T) / np.sum(p * v)
        x = x_next
    # Finalization.
    info["iter"] = iter
    info["time"] = np.cumsum(info["itertime"])
    info["totaltime"] = info["time"][iter]
    return x, info


#synthetic datasets from  arXiv:1907.06698

import numpy as np
import pandas as pd

def parr_synthetic0(N=2000):
    X=np.random.uniform(0,10,(N,3))
    y=X[:,0]*X[:,0]+X[:,0]*X[:,1]+X[:,2]+5*X[:,0]*np.sin(3*X[:,1])+10
    return X,y

def parr_synthetic1(N=2000,sigma=1):
    X=np.random.uniform(-2,2,(N,2))
    eps = np.random.normal(size=N)*sigma
    y=X[:,0]*X[:,0]+X[:,1]+eps+10
    return X,y

def parr_synthetic2():
    #AZ = 90, CA = 70, CO = 40, NV = 80, WA = 60
    base=np.array([90,70,40,80,60])
    names = ["AZ","CA","CO","NV","WA"]
    X = np.array([[[state,day] for state in range(len(base))] for day in range(365)]).flatten().reshape(len(base)*365,2)
    y = base[X[:,0]] + 10 * np.sin(2*math.pi/365*X[:,1]+math.pi)+4*np.random.normal(size=365*len(base))
    return X,y

def parr_synthetic3(N=2000):
    sex = np.random.randint(0,2,N) #0=male 1=female
    pregnant = sex * np.random.randint(0,2,N)
    heightm = 5*12+8+np.random.uniform(-7,8,N)
    heightf = 5*12+5+np.random.uniform(-4.5,5,N)
    height = sex*heightf + (1-sex)*heightm
    education = np.array([12,10])[sex]+np.random.uniform(0,8,N)
    X = np.stack([sex,pregnant,height,education],axis=1)
    y = 120 + 10*(height- np.amin(height))+ 40 * pregnant - 1.5*education
    return X,y

class Parr_Synthetic_DataFrame:
    def __init__(self,N=2000, dataset=0,sigma=1):
        if(dataset==0):
            X,y = parr_synthetic0(2*N)
        elif(dataset==1):
            X,y = parr_synthetic1(2*N,sigma=sigma)
        elif(dataset==2):
            X,y = parr_synthetic2()
        elif(dataset==3):
            X,y = parr_synthetic3(2*N)

        self.df_train = pd.DataFrame(data=X[:N],columns = ["X"+str(i) for i in range(len(X[0]))])
        self.df_train["Y"]=y[:N]
        self.df_test = pd.DataFrame(data=X[N:],columns = ["X"+str(i) for i in range(len(X[0]))])
        self.df_test["Y"]=y[N:]

        self.columns_features = [x for x in self.df_train.columns if x!="Y"]
        self.columns_target = "Y"
        self.column_names = np.array(self.df_train.columns)

        self.columns_all = self.df_train.columns
        self.columns_types = self.df_train.dtypes
        self.columns_cont = [x for x in self.df_train.columns if self.df_train[x].dtype==np.float64]
        self.columns_cat = [x for x in self.df_train.columns if self.df_train[x].dtype==np.int64]
        self.columns_cat_unique_vals = [len(self.df_train[c].unique()) for c in self.columns_cat]

        self.df=pd.concat([self.df_train,self.df_test])

    def get_df(self):
        return self.df

    def __len__(self):
        return len(self.df)

    def get_train_df(self):
        return self.df_train

    def get_test_df(self):
        return self.df_test

    def get_val_df(self):
        return None


"""
Limits
======

Implemented according to the PhD thesis
http://www.cybertester.com/data/gruntz.pdf, which contains very thorough
descriptions of the algorithm including many examples.  We summarize here
the gist of it.

All functions are sorted according to how rapidly varying they are at
infinity using the following rules. Any two functions f and g can be
compared using the properties of L:

L=lim  log|f(x)| / log|g(x)|           (for x -> oo)

We define >, < ~ according to::

    1. f > g .... L=+-oo

        we say that:
        - f is greater than any power of g
        - f is more rapidly varying than g
        - f goes to infinity/zero faster than g

    2. f < g .... L=0

        we say that:
        - f is lower than any power of g

    3. f ~ g .... L!=0, +-oo

        we say that:
        - both f and g are bounded from above and below by suitable integral
          powers of the other

Examples
========
::
    2 < x < exp(x) < exp(x**2) < exp(exp(x))
    2 ~ 3 ~ -5
    x ~ x**2 ~ x**3 ~ 1/x ~ x**m ~ -x
    exp(x) ~ exp(-x) ~ exp(2x) ~ exp(x)**2 ~ exp(x+exp(-x))
    f ~ 1/f

So we can divide all the functions into comparability classes (x and x^2
belong to one class, exp(x) and exp(-x) belong to some other class). In
principle, we could compare any two functions, but in our algorithm, we
don't compare anything below the class 2~3~-5 (for example log(x) is
below this), so we set 2~3~-5 as the lowest comparability class.

Given the function f, we find the list of most rapidly varying (mrv set)
subexpressions of it. This list belongs to the same comparability class.
Let's say it is {exp(x), exp(2x)}. Using the rule f ~ 1/f we find an
element "w" (either from the list or a new one) from the same
comparability class which goes to zero at infinity. In our example we
set w=exp(-x) (but we could also set w=exp(-2x) or w=exp(-3x) ...). We
rewrite the mrv set using w, in our case {1/w, 1/w^2}, and substitute it
into f. Then we expand f into a series in w::

    f = c0*w^e0 + c1*w^e1 + ... + O(w^en),       where e0<e1<...<en, c0!=0

but for x->oo, lim f = lim c0*w^e0, because all the other terms go to zero,
because w goes to zero faster than the ci and ei. So::

    for e0>0, lim f = 0
    for e0<0, lim f = +-oo   (the sign depends on the sign of c0)
    for e0=0, lim f = lim c0

We need to recursively compute limits at several places of the algorithm, but
as is shown in the PhD thesis, it always finishes.

Important functions from the implementation:

compare(a, b, x) compares "a" and "b" by computing the limit L.
mrv(e, x) returns list of most rapidly varying (mrv) subexpressions of "e"
rewrite(e, Omega, x, wsym) rewrites "e" in terms of w
leadterm(f, x) returns the lowest power term in the series of f
mrv_leadterm(e, x) returns the lead term (c0, e0) for e
limitinf(e, x) computes lim e  (for x->oo)
limit(e, z, z0) computes any limit by converting it to the case x->oo

All the functions are really simple and straightforward except
rewrite(), which is the most difficult/complex part of the algorithm.
When the algorithm fails, the bugs are usually in the series expansion
(i.e. in SymPy) or in rewrite.

This code is almost exact rewrite of the Maple code inside the Gruntz
thesis.

Debugging
---------

Because the gruntz algorithm is highly recursive, it's difficult to
figure out what went wrong inside a debugger. Instead, turn on nice
debug prints by defining the environment variable SYMPY_DEBUG. For
example:

[user@localhost]: SYMPY_DEBUG=True ./bin/isympy

In [1]: limit(sin(x)/x, x, 0)
limitinf(_x*sin(1/_x), _x) = 1
+-mrv_leadterm(_x*sin(1/_x), _x) = (1, 0)
| +-mrv(_x*sin(1/_x), _x) = set([_x])
| | +-mrv(_x, _x) = set([_x])
| | +-mrv(sin(1/_x), _x) = set([_x])
| |   +-mrv(1/_x, _x) = set([_x])
| |     +-mrv(_x, _x) = set([_x])
| +-mrv_leadterm(exp(_x)*sin(exp(-_x)), _x, set([exp(_x)])) = (1, 0)
|   +-rewrite(exp(_x)*sin(exp(-_x)), set([exp(_x)]), _x, _w) = (1/_w*sin(_w), -_x)
|     +-sign(_x, _x) = 1
|     +-mrv_leadterm(1, _x) = (1, 0)
+-sign(0, _x) = 0
+-limitinf(1, _x) = 1

And check manually which line is wrong. Then go to the source code and
debug this function to figure out the exact problem.

"""
from __future__ import print_function, division

from sympy.core import Basic, S, oo, Symbol, I, Dummy, Wild, Mul
from sympy.functions import log, exp
from sympy.series.order import Order
from sympy.simplify.powsimp import powsimp, powdenest
from sympy import cacheit

from sympy.core.compatibility import reduce

from sympy.utilities.timeutils import timethis
timeit = timethis('gruntz')

from sympy.utilities.misc import debug_decorator as debug

def compare(a, b, x):
    """Returns "<" if a<b, "=" for a == b, ">" for a>b"""
    # log(exp(...)) must always be simplified here for termination
    la, lb = log(a), log(b)
    if isinstance(a, Basic) and a.func is exp:
        la = a.args[0]
    if isinstance(b, Basic) and b.func is exp:
        lb = b.args[0]

    c = limitinf(la/lb, x)
    if c == 0:
        return "<"
    elif c.is_infinite:
        return ">"
    else:
        return "="

class SubsSet(dict):
    """
    Stores (expr, dummy) pairs, and how to rewrite expr-s.

    The gruntz algorithm needs to rewrite certain expressions in term of a new
    variable w. We cannot use subs, because it is just too smart for us. For
    example::

        > Omega=[exp(exp(_p - exp(-_p))/(1 - 1/_p)), exp(exp(_p))]
        > O2=[exp(-exp(_p) + exp(-exp(-_p))*exp(_p)/(1 - 1/_p))/_w, 1/_w]
        > e = exp(exp(_p - exp(-_p))/(1 - 1/_p)) - exp(exp(_p))
        > e.subs(Omega[0],O2[0]).subs(Omega[1],O2[1])
        -1/w + exp(exp(p)*exp(-exp(-p))/(1 - 1/p))

    is really not what we want!

    So we do it the hard way and keep track of all the things we potentially
    want to substitute by dummy variables. Consider the expression::

        exp(x - exp(-x)) + exp(x) + x.

    The mrv set is {exp(x), exp(-x), exp(x - exp(-x))}.
    We introduce corresponding dummy variables d1, d2, d3 and rewrite::

        d3 + d1 + x.

    This class first of all keeps track of the mapping expr->variable, i.e.
    will at this stage be a dictionary::

        {exp(x): d1, exp(-x): d2, exp(x - exp(-x)): d3}.

    [It turns out to be more convenient this way round.]
    But sometimes expressions in the mrv set have other expressions from the
    mrv set as subexpressions, and we need to keep track of that as well. In
    this case, d3 is really exp(x - d2), so rewrites at this stage is::

        {d3: exp(x-d2)}.

    The function rewrite uses all this information to correctly rewrite our
    expression in terms of w. In this case w can be choosen to be exp(-x),
    i.e. d2. The correct rewriting then is::

        exp(-w)/w + 1/w + x.
    """
    def __init__(self):
        self.rewrites = {}

    def __repr__(self):
        return super(SubsSet, self).__repr__() + ', ' + self.rewrites.__repr__()

    def __getitem__(self, key):
        if not key in self:
            self[key] = Dummy()
        return dict.__getitem__(self, key)

    def do_subs(self, e):
        """Substitute the variables with expressions"""
        for expr, var in self.items():
            e = e.subs(var, expr)
        return e

    def meets(self, s2):
        """Tell whether or not self and s2 have non-empty intersection"""
        return set(self.keys()).intersection(list(s2.keys())) != set()

    def union(self, s2, exps=None):
        """Compute the union of self and s2, adjusting exps"""
        res = self.copy()
        tr = {}
        for expr, var in s2.items():
            if expr in self:
                if exps:
                    exps = exps.subs(var, res[expr])
                tr[var] = res[expr]
            else:
                res[expr] = var
        for var, rewr in s2.rewrites.items():
            res.rewrites[var] = rewr.subs(tr)
        return res, exps

    def copy(self):
        """Create a shallow copy of SubsSet"""
        r = SubsSet()
        r.rewrites = self.rewrites.copy()
        for expr, var in self.items():
            r[expr] = var
        return r

@debug
def mrv(e, x):
    """Returns a SubsSet of most rapidly varying (mrv) subexpressions of 'e',
       and e rewritten in terms of these"""
    e = powsimp(e, deep=True, combine='exp')
    if not isinstance(e, Basic):
        raise TypeError("e should be an instance of Basic")
    if not e.has(x):
        return SubsSet(), e
    elif e == x:
        s = SubsSet()
        return s, s[x]
    elif e.is_Mul or e.is_Add:
        i, d = e.as_independent(x)  # throw away x-independent terms
        if d.func != e.func:
            s, expr = mrv(d, x)
            return s, e.func(i, expr)
        a, b = d.as_two_terms()
        s1, e1 = mrv(a, x)
        s2, e2 = mrv(b, x)
        return mrv_max1(s1, s2, e.func(i, e1, e2), x)
    elif e.is_Pow:
        b, e = e.as_base_exp()
        if b == 1:
            return SubsSet(), b
        if e.has(x):
            return mrv(exp(e * log(b)), x)
        else:
            s, expr = mrv(b, x)
            return s, expr**e
    elif e.func is log:
        s, expr = mrv(e.args[0], x)
        return s, log(expr)
    elif e.func is exp:
        # We know from the theory of this algorithm that exp(log(...)) may always
        # be simplified here, and doing so is vital for termination.
        if e.args[0].func is log:
            return mrv(e.args[0].args[0], x)
        # if a product has an infinite factor the result will be
        # infinite if there is no zero, otherwise NaN; here, we
        # consider the result infinite if any factor is infinite
        li = limitinf(e.args[0], x)
        if any(_.is_infinite for _ in Mul.make_args(li)):
            s1 = SubsSet()
            e1 = s1[e]
            s2, e2 = mrv(e.args[0], x)
            su = s1.union(s2)[0]
            su.rewrites[e1] = exp(e2)
            return mrv_max3(s1, e1, s2, exp(e2), su, e1, x)
        else:
            s, expr = mrv(e.args[0], x)
            return s, exp(expr)
    elif e.is_Function:
        l = [mrv(a, x) for a in e.args]
        l2 = [s for (s, _) in l if s != SubsSet()]
        if len(l2) != 1:
            # e.g. something like BesselJ(x, x)
            raise NotImplementedError("MRV set computation for functions in"
                                      " several variables not implemented.")
        s, ss = l2[0], SubsSet()
        args = [ss.do_subs(x[1]) for x in l]
        return s, e.func(*args)
    elif e.is_Derivative:
        raise NotImplementedError("MRV set computation for derviatives"
                                  " not implemented yet.")
        return mrv(e.args[0], x)
    raise NotImplementedError(
        "Don't know how to calculate the mrv of '%s'" % e)

def mrv_max3(f, expsf, g, expsg, union, expsboth, x):
    """Computes the maximum of two sets of expressions f and g, which
    are in the same comparability class, i.e. max() compares (two elements of)
    f and g and returns either (f, expsf) [if f is larger], (g, expsg)
    [if g is larger] or (union, expsboth) [if f, g are of the same class].
    """
    if not isinstance(f, SubsSet):
        raise TypeError("f should be an instance of SubsSet")
    if not isinstance(g, SubsSet):
        raise TypeError("g should be an instance of SubsSet")
    if f == SubsSet():
        return g, expsg
    elif g == SubsSet():
        return f, expsf
    elif f.meets(g):
        return union, expsboth

    c = compare(list(f.keys())[0], list(g.keys())[0], x)
    if c == ">":
        return f, expsf
    elif c == "<":
        return g, expsg
    else:
        if c != "=":
            raise ValueError("c should be =")
        return union, expsboth

def mrv_max1(f, g, exps, x):
    """Computes the maximum of two sets of expressions f and g, which
    are in the same comparability class, i.e. mrv_max1() compares (two elements of)
    f and g and returns the set, which is in the higher comparability class
    of the union of both, if they have the same order of variation.
    Also returns exps, with the appropriate substitutions made.
    """
    u, b = f.union(g, exps)
    return mrv_max3(f, g.do_subs(exps), g, f.do_subs(exps),
                    u, b, x)

@debug
@cacheit
@timeit
def sign(e, x):
    """
    Returns a sign of an expression e(x) for x->oo.

    ::

        e >  0 for x sufficiently large ...  1
        e == 0 for x sufficiently large ...  0
        e <  0 for x sufficiently large ... -1

    The result of this function is currently undefined if e changes sign
    arbitarily often for arbitrarily large x (e.g. sin(x)).

    Note that this returns zero only if e is *constantly* zero
    for x sufficiently large. [If e is constant, of course, this is just
    the same thing as the sign of e.]
    """
    from sympy import sign as _sign
    if not isinstance(e, Basic):
        raise TypeError("e should be an instance of Basic")

    if e.is_positive:
        return 1
    elif e.is_negative:
        return -1
    elif e.is_zero:
        return 0

    elif not e.has(x):
        return _sign(e)
    elif e == x:
        return 1
    elif e.is_Mul:
        a, b = e.as_two_terms()
        sa = sign(a, x)
        if not sa:
            return 0
        return sa * sign(b, x)
    elif e.func is exp:
        return 1
    elif e.is_Pow:
        s = sign(e.base, x)
        if s == 1:
            return 1
        if e.exp.is_Integer:
            return s**e.exp
    elif e.func is log:
        return sign(e.args[0] - 1, x)

    # if all else fails, do it the hard way
    c0, e0 = mrv_leadterm(e, x)
    return sign(c0, x)

@debug
@timeit
@cacheit
def limitinf(e, x):
    """Limit e(x) for x-> oo"""
    # rewrite e in terms of tractable functions only
    e = e.rewrite('tractable', deep=True)

    if not e.has(x):
        return e  # e is a constant
    if e.has(Order):
        e = e.expand().removeO()
    if not x.is_positive:
        # We make sure that x.is_positive is True so we
        # get all the correct mathematical behavior from the expression.
        # We need a fresh variable.
        p = Dummy('p', positive=True, finite=True)
        e = e.subs(x, p)
        x = p
    c0, e0 = mrv_leadterm(e, x)
    sig = sign(e0, x)
    if sig == 1:
        return S.Zero  # e0>0: lim f = 0
    elif sig == -1:  # e0<0: lim f = +-oo (the sign depends on the sign of c0)
        if c0.match(I*Wild("a", exclude=[I])):
            return c0*oo
        s = sign(c0, x)
        # the leading term shouldn't be 0:
        if s == 0:
            raise ValueError("Leading term should not be 0")
        return s*oo
    elif sig == 0:
        return limitinf(c0, x)  # e0=0: lim f = lim c0

def moveup2(s, x):
    r = SubsSet()
    for expr, var in s.items():
        r[expr.subs(x, exp(x))] = var
    for var, expr in s.rewrites.items():
        r.rewrites[var] = s.rewrites[var].subs(x, exp(x))
    return r

def moveup(l, x):
    return [e.subs(x, exp(x)) for e in l]

@debug
@timeit
def calculate_series(e, x, logx=None):
    """ Calculates at least one term of the series of "e" in "x".

    This is a place that fails most often, so it is in its own function.
    """
    from sympy.polys import cancel

    for t in e.lseries(x, logx=logx):
        t = cancel(t)

        if t.has(exp) and t.has(log):
            t = powdenest(t)

        if t.simplify():
            break

    return t

@debug
@timeit
@cacheit
def mrv_leadterm(e, x):
    """Returns (c0, e0) for e."""
    Omega = SubsSet()
    if not e.has(x):
        return (e, S.Zero)
    if Omega == SubsSet():
        Omega, exps = mrv(e, x)
    if not Omega:
        # e really does not depend on x after simplification
        series = calculate_series(e, x)
        c0, e0 = series.leadterm(x)
        if e0 != 0:
            raise ValueError("e0 should be 0")
        return c0, e0
    if x in Omega:
        # move the whole omega up (exponentiate each term):
        Omega_up = moveup2(Omega, x)
        e_up = moveup([e], x)[0]
        exps_up = moveup([exps], x)[0]
        # NOTE: there is no need to move this down!
        e = e_up
        Omega = Omega_up
        exps = exps_up
    #
    # The positive dummy, w, is used here so log(w*2) etc. will expand;
    # a unique dummy is needed in this algorithm
    #
    # For limits of complex functions, the algorithm would have to be
    # improved, or just find limits of Re and Im components separately.
    #
    w = Dummy("w", real=True, positive=True, finite=True)
    f, logw = rewrite(exps, Omega, x, w)
    series = calculate_series(f, w, logx=logw)
    return series.leadterm(w)

def build_expression_tree(Omega, rewrites):
    r""" Helper function for rewrite.

    We need to sort Omega (mrv set) so that we replace an expression before
    we replace any expression in terms of which it has to be rewritten::

        e1 ---> e2 ---> e3
                 \
                  -> e4

    Here we can do e1, e2, e3, e4 or e1, e2, e4, e3.
    To do this we assemble the nodes into a tree, and sort them by height.

    This function builds the tree, rewrites then sorts the nodes.
    """
    class Node:
        def ht(self):
            return reduce(lambda x, y: x + y,
                          [x.ht() for x in self.before], 1)
    nodes = {}
    for expr, v in Omega:
        n = Node()
        n.before = []
        n.var = v
        n.expr = expr
        nodes[v] = n
    for _, v in Omega:
        if v in rewrites:
            n = nodes[v]
            r = rewrites[v]
            for _, v2 in Omega:
                if r.has(v2):
                    n.before.append(nodes[v2])

    return nodes

@debug
@timeit
def rewrite(e, Omega, x, wsym):
    """e(x) ... the function
    Omega ... the mrv set
    wsym ... the symbol which is going to be used for w

    Returns the rewritten e in terms of w and log(w). See test_rewrite1()
    for examples and correct results.
    """
    from sympy import ilcm
    if not isinstance(Omega, SubsSet):
        raise TypeError("Omega should be an instance of SubsSet")
    if len(Omega) == 0:
        raise ValueError("Length can not be 0")
    # all items in Omega must be exponentials
    for t in Omega.keys():
        if not t.func is exp:
            raise ValueError("Value should be exp")
    rewrites = Omega.rewrites
    Omega = list(Omega.items())

    nodes = build_expression_tree(Omega, rewrites)
    Omega.sort(key=lambda x: nodes[x[1]].ht(), reverse=True)

    # make sure we know the sign of each exp() term; after the loop,
    # g is going to be the "w" - the simplest one in the mrv set
    for g, _ in Omega:
        sig = sign(g.args[0], x)
        if sig != 1 and sig != -1:
            raise NotImplementedError('Result depends on the sign of %s' % sig)
    if sig == 1:
        wsym = 1/wsym  # if g goes to oo, substitute 1/w
    # O2 is a list, which results by rewriting each item in Omega using "w"
    O2 = []
    denominators = []
    for f, var in Omega:
        c = limitinf(f.args[0]/g.args[0], x)
        if c.is_Rational:
            denominators.append(c.q)
        arg = f.args[0]
        if var in rewrites:
            if not rewrites[var].func is exp:
                raise ValueError("Value should be exp")
            arg = rewrites[var].args[0]
        O2.append((var, exp((arg - c*g.args[0]).expand())*wsym**c))

    # Remember that Omega contains subexpressions of "e". So now we find
    # them in "e" and substitute them for our rewriting, stored in O2

    # the following powsimp is necessary to automatically combine exponentials,
    # so that the .subs() below succeeds:
    # TODO this should not be necessary
    f = powsimp(e, deep=True, combine='exp')
    for a, b in O2:
        f = f.subs(a, b)

    for _, var in Omega:
        assert not f.has(var)

    # finally compute the logarithm of w (logw).
    logw = g.args[0]
    if sig == 1:
        logw = -logw  # log(w)->log(1/w)=-log(w)

    # Some parts of sympy have difficulty computing series expansions with
    # non-integral exponents. The following heuristic improves the situation:
    exponent = reduce(ilcm, denominators, 1)
    f = f.subs(wsym, wsym**exponent)
    logw /= exponent

    return f, logw

def gruntz(e, z, z0, dir="+"):
    """
    Compute the limit of e(z) at the point z0 using the Gruntz algorithm.

    z0 can be any expression, including oo and -oo.

    For dir="+" (default) it calculates the limit from the right
    (z->z0+) and for dir="-" the limit from the left (z->z0-). For infinite z0
    (oo or -oo), the dir argument doesn't matter.

    This algorithm is fully described in the module docstring in the gruntz.py
    file. It relies heavily on the series expansion. Most frequently, gruntz()
    is only used if the faster limit() function (which uses heuristics) fails.
    """
    if not z.is_Symbol:
        raise NotImplementedError("Second argument must be a Symbol")

    # convert all limits to the limit z->oo; sign of z is handled in limitinf
    r = None
    if z0 == oo:
        r = limitinf(e, z)
    elif z0 == -oo:
        r = limitinf(e.subs(z, -z), z)
    else:
        if str(dir) == "-":
            e0 = e.subs(z, z0 - 1/z)
        elif str(dir) == "+":
            e0 = e.subs(z, z0 + 1/z)
        else:
            raise NotImplementedError("dir must be '+' or '-'")
        r = limitinf(e0, z)

    # This is a bit of a heuristic for nice results... we always rewrite
    # tractable functions in terms of familiar intractable ones.
    # It might be nicer to rewrite the exactly to what they were initially,
    # but that would take some work to implement.
    return r.rewrite('intractable', deep=True)


import numpy as np
import matplotlib.pyplot as plt

from scipy.optimize import newton
from scipy.integrate import quad

def functional(x, f, df_dx):
    """The integrand of the time integral to be minimized for a path f(x)."""

    return np.sqrt((1. + df_dx(x)**2) / (2 * g * f(x)))

def cycloid(x_2, y_2, N=100):
    """Return the path of Brachistochrone curve from (0, 0) to (x_2, y_2).

    The Brachistochrone curve is the path down which a bead will slide without
    friction between two points in the least time (an arc of a cycloid).
    Returned as an array of N values of (x, y) between (0, 0) and (x_2, y_2).

    Solves for the value of theta_2 to correspond to (x_2, y_2), assuming
    it starts from the origin, from the given ratio of y_2/x_2, which should
    equal
    """

    # First find theta_2 from (x_2, y_2) numerically (by Newton-Rapheson).
    def find_theta(theta):
        """Equation that will be zero for theta=theta_2 with
            (x, y) = (x_2, y_2)."""
        return y_2/x_2 - (1. - np.cos(theta))/(theta - np.sin(theta))
    theta_2 = newton(find_theta, np.pi/2.)  # Second argument is first guess.

    # The radius of the circle generating the cycloid
    a = y_2 / (1. - np.cos(theta_2))

    # Set up array of x and y points from the origin to (x_2, y_2)
    theta_1 = 0.
    theta_pts = np.linspace(theta_1, theta_2, N)
    x_pts = a * (theta_pts - np.sin(theta_pts))
    y_pts = a * (1. - np.cos(theta_pts))

    # The time of travel
    T = theta_2 * np.sqrt(a / g)
    print(f'T(cycloid) = {T:.3f}')
    return x_pts, y_pts, T

def parabola(x_2, y_2, N=100):
    """Return the path of a parabolic arc between (0,0) to (x_2, y_2).

    The parabola used is the one with a vertical tangent at (0,0).

    """

    c = y_2**2/x_2

    def f_parabola(x):
        """Equation of a parabola going through the origin and (x_2, y_2)."""
        return np.sqrt(c*x)
    def df_parabola_dx(x):
        return c/2/f_parabola(x)

    x_pts = np.linspace(0, x_2, N)
    y_pts = f_parabola(x_pts)

    # Calculate the time of travel by numerical integration.
    T, T_error = quad(functional, 0., x_2, args=(f_parabola, df_parabola_dx))
    print(f'T(parabola) = {T:.3f}')
    return x_pts, y_pts, T

def circle(x_2, y_2, N=100):
    """Return the path of a circular arc between (0,0) to (x_2, y_2).

    The circle used is the one with a vertical tangent at (0,0).

    """

    # Circle radius
    ### r =

    ###def f_circle(x):
        ### return
    ###def df_circle_dx(x):
        ###return (r-x)/f_circle(x)

    x_pts = np.linspace(0, x_2, N)
    y_pts = f_circle(x_pts)

    # Calculate the time of travel by numerical integration.
    ###T, T_error =
    ###print(f'T(circle) = {T:.3f}')
    return x_pts, y_pts, T

###def linear(x_2, y_2, N=100):
    ### """Return the path of a straight line from (0,0) to (x_2, y_2)."""

    ### m = =
    ### x_pts =
    ### y_pts =

    ### def f_linear(x):
        ### return
    ### def df_linear_dx(x):
        ### return

    # Calculate time of travel
    ### T_exact =
    ### T, T_error =

    ### print(f'T(linear) = {T:.3f}, (exact = {T_exact:.3f})')
    ### return x_pts, y_pts, T

# Acceleration due to gravity (mks units); final position of bead (m).
g = 9.81
x_2, y_2 = 1., 0.65

# Plot a figure comparing the four paths.
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot(1,1,1)

### for curve in ('cycloid', 'parabola', 'circle', 'linear'):
for curve in ('cycloid', 'parabola'):
    x, y, T = globals()[curve](x_2, y_2)
    ax.plot(x, y, lw=3, alpha=0.5, label=f'{curve}: {T:.3f} s')
ax.legend()

ax.set_xlabel('$x$')
ax.set_ylabel('$y$')
ax.set_xlim(0, x_2)
ax.set_ylim(0.8, 0)
plt.savefig('brachistochrone_homework.png')
plt.show()


"""
Author: Peratham Wiriyathammabhum

"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.sparse.linalg as linalg
from graph_utils import *
from kmeans import kmeans

def spectral_clustering(X, mode='affinity', k=3, knn=4, eta=0.01, sigma=2.5, max_iters=200):
	"""
	Perform spectral clustering on an input row matrix X.
	mode \in {'affinity','neighborhood','gaussian'}
	See: http://www.math.ucsd.edu/~fan/research/revised.html
		http://www.math.ucsd.edu/~fan/research/cbms.pdf
	"""
	ni, nd = X.shape
	L = laplacian_graph(X, mode='affinity', knn=knn, eta=eta, sigma=sigma)

	vals, vecs = linalg.eigs(L, k=k, which='SR')
	# ind = np.argsort(vals, axis=0)
	# vals = vals[ind]
	# vecs = vecs[:, ind]

	mu = kmeans(vecs, k=k, thres=10**-5, max_iters=max_iters)

	dist = ((vecs[:,None,:] - mu[None,:,:])**2).sum(axis=2)
	cidx = np.argmin(dist, axis=1)
	return mu, cidx

def main(opts):
	k = opts['k']
	knn = opts['knn']
	eta = opts['eta']
	sigma = opts['sigma']
	n_samples = opts['n_samples']
	mode = opts['mode']
	max_iters = opts['max_iters']

	# load data
	from sklearn import datasets
	two_moons = datasets.make_moons(n_samples=n_samples, noise=.05)
	X, y = two_moons
	# data plot
	fig = plt.figure()
	ax = fig.add_subplot()

	for c in range(2):
		cluster_members = [X[i] for i in range(len(X)) if y[i] == c]
		cluster_members = np.array(cluster_members)
		ax.scatter(cluster_members[:,0], cluster_members[:,1],  s= 0.5)
	input("Press Enter to continue...")

	# clustering
	_, cidx = spectral_clustering(X, mode=mode, k=k, knn=knn, eta=eta, sigma=sigma, max_iters=max_iters)

	# plot
	fig = plt.figure()
	ax = fig.add_subplot()
	for c in range(2):
		cluster_members = [X[i] for i in range(len(X)) if cidx[i] == c]
		cluster_members = np.array(cluster_members)

		ax.scatter(cluster_members[:,0], cluster_members[:,1],  s= 0.5)
	input("Press Enter to continue...")
	"""
	For details see:
	http://www.cs.cmu.edu/~aarti/Class/10701/readings/Luxburg06_TR.pdf
	https://ai.stanford.edu/~ang/papers/nips01-spectral.pdf
	https://www.cs.upc.edu/~argimiro/mytalks/jmda.pdf
	"""
	return

if __name__ == '__main__':
	import argparse

	parser = argparse.ArgumentParser(description='run spectral clustering.')
	parser.add_argument('--k', dest='k',
					  help='number of clusters',
					  default=2, type=int)
	parser.add_argument('--knn', dest='knn',
					  help='number of neighbors',
					  default=4, type=int)
	parser.add_argument('--eta', dest='eta',
					  help='distance eta',
					  default=0.01, type=int)
	parser.add_argument('--sigma', dest='sigma',
					  help='kernel width',
					  default=2.5, type=int)
	parser.add_argument('--n_samples', dest='n_samples',
					  help='number of data points',
					  default=2000, type=int)
	parser.add_argument('--mode', dest='mode',
					  help='graph construction mode \in {affinity, nearestneighbor, gaussian}',
					  default='affinity', type=str)
	parser.add_argument('--max_iters', dest='max_iters',
					  help='number of kmeans iterations to train',
					  default=200, type=int)
	args = parser.parse_args()
	opts = vars(args)

	main(opts)


from scipy import stats
from functools import reduce
import numpy as np

class GSNB:
    def __init__(self):
        self.pos_dists=[]
        self.neg_dists=[]

    def fit(self,X,Y):
        pos_X=X[Y==1,:]
        neg_X=X[Y!=1,:]

        self.pos_dists=list(zip(np.mean(pos_X,axis=0),np.std(pos_X,axis=0)))
        self.neg_dists=list(zip(np.mean(neg_X,axis=0),np.std(neg_X,axis=0)))

    def predict(self,X,epsilon=1e-5):

        pos_mean=list(map(lambda x:x[0], self.pos_dists))
        pos_std=list(map(lambda x:x[1], self.pos_dists))

        neg_mean=list(map(lambda x:x[0], self.neg_dists))
        neg_std=list(map(lambda x:x[1], self.neg_dists))

        results=[]
        for i in range(len(X)):
            instance=X[i,:]
            probs_pos=stats.norm.pdf(x=instance,loc=pos_mean,scale=pos_std)
            probs_pos=np.nan_to_num(probs_pos)
#             prob_pos=reduce((lambda x,y: x*y), probs_pos)
            log_prob_pos=sum(np.log(probs_pos+epsilon))
            probs_neg=stats.norm.pdf(x=instance,loc=neg_mean,scale=neg_std)
            probs_neg=np.nan_to_num(probs_neg)
#             prob_neg=reduce((lambda x,y: x*y), probs_neg)
            log_prob_neg=sum(np.log(probs_neg+epsilon))

            results.append(log_prob_pos-log_prob_neg)
#             print(log_prob_pos,log_prob_neg)
        return np.array(results)


from scipy.stats import linregress

def calc_calib_line(x_shift, y_shift, k_px_um, Il, Iz, Isum=None, normalization=False, shift_vs_sig=True):
    """
    Calculate coefficient of calibration line shift to signal of signal to shift.
    Calculations done only on lateral axis (LR signal)
    Shifts taken in nm units.

    Parameters
    -------
    x_shift: np-array
        dx shifts of image
    y_shift: np.array
        dy shifts of image
    k_px_um: float
        px to um scale (from px size)
    Il: np.array
        LR (lateral) intensity difference signal
    Iz: np.array
        TB (vertical) intensity difference signal
    Isum: np.array
        sum intensity signal
    normalization: boolean
        if True, doing calculation for signal normalized over Isum

    Returns
    -------
    k: float
        slope of linear fit
    b: float
        intercept of linear fit
    """
    x_shift_nm = (x_shift/k_px_um)*1E3
    y_shift_nm = (y_shift/k_px_um)*1E3
    if normalization:
        if shift_vs_sig:
            #calculate k, b for shift vs intensity, normalized over Isum for x
            k_x, b_x, _, _, _ = linregress(Il/Isum, x_shift_nm)
            k_y, b_y, _, _, _ = linregress(Iz/Isum, y_shift_nm)
        else:
            #k, b for intensity vs shift, normalized over Isum
            k_x, b_x, _, _, _ = linregress(x_shift_nm, Il/Isum)
            k_y, b_y, _, _, _ = linregress(y_shift_nm, Iz/Isum)
    else: # k, b for non-normalized intensity signal
        if shift_vs_sig:
            k_x, b_x, _, _, _ = linregress(Il, x_shift_nm)
            k_y, b_y, _, _, _ = linregress(Iz, y_shift_nm)
        else:
            k_x, b_x, _, _, _ = linregress(x_shift_nm, Il)
            k_y, b_y, _, _, _ = linregress(y_shift_nm, Iz)
    if b_x>=0:
        print('The calibration line for x axis (a.u. to nm) has formula {}*x+{}\n'.format(round(k_x, 3), round(b_x, 3)))
    else:
        print('The calibration line for x axis (a.u. to nm) has formula {}*x{}\n'.format(round(k_x, 3), round(b_x, 3)))
    if b_y>=0:
        print('The calibration line for y axis (a.u. to nm) has formula {}*x+{}\n'.format(round(k_y, 3), round(b_y, 3)))
    else:
        print('The calibration line for y axis (a.u. to nm) has formula {}*x{}\n'.format(round(k_y, 3), round(b_y, 3)))
    return k_x, b_x, k_y, b_y


import numpy as np
import matplotlib.pyplot as plt
from matplotlib import style
style.use('Solarize_Light2')

mean = lambda nums : sum(nums) / len(nums)

def main():
    draw_regression_line([10.908, 12.344, 13.87, 5.706], [0.348, 0.38, 0.46, 0.16])

def draw_regression_line(xs, ys):
    if len(xs) != len(ys):
        raise ValueError('data sets x and y must be of equal length')
    xs = np.array(xs, dtype= np.float64)
    ys = np.array(ys, dtype= np.float64)
    m, b = regression(xs, ys)
    regression_line = [ (m*x) + b for x in xs]
    r_squared = coeff_of_determination(ys, regression_line)

    points = plt.scatter(xs, ys, label = '')

    line = plt.plot(xs, regression_line, label = 'R² = {}'.format(r_squared))
    print('y = {}x + {}'.format(m, b))
    print('R² = {}'.format(r_squared))

    plt.title('y = {}x + {}'.format(m, b))
    plt.xlabel('')
    plt.ylabel('y')
    plt.legend()
    plt.show()

def regression(xs, ys):
    m = ( (mean(xs)*mean(ys) - mean(xs*ys))  /
        (mean(xs)**2 - mean(xs**2)) )
    b = mean(ys) - (m*mean(xs))
    return m, b

def squared_error(y_points, regression_line):
    return sum( (regression_line - y_points)**2)
def coeff_of_determination(y_points, regression_line):
    y_mean_line = [mean(y_points) for y in regression_line]
    squared_error_regr = squared_error(y_points, regression_line)
    squared_error_y_mean = squared_error(y_points, y_mean_line)
    return 1 - (squared_error_regr / squared_error_y_mean)
if __name__ == '__main__':
    main()


from __future__ import print_function
from scipy.constants import G
from SimPEG.utils import mkvc
import numpy as np

def GravSphereFreeSpace(x, y, z, R, xc, yc, zc, rho):
    """
        Computing the gravity response of a sphere in free-space.
        >> Input
        x, y, z:   Observation locations
        R:     radius of the sphere
        xc, yc, zc: Location of the sphere
        rho: Density of sphere

    """
    if ~np.size(x) == np.size(y) == np.size(z):
        print("Specify same size of x, y, z")
        return

    unit_conv = 1e8  # Unit conversion from SI to (mgal*g/cc)
    x = mkvc(x)
    y = mkvc(y)
    z = mkvc(z)

    rx = x - xc
    ry = y - yc
    rz = z - zc

    r = np.sqrt((rx) ** 2 + (ry) ** 2 + (rz) ** 2)

    M = np.empty_like(x)  # create a vector of "Ms" if the point is outide
    M[r >= R] = R ** 3 * 4.0 / 3.0 * np.pi * rho  # outside points
    M[r < R] = r[r < R] ** 3 * 4.0 / 3.0 * np.pi * rho  # inside points

    g = -G * (1.0 / r ** 2) * M * unit_conv

    gx = g * (rx / r)
    gy = g * (ry / r)
    gz = g * (rz / r)

    return gx, gy, gz

def GravityGradientSphereFreeSpace(x, y, z, R, xc, yc, zc, rho):
    """
        Computing the induced response of magnetic sphere in free-space.
        >> Input
        x, y, z:   Observation locations
        R:     radius of the sphere
        xc, yc, zc: Location of the sphere
        rho: Density of sphere

    """
    if ~np.size(x) == np.size(y) == np.size(z):
        print("Specify same size of x, y, z")
        return

    unit_conv = 1e12  # Unit conversion from SI to (Eotvos)
    x = mkvc(x)
    y = mkvc(y)
    z = mkvc(z)

    rx = x - xc
    ry = y - yc
    rz = z - zc
    rx2 = rx * rx
    ry2 = ry * ry
    rz2 = rz * rz

    r = np.sqrt(rx2 + ry2 + rz2)
    bot = r * r * r * r * r

    M = np.empty_like(x)  # create a vector of "Ms" if the point is outide
    M[r >= R] = R ** 3 * 4.0 / 3.0 * np.pi * rho  # outside points
    M[r < R] = r[r < R] ** 3 * 4.0 / 3.0 * np.pi * rho  # inside points

    g = G * (1.0 / bot) * M * unit_conv

    gxx = g * (2 * rx2 - ry2 - rz2)
    gyy = g * (2 * ry2 - rx2 - rz2)
    gzz = -gxx - gyy
    gxy = g * (3 * rx * ry)
    gxz = g * (3 * rx * rz)
    gyz = g * (3 * ry * rz)

    return gxx, gxy, gxz, gyy, gyz, gzz


import numpy

# child can hop 1, 2, or 3 steps.
# how many ways to go n steps

def steps1(n):
    if n == 1:
        return 1  # 1
    if n == 2:
        return 2  # 11, 2
    if n == 3:
        return 4  # 111, 12, 21, 3
    if n > 3:
        return steps(n-1) + steps(n-2) + steps(n-3)

cache = numpy.zeros(50)

def steps2(n):
    if n == 1:
        return 1  # 1
    if n == 2:
        return 2  # 11, 2
    if n == 3:
        return 4  # 111, 12, 21, 3
    if n > 3:
        if cache[n-1] != 0:
            sm1 = cache[n-1]
        else:
            sm1 = steps2(n-1)

        if cache[n-2] != 0:
            sm2 = cache[n-2]
        else:
            sm2 = steps2(n-2)

        if cache[n-3] != 0:
            sm3 = cache[n-3]
        else:
            sm3 = steps2(n-3)

        cache[n] = sm1 + sm2 + sm3
        return cache[n]

def steps3(n):

    c = numpy.zeros(50)
    c[1] = 1
    c[2] = 2
    c[3] = 4
    for i in xrange(4,n+1):
        c[i] = c[i-1] + c[i-2] + c[i-3]

    return c[n]

print steps3(30)


from numpy import exp, array, random, dot

class SimpleNeuralNetwork():
    def __init__(self):

        #seed random number
        random.seed(1)

        # Model a single neuron, with 4 input connections and 1 output connection.
        # Assign random weights to a 4 x 1 matrix, with values in the range -1 to 1 and mean 0.
        self.weights = 2 * random.random((4, 1)) - 1

    # The Sigmoid function, it could be tanh, etc..
    # Pass the weighted sum of the inputs through this function to normalise them between 0 and 1.
    def __sigmoid(self, x):
        return 1 / (1 + exp(-x))

    # Gradient of the Sigmoid curve.
    def __sigmoid_derivative(self, x):
        return x * (1 - x)

    # Adjusting the weights each time.
    def train(self, training_inputs, training_outputs, number_of_training_iterations):
        for iteration in range(number_of_training_iterations):
            # Pass the training set through our neural network (a single neuron).
            output = self.predict(training_inputs)

            # Calculate the error (The difference between the desired output and the predicted output).
            error = training_outputs - output

            # Multiply the error by the input and again by the gradient of the Sigmoid curve.
            # Means inputs, which are zero, do not cause changes to the weights.
            adjustment = dot(training_inputs.T, error * self.__sigmoid_derivative(output))

            # Adjust the weights.
            self.weights += adjustment

    # Prediction.
    def predict(self, inputs):
        # Pass inputs through our neural network (our single neuron).
        return self.__sigmoid(dot(inputs, self.weights))

if __name__=="__main__":
    neural_network=SimpleNeuralNetwork()

    print("Random starting weights: ")
    print(neural_network.weights)

    #defining training inputs and outputs
    training_inputs=array([[0,0,1,0],[1,1,1,1],[1,0,1,1],[0,1,1,1]])
    training_outputs=array([[0,1,1,0]]).T

    #train 5000 times
    neural_network.train(training_inputs,training_outputs,5000)

    print("Weights after training: ")
    print(neural_network.weights)

    # Test the neural network
    print("Predicting situation [0, 1, 0, 1] -> : ")
    print(neural_network.predict(array([0, 1, 0, 1])))


from math import sqrt
from math import cos
from math import atan2
from math import sin
import numpy as np

class Body:
    ## All vectors are decomposed in x-axis and y-axis
    ## p = (x,y) -> Actual Position Vector
    ## v = (x,y) -> Actual Velocity Vector
    ## pos_x -> List of all x-axis positions over time
    ## pos_y -> List of all y-axis positions over time
    ## color -> Orbit trace and planet color

    def __init__(self, name_of = 'UNNAMED', color_of = 'gray'):
        self.name = name_of
        self.color = color_of
        self.mass = 0
        self.v_x = self.v_y = self.v_x0 = self.v_y0 = self.v_z = self.v_z0 = 0
        self.p_x = self.p_y = self.p_x0 = self.p_y0 = self.p_z = self.p_z0 = 0
        self.r = np.zeros(3)
        self.r_0 = np.zeros(3)
        self.v = np.zeros(3)
        self.v_0 = np.zeros(3)
        self.pos_x = []
        self.pos_y = []
        self.pos_z = []
        self.angle = 0
        self.kinetic = 0
        self.potential = 0
        self.ang_mo = 0

    def acceleration(self, bodies, pos = np.array([0,0,0]), retpe = False, G = 6.67428e-11, retm = False):
        acc = [0,0,0]
        pe = 0
        m = 0
        for other in bodies:
            if self is other:
                continue
            d_vec = other.r - pos
            d = np.linalg.norm(d_vec) ## Calculate the distance between the bodies
            if d == 0:
                print(other.r)
                print(pos)
                raise ValueError('The bodies %r and %r collided' %(self.name, other.name))

            ## Compute acceleration OTHER causes on THIS
            a = -G * other.mass * pos / d**3

            if retm:
                v = np.linalg.norm(self.v)**2 #v^2
                dsun = np.linalg.norm(self.r)
                try:
                    m += sqrt(dsun * (self.mass**2 * v) - (self.r[0] * self.v[0] * self.mass + self.mass * self.r[1] * self.v[1])**2)
                except:
                    m += 0

            ## Compute potential Energy
            if retpe:
                pe += G * other.mass * self.mass / d
            acc = acc + a

        if retpe and retm:
            return acc[0], acc[1], acc[2], pe, m
        else:
            return acc[0], acc[1], acc[2]


from math import exp
import numpy as np
import random

from simulated_annealing import solution as sol

class Simulated_annealing:

    def __init__(self, max_n_iteration, initial_solution):
        import numpy as np
        self.temp = abs(
            initial_solution.get_penalty() - initial_solution.avg_neighbourhood_penalty()) / 0.6931  # calculate the initial temperature
        self.counter = 0
        self.plateau_size = 20 * len(initial_solution.get_neighbours())
        self.plateau_counter = 0
        self.alpha = 0.99999
        self.decay_time = max_n_iteration
        self.solution = initial_solution
        self.neigborhood = initial_solution.get_neighbours()
        self.avg_solution_penalty = 0
        self.initial_solution_penalty = initial_solution.get_penalty()

    def solution_update(self,num_mutation):
        import random
        neighbourhood=[]
        delta_neighbourhood=[]
        # create a neighboorhood for local search -> fixed size=5, because otherwise it will take too long time
        for i in range(5):
            new_solution = self.solution.get_random_neighbour(num_mutation)
            neighbourhood.append(new_solution)
            delta = new_solution.get_penalty() - self.solution.get_penalty()
            delta_neighbourhood.append(delta)
        # select the best neighbourhood: the one that mostly improves the objective function
        index=delta_neighbourhood.index(min(delta_neighbourhood))
        new_solution=neighbourhood[index]
        delta=delta_neighbourhood[index]

        if random.uniform(-10, 0) < -delta / self.temp:
            self.solution = new_solution

    def warming_solution_update(self, max_iterations):
        k = 0
        for i in range(1, max_iterations):
            new_solution = self.solution.get_random_neighbour(1)
            self.avg_solution_penalty += new_solution.get_penalty()
            if random.uniform(0, 1) > 0.4:
                k += 1
                self.solution = new_solution
        avg = self.avg_solution_penalty / max_iterations
        self.temp = abs(self.initial_solution_penalty - avg) / 0.6931
        print(f"Warming up completed, {k} solutions accepted , initial temperature set as {self.temp}")

    def solution_update_exp(self,num_mutation):
        new_solution = self.solution.get_random_neighbour(num_mutation)
        delta = new_solution.get_penalty() - self.solution.get_penalty()
        if random.uniform(0, 1) < np.exp(-delta / self.temp):
            self.solution = new_solution

    def run(self,num_exams):
        num_mutation = round(num_exams / 2)
        self.warming_solution_update(5000)
        while self.counter != self.decay_time and round(self.temp, 100) > 0:
            self.solution_update(num_mutation)
            if self.plateau_size != self.plateau_counter:
                self.counter += 1
                self.plateau_counter += 1

            else:
                num_mutation = round(num_mutation / 2)
                print(
                    f"iteration {self.counter} | score {self.solution.get_penalty()} | current temperature {self.temp}")
                self.plateau_counter = 0
                self.temp = self.temp * self.alpha ** self.counter
        print(f"simulated annealing completed, temperature is {self.temp} and penalty is {self.solution.get_penalty()}")
        return self.solution


import numpy as np

def calc_dist_chord(ra_ref, dec_ref, ra, dec):

    return 2*np.sqrt(np.sin((dec - dec_ref)/2)**2
                     + np.cos(dec_ref)*np.cos(dec)*np.sin((ra - ra_ref)/2)**2)

def calc_dist(ra_ref, dec_ref, ra, dec):

    """

    Function calcdist

    Distance between a point raref, decref and an array ra, dec.  All
    positions should be in radians, and ra and dec should have
    matching shapes.

    Inputs:
    ra_ref: float, reference right ascension in radians
    dec_ref: float, reference declination in radians
    ra: array, list of right ascensions in radians
    dec: array, list of declinations in radians

    Returns:
    an array of distances, same shape as ra, measured in radians

    """

    return np.arccos(np.sin(dec)*np.sin(dec_ref) +
                     np.cos(dec)*np.cos(dec_ref)*np.cos(ra_ref - ra))

def calc_all_dist(ra1, dec1, ra2, dec2, chord=False):

    """.

    Function calc_all_dist.

    Computes distances between all pairs of points in the 1d arrays
    ra1, dec1.  Outputs a 2D array of size n x m, where n is the
    dimensionality of ra1, and m is the dimensionality of ra2.

    Inputs:
    ra1: array of ra (in radians)
    dec1: array of dec (in radians)
    ra2: second array of ra
    dec2: second array of dec

    Returns:
    dist: 2d array of distances between the points in ra1, dec1 and ra2, dec2, in radians

    """

    dist = np.zeros((ra1.shape[0], ra2.shape[0]), np.float32)
    for j in range(ra1.shape[0]):
        if chord:
            dist[j] = calc_dist_chord(ra1[j], dec1[j], ra2, dec2)
        else:
            dist[j] = calc_dist(ra1[j], dec1[j], ra2, dec2)
    return dist

def calcbestdist(ralist, declist, ra, dec):

    """

    Function calcbestdist

    Find the closest point in the input lists ralist and declist to
    each position defined by the input arrays ra and dec.

    Inputs:
    ralist: array-like, list of reference right ascensions in radians
    declist: array-like, list of reference declinations in radians
    ra: array-like, list of right ascensions in radians
    dec: array-like, list of declinations in radians

    Outputs:
    closest_pt: array giving the index of the closest element in ralist, declist for each pair of elements ra, dec
    bestdist: array giving the distance (in radians) of the closest element in ralist, declist for each pair of elements ra, dec
    n: array giving the number of stars that are closer to each element in ralist, declist than to any other element

    """

    bestdist = np.zeros(ra.shape) + 1e100
    closest_pt = np.zeros(ra.shape).astype(int)

    for i in range(len(ralist)):
        dist = calc_dist(ralist[i], declist[i], ra, dec)
        indx = np.where(dist < bestdist)
        closest_pt[indx] = i
        bestdist[indx] = dist[indx]

    n = np.zeros(ralist.shape).astype(int)

    for i in range(len(ralist)):
        n[i] = np.sum(closest_pt == i)

    return [closest_pt, bestdist, n]


#zadanie_3 Paweł Gałka
import numpy

def get_weight(matrix):
    w, v = numpy.linalg.eig(matrix)
    current = w[0]
    vector = v[:,0]
    vector_normalized = vector/vector.sum()

    for i in range(1,len(w)):
        if numpy.isreal(w[i]) and current<w[i]:
            current = w[i]
            vector =  v[:,i]
            vector_normalized = vector/vector.sum()
    return numpy.real(current),numpy.real(vector_normalized)

def normalize2Subcat(v1,v2,vSubcat):
    matrix = v1*vSubcat.item(0)+v2*vSubcat.item(1)
    return matrix/matrix.sum()

matrixPrize = numpy.matrix([[1,1/5,3], [5,1,9], [1/3,1/9,1]])
matrixFuel = numpy.matrix([[1,3,1/3], [1/3,1,1/5],[3,5,1]])
matrixSafety = numpy.matrix([[1,5,1/5], [1/5,1,1/9],[5,9,1]])
matrixTrunk = numpy.matrix([[1,5,3], [1/5,1,1/2], [1/3,2,1]])
matrixPassengers = numpy.matrix([[1,1,3], [1,1,3], [1/3,1/3,1]])

matrixCost = numpy.matrix([[1,1/2],[2,1]])
matrixCapacity = numpy.matrix([[1,3], [1/3,1]])

matrixFinal = numpy.matrix([[1,1/3,2], [3,1,1/2], [1/2,2,1]])

wCost,vCost = get_weight(matrixCost)
wCapacity, vCapacity = get_weight(matrixCapacity)
wPrize,vPrize = get_weight(matrixPrize)
wFuel,vFuel = get_weight(matrixFuel)
wSafety,vSafety = get_weight(matrixSafety)
wTrunk,vTrunk = get_weight(matrixTrunk)
wPassengers,vPassengers = get_weight(matrixPassengers)

rankCost = normalize2Subcat(vPrize,vFuel,vCost)
rankCapacity = normalize2Subcat(vTrunk,vPassengers,vCapacity)

wC,vC = get_weight(matrixFinal)

R = numpy.concatenate([rankCost,vSafety,rankCapacity]).reshape(3,3).transpose()*vC

print('vector Prize',vPrize)
print('vector Fuel',vFuel)
print('vector Cost',rankCost)
print('vector Safety',vSafety)
print('vector Trunk',vTrunk)
print('vector Passengers',vPassengers)
print('vector Capacity',rankCapacity)
print('vector Matrix Final',vC)
print('rank',R)


'''
Utility functions.
'''

import numpy as np
from functools import partial

def fwht(x):
    """Recursive implementation of the 1D Cooley-Tukey FFT"""
    # x = np.asarray(x, dtype=float)
    N = x.shape[0]
    if N == 1:
        return x
    else:
        X_even = fwht(x[0:(N//2)])
        X_odd = fwht(x[(N//2):])
        return np.concatenate([(X_even + X_odd),
                               (X_even - X_odd)])

def bin_to_dec(x):
    n = len(x)
    c = 2**(np.arange(n)[::-1])
    return c.dot(x).astype(np.int)

def dec_to_bin(x, num_bits):
    assert x < 2**num_bits, "number of bits are not enough"
    u = bin(x)[2:].zfill(num_bits)
    u = list(u)
    u = [int(i) for i in u]
    return np.array(u)

def binary_ints(m):
    '''
    Returns a matrix where row 'i' is dec_to_bin(i, m), for i from 0 to 2 ** m - 1.
    From https://stackoverflow.com/questions/28111051/create-a-matrix-of-binary-representation-of-numbers-in-python.
    '''
    a = np.arange(2 ** m, dtype=int)[np.newaxis,:]
    b = np.arange(m, dtype=int)[::-1,np.newaxis]
    return np.array(a & 2**b > 0, dtype=int)

def base_ints(q, m):
    '''
    Returns a matrix where row 'i' is the base-q representation of i, for i from 0 to q ** m - 1.
    Covers the functionality of binary_ints when n = 2, but binary_ints is faster for that case.
    '''
    get_row = lambda i: np.array([int(j) for j in np.base_repr(i, base=q).zfill(m)])
    return np.vstack((get_row(i) for i in range(q ** m)))

def polymod(p1, p2, q, m):
    '''
    Computes p1 modulo p2, and takes the coefficients modulo q.
    '''
    p1 = np.trim_zeros(p1, trim='f')
    p2 = np.trim_zeros(p2, trim='f')
    while len(p1) >= len(p2) and len(p1) > 0:
        p1 -= p1[0] // p2[0] * np.pad(p2, (0, len(p1) - len(p2)))
        p1 = np.trim_zeros(p1, trim='f')
    return np.pad(np.mod(p1, q), (m + 1 - len(p1), 0))

def rref(A, b, q):
    '''
    Row reduction, to easily solve finite field systems.
    '''
    raise NotImplementedError()

def sign(x):
    '''
    Replacement for np.sign that matches the convention (footnote 2 on page 11).
    '''
    return (1 - np.sign(x)) // 2

def flip(x):
    '''
    Flip all bits in the binary array x.
    '''
    return np.bitwise_xor(x, 1)


import math

def entropy(dist):
    """Returns the entropy of a counter.

    If used by a decision tree learning algorithm, the goal is to minimize the entropy inside each
    leaf.

    Parameters:
        counts (dict)

    Example:

        >>> from creme import proba
        >>> from scipy import stats

        >>> events = [
        ...     'sunny', 'sunny',
        ...     'rainy', 'rainy', 'rainy',
        ...     'snowy', 'snomy', 'snowy', 'snomy', 'snowy'
        ... ]

        >>> dist = proba.Multinomial()

        >>> for e in events:
        ...     dist = dist.update(e)

        >>> entropy(dist)
        1.970950...

        >>> stats.entropy([dist.pmf(c) for c in dist], base=2)
        1.970950...

    References:
        1. `A Simple Explanation of Information Gain and Entropy <https://victorzhou.com/blog/information-gain/>`_
        2. `Calculating entropy <https://www.johndcook.com/blog/2013/08/17/calculating-entropy/>`_

    """
    return -sum(dist.pmf(c) * math.log2(dist.pmf(c)) for c in dist)

def gini(dist):
    """Returns the Gini impurity of a counter.

    If used by a decision tree learning algorithm, the goal is to minimize the Gini impurity inside
    each leaf.

    Parameters:
        dist (proba.Multinomial)

    Example:

        >>> from creme import proba
        >>> from scipy import stats

        >>> events = [
        ...     'sunny', 'sunny',
        ...     'rainy', 'rainy', 'rainy',
        ...     'snowy', 'snomy', 'snowy', 'snomy', 'snowy'
        ... ]

        >>> dist = proba.Multinomial()

        >>> for e in events:
        ...     dist = dist.update(e)

        >>> gini(dist)
        0.74

    References:
        1. `A Simple Explanation of Gini Impurity <https://victorzhou.com/blog/gini-impurity/>`_

    """
    return sum(dist.pmf(c) * (1 - dist.pmf(c)) for c in dist)


import numpy as np                      #for arrays and matrices
import matplotlib.pyplot as plt         #for ploting
import scipy                            #for math calculations
from mpl_toolkits.mplot3d import Axes3D #for 3D ploting
import math                             #for math constants
from matplotlib import collections as matcoll
from scipy import signal                #for signal analysis
from scipy.fftpack import fft           #for fourier spectrum

########### QUESTION 1 #########

##PARAMETERS
Fm = 3000 #kHz
Tm = 1 / 3000 #sec
A = 1 #V
AM = 3
N_periods = 4 #periods displayed

#INITIAL SIGNAL
t = np.linspace(0, N_periods*Tm, 8000+1)
y = A*np.cos(2*math.pi*Fm*t)*np.cos(2*math.pi*(AM+2)*Fm*t)
plt.ticklabel_format(style='sci', axis='x', scilimits=(0,0)) #for automatic x-scale (10^-3)
plt.plot(t, y, '-', marker="", markersize=4) #for markers
plt.show()

#Α ΕΡΏΤΗΜΑ
#version 1
def sign_sampling(coeff):
    t_freq = np.linspace(0, N_periods*Tm, N_periods*coeff+1)
    y_freq = A*np.cos(2*math.pi*Fm*t_freq)*np.cos(2*math.pi*(AM+2)*Fm*t_freq)
    plt.vlines(t_freq, [0], y_freq, linewidth=0.3)
    plt.ticklabel_format(style='sci', axis='x', scilimits=(0,0)) #for automatic x-scale (10^-3)
    plt.plot(t_freq, y_freq, '.', marker=".", markersize=4)
    # plt.plot(t, y, '-', t_freq, y_freq, '.', N_periods*Tm, y[0])
    plt.show()

#version 2
def sign_sampling_2(coeff):
    t_freq = np.linspace(0, N_periods*Tm, N_periods*coeff)
    y_freq = A*np.cos(2*math.pi*Fm*t_freq)*np.cos(2*math.pi*(AM+2)*Fm*t_freq)
    plt.plot(t, y, '-', t_freq, y_freq, '.', N_periods*Tm, y[0])
    plt.show()

#Ι) -> fs1=20fm
##### version 1 #####
# t_20 = np.linspace(0, 4/3000, 80, endpoint=False)
# y_20 = signal.resample(y, 80)
# plt.vlines(t_20, [0], y_20, linewidth=0.3)
# plt.plot(t_20, y_20, '.', marker=".", markersize=4)
# plt.show()

##### version 2 #####
# t_20 = np.linspace(0,4/3000,80)
# y_20 = A*np.cos(2*math.pi*3000*t_20)*np.cos(2*math.pi*(AM+2)*3000*t_20)
# plt.vlines(t_20, [0], y_20, linewidth=0.3)
# plt.plot(t_20, y_20, '.', marker=".", markersize=4)
# plt.show()

##### version 3 #####
sign_sampling(20)

##### version 4 #####
# t = np.arange(0, 4/3000, 1/(100*3000))
# y = A*np.cos(2*math.pi*3000*t)*np.cos(2*math.pi*(AM+2)*3000*t)
# plt.stem(t, y, use_line_collection=True)
# plt.show()

#ΙΙ) -> fs2=100fm
##### version 1 #####
# t_100 = np.linspace(0, 4/3000, 400, endpoint=False)
# y_100 = signal.resample(y, 400)
# plt.vlines(t_100, [0], y_100, linewidth=0.3)
# plt.plot(t_100, y_100, '.', marker=".", markersize=4)
# plt.show()

##### version 2 #####
# t_100 = np.linspace(0,4/3000,400)
# y_100 = A*np.cos(2*math.pi*3000*t_100)*np.cos(2*math.pi*(AM+2)*3000*t_100)
# plt.vlines(t_100, [0], y, linewidth=0.3)
# plt.plot(t_100, y_100, '.', marker=".", markersize=4)
# plt.show()

##### version 3 #####
sign_sampling(100)

#III)
t_20 = np.linspace(0, N_periods*Tm, 80+1)
y_20 = A*np.cos(2*math.pi*Fm*t_20)*np.cos(2*math.pi*(AM+2)*Fm*t_20)
plt.vlines(t_20, [0], y_20, linewidth=0.8, colors="b")

t_100 = np.linspace(0, N_periods/Fm, 400+1)
y_100 = A*np.cos(2*math.pi*Fm*t_100)*np.cos(2*math.pi*(AM+2)*Fm*t_100)
plt.vlines(t_100, [0], y_100, linewidth=0.3)

plt.plot(t_20, y_20, '.', t_100, y_100, '.', 4/3000, y[0])
plt.legend(['fs1', 'fs2'])
plt.show()

#B ΕΡΏΤΗΜΑ
# t_5 = np.linspace(0,4/3000,20)
# y_5 = A*np.cos(2*math.pi*3000*t_5)*np.cos(2*math.pi*(AM+2)*3000*t_5)
# plt.vlines(t_5, [0], y_5, linewidth=0.3)
# plt.plot(t_5, y_5, '.', marker=".", markersize=4)
# plt.show()
sign_sampling(5)

#fourier spectrum
signal_fft = fftpack.fft(y)
Amplitude = np.abs(signal_fft)
Power = Amplitude**2
Angle = np.angle(signal_fft)
sample_freq = fftpack.fftfreq(y.size, d=Tm/8001)
Amp_freq = np.array([Amplitude, sample_freq])

plt.plot(abs(signal_fft), "o")
plt.show()

plt.subplot(2, 1, 1)
plt.plot(t, y)

t = np.linspace(0, N_periods*Tm, 8000+1)
y = A*np.cos(2*math.pi*Fm*t)*np.cos(2*math.pi*(AM+2)*Fm*t)
yf = fft(y)
tf = np.linspace(0.0, 1.0/(2.0*Tm), 8001//2)

plt.plot(tf, 2.0/8001 * np.abs(yf[0:8001//2]))
plt.grid()
plt.show()

from scipy.fft import fft
# Number of sample points
N = 600
# sample spacing
T = 1.0 / 800.0
x = np.linspace(0.0, N*T, N)
y = A*np.cos(2*math.pi*Fm*x)*np.cos(2*math.pi*(AM+2)*Fm*x)
yf = fft(y)
xf = np.linspace(0.0, 1.0/(2.0*T), N//2)
import matplotlib.pyplot as plt
plt.plot(xf, 2.0/N * np.abs(yf[0:N//2]))
plt.grid()
plt.show()

########### FINAL #######
#number of samples
N = 100*4
# sample spacing
T = 1.0 / (100.0*3000.0)
x = np.linspace(0.0, N*T, N)
y = np.cos(3000.0 * 2.0*np.pi*x)*np.cos(5.0*3000.0 * 2.0*np.pi*x)
plt.subplot(2, 1, 1)
plt.ticklabel_format(style='sci', axis='x', scilimits=(0,0)) #for automatic x-scale (10^-3)
plt.plot(x, y)
yf = fft(y)
xf = fftpack.fftfreq(N, T)
xf = fftpack.fftshift(xf)
yplot = fftpack.fftshift(yf)
plt.subplot(2, 1, 2)
plt.xlim(0, 8*3000)
plt.xticks(np.arange(0, 11*3000, step=3000))
plt.plot(xf, 4.0/N * np.abs(yplot))
plt.grid()
plt.show()


"""
Adapting Euler method to handle 2nd order ODEs
Srayan Gangopadhyay
2020-05-16
"""

import numpy as np
import matplotlib.pyplot as plt

"""
y' = dy/dx
For a function of form y'' = f(x, y, y')
Define y' = v so y'' = v'
"""

def func(y, v, x):  # RHS of v' = in terms of y, v, x
    return x + v - 3*y

# PARAMETERS
y0 = 1  # y(x=0) =
v0 = -2  # y'(x=0) =
delta = 0.01  # step size
end = 4  # x-value to stop integration

steps = int(end/delta) + 1  # number of steps
x = np.linspace(0, end, steps)  # array of x-values (discrete time)
y = np.zeros(steps)  # empty array for solution
v = np.zeros(steps)
y[0] = y0  # inserting initial value
v[0] = v0

# INTEGRATING
for i in range(1, steps):
    v[i] = v[i-1] + (delta*func(y[i-1], v[i-1], x[i-1]))
    y[i] = y[i-1] + (delta*v[i-1])

plt.plot(x, y, label='Approx. soln (Euler)')
plt.plot(x, y, 'o')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.show()