# -*- coding: utf-8 -*-

import numpy as np
import scipy.stats as sts
from tqdm import tqdm
import os
# from pywt import WaveletPacket
epsn = 1e-8

# =================== STATISTICAL FEATURES IN TIME DOMAIN =====================
def mean_fea(a):
	return np.mean(a)

def rms_fea(a):
	return np.sqrt(np.mean(np.square(a)))

def sr_fea(a):
	return np.square(np.mean(np.sqrt(np.abs(a))))

def am_fea(a):
	return np.mean(np.abs(a))

def skew_fea(a):
	return np.mean((a-mean_fea(a))**3)

def kurt_fea(a):
	return np.mean((a-mean_fea(a))**4)

def max_fea(a):
	return np.max(a)

def min_fea(a):
	return np.min(a)

def pp_fea(a):
	return max_fea(a)-min_fea(a)

def var_fea(a):
	n = len(a)
	return np.sum((a-mean_fea(a))**2)/(n-1)

def waveform_index(a):
	return rms_fea(a)/(am_fea(a)+epsn)

def peak_index(a):
	return max_fea(a)/(rms_fea(a)+epsn)

def impluse_factor(a):
	return max_fea(a)/(am_fea(a)+epsn)

def tolerance_index(a):
	return max_fea(a)/(sr_fea(a)+epsn)

def skew_index(a):
	n = len(a)
	temp1 = np.sum((a-mean_fea(a))**3)
	temp2 = (np.sqrt(var_fea(a)))**3
	return temp1/((n-1)*temp2)

def kurt_index(a):
	n = len(a)
	temp1 = np.sum((a-mean_fea(a))**4)
	temp2 = (np.sqrt(var_fea(a)))**4
	return temp1/((n-1)*temp2)
# ============================= END ======================================
# def wave_fea(a):
# 	wp = WaveletPacket(a,'db1', maxlevel=8)
# 	nodes = wp.get_level(8, "freq")
# 	return np.linalg.norm(np.array([n.data for n in nodes]), 2)

# =============== STATISTICAL FEATURES IN TIME DOMAIN =======================
# def fft_fft(sequence_data):
# 	fft_trans = np.abs(np.fft.fft(sequence_data))
# 	dc = fft_trans[0]
# 	freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
# 	freq_sum_ = np.sum(freq_spectrum)
# 	return dc, freq_spectrum, freq_sum_

def fft_mean(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	return np.mean(freq_spectrum)

def fft_var(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	return np.var(freq_spectrum)

# def fft_std(sequence_data):
# 	def fft_fft(sequence_data):
# 		fft_trans = np.abs(np.fft.fft(sequence_data))
# 		# dc = fft_trans[0]
# 		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
# 		_freq_sum_ = np.sum(freq_spectrum)
# 		return freq_spectrum, _freq_sum_
# 	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
# 	return np.std(freq_spectrum)

def fft_entropy(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	pr_freq = freq_spectrum * 1.0 / _freq_sum_
	entropy = -1 * np.sum([np.log2(p+1e-5) * p for p in pr_freq])
	return entropy

def fft_energy(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	return np.sum(freq_spectrum ** 2) / len(freq_spectrum)

def fft_skew(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	_fft_mean, _fft_std = fft_mean(sequence_data), fft_std(sequence_data)
	return np.mean([0 if _fft_std < epsn else np.power((x - _fft_mean) / _fft_std, 3)
					for x in freq_spectrum])

def fft_kurt(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	_fft_mean, _fft_std = fft_mean(sequence_data), fft_std(sequence_data)
	return np.mean([0 if _fft_std < epsn else np.power((x - _fft_mean) / _fft_std, 4)
					for x in freq_spectrum])

def fft_shape_mean(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	shape_sum = np.sum([x * freq_spectrum[x]
						for x in range(len(freq_spectrum))])
	return 0 if _freq_sum_ < epsn else shape_sum * 1.0 / _freq_sum_

def fft_shape_std(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	shape_mean = fft_shape_mean(sequence_data)
	var = np.sum([0 if _freq_sum_ < epsn else np.power((x - shape_mean), 2) * freq_spectrum[x]
				  for x in range(len(freq_spectrum))]) / _freq_sum_
	return np.sqrt(var)

def fft_shape_skew(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	shape_mean = fft_shape_mean(sequence_data)
	return np.sum([np.power((x - shape_mean), 3) * freq_spectrum[x]
				   for x in range(len(freq_spectrum))]) / _freq_sum_

def fft_shape_kurt(sequence_data):
	def fft_fft(sequence_data):
		fft_trans = np.abs(np.fft.fft(sequence_data))
		# dc = fft_trans[0]
		freq_spectrum = fft_trans[1:int(np.floor(len(sequence_data) * 1.0 / 2)) + 1]
		_freq_sum_ = np.sum(freq_spectrum)
		return freq_spectrum, _freq_sum_
	freq_spectrum, _freq_sum_ = fft_fft(sequence_data)
	shape_mean = fft_shape_mean(sequence_data)
	return np.sum([np.power((x - shape_mean), 4) * freq_spectrum[x] - 3
				   for x in range(len(freq_spectrum))]) / _freq_sum_
# =================================== END =====================================


#!/usr/bin/env python

"""
This script tries to discover the secret behind the Winograd algorithm.
"""

import numpy as np

def FIR(x, b):
    """
    Standard FIR algorithm implementation.

    :param x: an input array
    :param b: a coefficient array of length r
    :return: FIR result
    """
    r = len(b)
    y = np.zeros(len(x) - r + 1)
    for i in range(len(y)):
        y[i] = np.sum(x[i:i+r] * b)
    return y

def winograd_FIR(x, b, p = 2):
    """
    Winograd FIR algorithm implementation.

    :param x: an input array
    :param b: a coefficient array of length r
    :param p: Winograd matrix, number of rows
    :return: FIR result
    """

    r = len(b)
    n = len(x)
    y = np.zeros(n - r + 1)
    m = np.zeros(r + p - 1)

    assert p == 2, "only support p = 2"
    assert len(y) % p == 0, "len(y) should be divisible by p"

    num_tiles = int(len(y) / p)

    for t in range(num_tiles):
        i = t * p
        d = x[i:i+len(m)]

        m[0] = (d[0] - d[2]) * b[0]
        m[1] = (d[1] + d[2]) * (b[0] + b[1] + b[2]) / 2
        m[2] = (d[2] - d[1]) * (b[0] - b[1] + b[2]) / 2
        m[3] = (d[1] - d[3]) * b[2]

        y[i] = m[0] + m[1] + m[2]
        y[i + 1] = m[1] - m[2] - m[3]

    return y

def winograd_FIR_mm(x, b, p = 2):
    """
    Winograd FIR implemented by matrix multiplication.

    :param x: an input array
    :param b: a coefficient array of length r
    :param p: Winograd matrix, number of rows
    :return: FIR result
    """

    B = np.array([[1, 0, -1, 0], [0, 1, 1, 0], [0, -1, 1, 0], [0, 1, 0, -1]]).T
    G = np.array([[1, 0, 0], [0.5, 0.5, 0.5], [0.5, -0.5, 0.5], [0, 0, 1]])
    A = np.array([[1, 1, 1, 0], [0, 1, -1, -1]]).T

    r = len(b)
    n = len(x)
    y = np.zeros(n - r + 1)
    m = np.zeros(r + p - 1)

    assert p == 2, "only support p = 2"
    assert len(y) % p == 0, "len(y) should be divisible by p"

    num_tiles = int(len(y) / p)

    for t in range(num_tiles):
        i = t * p
        d = x[i:i+len(m)]
        g = b

        y[i:i+2] = np.dot(A.T, np.dot(G, g) * np.dot(B.T, d))

    return y

if __name__ == '__main__':
    n = 16
    r = 3
    x = np.random.random(n)
    b = np.random.random(r)

    assert np.isclose(FIR(x, b), winograd_FIR(x, b)).all(), \
           "FIR and winograd should match"
    assert np.isclose(FIR(x, b), winograd_FIR_mm(x, b)).all(), \
           "FIR and winograd (MM) should match"

    print("FIR PASSED")


import numpy as np

def amean(*args):
    return sum(args)/len(args)
def gmean(*args):
    return np.array(args).prod()**(1.0/len(args))
def hmean(*args):
    return len(args)*(1.0/sum(np.array(args)**(-1)))
def median(*args):
    l = np.array(args)
    l=np.sort(l)
    leng = len(l)
    if leng%2 ==1:
        return l[int((leng+1)/2)-1]
    else:
        lf = np.floor(leng/2)
        lf = int(lf)
        return (l[lf-1]+l[lf])/2

def riesum(func,strt,stp):
    dx = 1e-6
    val = np.arange(strt,stp,dx)
    return sum(func(val))*dx
def derivative(func,at):
    d = 1e-7
    dm = -1*d
    t1 = (func(at+d)-func(at))/d
    t2 = (func(at+dm)-func(at))/dm
    return amean(t1,t2)


import pandas as pd
import numpy as np
import scipy.optimize as opt

def loglike(beta, Y, X):
    """ Log likelihood function.

    Parameters
    ----------
    beta: matrix.
        N x 1 matrix of the coefficients.
        N is the number of observations.

    Y : matrix
        N x 1 matrix of the dependent variable.
        N is the number of observations.

    X : matrix
        N x K matrix of the independent variables.
        N is the number of observations.
        K is the number of independent variables.

    Returns
    -------
    logvalue : matrix
        Log likelihood function
    """

    logvalue = - (Y[None, :] @ X @ beta - np.sum(np.log(1 + np.exp(X @ beta))))

    return(logvalue[0])

def inverselogit(beta, Y, X):
    """ Inverse of logit function.

    Parameters
    ----------
    beta: matrix.
        N x 1 matrix of the coefficients.
        N is the number of observations.

    Y : matrix
        N x 1 matrix of the dependent variable.
        N is the number of observations.

    X : matrix
        N x K matrix of the independent variables.
        N is the number of observations.
        K is the number of independent variables.

    Returns
    -------
    inverse : matrix
        Inverse of logit function
    """

    inverse = 1 / (1 + np.exp(-X @ beta))

    return(inverse)

def dlogit(beta, Y, X):
    """ Derivative of logit function.

    Parameters
    ----------
    beta: matrix.
        N x 1 matrix of the coefficients.
        N is the number of observations.

    Y : matrix
        N x 1 matrix of the dependent variable.
        N is the number of observations.

    X : matrix
        N x K matrix of the independent variables.
        N is the number of observations.
        K is the number of independent variables.

    Returns
    -------
    dlogit : matrix
        Derivative of the logit function
    """

    dlogit = sum(inverselogit(beta, Y, X)*(1 - inverselogit(beta, Y, X)))

    return(dlogit)

def logithessian(beta, Y, X):
    """ Hessian matrix of logit function.

    Parameters
    ----------
    beta: matrix.
        N x 1 matrix of the coefficients.
        N is the number of observations.

    Y : matrix
        N x 1 matrix of the dependent variable.
        N is the number of observations.

    X : matrix
        N x K matrix of the independent variables.
        N is the number of observations.
        K is the number of independent variables.

    Returns
    -------
    hessian : matrix
        Hessian matrix
    """

    dp = np.diag(inverselogit(beta, Y, X)*(1 - inverselogit(beta, Y, X)))
    hessian = X.T @ dp @ X

    return(hessian)

def logit(Y, X):

    """ Logistic regression of Y on X.

    Parameters
    ----------
    Y : matrix
        N x 1 matrix of the dependent variable.
        N is the number of observations.

    X : matrix
        N x K matrix of the independent variables.
        N is the number of observations.
        K is the number of independent variables.

    Returns
    -------
    (coef, test_status)

    coef : matrix
        K x 1 matrix of coefficients

    test_status : string
        Status of the optimization: Success or False

    """

    # Initial beta values
    k = np.size(X, 1)
    beta0 = np.zeros(k)

    # Optimization
    beta = opt.minimize(
        loglike, beta0, args = (Y, X), method = "BFGS", hess = logithessian
    )

    # Coefficients
    coef = beta.x
    test_status = beta.success

    # Standard errors
    cov = -dlogit(coef, Y, X)
    se = np.sqrt(cov.diagonal().T)

    # Return
    return (coef, se, test_status)


import numpy as np

import pandas as pd

class Perceptron(object):

    def __init__(self, no_of_inputs, threshold=100, learning_rate=0.01):
        self.threshold = threshold
        self.learning_rate = learning_rate
        self.weights = np.zeros(no_of_inputs)
        self.bias = 0

    def predict(self, inputs):
        sum = np.dot(inputs, self.weights) + self.bias  # dot product matrx multiplication bias add
        if sum > 0:
            activation = 1
        else:
            activation = 0
        return activation

    def train(self, train_data, labels):
        for _ in range(self.threshold):
            for inputs, label in zip(train_data, labels):
                prediction = self.predict(inputs)
                self.bias += self.learning_rate * (label - prediction)
                self.weights += self.learning_rate * (label - prediction) * inputs

                #leru type of sigmoid function.
                #to predict either 1/0.

# read and  modify data
data = pd.read_csv(r'C:\Users\ALPARSLAN\Desktop\perceptron.csv')
data = data[:]
data[4] = np.where(data.iloc[:, -1] == 'Iris-setosa', 0, 1)
train_input = data.iloc[:, 0:4].values
labels = data.iloc[:, -1].values
# activate the perceptron
perceptron = Perceptron(4)
# train the perceptron
perceptron.train(train_input, labels)

# test the perceptron
test_data = pd.read_csv(r'C:\Users\ALPARSLAN\Desktop\perceptron.test.csv')

test_data = test_data[:]
test_data[4] = np.where(test_data.iloc[:, -1] == 'Iris-setosa', 0, 1)

test_data_input = test_data.iloc[:, 0:4].values

# count number of total missclassified
miss_classified = 0
miss_classified_list = []
test_data.iat[0, 5]
for i in range(0, test_data_input.shape[0]):
    test_prediction1 = perceptron.predict(test_data_input[i])
    miss_classified_list.append(test_prediction1)
    if test_prediction1 != test_data.iat[i, 5]:
        miss_classified = miss_classified + 1
accuracy = (1 - (miss_classified / test_data.shape[0])) * 100
print("Accuracy: ", accuracy)  # Accuracy is here giving 100%, with the test data as provided in Assignment.

# take a input from user (UI input of Data)
listFromUser = [0, 0, 0, 0]
for i in range(0, 4):
    userData = float(input('enter i th value :  '))
    listFromUser[i] = userData
print('the data you have Provided is\n=> ', listFromUser)

output_prediction = perceptron.predict(listFromUser)
if output_prediction == 0:
    print('According to the input you have given, : Iris-setosa')
elif output_prediction == 1:
    print('According to the input you have given : Iris-versicolor')


import math
from typing import List, Optional

import numpy as np

from ._math import lcm
from ._overrides import set_module
from ._prime import factors

__all__ = [
    "totatives", "euler_phi", "carmichael_lambda", "is_cyclic",
    "is_primitive_root", "primitive_root", "primitive_roots",
]

@set_module("galois")
def totatives(n: int) -> List[int]:
    r"""
    Returns the positive integers (totatives) in :math:`[1, n)` that are coprime to :math:`n`.

    The totatives of :math:`n` form the multiplicative group :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}`.

    Parameters
    ----------
    n : int
        A positive integer.

    Returns
    -------
    list
        The totatives of :math:`n`.

    References
    ----------
    * Section 2.4.3 from https://cacr.uwaterloo.ca/hac/about/chap2.pdf
    * https://oeis.org/A000010

    Examples
    --------
    .. ipython:: python

        n = 20
        totatives = galois.totatives(n); totatives
        phi = galois.euler_phi(n); phi
        len(totatives) == phi
    """
    if not isinstance(n, (int, np.integer)):
        raise TypeError(f"Argument `n` must be an integer, not {type(n)}.")
    if not n > 0:
        raise ValueError(f"Argument `n` must be a positive integer, not {n}.")

    if n == 1:
        return [0]
    else:
        return [t for t in range(1, n) if math.gcd(n, t) == 1]

@set_module("galois")
def euler_phi(n: int) -> int:
    r"""
    Counts the positive integers (totatives) in :math:`[1, n)` that are coprime to :math:`n`.

    Parameters
    ----------
    n : int
        A positive integer.

    Returns
    -------
    int
        The number of totatives that are coprime to :math:`n`.

    Notes
    -----
    This function implements the Euler totient function

    .. math:: \phi(n) = n \prod_{p\ |\ n} \bigg(1 - \frac{1}{p}\bigg) = \prod_{i=1}^{k} p_i^{e_i-1} \big(p_i - 1\big)

    for prime :math:`p` and the prime factorization :math:`n = p_1^{e_1} \dots p_k^{e_k}`.

    References
    ----------
    * Section 2.4.1 from https://cacr.uwaterloo.ca/hac/about/chap2.pdf
    * https://oeis.org/A000010

    Examples
    --------
    .. ipython:: python

        n = 20
        phi = galois.euler_phi(n); phi

        # Find the totatives that are coprime with n
        totatives = [k for k in range(n) if math.gcd(k, n) == 1]; totatives

        # The number of totatives is phi
        len(totatives) == phi

        # For prime n, ϕ(n) = n - 1
        galois.euler_phi(13)
    """
    if not isinstance(n, (int, np.integer)):
        raise TypeError(f"Argument `n` must be an integer, not {type(n)}.")
    if not n > 0:
        raise ValueError(f"Argument `n` must be a positive integer, not {n}.")

    if n == 1:
        return 1

    p, e = factors(n)

    phi = 1
    for pi, ei in zip(p, e):
        phi *= pi**(ei - 1) * (pi - 1)

    return phi

@set_module("galois")
def carmichael_lambda(n: int) -> int:
    r"""
    Finds the smallest positive integer :math:`m` such that :math:`a^m \equiv 1\ (\textrm{mod}\ n)` for
    every integer :math:`a` in :math:`[1, n]` that is coprime to :math:`n`.

    This function implements the Carmichael function :math:`\lambda(n)`.

    Parameters
    ----------
    n : int
        A positive integer.

    Returns
    -------
    int
        The smallest positive integer :math:`m` such that :math:`a^m \equiv 1 (\textrm{mod}\ n)` for
        every :math:`a` in :math:`[1, n]` that is coprime to :math:`n`.

    References
    ----------
    * https://oeis.org/A002322

    Examples
    --------
    The Carmichael lambda function and Euler totient function are often equal. However, there are notable exceptions.

    .. ipython:: python

        [galois.euler_phi(n) for n in range(1, 20)]
        [galois.carmichael_lambda(n) for n in range(1, 20)]

    For prime :math:`n`, :math:`\phi(n) = \lambda(n) = n - 1`. And for most composite :math:`n`, :math:`\phi(n) = \lambda(n) < n - 1`.

    .. ipython:: python

        n = 9
        phi = galois.euler_phi(n); phi
        lambda_ = galois.carmichael_lambda(n); lambda_
        totatives = galois.totatives(n); totatives

        for power in range(1, phi + 1):
            y = [pow(a, power, n) for a in totatives]
            print("Power {}: {} (mod {})".format(power, y, n))

        galois.is_cyclic(n)

    When :math:`\phi(n) \ne \lambda(n)`, the multiplicative group :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is not cyclic.
    See :func:`galois.is_cyclic`.

    .. ipython:: python

        n = 8
        phi = galois.euler_phi(n); phi
        lambda_ = galois.carmichael_lambda(n); lambda_
        totatives = galois.totatives(n); totatives

        for power in range(1, phi + 1):
            y = [pow(a, power, n) for a in totatives]
            print("Power {}: {} (mod {})".format(power, y, n))

        galois.is_cyclic(n)
    """
    if not isinstance(n, (int, np.integer)):
        raise TypeError(f"Argument `n` must be an integer, not {type(n)}.")
    if not n > 0:
        raise ValueError(f"Argument `n` must be a positive integer, not {n}.")

    if n == 1:
        return 1

    p, e = factors(n)

    lambdas = []
    for i in range(len(p)):
        # Carmichael function for prime powers
        if p[i] == 2 and e[i] > 2:
            l = euler_phi(p[i]**e[i]) // 2
        else:
            l = euler_phi(p[i]**e[i])
        lambdas.append(l)

    return lcm(*lambdas)

@set_module("galois")
def is_cyclic(n: int) -> bool:
    r"""
    Determines whether the multiplicative group :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic.

    Parameters
    ----------
    n : int
        A positive integer.

    Returns
    -------
    bool
        `True` if the multiplicative group :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic.

    Notes
    -----
    The multiplicative group :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is the set of positive integers :math:`1 \le a < n`
    that are coprime with :math:`n`. :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` being cyclic means that some primitive root of :math:`n`,
    or generator, :math:`g` can generate the group :math:`\{g^0, g^1, g^2, \dots, g^{\phi(n)-1}\}`, where
    :math:`\phi(n)` is Euler's totient function and calculates the order of the group. If :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic,
    the number of primitive roots is found by :math:`\phi(\phi(n))`.

    :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is *cyclic* if and only if :math:`n` is :math:`2`, :math:`4`, :math:`p^k`, or :math:`2p^k`,
    where :math:`p` is an odd prime and :math:`k` is a positive integer.

    Examples
    --------
    The elements of :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` are the positive integers less than :math:`n` that are coprime with :math:`n`.
    For example, :math:`(\mathbb{Z}/14\mathbb{Z}){^\times} = \{1, 3, 5, 9, 11, 13\}`.

    .. ipython:: python

        # n is of type 2*p^e, which is cyclic
        n = 14
        galois.is_cyclic(n)
        Znx = set(galois.totatives(n)); Znx
        phi = galois.euler_phi(n); phi
        len(Znx) == phi

        # The primitive roots are the elements in Znx that multiplicatively generate the group
        for a in Znx:
            span = set([pow(a, i, n) for i in range(1, phi + 1)])
            primitive_root = galois.is_primitive_root(a, n)
            print("Element: {:2d}, Span: {:<20}, Primitive root: {}".format(a, str(span), primitive_root))

        # Find the smallest primitive root
        galois.primitive_root(n)
        # Find all primitive roots
        roots = galois.primitive_roots(n); roots

        # Euler's totient function ϕ(ϕ(n)) counts the primitive roots of n
        len(roots) == galois.euler_phi(phi)

    A counterexample is :math:`n = 15 = 3 \cdot 5`, which doesn't fit the condition for cyclicness.
    :math:`(\mathbb{Z}/15\mathbb{Z}){^\times} = \{1, 2, 4, 7, 8, 11, 13, 14\}`. Since the group is not cyclic, it has no primitive roots.

    .. ipython:: python

        # n is of type p1^e1 * p2^e2, which is not cyclic
        n = 15
        galois.is_cyclic(n)
        Znx = set(galois.totatives(n)); Znx
        phi = galois.euler_phi(n); phi
        len(Znx) == phi

        # The primitive roots are the elements in Znx that multiplicatively generate the group
        for a in Znx:
            span = set([pow(a, i, n) for i in range(1, phi + 1)])
            primitive_root = galois.is_primitive_root(a, n)
            print("Element: {:2d}, Span: {:<13}, Primitive root: {}".format(a, str(span), primitive_root))

        # Find the smallest primitive root
        galois.primitive_root(n)
        # Find all primitive roots
        roots = galois.primitive_roots(n); roots

        # Note the max order of any element is 4, not 8, which is Carmichael's lambda function
        galois.carmichael_lambda(n)

    For prime :math:`n`, a primitive root modulo :math:`n` is also a primitive element of the Galois field :math:`\mathrm{GF}(n)`. A
    primitive element is a generator of the multiplicative group :math:`\mathrm{GF}(p)^{\times} = \{1, 2, \dots, p-1\} = \{g^0, g^1, g^2, \dots, g^{\phi(n)-1}\}`.

    .. ipython:: python

        # n is of type p, which is cyclic
        n = 7
        galois.is_cyclic(n)
        Znx = set(galois.totatives(n)); Znx
        phi = galois.euler_phi(n); phi
        len(Znx) == phi

        # The primitive roots are the elements in Znx that multiplicatively generate the group
        for a in Znx:
            span = set([pow(a, i, n) for i in range(1, phi + 1)])
            primitive_root = galois.is_primitive_root(a, n)
            print("Element: {:2d}, Span: {:<18}, Primitive root: {}".format(a, str(span), primitive_root))

        # Find the smallest primitive root
        galois.primitive_root(n)
        # Find all primitive roots
        roots = galois.primitive_roots(n); roots

        # Euler's totient function ϕ(ϕ(n)) counts the primitive roots of n
        len(roots) == galois.euler_phi(phi)
    """
    if not isinstance(n, (int, np.integer)):
        raise TypeError(f"Argument `n` must be an integer, not {type(n)}.")
    if not n > 0:
        raise ValueError(f"Argument `n` must be a positive integer, not {n}.")

    if n == 1:
        return True

    p, e = factors(n)

    if n in [2, 4]:
        return True
    elif len(p) == 2 and 2 in p and e[p.index(2)] == 1:
        # n = 2 * p^e
        return True
    elif len(p) == 1 and p[0] != 2:
        # n = p^e
        return True
    else:
        # n does not represent a cyclic group
        return False

@set_module("galois")
def is_primitive_root(g: int, n: int) -> bool:
    r"""
    Determines if :math:`g` is a primitive root modulo :math:`n`.

    Parameters
    ----------
    g : int
        A positive integer that may be a primitive root modulo :math:`n`.
    n : int
        A positive integer.

    Returns
    -------
    bool
        `True` if :math:`g` is a primitive root modulo :math:`n`.

    Notes
    -----
    :math:`g` is a primitive root if the totatives of :math:`n`, the positive integers :math:`1 \le a < n`
    that are coprime with :math:`n`, can be generated by powers of :math:`g`. Alternatively said, :math:`g` is a primitive root
    modulo :math:`n` if and only if :math:`g` is a generator of the multiplicative group of integers modulo :math:`n`,
    :math:`(\mathbb{Z}/n\mathbb{Z}){^\times} = \{g^0, g^1, g^2, \dots, g^{\phi(n)-1}\}` where :math:`\phi(n)` is order of the group.
    If :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic, the number of primitive roots modulo :math:`n` is given by :math:`\phi(\phi(n))`.

    Examples
    --------
    .. ipython:: python

        galois.is_primitive_root(2, 7)
        galois.is_primitive_root(3, 7)
        galois.primitive_roots(7)
    """
    if not isinstance(g, (int, np.integer)):
        raise TypeError(f"Argument `g` must be an integer, not {type(g)}.")
    if not isinstance(n, (int, np.integer)):
        raise TypeError(f"Argument `n` must be an integer, not {type(n)}.")
    if not n > 0:
        raise ValueError(f"Argument `n` must be a positive integer, not {n}.")
    if not 0 < g < n:
        raise ValueError(f"Argument `g` must be a positive integer less than `n`, not {g}.")

    if n == 2:
        # Euler totient of 2 is 1. We cannot compute the prime factorization of 1. There is only one
        # primitive root modulo 2 and it's 1.
        return g == 1

    phi = euler_phi(n)  # Number of non-zero elements in the multiplicative group Z/nZ
    primes, _ = factors(phi)

    return pow(g, phi, n) == 1 and all(pow(g, phi // p, n) != 1 for p in primes)

@set_module("galois")
def primitive_root(n: int, start: int = 1, stop: Optional[int] = None, reverse: bool = False) -> Optional[int]:
    r"""
    Finds the smallest primitive root modulo :math:`n`.

    Parameters
    ----------
    n : int
        A positive integer.
    start : int, optional
        Starting value (inclusive) in the search for a primitive root. The default is 1. The resulting primitive
        root, if found, will be :math:`\textrm{start} \le g < \textrm{stop}`.
    stop : int, optional
        Stopping value (exclusive) in the search for a primitive root. The default is `None` which corresponds to :math:`n`.
        The resulting primitive root, if found, will be :math:`\textrm{start} \le g < \textrm{stop}`.
    reverse : bool, optional
        Search for a primitive root in reverse order, i.e. find the largest primitive root first. Default is `False`.

    Returns
    -------
    None, int
        The smallest primitive root modulo :math:`n`. Returns `None` if no primitive roots exist.

    Notes
    -----
    :math:`g` is a primitive root if the totatives of :math:`n`, the positive integers :math:`1 \le a < n`
    that are coprime with :math:`n`, can be generated by powers of :math:`g`. Alternatively said, :math:`g` is a primitive root
    modulo :math:`n` if and only if :math:`g` is a generator of the multiplicative group of integers modulo :math:`n`,
    :math:`(\mathbb{Z}/n\mathbb{Z}){^\times} = \{g^0, g^1, g^2, \dots, g^{\phi(n)-1}\}` where :math:`\phi(n)` is order of the group.
    If :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic, the number of primitive roots modulo :math:`n` is given by :math:`\phi(\phi(n))`.

    References
    ----------
    * V. Shoup. Searching for primitive roots in finite fields. https://www.ams.org/journals/mcom/1992-58-197/S0025-5718-1992-1106981-9/S0025-5718-1992-1106981-9.pdf
    * L. K. Hua. On the least primitive root of a prime. https://www.ams.org/journals/bull/1942-48-10/S0002-9904-1942-07767-6/S0002-9904-1942-07767-6.pdf
    * http://www.numbertheory.org/courses/MP313/lectures/lecture7/page1.html

    Examples
    --------
    The elements of :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` are the positive integers less than :math:`n` that are coprime with :math:`n`.
    For example, :math:`(\mathbb{Z}/14\mathbb{Z}){^\times} = \{1, 3, 5, 9, 11, 13\}`.

    .. ipython:: python

        # n is of type 2*p^k, which is cyclic
        n = 14
        galois.is_cyclic(n)

        # The congruence class coprime with n
        Znx = set([a for a in range(1, n) if math.gcd(n, a) == 1]); Znx

        # Euler's totient function counts the "totatives", positive integers coprime with n
        phi = galois.euler_phi(n); phi

        len(Znx) == phi

        # The primitive roots are the elements in Znx that multiplicatively generate the group
        for a in Znx:
            span = set([pow(a, i, n) for i in range(1, phi + 1)])
            primitive_root = span == Znx
            print("Element: {:2d}, Span: {:<20}, Primitive root: {}".format(a, str(span), primitive_root))

        # Find the smallest primitive root
        galois.primitive_root(n)
        # Find all primitive roots
        roots = galois.primitive_roots(n); roots

        # Euler's totient function ϕ(ϕ(n)) counts the primitive roots of n
        len(roots) == galois.euler_phi(phi)

    A counterexample is :math:`n = 15 = 3 \cdot 5`, which doesn't fit the condition for cyclicness.
    :math:`(\mathbb{Z}/15\mathbb{Z}){^\times} = \{1, 2, 4, 7, 8, 11, 13, 14\}`.

    .. ipython:: python

        # n is of type p1^k1 * p2^k2, which is not cyclic
        n = 15
        galois.is_cyclic(n)

        # The congruence class coprime with n
        Znx = set([a for a in range(1, n) if math.gcd(n, a) == 1]); Znx

        # Euler's totient function counts the "totatives", positive integers coprime with n
        phi = galois.euler_phi(n); phi

        len(Znx) == phi

        # The primitive roots are the elements in Znx that multiplicatively generate the group
        for a in Znx:
            span = set([pow(a, i, n) for i in range(1, phi + 1)])
            primitive_root = span == Znx
            print("Element: {:2d}, Span: {:<13}, Primitive root: {}".format(a, str(span), primitive_root))

        # Find the smallest primitive root
        galois.primitive_root(n)
        # Find all primitive roots
        roots = galois.primitive_roots(n); roots

        # Note the max order of any element is 4, not 8, which is Carmichael's lambda function
        galois.carmichael_lambda(n)

    The algorithm is also efficient for very large :math:`n`.

    .. ipython:: python

        n = 1000000000000000035000061
        galois.primitive_root(n)
    """
    try:
        return next(_primitive_roots(n, start=start, stop=stop, reverse=reverse))
    except StopIteration:
        return None

@set_module("galois")
def primitive_roots(n: int, start: int = 1, stop: Optional[int] = None, reverse: bool = False) -> List[int]:
    r"""
    Finds all primitive roots modulo :math:`n`.

    Parameters
    ----------
    n : int
        A positive integer.
    start : int, optional
        Starting value (inclusive) in the search for a primitive root. The default is 1. The resulting primitive
        roots, if found, will be :math:`\textrm{start} \le x < \textrm{stop}`.
    stop : int, optional
        Stopping value (exclusive) in the search for a primitive root. The default is `None` which corresponds to `n`.
        The resulting primitive roots, if found, will be :math:`\textrm{start} \le x < \textrm{stop}`.
    reverse : bool, optional
        List all primitive roots in descending order, i.e. largest to smallest. Default is `False`.

    Returns
    -------
    list
        All the positive primitive :math:`n`-th roots of unity, :math:`x`.

    Notes
    -----
    :math:`g` is a primitive root if the totatives of :math:`n`, the positive integers :math:`1 \le a < n`
    that are coprime with :math:`n`, can be generated by powers of :math:`g`. Alternatively said, :math:`g` is a primitive root
    modulo :math:`n` if and only if :math:`g` is a generator of the multiplicative group of integers modulo :math:`n`,
    :math:`(\mathbb{Z}/n\mathbb{Z}){^\times} = \{g^0, g^1, g^2, \dots, g^{\phi(n)-1}\}` where :math:`\phi(n)` is order of the group.
    If :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` is cyclic, the number of primitive roots modulo :math:`n` is given by :math:`\phi(\phi(n))`.

    References
    ----------
    * V. Shoup. Searching for primitive roots in finite fields. https://www.ams.org/journals/mcom/1992-58-197/S0025-5718-1992-1106981-9/S0025-5718-1992-1106981-9.pdf
    * http://www.numbertheory.org/courses/MP313/lectures/lecture7/page1.html

    Examples
    --------
    The elements of :math:`(\mathbb{Z}/n\mathbb{Z}){^\times}` are the positive integers less than :math:`n` that are coprime with :math:`n`.
    For example, :math:`(\mathbb{Z}/14\mathbb{Z}){^\times} = \{1, 3, 5, 9, 11, 13\}`.

    .. ipython:: python

        # n is of type 2*p^k, which is cyclic
        n = 14
        galois.is_cyclic(n)

        # The congruence class coprime with n
        Znx = set([a for a in range(1, n) if math.gcd(n, a) == 1]); Znx

        # Euler's totient function counts the "totatives", positive integers coprime with n
        phi = galois.euler_phi(n); phi

        len(Znx) == phi

        # The primitive roots are the elements in Znx that multiplicatively generate the group
        for a in Znx:
            span = set([pow(a, i, n) for i in range(1, phi + 1)])
            primitive_root = span == Znx
            print("Element: {:2d}, Span: {:<20}, Primitive root: {}".format(a, str(span), primitive_root))

        # Find the smallest primitive root
        galois.primitive_root(n)
        # Find all primitive roots
        roots = galois.primitive_roots(n); roots

        # Euler's totient function ϕ(ϕ(n)) counts the primitive roots of n
        len(roots) == galois.euler_phi(phi)

    A counterexample is :math:`n = 15 = 3 \cdot 5`, which doesn't fit the condition for cyclicness.
    :math:`(\mathbb{Z}/15\mathbb{Z}){^\times} = \{1, 2, 4, 7, 8, 11, 13, 14\}`.

    .. ipython:: python

        # n is of type p1^k1 * p2^k2, which is not cyclic
        n = 15
        galois.is_cyclic(n)

        # The congruence class coprime with n
        Znx = set([a for a in range(1, n) if math.gcd(n, a) == 1]); Znx

        # Euler's totient function counts the "totatives", positive integers coprime with n
        phi = galois.euler_phi(n); phi

        len(Znx) == phi

        # The primitive roots are the elements in Znx that multiplicatively generate the group
        for a in Znx:
            span = set([pow(a, i, n) for i in range(1, phi + 1)])
            primitive_root = span == Znx
            print("Element: {:2d}, Span: {:<13}, Primitive root: {}".format(a, str(span), primitive_root))

        # Find the smallest primitive root
        galois.primitive_root(n)
        # Find all primitive roots
        roots = galois.primitive_roots(n); roots

        # Note the max order of any element is 4, not 8, which is Carmichael's lambda function
        galois.carmichael_lambda(n)
    """
    return list(_primitive_roots(n, start=start, stop=stop, reverse=reverse))

def _primitive_roots(n, start=1, stop=None, reverse=False):
    if n in [1, 2]:
        yield n - 1
        return

    stop = n if stop is None else stop
    if not isinstance(n, (int, np.integer)):
        raise TypeError(f"Argument `n` must be an integer, not {type(n)}.")
    if not isinstance(start, (int, np.integer)):
        raise TypeError(f"Argument `start` must be an integer, not {type(start)}.")
    if not isinstance(stop, (int, np.integer)):
        raise TypeError(f"Argument `stop` must be an integer, not {type(stop)}.")
    if not isinstance(reverse, bool):
        raise TypeError(f"Argument `reverse` must be a bool, not {type(reverse)}.")
    if not 1 <= start < stop <= n:
        raise ValueError(f"Arguments must satisfy `1 <= start < stop <= n`, not `1 <= {start} < {stop} <= {n}`.")

    if not is_cyclic(n):
        return

    n = int(n)  # Needed for the pow() function
    phi = euler_phi(n)  # Number of non-zero elements in the multiplicative group Z/nZ
    primes, _ = factors(phi)

    if phi == n - 1 or n % 2 == 1:
        # For prime n or odd n, must test all elements
        possible_roots = range(start, stop)
    else:
        # For even n, only have to test odd elements
        if start % 2 == 0:
            start += 1  # Make start odd
        possible_roots = range(start, stop, 2)

    if reverse:
        possible_roots = reversed(possible_roots)

    for r in possible_roots:
        if pow(r, phi, n) == 1 and all(pow(r, phi // p, n) != 1 for p in primes):
            yield r


import numpy as np
import matplotlib.pyplot as plt
import sys
from locale import atof

#Problème de Cauchy : (y0,f)
#Ordre des paramètres de f : t,y
#Les y seront des scalaires, ou en dimension supérieure, des arrays (et pas des listes)

#Calculs d'une valeur yp d'une fonction à partir de la valeur y précédente en fonction f de celle-ci, du point d'évaluation t, et du pas h par différentes méthodes

def step_euler(y,t,h,f):
	yp=y+h*f(t,y)
	return yp

def step_middle(y,t,h,f):
	y_half=y+(h/2.)*f(t,y)
	p=f(t+h/2.,y_half)
	yp=y+h*p
	return yp

def step_heun(y,t,h,f):
	p1=f(t,y)
	y2=y+h*p1
	p2=f(t+h,y2)
	yp=y+h*(p1+p2)/2.
	return yp

def step_rk4(y,t,h,f):
	p1=f(t,y)
	y2=y+(h/2.)*p1
	p2=f(t+h/2.,y2)
	y3=y+(h/2.)*p2
	p3=f(t+h/2.,y3)
	y4=y+h*p3
	p4=f(t+h,y4)
	yp=y+(h/6.)*(p1+2*p2+2*p3+p4)
	return yp

#Construit un tableau de n valeurs par appels successifs d'une méthode à un pas

def meth_n_step(y0,t0,N,h,f,meth):
	if (type(y0)) is np.ndarray:
		values=np.zeros((N,y0.shape[0]))
	else:
		values=np.zeros(N)
	t=t0
	values[0]=y0
	for i in range(1,N):
		t+=h
		values[i]=meth(values[i-1],t,h,f)
	return values

#Compare le maximum des différences entre les points communs de deux arrays y1 et y2 à un epsilon donné.

def step_compare(y1,y2,eps):
	y=np.zeros(len(y1))
	for i in range(len(y1)):
		if type(y1[0]) is np.ndarray:
			y[i]=np.linalg.norm(y1[i]-y2[2*i])
		else:
			y[i]=abs(y1[i]-y2[2*i])
	return max(y)>eps

#Calcule avec un N doublé à chaque itération N valeurs y sur l'intervalle |t0,tf] jusqu'à ce que le maximum des différences de deux itérations soit inférieur ou égal à epsilon.

def meth_epsilon(y0,t0,tf,eps,f,meth):
	N=10
	h=(tf-t0)/N
	y1=meth_n_step(y0,t0,N,h,f,meth)
	N*=2
	h=(tf-t0)/N
	y2=meth_n_step(y0,t0,N,h,f,meth)
	while(step_compare(y1,y2,eps)):
		N*=2
		h/=2.
		y1=y2
		y2=meth_n_step(y0,t0,N,h,f,meth)
	return y2

#Récupération des arguments (epsilon et choix de l'affichage d'erreur)

def opts():
	if len(sys.argv)==1:
		eps=1e-2
		print("Epsilon par défaut : ",eps)
		print("Pour une valeur autre : python 3", sys.argv[0]," eps avec epsilon le critère de convergence souhaité")
	else:
		eps=atof(sys.argv[1])
		print("Epsilon choisi : ",eps)
	error=input("Afficher le graphe d'erreur des méthodes ? Y/N ")
	if error=="Y" or error=="y":
		error=True
	elif error=="N" or error=="n":
		error=False
	else:
		print("Réponse invalide")
		exit()
	return eps,error

#Tracé des valeurs en fonction du temps (réelles et approchées), éventuellement des graphes d'erreur

def compare_plot(values,sol,start,end,title,error=False):
	X=np.linspace(start,end,len(values))
	c_values=[sol(X[i]) for i in range(len(X))]
	plt.clf()
	plt.plot(X,c_values,label="Solution réelle")
	plt.plot(X,values,label="Solution approchée")
	plt.legend()
	plt.xlabel("Temps")
	plt.ylabel("Valeurs des coordonnées")
	plt.title(title)
	plt.show()
	if error==True:
		plt.clf()
		errors=[abs(c_values[i]-values[i]) for i in range(len(X))]
		plt.plot(X,errors,label="Erreur sur l'intervalle")
		plt.legend()
		plt.xlabel("Temps")
		plt.ylabel("Erreur")
		plt.title("Graphe d'erreur")
		plt.show()

#Tracé d'un portrait de phase en deux dimensions

def phase_graph(values,sol,start,end,title):
	X=np.linspace(0,end,len(values))
	c_values=[sol(X[i]) for i in range(len(X))]
	values_x=[values[i][0] for i in range(len(values))]
	values_y=[values[i][1] for i in range(len(values))]
	c_values_x=[c_values[i][0] for i in range(len(values))]
	c_values_y=[c_values[i][1] for i in range(len(values))]
	plt.clf()
	plt.plot(values_x,values_y,label="Portrait de phase approché")
	plt.plot(c_values_x,c_values_y,label="Portrait de phase réel")
	plt.xlabel("Coordonnée X")
	plt.ylabel("Coordonnée Y")
	plt.legend()
	plt.title(title)
	plt.show()

#Tracé du champ des tangentes en dimension (dim) 1 ou 2, avec une abscisse X (coïncidant en dimension 1 avec les temps t), entre a et b sur les deux axes, et selon la fonction f. title est le titre du graphe produit.

def champ_tangentes(dim,a,b,K,f,title):
	N=K*1j

	Y, X = np.mgrid[a:b:N, a:b:N]
	if(dim==1):
		U=X
		V=f(X,Y)
	else:
		U,V=f(0,[X,Y])

	normes = np.sqrt(U**2 + V**2)
	UU = U/normes
	VV = V/normes
	plt.axis([a, b, a, b])

	plt.quiver(X, Y, UU, VV,headlength=5, width=0.002)
	plt.title(title)
	plt.show()

if __name__=="__main__":
	(eps,error)=opts()
	print("\nCas de test en dimension 1 : (1,y'=y/(1+t**2))")
	f=lambda t,y: y/(1+t**2) #Prudence : écrire des f compatibles avec des arrays
	title="Champ des tangentes avec f(t,y)=y/(1+t^2)"
	champ_tangentes(1,1e-15,5,20,f,title) #Pour ne pas donner zéro à la division...
	sol=lambda t: np.exp(np.arctan(t))
	title="Résultat avec méthode d'Euler"
	print(title)
	values=meth_epsilon(1,0,5,eps,f,step_euler)
	compare_plot(values,sol,0,5,title,error)
	title="Résultat avec méthode du point milieu"
	print(title)
	values=meth_epsilon(1,0,5,eps,f,step_middle)
	compare_plot(values,sol,0,5,title,error)
	title="Résultat avec méthode de Heun"
	print(title)
	values=meth_epsilon(1,0,5,eps,f,step_heun)
	compare_plot(values,sol,0,5,title,error)
	title="Résultat avec méthode de Runge-Kutta d'ordre 4"
	print(title)
	values=meth_epsilon(1,0,5,eps,f,step_rk4)
	compare_plot(values,sol,0,5,title,error)

	print("\nCas de test en dimension 2 : ((1,0),y'=(-y2,y1))")
	f=lambda t,y: np.array([-y[1],y[0]])
	title="Champ des tangentes avec f(t,Y)=(-Y[1],Y[0])"
	print(title)
	champ_tangentes(2,-2*np.pi,2*np.pi,20,f,title)
	sol=lambda t: [np.cos(t),np.sin(t)]
	title="Résultat avec méthode d'Euler"
	print(title)
	values=meth_epsilon(np.array([1,0]),0,2*np.pi,eps,f,step_euler)
	compare_plot(values,sol,0,2*np.pi,title,error)
	phase_graph(values,sol,0,2*np.pi,title)
	title="Résultat avec méthode du point milieu"
	print(title)
	values=meth_epsilon(np.array([1,0]),0,2*np.pi,eps,f,step_middle)
	compare_plot(values,sol,0,2*np.pi,title,error)
	phase_graph(values,sol,0,2*np.pi,title)
	title="Résultat avec méthode de Heun"
	print(title)
	values=meth_epsilon(np.array([1,0]),0,2*np.pi,eps,f,step_heun)
	compare_plot(values,sol,0,2*np.pi,title,error)
	phase_graph(values,sol,0,2*np.pi,title)
	title="Résultat avec méthode de Runge-Kutta d'ordre 4"
	print(title)
	values=meth_epsilon(np.array([1,0]),0,2*np.pi,eps,f,step_rk4)
	compare_plot(values,sol,0,2*np.pi,title,error)
	phase_graph(values,sol,0,2*np.pi,title)


# License: MIT

import numpy as np
from scipy import stats

def zero_one_normalization(X, lower=None, upper=None):
    if lower is None:
        lower = np.min(X, axis=0)
    if upper is None:
        upper = np.max(X, axis=0)

    X_normalized = np.true_divide((X - lower), (upper - lower))

    return X_normalized, lower, upper

def zero_one_unnormalization(X_normalized, lower, upper):
    return lower + (upper - lower) * X_normalized

def zero_mean_unit_var_normalization(X, mean=None, std=None):
    if mean is None:
        mean = np.mean(X, axis=0)
    if std is None:
        std = np.std(X, axis=0) + 1e-4

    X_normalized = (X - mean) / std

    return X_normalized, mean, std

def zero_mean_unit_var_unnormalization(X_normalized, mean, std):
    return X_normalized * std + mean

def bilog_transform(X: np.ndarray):
    """Magnify the difference between X and 0"""
    X = X.copy()
    idx = (X >= 0)
    X[idx] = np.log(1 + X[idx])
    X[~idx] = -np.log(1 - X[~idx])
    return X

def gaussian_transform(X: np.ndarray):
    """
    Transform data into Gaussian by applying psi = Phi^{-1} o F where F is the truncated ECDF.

    References:
    [1] Andrew Gordon Wilson and Zoubin Ghahramani. Copula processes.
        In Proceedings of the 23rd International Conference on Neural Information Processing
        Systems - Volume 2, NIPS’10, pages 2460–2468, USA, 2010. Curran Associates Inc.
    [2] Salinas, D.; Shen, H.; and Perrone, V. 2020.
        A Quantile-based Approach for Hyperparameter Transfer Learning.
        In International conference on machine learning, 7706–7716.
    """
    if X.ndim == 2:
        z = np.hstack([
            gaussian_transform(x.reshape(-1)).reshape(-1, 1)
            for x in np.hsplit(X, X.shape[1])
        ])
        return z
    assert X.ndim == 1

    def winsorized_delta(n):
        return 1.0 / (4.0 * n ** 0.25 * np.sqrt(np.pi * np.log(n)))

    def truncated_quantile(X):
        idx = np.argsort(X)
        rank = np.argsort(idx)
        quantile = rank / (X.shape[0] - 1)
        delta = winsorized_delta(X.shape[0])
        return np.clip(quantile, a_min=delta, a_max=1 - delta)

    return stats.norm.ppf(truncated_quantile(X))

_func_dict = {
    'bilog': bilog_transform,
    'gaussian': gaussian_transform,
    None: lambda x: x,
}

def get_transform_function(transform: str):
    if transform in _func_dict.keys():
        return _func_dict[transform]
    else:
        raise ValueError('Invalid transform: %s' % (transform, ))


import numpy as np
import torch
import torch.distributions as td

# multivariate gaussian distribution, represented by mean mu and cov matrix Sigma

# diagonal gaussian are a special case with diagonal covariance matrix (then represented by the diagonal vector)
mu = torch.Tensor([0.0, 0.0, 0.0])
stddev = torch.Tensor([1.0, 1.0, 1.0])

# common choice: represent std dev as log std dev
mu = torch.Tensor([0.0, 0.0, 0.0])
logstddev = torch.Tensor([0.0, 0.0, 0.0])  # in this way, it can be any real value

# normal
mu = torch.Tensor([0.0, 2.0])
logstddev = torch.Tensor([0.0, -1.0])  # in this way, it can be any real value
dist = td.Normal(loc=mu, scale=np.exp(logstddev))
samples = dist.sample(sample_shape=[10000])

import matplotlib.pyplot as plt

plt.hist(samples[:, 0])
plt.hist(samples[:, 1])
plt.show()

# pytorch accept Normal dist with negative std dev, however not sampling with neg stddev when using torch.normal(...)
plt.clf()
dist1 = td.Normal(loc=0, scale=-15)
dist2 = td.Normal(loc=0, scale=15)
samples1 = dist1.sample(sample_shape=[1000000])
samples2 = dist2.sample(sample_shape=[1000000])
bins = np.linspace(-100, 100, 100)
plt.hist(samples1, bins=bins)
plt.hist(samples2, bins=bins)
plt.show()

# log prob: computed vs analytical
mu = torch.Tensor([0.0])
logstddev = torch.Tensor([0.0])
dist = td.Normal(loc=mu, scale=np.exp(logstddev))
action = torch.tensor([0.0])
torch_logprob = dist.log_prob(action)

n_log_2pi = action.shape[0] * np.log(2 * np.pi)
my_logprob = -.5 * (sum(((action - mu) ** 2) / (np.exp(logstddev) ** 2) + 2 * logstddev) + n_log_2pi)
print(f"log prob = {torch_logprob} -> prob = {np.exp(torch_logprob)}")
print(f"my log prob = {my_logprob} -> my prob = {np.exp(my_logprob)}")


"""
  Name     : c12_36_terminal_stock_price_distribution.py
  Book     : Python for Finance (2nd ed.)
  Publisher: Packt Publishing Ltd.
  Author   : Yuxing Yan
  Date     : 6/6/2017
  email    : yany@canisius.edu
             paulyxy@hotmail.com
"""
import scipy as sp
import matplotlib.pyplot as plt
from scipy import zeros, sqrt, shape
#input area
S0 = 9.15               # stock price at time zero
T =1.                   # years
n_steps=100.            # number of steps
mu =0.15                # expected annual return
sigma = 0.2             # volatility (annual)
sp.random.seed(12345)   # fix those random numbers
n_simulation = 1000     # number of simulation
dt =T/n_steps
#
S = zeros([n_simulation], dtype=float)
x = range(0, int(n_steps), 1)
for j in range(0, n_simulation):
    tt=S0
    for i in x[:-1]:
        e=sp.random.normal()
        tt+=tt*(mu-0.5*pow(sigma,2))*dt+sigma*tt*sqrt(dt)*e;
        S[j]=tt
#
plt.title('Histogram of terminal price')
plt.ylabel('Number of frequencies')
plt.xlabel('Terminal price')
plt.figtext(0.5,0.8,'S0='+str(S0)+',mu='+str(mu)+',sigma='+str(sigma))
plt.figtext(0.5,0.76,'T='+str(T)+', steps='+str(int(n_steps)))
plt.figtext(0.5,0.72,'Number of terminal prices='+str(int(n_simulation)))
plt.hist(S)
plt.show()


import numpy as np
import scipy as sp
from numpy.core.umath_tests import inner1d

def is_symPD_svd(M, verbatim = False, sym_tol = 1E-5, psd_tol = 1E-5):
    """ Check if matrix is symmetric and positive definite up to some error using SVD """

    # Symmetric
    symdiff = abs(M - M.T).max()
    is_sym = symdiff < sym_tol

    # Eigenvalues
    eigvals = np.linalg.eigvals(M)
    eig_real_max, eig_real_min = eigvals.real.max(), eigvals.real.min()
    eig_imag_max, eig_imag_min = eigvals.imag.max(), eigvals.imag.min()

    # SVD
    # For M = U*S*V.T check if M = V*S*V.T also
    (u, s, vh) = sp.linalg.svd(M)
    vsvt = np.dot(vh.T * s, vh)
    svddiff = abs(M - vsvt).max()
    is_psd = svddiff < psd_tol

    if verbatim:
        if is_sym:
            print('Symmetric: error = {} < {}, OK'.format(symdiff, sym_tol))
        else:
            print('WARNING! Symmetric: error = {} > {}, NOT OK'.format(symdiff, sym_tol))

        print('Eigenvalues real part: min = {}, max = {}'.format(eig_real_min, eig_real_max))
        print('Eigenvalues imag part: min = {}, max = {}'.format(eig_imag_min, eig_imag_max))

        if is_psd:
            print('Postive definite, M - VSV.T where M = USV.T (SVD): error = {} < {}, OK'.format(svddiff, psd_tol))
        else:
            print('WARNING! Postive definite, M - VSV.T where M = USV.T (SVD): error = {} > {}, NOT OK'.format(svddiff, psd_tol))

    return is_sym & is_psd

def isPD_chol(B):
    """Returns true when input is positive-definite, via Cholesky"""
    try:
        _ = sp.linalg.cholesky(B)
        return True
    except sp.linalg.LinAlgError:
        return False

def try_jitchol(B):
    """Returns true AND cholesky factor when input is positive-definite, via Cholesky"""
    try:
        L = jitchol(B)
        return True, L
    except sp.linalg.LinAlgError:
        return False, None

def isPD_det(B):
    """Returns true when input is positive-definite, via Determinant"""
    if sp.linalg.det(B) <= 0: return False
    return True

def nearestPD(A, check = 'chol'):
    """Find the nearest positive-definite matrix to input

    A Python/Numpy port of John D'Errico's `nearestSPD` MATLAB code [1], which
    credits [2].

    [1] https://www.mathworks.com/matlabcentral/fileexchange/42885-nearestspd

    [2] N.J. Higham, "Computing a nearest symmetric positive semidefinite
    matrix" (1988): https://doi.org/10.1016/0024-3795(88)90223-6

    *** Modified code to include different checks for PD depending on use ***
    Can check using
    Cholesky    - check = 'chol'
    SVD         - check = 'svd'
    Determinant - check = 'det'
    *************************************************************************
    """

    assert check in ['chol', 'svd', 'det'], 'Unknown checking function'

    B = (A + A.T) / 2
    _, s, V = sp.linalg.svd(B)

    H = np.dot(V.T, np.dot(np.diag(s), V))

    A2 = (B + H) / 2

    A3 = (A2 + A2.T) / 2

    # Checking function
    if check == 'chol':
        isPD = isPD_chol
    elif check == 'svd':
        isPD = is_symPD_svd
    else:
        isPD = isPD_det

    if isPD(A3):
        return A3

    spacing = np.spacing(np.linalg.norm(A))
    I = np.eye(A.shape[0])
    k = 1
    while not isPD(A3):
        mineig = np.min(np.real(sp.linalg.eigvals(A3)))
        A3 += I * (-mineig * k**2 + spacing)
        k += 1

    return A3

def jitchol(A, maxtries = 5):
    """ Cholesky with jitter """
    A = np.ascontiguousarray(A)
    L, info = sp.linalg.lapack.dpotrf(A, lower=1)
    if info == 0:
        return L
    else:
        diagA = np.diag(A)
        if np.any(diagA <= 0.):
            raise sp.linalg.LinAlgError("not pd: non-positive diagonal elements")
        jitter = diagA.mean() * 1e-6
        num_tries = 1
        while num_tries <= maxtries and np.isfinite(jitter):
            try:
                L = sp.linalg.cholesky(A + np.eye(A.shape[0]) * jitter, lower=True)
                print('(!chol jitter added : '+ jitter + ')', end = '')
                return L
            except:
                jitter *= 10
            finally:
                num_tries += 1
        raise sp.linalg.LinAlgError("not positive definite, even with jitter.")
    import traceback
    try: raise
    except:
        print('\n'.join(['Added jitter of {:.10e}'.format(jitter),
            '  in '+traceback.format_list(traceback.extract_stack(limit=3)[-2:-1])[0][2:]]))
    return L

def triang_solve(A, B, lower = True, trans = False, unitdiag = False):
    """
    Wrapper for lapack dtrtrs function
    DTRTRS solves a triangular system of the form
        A * X = B  or  A**T * X = B,
    where A is a triangular matrix of order N, and B is an N-by-NRHS
    matrix.  A check is made to verify that A is nonsingular.
    :param A: Matrix A(triangular)
    :param B: Matrix B
    :param lower: is matrix lower (true) or upper (false)
    :param trans: calculate A**T * X = B (true) or A * X = B (false)

    :returns: Solution to A * X = B or A**T * X = B
    """

    lower_num = 1 if lower else 0
    trans_num = 1 if trans else 0
    unitdiag_num = 1 if unitdiag else 0

    A = np.asfortranarray(A)
    #Note: B does not seem to need to be F ordered!
    return np.matrix(sp.linalg.lapack.dtrtrs(A, B, lower=lower_num, trans=trans_num, unitdiag=unitdiag_num)[0])

def mulinv_solve(F, B, triang = True):
    """
    Returns C = A^{-1} * B where A = F*F^{T}

    triang = True -> when F is LOWER triangular. This gives faster calculation

    """
    if triang:
        tmp = triang_solve(F, B) # F*tmp = B
        C = triang_solve(F, tmp, trans = True) # F.T*C = tmp

    else:
        tmp = np.matrix(sp.linalg.solve(F, B)) # F*tmp = B
        C = np.matrix(sp.linalg.solve(F.T, tmp)) # F.T*C = tmp

    return C

def mulinv_solve_rev(F, B, triang = True):
    """
    Reversed version of mulinv_solve

    Returns C = B * A^{-1} where A = F*F^{T}

    triang = True -> when F is LOWER triangular. This gives faster calculation

    """
    return mulinv_solve(F, B.T, triang).T

def symmetrify(A, upper=False):
    """ Create symmetric matrix from triangular matrix """
    triu = np.triu_indices_from(A,k=1)
    if upper:
        A.T[triu] = A[triu]
    else:
        A[triu] = A.T[triu]

def chol_inv(L):
    """
    Return inverse of matrix A = L*L.T where L is lower triangular
    Uses LAPACK function dpotri
    """
    A_inv, info = sp.linalg.lapack.dpotri(L, lower=1)
    A_inv = np.matrix(A_inv)
    symmetrify(A_inv)
    return A_inv

def traceprod(A, B):
    """
    Calculate trace(A*B) for two matrices A and B
    """
    return np.sum(np.core.umath_tests.inner1d(np.array(A), np.array(B).T))


# 二行データ記述統計
def describe2(data1, data2):
    # 必要なライブラリをインポート
    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt

    # 元データ出力
    print(data1, data2, sep="\n")

    # サンプルサイズ
    def length(data):
        count = 0
        for d in data:
            count += 1
        return count

    # 合計値
    def sum_value(data):
        sum_value = 0
        for d in data:
            sum_value += d
        return sum_value

    # 算術平均
    def mean(data):
        return sum_value(data) / length(data)

    # 幾何平均
    # 比率や割合等について適用する
    # 0以下の値が含まれていた場合、規格外としてErrorを出す
    def geometric_mean(data):
        try:
            ds = 1
            n = length(data)
            for d in data:
                ds *= d
            return (ds)**(1/n)
        except:
            return 'Error'

    # 調和平均
    # 時速の平均等に適用する
    # 0以下の値が含まれていた場合、規格外としてErrorを出す
    def harmonic_mean(data):
        try:
            ds = 0
            n = length(data)
            for d in data:
                ds += 1/d
            return 1 / ((1/n) * ds)
        except:
            return 'Error'

    # 平均偏差
    def meand(data):
        n = length(data)
        m = mean(data)
        s = 0
        for d in data:
            s += abs(d - m)
        return s / n

    # 母分散
    def var_p(data):
        var_p = 0
        m = mean(data)
        for d in data:
            var_p += (d - m)**2
        return var_p / length(data)

    # 不偏分散
    def var_s(data):
        var_s = 0
        m = mean(data)
        for d in data:
            var_s += (d - m)**2
        return var_s / (length(data)-1)

    # 母標準偏差
    def std_p(data):
        return var_p(data)**0.5

    # 不偏標準偏差..........................未完成？
    # 単に不偏分散の平方根を取ったものは、厳密には「母標準偏差の不偏推定量」ではないらしい
    def std_s(data):
        return var_s(data)**0.5

    # 標準誤差
    def std_e(data):
        return std_s(data) / length(data)**0.5

    # 母平均の95%信頼区間(母分散既知)
    def mean_95cl_known(data):
        n = length(data)
        m = mean(data)
        s = std_p(data)
        return "({:.2f}, {:.2f})".format(m-1.96*(s/n)**0.5, m+1.96*(s/n)**0.5)

    # ソート（クイックソート）
    def quick_sort(data):
        n = length(data)
        pivot = data[n//2]
        left, middle, right = [], [], []
        for i in range(n):
            if data[i] < pivot:
                left = left + [data[i]]
            elif pivot < data[i]:
                right = right + [data[i]]
            else:
                middle = middle + [data[i]]
        if left:
            left = quick_sort(left)
        if right:
            right = quick_sort(right)
        return left + middle + right

    # 最小値
    def min_value(data):
        data_sorted = quick_sort(data)
        return data_sorted[0]

    # 最大値
    def max_value(data):
        data_sorted = quick_sort(data)
        return data_sorted[length(data)-1]

    # 第一四分位数
    def quartile1(data):
        data_sorted = quick_sort(data)
        q1 = length(data) * (1/4)
        return data_sorted[int(q1)]

    # 中央値
    def median(data):
        data_sorted = quick_sort(data)
        q2 = length(data) / 2
        if q2%1 != 0:
            return data_sorted[int(q2)]
        else:
            return (data_sorted[int(q2)] + data_sorted[int(q2)+1]) / 2

    # 第三四分位数
    def quartile3(data):
        data_sorted = quick_sort(data)
        q3 = length(data) * (3/4)
        return data_sorted[int(q3)]

    # 四分位偏差
    def quartile_range(data):
        return quartile3(data) - quartile1(data)

    # ミッドレンジ
    def mid_range(data):
        return (max_value(data) + min_value(data)) / 2

    # レンジ
    def all_range(data):
        return max_value(data) - min_value(data)

    # 最頻値
    def mode(data):
        # 個数を数える
        data_sorted = quick_sort(data)
        discoverd = {}
        for d in data_sorted:
            if d not in discoverd.keys():
                discoverd[d] = 1
            else:
                discoverd[d] += 1

        # 個数が最大のデータを検索する(複数個ある場合は全て出力する)
        discoverd_keys, discoverd_values = list(discoverd.keys()), list(discoverd.values())
        max_cnt = max_value(discoverd_values)
        mode = []
        for i, value in enumerate(discoverd_values):
            if max_cnt == value:
                mode += [discoverd_keys[i]]

        return str(mode)

    # 変動係数(coefficient of variance)
    # 単位が無く、直接の比較が困難な場合に平均を考慮した上での比率の比較ができる
    # 比率の比較なので、比例尺度のみ使用できる
    # 標準偏差は母分散を用いて計算する(統計学入門のp38式より)
    def cov(data):
        return std_p(data) / mean(data)

    # ジニ係数(gini coefficient)
    # 不平等度の指標として用いられる
    # 面積を表すので普通は負の値を取らないが、定義通りに計算して出力する。
    def gini(data):
        n = length(data)
        m = mean(data)
        s = 0
        for d1 in data:
            for d2 in data:
                s += abs(d1 - d2)
        return s / (2 * n**2 * m)

    # 歪度(skewness)
    # 母集団の分布の非対称性の程度・方向性を示す推定統計量
    # 値が正なら右の裾が長く、負なら左の裾が長い分布となっている
    def skewness(data):
        n = length(data)
        m = mean(data)
        s = std_s(data)

        three = 0
        for d in data:
            three += ((d-m) / s)**3

        return (n/((n-1)*(n-2))) * three

    # 尖度(kurtosis)
    # 母集団の分布の中心周囲部分の尖り具合を表す推定統計量
    # 値が正なら正規分布より尖っており、負なら正規分布より丸く鈍い形をしている
    def kurtosis(data):
        n = length(data)
        m = mean(data)
        s = std_s(data)

        four = 0
        for d in data:
            four += (d-m)**4 / s**4

        return ((n*(n+1))/((n-1)*(n-2)*(n-3))) * four - (3*(n-1)**2)/((n-2)*(n-3))

    # ジャック-ベラ検定(Jarque–Bera test)
    # 標本が正規分布に従っているかどうかを検定する
    # 帰無仮説：標本が正規分布に従う
    # 対立仮説：標本が正規分布に従わない
    # ソース：https://ja.wikipedia.org/wiki/ジャック–ベラ検定
    def jarque_bera(data):
        n = length(data)
        m = mean(data)

        s2, s3, s4 = 0, 0, 0
        for d in data:
            s2 += (d - m)**2
            s3 += (d - m)**3
            s4 += (d - m)**4

        # 標本歪度
        S = (s3/n) / (s2/n)**(3/2)
        # 標本尖度
        K = (s4/n) / (s2/n)**2

        JB = (n/6) * (S**2 + (1/4)*(K-3)**2)
        return JB

    # 母共分散
    # サンプルサイズが異なる場合、Errorを返す
    def cov_p(data1, data2):
        if length(data1) != length(data2): return 'Error'
        m1 = mean(data1)
        m2 = mean(data2)
        cov_p = 0
        for d1, d2 in zip(data1, data2):
            cov_p += (d1-m1) * (d2-m2)
        return cov_p / length(data1)

    # 不偏共分散
    # サンプルサイズが異なる場合、Errorを返す
    def cov_s(data1, data2):
        if length(data1) != length(data2): return 'Error'
        m1 = mean(data1)
        m2 = mean(data2)
        cov_s = 0
        for d1, d2 in zip(data1, data2):
            cov_s += (d1-m1) * (d2-m2)
        return cov_s / (length(data1)-1)

    # ピアソンの積率相関係数(Pearson product-moment correlation coefficient)
    # サンプルサイズが異なる場合、Errorを返す
    def pearson_cor(data1, data2):
        if length(data1) != length(data2): return 'Error'
        return cov_s(data1, data2) / (std_s(data1)*std_s(data2))

    # ピアソン無相関検定(検定統計量t)
    # 検定統計量は自由度n-2のt分布に従う
    # 帰無仮説：母集団のピアソン相関係数は0である(相関はない)
    # 対立仮説：母集団のピアソン相関係数は0以外である(相関がある)
    # 注：値がマイナスになるが、両側検定だから絶対値で考えてよい
    # サンプルサイズが異なる場合、Errorを返す
    def pearson_cor_test(data1, data2):
        cor = pearson_cor(data1, data2)
        if cor == 'Error':
            return 'Error'
        elif cor > 0:
            return (cor*((length(data1)-2)**0.5)) / ((1-(cor**2))**0.5)
        else:
            return (-cor*(length(data1)-2)**0.5) / ((1-cor**2)**0.5)

    # リストのコピー生成
    def copy(data):
        copyed = []
        for d in data:
            copyed += [d]
        return copyed

    # ランク付け
    # 同順位は平均順位をつけ、同順位の数も返り値で返す
    # 引数deliteはウィルコクソンの符号順位検定にて順位の整合性を合わせる為に指定した
    def rank(data, delite=0):
        ranking = 0
        rank_num_list = []
        l = length(data) - delite
        reference, ranked = copy(data), copy(data)
        for d in data:
            count, rank_sum, rank_num = 0, 0, 0
            count_list = []
            max_val = max_value(reference)
            if max_val == -999999: break
            for d in data:
                if max_val == reference[count]:
                    reference[count] = -999999
                    count_list += [count]
                    rank_sum += l - ranking
                    rank_num += 1
                    ranking += 1
                count += 1
            for c in count_list:
                ranked[c] = rank_sum / rank_num
            if rank_num != 0:
                rank_num_list += [rank_num]
        return ranked, rank_num_list

    # スピアマンの順位相関係数(Spearman's rank correlation coefficient)........................ちょっとだけズレがある
    # サンプルサイズが異なる場合、Errorを返す
    def spearman_cor(data1, data2):
        n = length(data1)
        if n != length(data2): return 'Error'
        rank1, a = rank(data1)
        rank2, b = rank(data2)
        spearman = 0
        for d1, d2 in zip(rank1, rank2):
            spearman += (d1 - d2)**2
        return 1 - ((6*spearman) / (n*(n**2-1)))

    # スピアマン無相関検定(検定統計量t)
    # 検定統計量は自由度n-2のt分布に従う
    # 帰無仮説：母集団のスピアマン順位相関係数は0である(相関はない)
    # 対立仮説：母集団のスピアマン順位相関係数は0以外である(相関がある)
    # サンプルサイズが異なる場合、Errorを返す
    def spearman_cor_test(data1, data2):
        spearman =  spearman_cor(data1, data2)
        if spearman == 'Error': return 'Error'
        return spearman * ((length(data1)-1)/(1-spearman**2))**0.5

    # ケンドールの順位相関係数(Kendall's rank correlation coefficient)
    # サンプルサイズが異なる場合、Errorを返す
    # ソース：https://manabitimes.jp/math/1286
    # ソース2：https://bellcurve.jp/statistics/glossary/1307.html
    def kendall_cor(data1, data2):
        n1, n2 = length(data1), length(data2)
        if n1 != n2: return 'Error'

        plus, minus, zero1, zero2 = 0, 0, 0, 0
        for i in range(n1-1):
            for j in range(i+1, n2):
                d = (data1[i]-data1[j])*(data2[i]-data2[j])
                if d == 0:
                    if data1[i] == data1[j]: zero1 += 1
                    else: zero2 += 1
                elif d > 0: plus += 1
                else: minus += 1

        N = (n1*(n1-1)) / 2
        T = (plus - minus) / ((N-zero1)**0.5 * (N-zero2)**0.5)
        return T

    # ケンドール無相関検定(検定統計量z)
    # 検定統計量は標準正規分布に従う
    # サンプルサイズが異なる場合、Errorを返す
    # ソース：https://oceanone.hatenablog.com/entry/2020/04/28/022222
    def kendall_cor_test(data1, data2):
        kendall = kendall_cor(data1, data2)
        if kendall == 'Error': return 'Error'
        n = length(data1)
        return kendall / ((2*(2*n+5)) / (9*n*(n-1)))**0.5

    # 単回帰(simple regression)
    # data1を独立変数、data2を従属変数として、最小二乗法を用いて直線への当てはめを行う
    # y = bx + aとした時、bを回帰係数、aをy切片と呼び、これらを求める
    # また、独立変数が従属変数を決定する度合いとして決定係数も計算する(ピアソンの積率相関係数の二乗)
    # サンプルサイズが異なる場合、Errorを返す
    def simple_regression(data1, data2):
        nx, ny = length(data1), length(data2)
        if nx!=ny: return np.nan, np.nan, 'Error'
        mx, my = mean(data1), mean(data2)
        sx2 = sum_value(data1**2)
        sxy = 0
        for x, y in zip(data1, data2):
            sxy += x * y

        b = (sxy - nx*mx*my) / (sx2 - nx*mx**2)
        a = my - b*mx
        r2 = pearson_cor(data1, data2)**2
        return b, a, r2

    # 単回帰係数の無相関検定(検定統計量t)
    # 単回帰における回帰係数が0でない(=きちんと影響がある)事を検定する
    # 検定統計量は自由度n-2のt分布に従う
    # 帰無仮説：回帰係数が0である
    # 対立仮説：回帰係数が0ではない
    # サンプルサイズが異なる場合、Errorを返す
    def simple_regression_test(data1, data2):
        b, a, r2 = simple_regression(data1, data2)
        if r2 == 'Error': return np.nan, np.nan

        n, mx = length(data1), mean(data1)
        xx_, yy_ = 0, 0
        for d1, d2 in zip(data1, data2):
            xx_ += (d1 - mx)**2
            yy_ += (d2 - (b*d1 + a))**2

        return (b*(xx_)**0.5) / (yy_/(n-2))**0.5, n-2

    # 等分散の検定(F比)
    # 帰無仮説：2群間の母分散に差がない(等分散である)
    # 対立仮説：2群間の母分散に差がある(等分散でない)
    def f_test(data1, data2):
        s1, s2 = var_s(data1), var_s(data2)
        return (s1 / s2) if (s1 < s2) else (s2 / s1), "({}, {})".format(length(data1)-1, length(data2)-1)

    # 効果量1(Cohen's d)
    # 対応なし2標本t検定において、それがどの程度の効果を持つのかという指標
    # 目安...0.2:小、0.5:中、0.8:大
    # ソースでは母分散を使用していたが、母集団を推定するという役割を考え、自分の関数は普遍分散を使用している
    # ソース：https://bellcurve.jp/statistics/course/12765.html
    def ind_cohen_d(data1, data2):
        n1, n2 = length(data1), length(data2)
        bool_s = ((n1*var_s(data1) + n2*var_s(data2)) / (n1 + n2))**0.5
        return (mean(data1) - mean(data2)) / bool_s

    # 対応なし2標本t検定(Student t-test)
    # サンプルサイズの合計が100未満の時、検定統計量Tは自由度n1+n2-1のt分布に従う
    # サンプルサイズの合計が100以上の時、検定統計量zは標準正規分布表を参照して
    # 帰無仮説：2群間の母平均値に差がない(母平均値が等しい)
    # 対立仮説：2群間の母平均値に差がある(母平均値が異なる)
    def independent_ttest(data1, data2):
        n1, n2 = length(data1), length(data2)
        m1, m2 = mean(data1), mean(data2)
        s1, s2 = std_s(data1), std_s(data2)

        s = ((n1-1)*s1**2 + (n2-1)*s2**2) / (n1+n2-2)
        return (m1-m2) / (s * (1/n1 + 1/n2))**0.5, (n1+n2-1)

    # z検定(対応なし)
    # 2標本の母平均値に差があるかを検定する
    # 母分散が既知、またはサンプルサイズの合計が100以上の時、検定統計量zは標準正規分布表を参照して検定する
    # 帰無仮説：2群間の母平均値に差がない(母平均値が等しい)
    # 対立仮説：2群間の母平均値に差がある(母平均値が異なる)
    def independent_ztest(data1, data2):
        n1, n2 = length(data1), length(data2)
        m1, m2 = mean(data1), mean(data2)
        s1, s2 = std_p(data1), std_p(data2)

        z = (m1-m2) / ((s1/n1) + (s2/n2))**0.5
        return z

    # 効果量2(Cohen's d)
    # 対応あり2標本t検定において、それがどの程度の効果を持つのかという指標
    # 目安...0.2:小、0.5:中、0.8:大
    # サンプルサイズが異なる場合、Errorを返す
    # ソース：https://bookdown.org/sbtseiji/lswjamoviJ/ch-ttest.html#sec:cohensd
    def dep_cohen_d(data1, data2):
        if length(data1) != length(data2): return 'Error'
        diff = data1 - data2
        return mean(diff) / std_s(diff)

    # 対応あり2標本t検定(Paired Samples t-test)
    # 自由度n1-1のt分布に従う
    # 帰無仮説：2群間の母平均値に差がない(母平均値が等しい)
    # 対立仮説：2群間の母平均値に差がある(母平均値が異なる)
    # サンプルサイズが異なる場合、Errorを返す
    def dependent_ttest(data1, data2):
        if length(data1) != length(data2): return 'Error', 'Error'
        diff = data1 - data2
        n = length(diff)
        m = mean(diff)
        s = var_s(diff)
        return m / (s/n)**0.5, (n-1)

    # z検定(対応あり)
    # 2標本の母平均値に差があるかを検定する
    # 母分散が既知、またはサンプルサイズの合計が100以上の時、検定統計量zは標準正規分布表を参照して検定する
    # 帰無仮説：2群間の母平均値に差がない(母平均値が等しい)
    # 対立仮説：2群間の母平均値に差がある(母平均値が異なる)
    # サンプルサイズが異なる場合、Errorを返す
    def dependent_ztest(data1, data2):
        if length(data1) != length(data2): return 'Error'
        diff = data1 - data2
        n = length(diff)
        m = mean(diff)
        s = var_p(diff)

        d = sum_value(diff) / n
        return d / (s/n)**0.5

    # ウェルチのt検定(Welch t-test)
    # 対応なし2標本t検定であるが、こちらは等分散を仮定できない際に使用する
    # 一説によれば、標本が等分散かどうかによらず利用しても問題ないらしい
    # ウェルチ=サタスウェイト（Welch=Satterthwaite）の式により近似自由度vのt分布に従う
    # 帰無仮説：2群間の母平均値に差がない(母平均値が等しい)
    # 対立仮説：2群間の母平均値に差がある(母平均値が異なる)
    def welch_ttest(data1, data2):
        n1, n2 = length(data1), length(data2)
        m1, m2 = mean(data1), mean(data2)
        s1, s2 = var_s(data1), var_s(data2)
        t = (m1-m2) / ((s1/n1) + (s2/n2))**0.5
        v = int(((s1/n1) + (s2/n2))**2 / (s1**2/(n1**2*(n1-1)) + s2**2/(n2**2*(n2-1))))
        return t, v

    # 全データ同一ランク付け
    def rank_all(data1, data2):
        data_linked = list(data1) + list(data2)
        data_linked, dup = rank(data_linked)
        data1_len = length(data1)
        data1_dec = data_linked[:data1_len]
        data2_dec = data_linked[data1_len:]
        return data1_dec, data2_dec

    # ムード検定(Mood test)
    # 2標本の中央値を比較する
    # サンプルサイズが小さい時(20未満)はムード検定表を参照し、大きい時(20以上)は標準正規分布表を参照する。
    # 帰無仮説：2群間の母集団の中央値に差がない(母集団の中央値が等しい)
    # 対立仮説：2群間の母集団の中央値に差がある(母集団の中央値が異なる)
    def mood_test(data1, data2):
        m, n = length(data1), length(data2)
        N = m + n
        data_ranked = quick_sort(rank_all(data1, data2)[0])

        # サンプルサイズが小さい時の検定統計量(20未満)
        MD = 0
        for R in data_ranked:
            MD += (R - ((N+1)/2))**2

        # サンプルサイズが大きい時の検定統計量(20以上)
        E = (m*(N**2 - 1)) / 12
        V = (m*n*(N+1)*(N**2 - 4)) / 180

        return (MD - E) / V**0.5 if N>=20 else MD

    # マン=ホイットニーのU検定(Mann-Whitney u-test)
    # 対応のない2群間の中央値に差があるかを検定する
    # サンプルサイズが小さい時(n<=20)、検定統計量UはMann-Whitney検定表に基づいてp値を算出する
    # サンプルサイズが十分に大きい時(n>20)、検定統計量zは標準正規分布に従うかどうかを考え、正規分布表に基づいてp値を算出する
    # 注意：通常とは違い、検定統計量 < Mann-Whitney検定表の棄却域の時に帰無仮説を棄却する
    # 両側検定なので、検定統計量が負の値でも絶対値を取って考えれば良い
    # 帰無仮説：2群間の母集団は同じである
    # 対立仮説：2群間の母集団は異なる
    def mannwhitney_utest(data1, data2):
        n1, n2 = length(data1), length(data2)
        r1, r2 = rank_all(data1, data2)
        r1, r2 = sum_value(r1), sum_value(r2)
        # u1, u2の求め方はどちらでも良い
        #u1 = n1*n2 + ((n1*(n1+1)) / 2) - r1
        #u2 = n1*n2 + ((n2*(n2+1)) / 2) - r2
        u1 = r1 - (n1*(n1+1))/2
        u2 = r2 - (n2*(n2+1))/2
        U = u1 if (u1 < u2) else u2
        if (n1 <= 20 and n2 <= 20):
            return U
        else:
            z = (U - ((n1*n2)/2)) / (((n1*n2*(n1+n2+1)) / 12)**0.5)
            return z

    # ウィルコクソンの順位和検定(Wilcoxon rank sum test)
    # 対応のない2群間の中央値に差があるかを検定する
    # ウィルコクソンの順位和検定の数表を参照して有意差があるかどうか判定する
    # 数表から読み取ったa/bという表示は、検定統計量がa以下またはb以上の時に帰無仮説を棄却できるという意味である
    # マン=ホイットニーのU検定と実質的に同じ計算をしているため、同じ結果が返ってくるはずである
    # 帰無仮説：2群間の母集団は同じである
    # 対立仮説：2群間の母集団は異なる
    def wilcoxon_rstest(data1, data2):
        n1, n2 = length(data1), length(data2)
        r1, r2 = rank_all(data1, data2)
        r1, r2 = sum_value(r1), sum_value(r2)
        if r1 < r2:
            w = r1
            ew = (n1*(n1+n2+1)) / 2
        else:
            w = r2
            ew = (n2*(n1+n2+1)) / 2

        vw = (n1*n2*(n1+n2+1)) / 12
        return (ew-w) / (vw)**0.5

    # 符号検定(Sign test)
    # 対応のある2群間の中央値に差があるかを検定する(質的データである名義・順位尺度にも使える)
    # サンプルサイズが25以下の場合、二項定理から直接確率(p値)を計算する
    # サンプルサイズが25より大きい場合、標準正規分布表からp値を読み取る
    # 帰無仮説：2組の標本の中央値に差はない
    # 対立仮説：2組の標本の中央値に差がある
    # ソース：https://kusuri-jouhou.com/statistics/fugou.html
    def sign_test(data1, data2):
        n1, n2 = length(data1), length(data2)
        if n1 != n2: return 'Error'

        sign_data = data1 - data2
        plus, minus = 0, 0
        for d in sign_data:
            if d == 0: continue
            elif d > 0: plus += 1
            else: minus += 1
        n = plus + minus
        sign = plus if plus < minus else minus
        c = 0.5 if sign < (n/2) else -0.5

        if n <= 25:
            p = 1
            for i in range(n): p *= 0.5
            C = p
            for r in range(1, sign+1):
                nn, over, under = n, 1, 1
                for j in range(1, r+1):
                    over *= nn
                    under *= j
                    nn -= 1
                C += (over / under) * p
            return "({:.3f}, {})".format(C, 'p')
        else:
            m = n / 2
            s = n**0.5 / 2
            z = ((sign+c) - m) / s
            return "({:.3f}, {})".format(z, 'z')

    # ウィルコクソンの符号順位検定(Wilcoxon signed rank test)
    # 対応のある2群間の中央値に差があるかを検定する(量的データにのみ使用可能)
    # 帰無仮説：2組の標本の中央値に差はない
    # 対立仮説：2組の標本の中央値に差がある
    # サンプルサイズが異なる場合、Errorを返す
    def wilcoxon_srtest(data1, data2):
        if length(data1) != length(data2): return 'Error', 'Error'
        data_diff = data1 - data2
        count_p, count_n, delite = [], [], 0
        for i in range(length(data1)):
            if data_diff[i] > 0:
                count_p += [i]
            elif data_diff[i] < 0:
                count_n += [i]
                data_diff[i] = -data_diff[i]
            else: #差が0の場合は取り除く
                data_diff[i] = -999999
                delite += 1
        data_diff_ranked, dup = rank(data_diff, delite=delite)

        p_num, n_num = length(count_p), length(count_n)
        r_sum = 0
        if p_num >= n_num:
            for n in count_n:
                r_sum += data_diff_ranked[n]
            return r_sum, (p_num+n_num) #順位が付けられたデータの組の数がNである
        else:
            for p in count_p:
                r_sum += data_diff_ranked[p]
            return r_sum, (p_num+n_num)

    sr_b, sr_a, sr_r2 = simple_regression(data1, data2)
    srt_t, srt_df = simple_regression_test(data1, data2)
    f_test_f, f_test_df = f_test(data1, data2)
    ind_ttest_t, ind_ttest_df = independent_ttest(data1, data2)
    dep_ttest_t, dep_ttest_df = dependent_ttest(data1, data2)
    welch_ttest_t, welch_ttest_v = welch_ttest(data1, data2)
    wilcoxon_srtest_Tw, wilcoxon_srtest_df = wilcoxon_srtest(data1, data2)

    df1 = pd.DataFrame({
        'count':length(data1),
        'sum':sum_value(data1),
        'mean':mean(data1),
        'g_mean':geometric_mean(data1),
        'h_mean':harmonic_mean(data1),
        'meand':meand(data1),
        'var.p':var_p(data1),
        'var.s':var_s(data1),
        'std.p':std_p(data1),
        'std.s':std_s(data1),
        'std_e':std_e(data1),
        'mean_95cl_known':mean_95cl_known(data1),
        'min':min_value(data1),
        '25%':quartile1(data1),
        '50%':median(data1),
        '75%':quartile3(data1),
        'max':max_value(data1),
        '25-75%':quartile_range(data1),
        'mid-range':mid_range(data1),
        'range':all_range(data1),
        'mode':mode(data1),
        'cov':cov(data1),
        'gini':gini(data1),
        'skewness':skewness(data1),
        'kurtosis':kurtosis(data1),
        'cov.p':cov_p(data1, data2),
        'cov.s':cov_s(data1, data2),
        'Pearson_cor.t':pearson_cor(data1, data2),
        'Spearman_cor.t':spearman_cor(data1, data2),
        'Kendall_cor.z':kendall_cor(data1, data2),
        'simple_regression':"({:.3f}, {:.3f})".format(sr_b, sr_a),
        'simple_regression_test.t':srt_t,
        'Jarque-Bara_test.x2':jarque_bera(data1),
        'F_test.F':f_test_f,
        'ind_ztest.z':independent_ztest(data1, data2),
        'ind_ttest.t':ind_ttest_t,
        'ind_cohen_d':ind_cohen_d(data1, data2),
        'dep_ztest.z':dependent_ztest(data1, data2),
        'dep_ttest.t':dep_ttest_t,
        'dep_cohen_d':dep_cohen_d(data1, data2),
        'Welch_ttest.t':welch_ttest_t,
        'Mood_test.z':mood_test(data1, data2),
        'Mann-Whitney_utest.U':mannwhitney_utest(data1, data2),
        'Wilcoxon_rstest.Tw':wilcoxon_rstest(data1, data2),
        'sign_test.r':sign_test(data1, data2),
        'Wilcoxon_srtest.Tw':wilcoxon_srtest_Tw
    }, index=["data1"]).T

    df2 = pd.DataFrame({
        'count':length(data2),
        'sum':sum_value(data2),
        'mean':mean(data2),
        'g_mean':geometric_mean(data2),
        'h_mean':harmonic_mean(data2),
        'meand':meand(data2),
        'var.p':var_p(data2),
        'var.s':var_s(data2),
        'std.p':std_p(data2),
        'std.s':std_s(data2),
        'std_e':std_e(data2),
        'mean_95cl_known':mean_95cl_known(data2),
        'min':min_value(data2),
        '25%':quartile1(data2),
        '50%':median(data2),
        '75%':quartile3(data2),
        'max':max_value(data2),
        '25-75%':quartile_range(data2),
        'mid-range':mid_range(data2),
        'range':all_range(data2),
        'mode':mode(data2),
        'cov':cov(data2),
        'gini':gini(data2),
        'skewness':skewness(data2),
        'kurtosis':kurtosis(data2),
        'Pearson_cor.t':pearson_cor_test(data1, data2),
        'Spearman_cor.t':spearman_cor_test(data1, data2),
        'Kendall_cor.z':kendall_cor_test(data1, data2),
        'simple_regression':sr_r2,
        'simple_regression_test.t':srt_df,
        'Jarque-Bara_test.x2':jarque_bera(data2),
        'F_test.F':f_test_df,
        'ind_ttest.t':ind_ttest_df,
        'dep_ttest.t':dep_ttest_df,
        'Welch_ttest.t':welch_ttest_v,
        'Wilcoxon_srtest.Tw':wilcoxon_srtest_df
    }, index=["data2"]).T

    # 結果出力
    display(pd.concat([df1, df2], axis=1))

    # 種々のグラフをプロット
    # ヒストグラム
    fig, axes= plt.subplots(1,2)
    axes[0].hist(data1, bins=(1+int(np.log2(length(data1)))))
    axes[1].hist(data2, bins=(1+int(np.log2(length(data2)))))
    plt.show()

    # ヒストグラム(累積)
    fig, axes= plt.subplots(1,2)
    axes[0].hist(data1, bins=(1+int(np.log2(length(data1)))), cumulative=True)
    axes[1].hist(data2, bins=(1+int(np.log2(length(data2)))), cumulative=True)
    plt.show()

    # Q-Qプロット
    # 点が一直線に並んでいれば、そのデータの母集団の分布は正規分布に従う
    fig, axes= plt.subplots(1,2)
    st.probplot(data1, dist="norm", plot=axes[0])
    st.probplot(data2, dist="norm", plot=axes[1])
    plt.show()

    # 箱ひげ図
    plt.boxplot([data1, data2], positions=[1, 2])
    plt.title("Boxplot")
    plt.show()

    # バイオリンプロット
    plt.violinplot([data1, data2], positions=[1.2, 1.8])
    plt.title("Violinplot")
    plt.show()

    # イベントプロット
    plt.eventplot([data1, data2], orientation="vertical", lineoffsets=[2, 4], linewidth=0.75)
    plt.title("Eventplot")
    plt.show()

    # サンプルサイズが同じ場合のみ出力
    try:
        # グラフ
        plt.plot(data1, data2)
        plt.title("plot")
        plt.xlabel("data1")
        plt.ylabel("data2")
        plt.show()

        # 散布図
        plt.scatter(data1, data2)
        plt.title("Scatter")
        plt.xlabel("data1")
        plt.ylabel("data2")
        plt.show()

        # ステムプロット
        plt.stem(data1, data2)
        plt.title("Stem")
        plt.xlabel("data1")
        plt.ylabel("data2")
        plt.show()

        # ステッププロット
        plt.step(data1, data2)
        plt.title("Step")
        plt.xlabel("data1")
        plt.ylabel("data2")
        plt.show()

        # hist2d
        plt.hist2d(data1, data2)
        plt.title("Hist2d")
        plt.xlabel("data1")
        plt.ylabel("data2")
        plt.show()

    except:
        pass

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats as st

# 確率分布からデータを生成
data1 = pd.DataFrame(np.random.randint(-100, 101, 100), columns=["data1"])["data1"]
data2 = pd.DataFrame(np.random.randint(-100, 101, 100), columns=["data2"])["data2"]
describe2(data1, list(data2))

# 既存のライブラリで検証
print("PearsonrResult", st.pearsonr(data1, data2))
print(st.spearmanr(data1, data2))
print("indep_ttest", st.ttest_ind(data1, data2, equal_var=True))
print("dep_ttest", st.ttest_rel(data1, data2))
print("Welch", st.ttest_ind(data1, data2, equal_var=False))
print("Mood", st.mood(data1, data2))
print(st.mannwhitneyu(data1, data2, alternative='two-sided'))
print(st.wilcoxon(data1, data2))


"""
Linear algebra homework problem 2
"""
import numpy as np

np.set_printoptions(precision=2)

E = 3
t = 1

T = np.array([
    [E - t, t, 0, 0, 0],
    [t, E - t, t, 0, 0],
    [0, t, E - t, t, 0],
    [0, 0, t, E - t, t],
    [0, 0, 0, t, E - t]
])
C = np.array([
    [E - t, t, 0, 0, t],
    [t, E - t, t, 0, 0],
    [0, t, E - t, t, 0],
    [0, 0, t, E - t, t],
    [t, 0, 0, t, E - t]
])
T_inv = np.linalg.inv(T)
C_inv = np.linalg.inv(C)

# Evaluate the Sherman-Morrison formula for the bottom-left corner
u1 = np.array([0, 0, 0, 0, t])
v1 = np.array([1, 0, 0, 0, 0])
z1 = T_inv @ u1
w1 = T_inv.T @ v1
lam1 = np.dot(v1, z1)
sm1 = - np.tensordot(z1, w1, axes=0) / (1 + lam1)
C_sm = T_inv + sm1

# Evaluate the Sherman-Morrison formula for the top-right corner
u2 = np.array([t, 0, 0, 0, 0])
v2 = np.array([0, 0, 0, 0, 1])
z2 = C_sm @ u2
w2 = C_sm.T @ v2
lam2 = np.dot(v2, z2)
sm2 = - np.tensordot(z2, w2, axes=0) / (1 + lam2)
C_sm = C_sm + sm2

d_inv = T_inv - C_inv
d_sm = T_inv - C_sm

if __name__ == '__main__':
    print('COMMIT!!!!')
    print(f"Standard inversion:\n"
          f"C_inverse:\n{C_inv}\n"
          f"Delta:\n{d_inv}\n"
          f"\n"
          f"Sherman-Morrison:\n"
          f"C_inverse:\n{C_sm}\n"
          f"Delta:\n{d_sm}")


import numpy as np
from typing import Final
from fractions import Fraction

"""--------------------------------------
# Constants !
# --------------------------------------
# 1/Shape of Matrix ( Game strategies )
#  Basically  : 2x2 Game
 --------------------------------------"""
DIM: Final = (2, 2)
# Because we have matrix of couples ( payoff for Player 1 & Player 2 )
payoff_type = np.dtype(int, int)

class Solver:
    def __init__(self, payoff):
        self.M = payoff

    # This algo give us the result in linear time o(n) !
    def solve_pure(self):
        possible_nash1 = []
        possible_nash2 = []
        # Best responses for Player 1 against Player 2 strategies
        for j in range(DIM[1]):
            x_max = max(self.M[:, j], key=lambda x: x[0])
            a = [tuple(x) for x in self.M[:, j]]

            if(a[0]==a[1]):
                best_reply1 = [(a[0],(0,j)),(a[1],(1,j))]
            else:
                best_reply1 = [(tuple(x), (a.index(tuple(x)), j)) for x in self.M[:, j] if x[0] == x_max[0]]

            possible_nash1 += best_reply1
        print(possible_nash1)

        # Best responses for Player 2 against Player 1 strategies
        for i in range(DIM[0]):
            y_max = max(self.M[i, :], key=lambda y: y[1])
            a = [tuple(x) for x in self.M[i, :]]
            if(a[0]==a[1]):
                best_reply2 = [(a[0],(i,0)),(a[1],(i,1))]
            else:
                best_reply2 = [(tuple(y), (i, a.index(tuple(y)))) for y in self.M[i, :] if y[1] == y_max[1]]

            # best_reply2 = [tuple(y) for y in self.M[i,:] if y[1] == y_max]
            possible_nash2 += best_reply2
        print(possible_nash2)
        # Intersection of two lists give us the Nash equilibria !
        result = [ x[1] for x in list(set(possible_nash1) & set(possible_nash2))]
        print(result)

        return result

    """
        We will try to solve to equations of type : ( find p and q )
        a*p=b
        c*q=d
        ( How we got this a,b,c and d coeiffitiions ? )
        for example :
                      q                  1-q
                      C                   D
             ---------------------------------------------
    p      A        (3,-3)       |        (-2,2)
             ---------------------------------------------
    1-p    B        (-1,1)       |         (0,0)
             ---------------------------------------------

        If we apply the definition of mixed strategy Nash Equ we will get :
        For Player 1 :
        -3p + 1(1-p) = 2p + 0(1-p)
        For Player 2 :
        3q + (-2)(1-q) = -1q + 0(1-q)

        So our goal is to solve this linear system !
        Lets make it like the format in the start of this example :
        (-3-1-2+0)p = -1 + 0
        (3+2+1+0)q  = +2 + 0

        Cool we have now our a,b,c and d coeiff ! lets do our thing :
        p = 1/6
        q = 1/3
    """

    # This algo give us the results in constant time ! cool o(1)
    def solve_mixed(self):
        a = (self.M[0, 0][1] - self.M[1, 0][1] - self.M[0, 1][1] + self.M[1, 1][1])
        b = (-self.M[1, 0][1] + self.M[1, 1][1])

        c = (self.M[0, 0][0] - self.M[0, 1][0] - self.M[1, 0][0] + self.M[1, 1][0])
        d = (-self.M[0, 1][0] + self.M[1, 1][0])

        p = b / a if a != 0 else None
        q = d / c if c != 0 else None

        if(p and q):
            p=Fraction(p).limit_denominator()
            q=Fraction(q).limit_denominator()

        print(f'p = {p} \nq = {q}')
        return (p,q)

    # This algo give us the result in linear time o(n) !
    def solve_all(self):
        return (self.solve_pure(),self.solve_mixed())

    def solve(self, pure=1, mixed=1):
        if (pure and mixed):
            return self.solve_all()
        elif (pure and not mixed):
            return self.solve_pure()
        elif (not pure and mixed):
            return self.solve_mixed()
        else:
            return -1


"""
Common algorithms used for the computation and visualization of NURBS.
"""

import numpy as np

def cox_de_boor(knot_span, polynomial_degree, parameter, knot_vector):
    """
    Evaluate the B-spline basis function.

    Arguments
    ---------
    knot_span : int
        The knot_span where the basis is evaluated.
    polynomial_degree : int
        The polynomial degree of the basis.
    parameter : float
        The parameter for which the basis is evaluated.
    knot_vector : array_like
            The array of knots to construct the basis, with shape (1, ).

    Returns
    -------
    float
        The basis function evaluation.

    Raises
    ------
    ValueError
        If parameter is not between the first and last knot values.
    """
    if parameter < knot_vector[0] or parameter > knot_vector[-1]:
        raise ValueError("parameter value should be between the first and last knot values")

    # Base case
    if polynomial_degree == 0:
        is_end_of_parameter_space = np.isclose(parameter, knot_vector[knot_span + 1]) and \
                                    np.isclose(parameter, knot_vector[-1])
        if knot_vector[knot_span] <= parameter < knot_vector[knot_span + 1] or is_end_of_parameter_space:
            return 1.0
        else:
            return 0.0
    # Recursion
    else:
        # Numerator on the left hand side
        n1 = parameter - knot_vector[knot_span]
        # Denominator on the left hand side
        d1 = knot_vector[knot_span + polynomial_degree] - knot_vector[knot_span]
        # Quotient on the left hand side
        q1 = cox_de_boor_division(n1, d1)

        # Numerator on the right hand side
        n2 = knot_vector[knot_span + polynomial_degree + 1] - parameter
        # Denominator on the right hand side
        d2 = knot_vector[knot_span + polynomial_degree + 1] - knot_vector[knot_span + 1]
        # Quotient on the right hand side
        q2 = cox_de_boor_division(n2, d2)

        # Cox-De Boor's recursion formula
        return q1 * cox_de_boor(knot_span, polynomial_degree - 1, parameter, knot_vector) + \
               q2 * cox_de_boor(knot_span + 1, polynomial_degree - 1, parameter, knot_vector)

def cox_de_boor_division(numerator, denominator):
    """
    Handle division by zero defined for the Cox-De Boor's algorithm.

    Arguments
    ---------
    numerator : float
        The numerator of the division.
    denominator : float
        The denominator of the division.

    Returns
    -------
    float
        The division of numerator by denominator.
    """
    if np.isclose(denominator, 0.0):
        return 0.0

    else:
        return numerator / denominator

def evaluate_bspline_basis(polynomial_degree, knot_vector, control_points_count, samples):
    """
    Evaluate the B-spline basis function for the given samples.

    Arguments
    ---------
    polynomial_degree : int
        The polynomial degree of the basis.
    knot_vector : array_like
            The array of knots to construct the basis, with shape (1, ).
    control_points_count: int
        The number of control points.
    samples : array_like
        The array of samples coordinates in the parameter space, with shape (1, samples_count).

    Returns
    -------
    ndarray
        The array of basis functions evaluations, with shape (control_points_count, len(samples)).
    """
    # Initialize the array of basis evaluations
    evaluations = np.zeros((control_points_count, len(samples)))

    # Evaluate the shape functions for the given samples using the Cox-De Boor's algorithm
    for i in range(control_points_count):
        first_nonzero_sample = knot_vector[i]
        last_nonzero_sample = knot_vector[i + polynomial_degree + 1]

        # Perform the computation only where the shape function i has support
        for j, sample in enumerate(samples):
            if first_nonzero_sample <= sample <= last_nonzero_sample:
                evaluations[i, j] = cox_de_boor(i, polynomial_degree, sample, knot_vector)

    return evaluations

def get_index_to_nearest(array, value):
    """
    Get index of the nearest array entry to a given value.

    Arguments
    ---------
    array : array_like
        The array to search for the nearest value.
    value : float
        The value to look for.

    Returns
    -------
    ndarray
        The index of the nearest array entry.
    """
    return (np.abs(array - value)).argmin()

def replace_nearest(array, value):
    """
    Replace the nearest array entry to a given value for the given value.

    Arguments
    ---------
    array : array_like
        The array to replace the nearest value.
    value : float
        The value to replace.
    """
    # Get index of the nearest array entry to the given value
    index = get_index_to_nearest(array, value)

    # Replace the value in index
    array[index] = value

def is_open(knot_vector, polynomial_degree):
    """
    Check whether a knot vector is open.

    Arguments
    ---------
    knot_vector : array_like
        The knot vector.
    polynomial_degree : int
        The polynomial degree of the curve generated by knot_vector.

    Returns
    -------
    bool
        True if the knot vector is open or False otherwise.
    """
    # Check whether the first and last (polynomial_degree + 1) knots are the same
    for i in range(1, polynomial_degree + 1):
        if knot_vector[0] != knot_vector[i] or knot_vector[-1] != knot_vector[-1 - i]:
            return False
    if knot_vector[polynomial_degree + 1] == knot_vector[0] or\
       knot_vector[-1 - (polynomial_degree + 1)] == knot_vector[-1]:
        return False
    return True

def is_non_descending(knot_vector):
    """
    Check whether a knot vector is in non-descending order.

    Arguments
    ---------
    knot_vector : array_like
        The knot vector.

    Returns
    -------
    bool
        True if the knot vector is in non-descending order or False otherwise.
    """
    for i in range(len(knot_vector) - 1):
        if knot_vector[i + 1] < knot_vector[i]:
            return False
    return True

def generate_open_knot(control_points_count, polynomial_degree):
    """
    Generate a uniform open knot vector in the normalized range.

    Arguments
    ---------
    control_points_count : int
        The number of control points.
    polynomial_degree : int
        The polynomial degree.

    Returns
    -------
    np.ndarray
        True uniform open knot vector in the normalized range.

    Raises
    ------
    ValueError
        If control_points_count is smaller than polynomial_degree + 1.
    """
    if control_points_count < polynomial_degree + 1:
        raise ValueError("control_points_count should be at least polynomial_degree + 1")

    knot_vector = np.zeros(control_points_count + polynomial_degree + 1)

    # Set the last (polynomial_degree + 1) to 1
    knot_vector[control_points_count:] += 1

    # Calculate the delta between inner knots
    delta = 1 / (control_points_count - polynomial_degree)

    # Compute inner knots
    for i in range(polynomial_degree + 1, control_points_count):
        knot_vector[i] = knot_vector[i - 1] + delta

    return knot_vector


import numpy as np

class Quaternion:
    """Quaternions for 3D rotations"""
    def __init__(self, x):
        self.x = np.asarray(x, dtype=float)

    @classmethod
    def from_v_theta(cls, v, theta):
        """ Construct quaternion from unit vector v and rotation angle theta"""
        theta = np.asarray(theta)
        v = np.asarray(v)

        s = np.sin(0.5 * theta)
        c = np.cos(0.5 * theta)
        vnrm = np.sqrt(np.sum(v * v))

        q = np.concatenate([[c], s * v / vnrm])
        return cls(q)

    def get_array(self):
        return self.x

    def __eq__(self, other):
        return np.array_equal(self.x, other.x)

    def __ne__(self, other):
        return not (self==other)

    def __repr__(self):
       return "Quaternion:\n" + self.x.__repr__()

    def __mul__(self, other):
        # multiplication of two quaternions.
        prod = self.x[:, None] * other.x

        return self.__class__([(prod[0, 0] - prod[1, 1]
                                 - prod[2, 2] - prod[3, 3]),
                                (prod[0, 1] + prod[1, 0]
                                 + prod[2, 3] - prod[3, 2]),
                                (prod[0, 2] - prod[1, 3]
                                 + prod[2, 0] + prod[3, 1]),
                                (prod[0, 3] + prod[1, 2]
                                 - prod[2, 1] + prod[3, 0])])

    def as_v_theta(self):
        """Return the v, theta equivalent of the (normalized) quaternion"""
        # compute theta
        norm = np.sqrt((self.x ** 2).sum(0))
        assert(norm != 0)
        theta = 2 * np.arccos(self.x[0] / norm)

        # compute the unit vector
        v = np.array(self.x[1:], order='F', copy=True)
        length = np.sqrt(np.sum(v ** 2, 0))
        if length > 0.0:
            v /= length
        return v, theta

    def as_rotation_matrix(self):
        """Return the rotation matrix of the (normalized) quaternion
           https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
           Improving computation speed https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4435132/
           """
        v, theta = self.as_v_theta()
        c = np.cos(theta)
        s = np.sin(theta)

        return np.array([[v[0] * v[0] * (1. - c) + c,
                          v[0] * v[1] * (1. - c) - v[2] * s,
                          v[0] * v[2] * (1. - c) + v[1] * s],
                         [v[1] * v[0] * (1. - c) + v[2] * s,
                          v[1] * v[1] * (1. - c) + c,
                          v[1] * v[2] * (1. - c) - v[0] * s],
                         [v[2] * v[0] * (1. - c) - v[1] * s,
                          v[2] * v[1] * (1. - c) + v[0] * s,
                          v[2] * v[2] * (1. - c) + c]])