"""
nitrogen.math
-----------------

General mathematical tools and functions

"""

import numpy as np

def cumsimp(y, x, axis = 0):
    """
    Calculate the cumulative integral by Simpson's Rule

    Parameters
    ----------
    y : array_like
        The uniformly spaced samples
    x : array_like or scalar
        The sample points along the integration axis or, if scalar, the sample spacing.
    axis : int, optional
        The integration axis. The default is 0.

    Returns
    -------
    res : ndarray
        The result of cumulative integration.

    Notes
    -----

    The first element of the cumulative integral is
    fixed to zero. The second element is handled in one of two ways.
    If the length of `y` is only 2, then the trapezoid rule is used. Otherwise,
    the next element in `y` is used for quadratic interpolation.
    The remaining elements are evaluated via Simpson's 1/3 rule.

    """

    y = np.array(y)

    if y.shape[axis] < 1:
        raise ValueError('y must be have non-zero length along integration axis')

    if not np.isscalar(x):
        if len(x) != y.shape[axis]:
            raise ValueError('x must have the same integration points as y')
            # This is not strictly necessary because we assume the
            # integration points are uniform, but I will enforce this
            # check because it will probably catch some logical errors
            # or unintentional parameters

    # Move integration axis to the front
    y = np.moveaxis(y, axis, 0)

    res = np.zeros_like(y)

    # First element is always zero
    # res[0] = 0.0

    ylen = y.shape[0] # The integration length

    if ylen > 1:

        # Gather the sample spacing
        if np.isscalar(x):
            dx = x
        else:
            dx = x[1] - x[0]

        # Second element has two cases
        if ylen == 2:
            # 1) The total length is only 2
            # Use trapezoid rule
            res[1] = dx * 0.5 * (y[0] + y[1])
        else:
            # 2) The length is greater than 2
            #    Use quadratic interpolation for the
            #    first semgment ("half-way Simpson's Rule")
            res[1] = (dx/12) * (5*y[0] + 8*y[1] - y[2])

    if ylen > 2:
        for i in range(2,ylen):
            # For the third and later elements,
            # use the standard 1/3 Simpson's rule
            res[i] = res[i-2] + (dx/3) * (y[i-2] + 4*y[i-1] + y[i])

    # Move integration axis back to original position
    res = np.moveaxis(res, 0, axis)
    return res

def spech_fft(C,dt,sample_factor = 1, damping = 0.0):
    """
    Calculate the intensity spectrum from a hermitian
    autocorrelation function C.

    Parameters
    ----------
    C : ndarray
        The autocorrelation function.
    dt : float
        The time step.
    sample_factor : int
        The over-sampling factor. The default is 1.
    damping : float
        The Gaussian damping factor. The default is 0.

    Returns
    -------
    g : ndarray
        The (real) intensity spectrum
    freq : ndarray
        The angular frequency axis

    Notes
    -----
    The autocorrelation function is provided for :math:`t \geq 0`, i.e.
    C[0] is the :math:`t = 0` value. It is assumed that :math:`C(-t) = C(t)^*`.

    The returned spectrum approximates the Fourier transformation

    ..  math::

        g(\\omega) = \\frac{1}{2\\pi}\\, \\int_{-\\infty}^{\\infty} dt e^{i \\omega t} C(t)

    This normalization means that the angular frequency integral of
    :math:`g(\\omega)` equals :math:`C(0)`, which is typically unity.

    The calculated spectrum may be over-sampled by zero-padding the
    autocorrelation function. This sampling factor is controlled by
    `sample_factor`.

    A Gaussian window function is also applied to the autocorrelation
    function, of the form :math:`C(t) \\exp(-a (t/T)^2)`, where
    :math:`T` is length of the correlation function (before any padding).
    The damping factor :math:`a` is controlled by the `damping` keyword.
    At sufficiently large :math:`a`, the line shape becomes Gaussian
    with an angular frequency full-width at half-maximum equal to
    :math:`\\omega_\\text{FWHM} = 4 T^{-1} \\sqrt{a \\ln 2}`.

    """

    # First window and zero-pad as necessary
    tau = np.linspace(0.0, 1.0, len(C))
    C = C * np.exp(-damping * tau**2)
    # zero-pad
    sample_factor = int(sample_factor)
    if sample_factor < 1:
        raise ValueError("sample_factor must be a postive integer")
    Cz = np.zeros(len(C) * sample_factor, dtype = C.dtype)
    Cz[:len(C)] = C[:]

    #
    # Because C is hermitian, we can reconstruct
    # its negative-time values explicitly
    # (We could also just use numpy's hfft, but I will do it this way instead)
    fpos = Cz # t >= 0
    fneg = Cz[:0:-1].conj()  # t < 0

    f = np.concatenate((fneg, fpos)) # [-T,T]
    N = len(f)  # N is odd
    n = (N-1) // 2 # integer

    F = np.fft.fft(f, norm = None) # Use standard normalization fft
    k = np.concatenate((np.arange(0,-(n+1), -1), np.arange(n,0,-1)))

    t0 = -(N-1)/2 * dt

    # Correct for DFT phase
    G = (dt/(2*np.pi)) * np.exp(+1j * 2*np.pi * t0 / (N*dt)  * k) * F

    # Calculate angular frequency values
    dw = (2*np.pi) / (N*dt)
    freq = k * dw

    # Re-sort result in ascending frequency
    IDX = np.argsort(freq)
    freq = freq[IDX]
    g = np.real(G[IDX])

    return g, freq

def gaussianFWHM(x, fwhm, norm = 'area'):
    """
    Calculate a Gaussian function.

    Parameters
    ----------
    x : array_like
        Input array
    fwhm : scalar
        The full-width at half-maximum value.
    norm : {'area', 'amplitude'}
        The normalization convention. The default is 'area'. See Notes for
        definitions.

    Returns
    -------
    y : ndarray
        The result.

    Notes
    -----
    For `norm` = ``\`area\```, the integrated area is equal to
    unity. For `norm` = ``\`amplitude\```, the
    peak amplitude is equal to unity.

    """

    y = np.exp(-4*np.log(2) * (x/fwhm)**2)

    if norm == 'area':
        y *= np.sqrt(np.log(16)/np.pi) / fwhm
    elif norm == 'amplitude':
        pass
    else:
        raise ValueError('unexpected norm keyword')

    return y


import numpy as np
import random
import pickle

REWARD = -0.1
GAMMA = 0.7
DISCOUNT = 0.99

ACTIONS = [(1, 0), (0, -1), (-1, 0), (0, 1)]  # actions which can be D, L, U, R
N_ACTIONS = len(ACTIONS)

# Grid size based on amount of rows and columns
ROW = 4
COL = 4

# Grid with terminal states
u = [[0, 0, 0, 40],
     [0, 0, 0, 0],
     [0, 0, 0, 0],
     [10, 0, 0, 0]]

def maze_grid(arr, policy=False):
    """This function initializes the maze grid with all of the
    given rewards per state and prints it out nicely.
    The grid has 4 rows and 4 columns which is set in the cell above.
    """
    res = ""
    for r in range(ROW):
        res += "|"
        for c in range(COL):
            if r == 0 and c == 3:
                val = "F"
            elif r == 3 and c == 0:
                val = "F"
            elif r == 3 and c == 2:
                val += " S"

            else:
                if policy:
                    # arrow symbols
                    val = ['\u2193', '\u2190', '\u2191', '\u2192'][arr[r][c]]
                else:
                    val = str(arr[r][c])
            res += " " + val[:5].ljust(5) + " |"
        res += "\n"
    print(res)

def get_utility(rewards, r, c, action):
    """This function gets the utility of the state reached
    by performing the given action from the given state
    """
    dr, dc = ACTIONS[action]
    newR = r + dr
    newC = c + dc
    start = (newR == 3 and newC == 2)
    if newR < 0 or newC < 0 or newR >= ROW or newC >= COL or start:
        return rewards[r][c]
    else:
        return rewards[newR][newC]

def calculate_utility(rewards, r, c, action):
    """This function calculates the utility of a state given
    an action
    """
    if r == 3 and c == 1:
        u = -2
    elif r == 1 and c == 2:
        u = -10
    elif r == 1 and c == 3:
        u = -10
    else:
        u = REWARD

    u += 0.1 * DISCOUNT * get_utility(rewards, r, c, (action-1)%4)
    u += GAMMA * DISCOUNT * get_utility(rewards, r, c, action)
    u += 0.1 * DISCOUNT * get_utility(rewards, r, c, (action+1)%4)

    return u

def value_iteration(rewards):
    """This function initializes the maze grid with all of the
    given rewards per state and prints it out nicely.
    The grid has 4 rows and 4 columns which is set in the cell above.
    """
    print("During the value iteration:\n")
    while True:
        next_utility = [[0, 0, 0, 40],
                        [0, 0, 0, 0],
                        [0, 0, 0, 0],
                        [10, 0, 0, 0]]
        error = 0
        for r in range(ROW):
            for c in range(COL):
                terminal_states = [(r == 0 and c == 3),
                                   (r == 3 and c == 0),
                                   (r == 3 and c == 1)]
                if any(terminal_states):
                    continue
                next_utility[r][c] = max([calculate_utility(rewards, r, c, action) for action in range(N_ACTIONS)]) # Bellman update
                error = max(error, abs(next_utility[r][c]-rewards[r][c]))
        rewards = next_utility
        maze_grid(rewards)
        if error < ((1-GAMMA) / GAMMA):
            break
    return rewards

def optimal_policy(rewards):
    """This function gets the optimal policy from U
    """
    policy = [[-1,-1,-1,-1],
              [-1,-1,-10,-10],
              [-1,-1,-1,-1],
              [-1,-2,-1,-1]]

    for r in range(ROW):
        for c in range(COL):
            terminal_states = [(r == 0 and c == 3),
                               (r == 3 and c == 0),
                               (r == 3 and c == 1)]
            if any(terminal_states):
                continue
            # Choose the action that maximizes the utility
            maxAction, maxU = None, -float("inf")
            for action in range(N_ACTIONS):
                u = calculate_utility(rewards, r, c, action)
                if u > maxU:
                    maxAction, maxU = action, u
            policy[r][c] = maxAction
    return policy

def create_random_policy(row, col, min, max):
    """Creates a random policy.
    """
    pol = np.random.randint(min, max, size=(row, col))
    pol_grid = maze_grid(pol, True)
    return pol, pol_grid

# Print the initial environment
print("The initial maze grid is:\n")
maze_grid(u)

# Value iteration
rewards = value_iteration(u)
# print(rewards)

# Get the optimal policy from U and print it
policy = optimal_policy(rewards)
print("The optimal policy is:\n")
maze_grid(policy, True)


import numpy as np
# Setting the random seed, feel free to change it and see different solutions.
np.random.seed(42)

def sigmoid(x):
    return 1/(1+np.exp(-x))
def sigmoid_prime(x):
    return sigmoid(x)*(1-sigmoid(x))
def prediction(X, W, b):
    return sigmoid(np.matmul(X,W)+b)
def error_vector(y, y_hat):
    return [-y[i]*np.log(y_hat[i]) - (1-y[i])*np.log(1-y_hat[i]) for i in range(len(y))]
def error(y, y_hat):
    ev = error_vector(y, y_hat)
    return sum(ev)/len(ev)

# TODO: Fill in the code below to calculate the gradient of the error function.
# The result should be a list of three lists:
# The first list should contain the gradient (partial derivatives) with respect to w1
# The second list should contain the gradient (partial derivatives) with respect to w2
# The third list should contain the gradient (partial derivatives) with respect to b
def dErrors(X, y, y_hat):
    DErrorsDx1 = [-X[i][0] * (y[i] - y_hat[i])  for i in range(len(y))]
    DErrorsDx2 = [-X[i][1] * (y[i] - y_hat[i])  for i in range(len(y))]
    DErrorsDb = [-(y[i] - y_hat[i]) for i in range(len(y))]
    return DErrorsDx1, DErrorsDx2, DErrorsDb

# TODO: Fill in the code below to implement the gradient descent step.
# The function should receive as inputs the data X, the labels y,
# the weights W (as an array), and the bias b.
# It should calculate the prediction, the gradients, and use them to
# update the weights and bias W, b. Then return W and b.
# The error e will be calculated and returned for you, for plotting purposes.
def gradientDescentStep(X, y, W, b, learn_rate = 0.01):
    # TODO: Calculate the prediction
    # TODO: Calculate the gradient
    # TODO: Update the weights
    # This calculates the error
    y_hat = prediction(X, W, b)
    err = error_vector(y, y_hat)
    derivative_errors = dErrors(X, y, y_hat)
    W[0] -= sum(derivative_errors[0]) * learn_rate
    W[1] -= sum(derivative_errors[1]) * learn_rate
    b -= sum(derivative_errors[2]) * learn_rate
    e = error(y, y_hat)
    return W, b, e

# This function runs the perceptron algorithm repeatedly on the dataset,
# and returns a few of the boundary lines obtained in the iterations,
# for plotting purposes.
# Feel free to play with the learning rate and the num_epochs,
# and see your results plotted below.
def trainLR(X, y, learn_rate = 0.01, num_epochs = 100):
    x_min, x_max = min(X.T[0]), max(X.T[0])
    y_min, y_max = min(X.T[1]), max(X.T[1])
    # Initialize the weights randomly
    W = np.array(np.random.rand(2,1))*2 -1
    b = np.random.rand(1)[0]*2 - 1
    # These are the solution lines that get plotted below.
    boundary_lines = []
    errors = []
    for i in range(num_epochs):
        # In each epoch, we apply the gradient descent step.
        W, b, error = gradientDescentStep(X, y, W, b, learn_rate)
        boundary_lines.append((-W[0]/W[1], -b/W[1]))
        errors.append(error)
    return boundary_lines, errors

# read in data from data.csv
import pandas as pd

dataset = pd.read_csv('data.csv')
print(dataset)
# pass to trainLR function
# visualize using the returned data


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

from __future__ import division, print_function
from __future__ import absolute_import, unicode_literals
from ..objects import Vector3
import numpy as np

def normal(v1, v2, v3, normalise=True):
    """
    Generates a normal vector for 3 vertices.

    The result is a normalised vector.

    It is assumed the ordering is counter-clockwise starting
    at v1, v2 then v3::

        v1      v3
          \    /
            v2

    The vertices are Nd arrays and may be 1d or Nd.
    As long as the final axis is of size 3.

    For 1d arrays::
        >>> v1 = numpy.array( [ 1.0, 0.0, 0.0 ] )
        >>> v2 = numpy.array( [ 0.0, 0.0, 0.0 ] )
        >>> v3 = numpy.array( [ 0.0, 1.0, 0.0 ] )
        >>> vector.generate_normals( v1, v2, v3 )
        array([ 0.,  0., -1.])

    For Nd arrays::
        >>> v1 = numpy.array( [ [ 1.0, 0.0, 0.0 ], [ 1.0, 0.0, 0.0 ] ] )
        >>> v2 = numpy.array( [ [ 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0 ] ] )
        >>> v3 = numpy.array( [ [ 0.0, 1.0, 0.0 ], [ 0.0, 1.0, 0.0 ] ] )
        >>> vector.generate_normals( v1, v2, v3 )
        array([[ 0.,  0., -1.],
               [ 0.,  0., -1.]])

    :param numpy.array v1: an Nd array with the final dimension
        being size 3. (a vector)
    :param numpy.array v2: an Nd array with the final dimension
        being size 3. (a vector)
    :param numpy.array v3: an Nd array with the final dimension
        being size 3. (a vector)
    :param boolean normalise_result: Specifies if the result should
        be normalised before being returned.
    """
    # make vectors relative to v2
    # we assume opengl counter-clockwise ordering
    a = v1 - v2
    b = v3 - v2
    n = Vector3(np.cross(b, a))
    if normalise:
        n.normalize()
    return n

def cross(v1, v2):
    """Calculates the cross-product of two vectors.

    :param numpy.array v1: an Nd array with the final dimension
        being size 3. (a vector)
    :param numpy.array v2: an Nd array with the final dimension
        being size 3. (a vector)
    :rtype: A np.array with shape v1.shape.
    """
    return np.cross(v1, v2)

def matrix3_view(mat):
    """
    Returns a view into the matrix in Matrix3 format.

    This is different from Matrix3.from_matrix4, in that
    changes to the returned matrix will also alter the original matrix.

    :rtype: numpy.array
    :return: A view into the matrix in the format of a matrix3 (shape (3,3)).
    """
    return mat[0:3, 0:3]


# 100 numpy exercises - my solution
# Keni Mou

#%% 1. Import the numpy package under the name `np` (★☆☆)
import numpy as np

#%% 2. Print the numpy version and the configuration (★☆☆)
print(np.__version__)
np.show_config()

#%% 3. Create a null vector of size 10 (★☆☆)
np.zeros(10)

#%% 4.  How to find the memory size of any array (★☆☆)
# NOTE: in byte. size: len, itemsize: byte
arr = np.zeros(10)
arr.size * arr.itemsize

#%% 5.  How to get the documentation of the numpy add function from the command line? (★☆☆)
np.info(np.add)

#%% 6.  Create a null vector of size 10 but the fifth value which is 1 (★☆☆)
arr = np.zeros(10)
arr[4] = 1
arr

#%% 7.  Create a vector with values ranging from 10 to 49 (★☆☆)
np.arange(10,50)

#%% 8.  Reverse a vector (first element becomes last) (★☆☆)
arr = np.arange(10)
arr[::-1]

#%% 9.  Create a 3x3 matrix with values ranging from 0 to 8 (★☆☆)
np.arange(9).reshape(3,3)

#%% 10. Find indices of non-zero elements from \[1,2,0,0,4,0\] (★☆☆)
# NOTE:
arr = np.array([1,2,0,0,4,0])
np.nonzero(arr)

#%% 11. Create a 3x3 identity matrix (★☆☆)
# NOTE:
np.eye(3)

#%% 12. Create a 3x3x3 array with random values (★☆☆)
np.random.rand(3,3,3)

#%% 13. Create a 10x10 array with random values and find the minimum and maximum values (★☆☆)
arr = np.random.rand(10,10)
arr.min(), arr.max()

#%% 14. Create a random vector of size 30 and find the mean value (★☆☆)
arr = np.random.rand(30)
arr.mean()

#%% 15. Create a 2d array with 1 on the border and 0 inside (★☆☆)
arr = np.ones((5,5))
arr[1:-1,1:-1]= 0
arr

#%% 16. How to add a border (filled with 0's) around an existing array? (★☆☆)
# NOTE: add a border, dimension should change
arr = np.random.randint(1, 10, size=(3,7))
np.pad(arr, 1, 'constant', constant_values=0)

#%% 17. What is the result of the following expression? (★☆☆)
0 * np.nan # nan
np.nan == np.nan # False
np.inf > np.nan # NOTE: False
np.nan - np.nan # nan
np.nan in set([np.nan]) # NOTE: True
0.3 == 3 * 0.1  # NOTE: False float has bad precision

#%% 18. Create a 5x5 matrix with values 1,2,3,4 just below the diagonal (★☆☆)
# NOTE: diag: If `v` is a 1-D array, return a 2-D array with `v` on the `k`-th diagonal
np.diag(1+np.arange(4),k=-1)

#%% 19. Create a 8x8 matrix and fill it with a checkerboard pattern (★☆☆)
# NOTE: INDEXING!!!
arr = np.zeros((8,8))
arr[::2,::2] = 1
arr[1::2,1::2] = 1
arr

#%% 20. Consider a (6,7,8) shape array, what is the index (x,y,z) of the 100th element?
x = 99//(7*8)
y = 99%(7*8)//8
z = 99%(7*8)%8
x,y,z
# NOTE: here is a built-in funciton...
print(np.unravel_index(99,(6,7,8)))

#%% 21. Create a checkerboard 8x8 matrix using the tile function (★☆☆)
np.tile(np.array([[0,1],[1,0]]), (4,4))

#%% 22. Normalize a 5x5 random matrix (★☆☆)
arr = np.random.rand(5,5)
(arr - arr.mean()) / arr.std()

#%% 23. Create a custom dtype that describes a color as four unsigned bytes (RGBA) (★☆☆)
# NOTE
color = np.dtype([
    ('R', np.ubyte, 1), # (name, dtype, size)
    ('G', np.ubyte, 1),
    ('B', np.ubyte, 1),
    ('A', np.ubyte, 1),
])

#%% 24. Multiply a 5x3 matrix by a 3x2 matrix (real matrix product) (★☆☆)
mat_1 = np.random.rand(5,3)
mat_2 = np.random.rand(3,2)
np.dot(mat_1, mat_2) # NOTE mat_1 @ mat_2 also works

#%% 25. Given a 1D array, negate all elements which are between 3 and 8, in place. (★☆☆)
arr = np.random.randint(10, size=20)
np.negative(arr, where=np.logical_and(arr>=3, arr<=8), out=arr)
arr

#%% 26. What is the output of the following script? (★☆☆)
print(sum(range(5),-1)) # 9
from numpy import *
print(sum(range(5),-1)) # NOTE: 10 because np.sum(a, axis)

#%%  27. Consider an integer vector Z, which of these expressions are legal? (★☆☆)
Z = np.random.randint(10, size=10)
print(Z)
Z**Z # legal
2 << Z >> 2 # legal NOTE: shift bytes
Z <- Z # legal NOTE: comparison
1j*Z # legal NOTE: complex number
Z/1/1 # legal
Z<Z>Z # illegal NOTE: need any or all

#%% 28. What are the result of the following expressions?
np.array(0) / np.array(0) # nan
np.array(0) // np.array(0) # 0
np.array([np.nan]).astype(int).astype(float) # array([-2.14748365e+09]) NOTE:

#%% 29. How to round away from zero a float array ? (★☆☆)
# NOTE
arr = np.random.uniform(-10,10,10)
np.copysign(np.ceil(np.abs(arr)), arr)

#%% 30. How to find common values between two arrays? (★☆☆)
arr_1 = np.random.randint(10, size=5)
arr_2 = np.random.randint(10, size=5)
np.intersect1d(arr_1, arr_2) # NOTE: numpy implementation
set(arr_1).intersection(set(arr_2)) # NOTE: python equivalent

#%% 31. How to ignore all numpy warnings (not recommended)? (★☆☆)
np.seterr(all="ignore") # NOTE: suicide

#%% 32. Is the following expressions true? (★☆☆)
np.sqrt(-1) == np.emath.sqrt(-1) # False

#%% 33. How to get the dates of yesterday, today and tomorrow? (★☆☆)
np.datetime64('today', 'D') - np.timedelta64(1, 'D')
np.datetime64('today', 'D')
np.datetime64('today', 'D') + np.timedelta64(1, 'D')

#%% 34. How to get all the dates corresponding to the month of July 2016? (★★☆)
np.arange('2016-07-01', '2016-08-01', dtype='datetime64') # NOTE: yes you can do this...

#%% 35. How to compute ((A+B)*(-A/2)) in place (without copy)? (★★☆)
A = np.random.rand(2,2)
B = np.random.rand(2,2)
np.add(A, B, out=B) # B = (A+B)
np.divide(A, 2, out=A) # A = A/2
np.negative(A, out=A) # A = -A/2
np.multiply(A, B, out=A) # A = (A+B)*(-A/2)

#%% 36. Extract the integer part of a random array using 5 different methods (★★☆)
arr = np.random.rand(10) * 20
print(arr)
arr.astype(int) # 1
arr//1 # 2
np.floor(arr) # 3
arr - arr%1 # 4
np.trunc(arr) # 5 NOTE: discard decimals regard less the sign

#%% 37. Create a 5x5 matrix with row values ranging from 0 to 4 (★★☆)
np.repeat(np.arange(5).reshape(1,5), 5, axis=0)

#%% 38. Consider a generator function that generates 10 integers and use it to build an array (★☆☆)
def gen():
    while True:
        yield np.random.randint(10)
# NOTE: build date from generator
np.fromiter(gen(), dtype=np.int32, count=10) # count means number of elements to read
#%% 39. Create a vector of size 10 with values ranging from 0 to 1, both excluded (★★☆)
# Not really sure what this is asking
np.linspace(0,10,12)[1:-1]

#%% 40. Create a random vector of size 10 and sort it (★★☆)
arr = np.random.rand(10)
arr.sort()
arr

#%% 41. How to sum a small array faster than np.sum? (★★☆)
# NOTE: reduce is faster than sum because sum callles add.reduce...
arr = np.random.rand(10)
np.add.reduce(arr)

#%% 42. Consider two random array A and B, check if they are equal (★★☆)
A = np.ones(5)
B = np.ones(6)
np.sum(A!=B) == 0
np.array_equal(A,B) # NOTE: numpy way, check shape and elements

#%% 43. Make an array immutable (read-only) (★★☆)
arr = np.random.rand(10)
arr.flags.writeable = False # NOTE

#%% 44. Consider a random 10x2 matrix representing cartesian coordinates, convert them to polar coordinates (★★☆)
arr = np.random.rand(10,2)
np.concatenate([np.sqrt(arr[:,0]**2+arr[:,1]**2).reshape(-1,1), np.arctan(arr[:,0]/arr[:,1]).reshape(-1,1)], axis=1)

#%% 45. Create random vector of size 10 and replace the maximum value by 0 (★★☆)
arr = np.random.rand(10)
arr[arr==arr.max()] = 0
arr[arr.argmax()] = 0 # NOTE better solutions
arr

#%% 46. Create a structured array with `x` and `y` coordinates covering the [0,1]x[0,1] area (★★☆)
arr = np.zeros((5,5), [('x', float), ('y', float)])
arr['x'], arr['y'] = np.meshgrid(np.linspace(0,1,5),np.linspace(0,1,5))
arr

#%% 47. Given two arrays, X and Y, construct the Cauchy matrix C (Cij =1/(xi - yj))
X = np.random.rand(10)
Y = np.random.rand(10)
1/np.subtract.outer(X,Y) # NOTE: returns a matrix

#%% 48. Print the minimum and maximum representable value for each numpy scalar type (★★☆)
for dtype in [np.int8, np.int32, np.int64]:
   print(np.iinfo(dtype).min, np.iinfo(dtype).max)
for dtype in [np.float32, np.float64]:
   print(np.finfo(dtype).min, np.finfo(dtype).max)

#%% 49. How to print all the values of an array? (★★☆)
np.set_printoptions(threshold=np.nan) # NOTE config

#%% 50. How to find the closest value (to a given scalar) in a vector? (★★☆)
arr = np.random.rand(10)
scalar = 0.2
arr[np.argmin(np.abs(arr-scalar))]

#%% 51. Create a structured array representing a position (x,y) and a color (r,g,b) (★★☆)
np.zeros(1, [
    ('pos', [
        ('x', np.uint, 1),
        ('y', np.uint, 1),
    ]),
    ('color', [
        ('r', np.uint, 1),
        ('g', np.uint, 1),
        ('b', np.uint, 1),
    ])
])

#%% 52. Consider a random vector with shape (100,2) representing coordinates, find point by point distances (★★☆)

#%% 53. How to convert a float (32 bits) array into an integer (32 bits) in place?
np.zeros(10, dtype=np.float32).astype(np.int32, copy=False)

#%% 54. How to read the following file? (★★☆)
# NOTE: read from text file
from io import StringIO
string = StringIO("1, 2, 3, 4, 5\n6,  ,  , 7, 8\n,  , 9,10,11")
np.genfromtxt(string, delimiter=',')

#%% 55. What is the equivalent of enumerate for numpy arrays? (★★☆)
arr = np.random.rand(3,4)
for i, v in np.ndenumerate(arr):
    print (i, v)

#%% 56. Generate a generic 2D Gaussian-like array (★★☆)

#%% 57. How to randomly place p elements in a 2D array? (★★☆)
arr = np.zeros((4,5))
mask = np.random.choice(np.arange(20), 3, replace=False)
np.put(arr, mask, 1) # NOTE: Replaces specified elements of an array with given values. Works on the flattened target array.
arr

#%% 58. Subtract the mean of each row of a matrix (★★☆)
arr = np.random.rand(4,5)
arr - arr.mean(axis=1, keepdims=True) # NOTE axis=1 is row

#%% 59. How to sort an array by the nth column? (★★☆)
arr = np.random.rand(4,5)
arr[arr[:,2].argsort()] # Returns the indices that would sort an array.

#%% 60. How to tell if a given 2D array has null columns? (★★☆)
arr = np.array([np.nan, 1])
sum(np.isnan(arr)) > 0

#%% 61. Find the nearest value from a given value in an array (★★☆)
arr = np.random.rand(20)
val = 0.2
arr[np.abs(arr-val).argmin()]

#%% 62. Considering two arrays with shape (1,3) and (3,1), how to compute their sum using an iterator? (★★☆)

#%% 63. Create an array class that has a name attribute (★★☆)

#%% 64. Consider a given vector, how to add 1 to each element indexed by a second vector (be careful with repeated indices)? (★★★)

#%% 65. How to accumulate elements of a vector (X) to an array (F) based on an index list (I)? (★★★)

#%% 66. Considering a (w,h,3) image of (dtype=ubyte), compute the number of unique colors (★★★)

#%% 67. Considering a four dimensions array, how to get sum over the last two axis at once? (★★★)

#%% 68. Considering a one-dimensional vector D, how to compute means of subsets of D using a vector S of same size describing subset  indices? (★★★)

#%% 69. How to get the diagonal of a dot product? (★★★)

#%% 70. Consider the vector \[1, 2, 3, 4, 5\], how to build a new vector with 3 consecutive zeros interleaved between each value? (★★★)

#%% [markdown]
# #### 71. Consider an array of dimension (5,5,3), how to mulitply it by an array with dimensions (5,5)? (★★★)

#%%

#%% [markdown]
# #### 72. How to swap two rows of an array? (★★★)

#%%

#%% [markdown]
# #### 73. Consider a set of 10 triplets describing 10 triangles (with shared vertices), find the set of unique line segments composing all the  triangles (★★★)

#%%

#%% [markdown]
# #### 74. Given an array C that is a bincount, how to produce an array A such that np.bincount(A) == C? (★★★)

#%%

#%% [markdown]
# #### 75. How to compute averages using a sliding window over an array? (★★★)

#%%

#%% [markdown]
# #### 76. Consider a one-dimensional array Z, build a two-dimensional array whose first row is (Z\[0\],Z\[1\],Z\[2\]) and each subsequent row is  shifted by 1 (last row should be (Z\[-3\],Z\[-2\],Z\[-1\]) (★★★)

#%%

#%% [markdown]
# #### 77. How to negate a boolean, or to change the sign of a float inplace? (★★★)

#%%

#%% [markdown]
# #### 78. Consider 2 sets of points P0,P1 describing lines (2d) and a point p, how to compute distance from p to each line i  (P0\[i\],P1\[i\])? (★★★)

#%%

#%% [markdown]
# #### 79. Consider 2 sets of points P0,P1 describing lines (2d) and a set of points P, how to compute distance from each point j (P\[j\]) to each line i (P0\[i\],P1\[i\])? (★★★)

#%%

#%% [markdown]
# #### 80. Consider an arbitrary array, write a function that extract a subpart with a fixed shape and centered on a given element (pad with a `fill` value when necessary) (★★★)

#%%

#%% [markdown]
# #### 81. Consider an array Z = \[1,2,3,4,5,6,7,8,9,10,11,12,13,14\], how to generate an array R = \[\[1,2,3,4\], \[2,3,4,5\], \[3,4,5,6\], ..., \[11,12,13,14\]\]? (★★★)

#%%

#%% [markdown]
# #### 82. Compute a matrix rank (★★★)

#%%

#%% [markdown]
# #### 83. How to find the most frequent value in an array?

#%%

#%% [markdown]
# #### 84. Extract all the contiguous 3x3 blocks from a random 10x10 matrix (★★★)

#%%

#%% [markdown]
# #### 85. Create a 2D array subclass such that Z\[i,j\] == Z\[j,i\] (★★★)

#%%

#%% [markdown]
# #### 86. Consider a set of p matrices wich shape (n,n) and a set of p vectors with shape (n,1). How to compute the sum of of the p matrix products at once? (result has shape (n,1)) (★★★)

#%%

#%% [markdown]
# #### 87. Consider a 16x16 array, how to get the block-sum (block size is 4x4)? (★★★)

#%%

#%% [markdown]
# #### 88. How to implement the Game of Life using numpy arrays? (★★★)

#%%

#%% [markdown]
# #### 89. How to get the n largest values of an array (★★★)

#%%

#%% [markdown]
# #### 90. Given an arbitrary number of vectors, build the cartesian product (every combinations of every item) (★★★)

#%%

#%% [markdown]
# #### 91. How to create a record array from a regular array? (★★★)

#%%

#%% [markdown]
# #### 92. Consider a large vector Z, compute Z to the power of 3 using 3 different methods (★★★)

#%%

#%% [markdown]
# #### 93. Consider two arrays A and B of shape (8,3) and (2,2). How to find rows of A that contain elements of each row of B regardless of the order of the elements in B? (★★★)

#%%

#%% [markdown]
# #### 94. Considering a 10x3 matrix, extract rows with unequal values (e.g. \[2,2,3\]) (★★★)

#%%

#%% [markdown]
# #### 95. Convert a vector of ints into a matrix binary representation (★★★)

#%%

#%% [markdown]
# #### 96. Given a two dimensional array, how to extract unique rows? (★★★)

#%%

#%% [markdown]
# #### 97. Considering 2 vectors A & B, write the einsum equivalent of inner, outer, sum, and mul function (★★★)

#%%

#%% [markdown]
# #### 98. Considering a path described by two vectors (X,Y), how to sample it using equidistant samples (★★★)?

#%%

#%% [markdown]
# #### 99. Given an integer n and a 2D array X, select from X the rows which can be interpreted as draws from a multinomial distribution with n degrees, i.e., the rows which only contain integers and which sum to n. (★★★)

#%%

#%% [markdown]
# #### 100. Compute bootstrapped 95% confidence intervals for the mean of a 1D array X (i.e., resample the elements of an array with replacement N times, compute the mean of each sample, and then compute percentiles over the means). (★★★)

#%%


"""
求矩阵的范数

"""
import numpy as np

def matrix_norm(X, ord):
    """
    求矩阵的范数
    :param X: 待求的矩阵
    :param ord: 范数类型，无穷大用np.inf表示
    :return: 范数
    """
    res = np.linalg.norm(X, ord)
    print("其{}范数值为：".format(ord))
    print(res)
    return res

if __name__ == '__main__':
    B = np.array([
        [0, 3/8, -2/8],
        [-4/11, 0, 1/11],
        [-6/12, -3/12, 0]
    ])
    matrix_norm(B, np.inf)


# -------------------------------------------------------------------------------------------------------------------- #
# Import packages
# -------------------------------------------------------------------------------------------------------------------- #
import numpy as np
import scipy.special
import scipy.integrate
import matplotlib as mpl
import matplotlib.pyplot as plt

from mpl_toolkits    import mplot3d
from .nurbs_basis_functions    import compute_basis_polynomials, compute_basis_polynomials_derivatives

# -------------------------------------------------------------------------------------------------------------------- #
# Define the NURBS curve class
# -------------------------------------------------------------------------------------------------------------------- #
class NurbsCurve:

    """ Create a NURBS (Non-Uniform Rational Basis Spline) curve object

        Parameters
        ----------
        control_points : ndarray with shape (ndim, n+1)
            Array containing the coordinates of the control points
            The first dimension of ´P´ spans the coordinates of the control points (any number of dimensions)
            The second dimension of ´P´ spans the u-direction control points (0,1,...,n)

        weights : ndarray with shape (n+1,)
            Array containing the weight of the control points

        degree : int
            Degree of the basis polynomials

        knots : ndarray with shape (r+1=n+p+2,)
            The knot vector in the u-direction
            Set the multiplicity of the first and last entries equal to ´p+1´ to obtain a clamped NURBS

        Notes
        -----
        This class includes methods to compute:

            - Curve coordinates for any number of dimensions
            - Analytic curve derivatives of any order and number of dimensions
            - The unitary tangent, normal and binormal vectors (Frenet-Serret reference frame) in 2D and 3D
            - The analytic curvature and torsion in 2D and 3D
            - The arc length of the curve in any number of dimensions.
                The arc length is compute by numerical quadrature using analytic derivative information

        The class can be used to represent polynomial and rational Bézier, B-Spline and NURBS curves
        The type of curve depends on the initialization arguments

            - Polymnomial Bézier: Provide the array of control points
            - Rational Bézier:    Provide the arrays of control points and weights
            - B-Spline:           Provide the array of control points, degree and knot vector
            - NURBS:              Provide the arrays of control points and weights, degree and knot vector

        In addition, this class supports operations with real and complex numbers
        The data type used for the computations is detected from the data type of the arguments
        Using complex numbers can be useful to compute the derivative of the shape using the complex step method

        References
        ----------
        The NURBS Book. See references to equations and algorithms throughout the code
        L. Piegl and W. Tiller
        Springer, second edition

        Curves and Surfaces for CADGD. See references to equations the source code
        G. Farin
        Morgan Kaufmann Publishers, fifth edition

        All references correspond to The NURBS book unless it is explicitly stated that they come from Farin's book

    """

    # ---------------------------------------------------------------------------------------------------------------- #
    # Select the type of curve based on the initialization arguments
    # ---------------------------------------------------------------------------------------------------------------- #
    def __init__(self, control_points=None, weights=None, degree=None, knots=None):

        # Void initialization
        if control_points is None and weights is None and degree is None and knots is None:
            pass

        # Polynomial Bezier curve initialization
        elif weights is None and degree is None and knots is None:

            # Set the curve type flag
            self.curve_type = 'Bezier'

            # Set the number of dimensions of the problem
            self.ndim = np.shape(control_points)[0]

            # Maximum index of the control points (counting from zero)
            n = np.shape(control_points)[1] - 1

            # Define the weight of the control points
            weights = np.ones((n + 1), dtype=control_points.dtype)

            # Define the order of the basis polynomials
            degree = n

            # Define the knot vector (clamped spline)
            knots = np.concatenate((np.zeros(degree), np.linspace(0, 1, n - degree + 2), np.ones(degree)))

        # Rational Bezier curve initialization
        elif degree is None and knots is None:

            # Set the curve type flag
            self.curve_type = 'R-Bezier'

            # Set the number of dimensions of the problem
            self.ndim = np.shape(control_points)[0]

            # Maximum index of the control points (counting from zero)
            n = np.shape(control_points)[1] - 1

            # Define the order of the basis polynomials
            degree = n

            # Define the knot vector (clamped spline)
            knots = np.concatenate((np.zeros(degree), np.linspace(0, 1, n - degree + 2), np.ones(degree)))

        # B-Spline curve initialization (both degree and knot vector are provided)
        elif weights is None and knots is not None:

            # Set the curve type flag
            self.curve_type = 'B-Spline'

            # Set the number of dimensions of the problem
            self.ndim = np.shape(control_points)[0]

            # Maximum index of the control points (counting from zero)
            n = np.shape(control_points)[1] - 1

            # Define the weight of the control points
            weights = np.ones((n + 1), dtype=control_points.dtype)

        # B-Spline curve initialization (degree is given but the knot vector is not provided)
        elif weights is None and knots is None:

            # Set the curve type flag
            self.curve_type = 'B-Spline'

            # Set the number of dimensions of the problem
            self.ndim = np.shape(control_points)[0]

            # Maximum index of the control points (counting from zero)
            n = np.shape(control_points)[1] - 1

            # Define the knot vector (clamped spline)
            knots = np.concatenate((np.zeros(degree), np.linspace(0, 1, n - degree + 2), np.ones(degree)))

            # Define the weight of the control points
            weights = np.ones((n + 1), dtype=control_points.dtype)

        # NURBS curve initialization
        else:

            # Set the curve type flag
            self.curve_type = 'NURBS'

            # Set the number of dimensions of the problem
            self.ndim = np.shape(control_points)[0]

        # Declare input variables as instance variables
        self.P = control_points
        self.W = weights
        self.p = degree
        self.U = knots

        # Knot vector in OpenCascade format
        self.U_values, self.U_mults = np.unique(self.U, return_counts=True)

    # ---------------------------------------------------------------------------------------------------------------- #
    # Compute the coordinates of the curve
    # ---------------------------------------------------------------------------------------------------------------- #
    def get_value(self, u):

        """ Evaluate the coordinates of the curve for the input u parametrization

        Parameters
        ----------
        u : scalar or ndarray with shape (N,)
            Parameter used to evaluate the curve

        Returns
        -------
        C : ndarray with shape (ndim, N)
            Array containing the coordinates of the curve
            The first dimension of ´C´ spans the ´(x,y,z)´ coordinates
            The second dimension of ´C´ spans the ´u´ parametrization sample points

        """

        # Evaluate the NURBS curve for the the input u-parametrization
        C = self.compute_nurbs_coordinates(self.P, self.W, self.p, self.U, u)

        return C

    @staticmethod
    def compute_nurbs_coordinates(P, W, p, U, u):

        """ Evaluate the coordinates of a NURBS curve for the input u parametrization

        This function computes the coordinates of the NURBS curve in homogeneous space using equation 4.5 and then
        maps the coordinates to ordinary space using the perspective map given by equation 1.16. See algorithm A4.1

        Parameters
        ----------
        P : ndarray with shape (ndim, n+1)
            Array containing the coordinates of the control points
            The first dimension of ´P´ spans the coordinates of the control points (any number of dimensions)
            The second dimension of ´P´ spans the u-direction control points ´(0,1,...,n)´

        W : ndarray with shape (n+1,)
            Array containing the weight of the control points

        p : int
            Degree of the B-Spline basis polynomials

        U : ndarray with shape (r+1=n+p+2,)
            Knot vector in the u-direction
            Set the multiplicity of the first and last entries equal to ´p+1´ to obtain a clamped spline

        u : scalar or ndarray with shape (N,)
            Parameter used to evaluate the curve

        Returns
        -------
        C : ndarray with shape (ndim, N)
            Array containing the coordinates of the curve
            The first dimension of ´C´ spans the ´(x,y,z)´ coordinates
            The second dimension of ´C´ spans the ´u´ parametrization sample points

        """

        # Check the shape of the input parameters
        if P.ndim > 2:           raise Exception('P must be an array of shape (ndim, n+1)')
        if W.ndim > 1:           raise Exception('W must be an array of shape (n+1,)')
        if not np.isscalar(p):   raise Exception('p must be an scalar')
        if U.ndim > 1:           raise Exception('U must be an array of shape (r+1=n+p+2,)')
        if np.isscalar(u):       u = np.asarray(u)
        elif u.ndim > 1:         raise Exception('u must be a scalar or an array of shape (N,)')

        # Shape of the array of control points
        n_dim, nn = np.shape(P)

        # Highest index of the control points (counting from zero)
        n = nn - 1

        # Compute the B-Spline basis polynomials
        N_basis = compute_basis_polynomials(n, p, U, u)

        # Map the control points to homogeneous space | P_w = (x*w,y*w,z*w,w)
        P_w = np.concatenate((P * W[np.newaxis, :], W[np.newaxis, :]), axis=0)

        # Compute the coordinates of the NURBS curve in homogeneous space
        # The summations over n is performed exploiting matrix multiplication (vectorized code)
        C_w = np.dot(P_w, N_basis)

        # Map the coordinates back to the ordinary space
        C = C_w[0:-1,:]/C_w[-1, :]

        return C

    @staticmethod
    def compute_bspline_coordinates(P, p, U, u):

        """ Evaluate the coordinates of a B-Spline curve for the input u parametrization

        This function computes the coordinates of a B-Spline curve as given by equation 3.1. See algorithm A3.1

        Parameters
        ----------
        P : ndarray with shape (ndim, n+1)
            Array containing the coordinates of the control points
            The first dimension of ´P´ spans the coordinates of the control points (any number of dimensions)
            The second dimension of ´P´ spans the u-direction control points ´(0,1,...,n)´

        p : int
            Degree of the B-Spline basis polynomials

        U : ndarray with shape (r+1=n+p+2,)
            Knot vector in the u-direction
            Set the multiplicity of the first and last entries equal to ´p+1´ to obtain a clamped spline

        u : scalar or ndarray with shape (N,)
            Parameter used to evaluate the curve

        Returns
        -------
        C : ndarray with shape (ndim, N)
            Array containing the coordinates of the curve
            The first dimension of ´C´ spans the ´(x,y,z)´ coordinates
            The second dimension of ´C´ spans the ´u´ parametrization sample points

        """

        # Check the shape of the input parameters
        if P.ndim > 2:           raise Exception('P must be an array of shape (ndim, n+1)')
        if not np.isscalar(p):   raise Exception('p must be an scalar')
        if U.ndim > 1:           raise Exception('U must be an array of shape (r+1=n+p+2,)')
        if np.isscalar(u):       u = np.asarray(u)
        elif u.ndim > 1:         raise Exception('u must be a scalar or an array of shape (N,)')

        # Highest index of the control points (counting from zero)
        n = np.shape(P)[1] - 1

        # Compute the B-Spline basis polynomials
        N_basis = compute_basis_polynomials(n, p, U, u)

        # Compute the coordinates of the B-Spline curve
        # The summation over n is performed exploiting matrix multiplication (vectorized code)
        C = np.dot(P, N_basis)

        return C

    # ---------------------------------------------------------------------------------------------------------------- #
    # Compute the derivatives of the curve
    # ---------------------------------------------------------------------------------------------------------------- #
    def get_derivative(self, u, order):

        """ Evaluate the derivative of the curve for the input u-parametrization

        Parameters
        ----------
        u : scalar or ndarray with shape (N,)
            Scalar or array containing the u-parameter used to evaluate the curve

        order : integer
            Order of the derivative

        Returns
        -------
        dC : ndarray with shape (ndim, N)
            Array containing the derivative of the desired order
            The first dimension of ´dC´ spans the ´(x,y,z)´ coordinates
            The second dimension of ´dC´ spans the ´u´ parametrization sample points

        """

        # Compute the array of curve derivatives up to order `derivative_order` and slice the desired values
        dC = self.compute_nurbs_derivatives(self.P, self.W, self.p, self.U, u, order)[order, ...]

        return dC

    def compute_nurbs_derivatives(self, P, W, p, U, u, up_to_order):

        """ Compute the derivatives of a NURBS curve in ordinary space up to the desired order

        This function computes the analytic derivatives of the NURBS curve in ordinary space using equation 4.8 and
        the derivatives of the NURBS curve in homogeneous space obtained from compute_bspline_derivatives()

        The derivatives are computed recursively in a fashion similar to algorithm A4.2

        Parameters
        ----------
        P : ndarray with shape (ndim, n+1)
            Array containing the coordinates of the control points
            The first dimension of ´P´ spans the coordinates of the control points ´(x,y,z)´
            The second dimension of ´P´ spans the u-direction control points ´(0,1,...,n)´

        W : ndarray with shape (n+1,)
            Array containing the weight of the control points

        p : int
            Degree of the B-Spline basis polynomials

        U : ndarray with shape (r+1=n+p+2,)
            Knot vector in the u-direction
            Set the multiplicity of the first and last entries equal to ´p+1´ to obtain a clamped spline

        u : scalar or ndarray with shape (N,)
            Parameter used to evaluate the curve

        up_to_order : integer
            Order of the highest derivative

        Returns
        -------
        nurbs_derivatives: ndarray of shape (up_to_order+1, ndim, Nu)
            The first dimension spans the order of the derivatives (0, 1, 2, ...)
            The second dimension spans the coordinates (x,y,z,...)
            The third dimension spans u-parametrization sample points

        """

        # Set the data type used to initialize arrays (set `complex` if an argument is complex and `float` if not)
        u = np.asarray(u)
        if (P.dtype == np.complex128) or (W.dtype == np.complex128) or (u.dtype == np.complex128):
            data_type = np.complex128
        else:
            data_type = np.float64

        # Map the control points to homogeneous space | P_w = (x*w,y*w,z*w,w)
        P_w = np.concatenate((P * W[np.newaxis, :], W[np.newaxis, :]), axis=0)

        # Compute the derivatives of the NURBS curve in homogeneous space
        bspline_derivatives = self.compute_bspline_derivatives(P_w, p, U, u, up_to_order)
        A_ders = bspline_derivatives[:, 0:-1, :]
        w_ders = bspline_derivatives[:, [-1], :]

        # Initialize array of derivatives
        n_dim, Nu = np.shape(P)[0], np.asarray(u).size                               # Get sizes
        nurbs_derivatives = np.zeros((up_to_order+1, n_dim, Nu), dtype=data_type)    # Initialize array with zeros

        # Compute the derivatives of up to the desired order
        # See algorithm A4.2 from the NURBS book
        for order in range(up_to_order+1):

            # Update the numerator of equation 4.8 recursively
            temp_numerator = A_ders[[order], ...]
            for i in range(1, order+1):
                temp_numerator -= scipy.special.binom(order, i) * w_ders[[i], ...] * nurbs_derivatives[[order-i], ...]

            # Compute the k-th order NURBS curve derivative
            nurbs_derivatives[order, ...] = temp_numerator/w_ders[[0], ...]

        return nurbs_derivatives

    @staticmethod
    def compute_bspline_derivatives(P, p, U, u, up_to_order):

        """ Compute the derivatives of a B-Spline (or NURBS curve in homogeneous space) up to order `derivative_order`

        This function computes the analytic derivatives of a B-Spline curve using equation 3.3. See algorithm A3.2

        Parameters
        ----------
        P : ndarray with shape (ndim, n+1)
            Array containing the coordinates of the control points
            The first dimension of ´P´ spans the coordinates of the control points ´(x,y,z)´
            The second dimension of ´P´ spans the u-direction control points ´(0,1,...,n)´

        p : int
            Degree of the B-Spline basis polynomials

        U : ndarray with shape (r+1=n+p+2,)
            Knot vector in the u-direction
            Set the multiplicity of the first and last entries equal to ´p+1´ to obtain a clamped spline

        u : scalar or ndarray with shape (N,)
            Parameter used to evaluate the curve

        up_to_order : integer
            Order of the highest derivative

        Returns
        -------
        bspline_derivatives: ndarray of shape (up_to_order+1, ndim, Nu)
            The first dimension spans the order of the derivatives (0, 1, 2, ...)
            The second dimension spans the coordinates (x,y,z,...)
            The third dimension spans u-parametrization sample points

        """

        # Set the data type used to initialize arrays (set `complex` if an argument is complex and `float` if not)
        u = np.asarray(u)
        if (P.dtype == np.complex128) or (u.dtype == np.complex128):
            data_type = np.complex128
        else:
            data_type = np.float64

        # Set the B-Spline coordinates as the zero-th derivative
        n_dim, Nu = np.shape(P)[0], np.asarray(u).size
        bspline_derivatives = np.zeros((up_to_order+1, n_dim, Nu), dtype=data_type)

        # Compute the derivatives of up to the desired order (start at index 1 and end at index `p`)
        # See algorithm A3.2 from the NURBS book
        for order_u in range(min(p, up_to_order) + 1):

            # Highest index of the control points (counting from zero)
            n = np.shape(P)[1] - 1

            # Compute the B-Spline basis polynomials
            N_basis = compute_basis_polynomials_derivatives(n, p, U, u, order_u)

            # Compute the coordinates of the B-Spline
            # The summations over n is performed exploiting matrix multiplication (vectorized code)
            bspline_derivatives[order_u, :, :] = np.dot(P, N_basis)

        # Note that derivative with order higher than `p` are not computed and are be zero from initialization
        # These zero-derivatives are required to compute the higher order derivatives of rational curves

        return bspline_derivatives

    # ---------------------------------------------------------------------------------------------------------------- #
    # Miscellaneous methods
    # ---------------------------------------------------------------------------------------------------------------- #
    def attach_nurbs(self, new_nurbs):

        """ Attatch a new NURBS curve to the end of the instance NURBS curve and return the merged NURBS curve"""

        # Check that the NURBS curves have the same degree
        if self.p != new_nurbs.p:
            raise Exception("In order to merge, the two NURBS curves must have the same degree")

        # Combine the control points
        P = np.concatenate((self.P, new_nurbs.P), axis=1)

        # Combine the control point weights
        W = np.concatenate((self.W, new_nurbs.W), axis=0)

        # Highest index of the control points
        n1 = np.shape(self.P)[1] - 1
        n2 = np.shape(new_nurbs.P)[1] - 1

        # Combine the knot vectors (inner knot has p+1 multiplicity)
        eps = 0
        U_start = np.zeros((self.p + 1,))
        U_end = np.ones((self.p + 1,))
        U_mid = np.ones((self.p + 1,)) / 2
        U_mid[0] = U_mid[0] - eps       # Quick and dirty fix for GMSH (avoid multiplicity equal to degree)
        U_mid[-1] = U_mid[-1] + eps     # Quick and dirty fix for GMSH (avoid multiplicity equal to degree)
        U1 = 0.00 + self.U[self.p + 1:n1 + 1] / 2
        U2 = 0.50 + new_nurbs.U[self.p + 1:n2 + 1] / 2
        U = np.concatenate((U_start, U1, U_mid, U2, U_end))

        # Create the merged NURBS curve
        mergedNurbs = NurbsCurve(control_points=P, weights=W, degree=self.p, knots=U)

        return mergedNurbs

    # ---------------------------------------------------------------------------------------------------------------- #
    # Compute the Frenet-Serret unitary vectors
    # ---------------------------------------------------------------------------------------------------------------- #
    def get_tangent(self, u):

        """ Evaluate the unitary tangent vector to the curve for the input u-parametrization

        The definition of the unitary tangent vector is given by equation 10.5 (Farin's textbook)

        Parameters
        ----------
        u : scalar or ndarray with shape (N,)
            Scalar or array containing the u-parameter used to evaluate the function

        Returns
        -------
        tangent : ndarray with shape (ndim, N)
            Array containing the unitary tangent vector
            The first dimension of ´tangent´ spans the ´(x,y,z)´ coordinates
            The second dimension of ´tangent´ spans the ´u´ parametrization sample points

        """

        # Compute the curve derivatives
        dC, = self.compute_nurbs_derivatives(self.P, self.W, self.p, self.U, u, up_to_order=1)[[1], ...]

        # Compute the tangent vector
        numerator = dC
        denominator = (np.sum(numerator ** 2, axis=0)) ** (1 / 2)
        tangent = numerator / denominator

        return tangent

    def get_normal(self, u):

        """ Evaluate the unitary normal vector to the curve for the input u-parametrization

        The definition of the unitary normal vector is given by equation 10.5 (Farin's textbook)

        Parameters
        ----------
        u : scalar or ndarray with shape (N,)
            Scalar or array containing the u-parameter used to evaluate the function

        Returns
        -------
        normal : ndarray with shape (ndim, N)
            Array containing the unitary normal vector
            The first dimension of ´normal´ spans the ´(x,y,z)´ coordinates
            The second dimension of ´normal´ spans the ´u´ parametrization sample points

        """

        # Compute the curve derivatives
        dC, ddC = self.compute_nurbs_derivatives(self.P, self.W, self.p, self.U, u, up_to_order=2)[[1, 2], ...]

        # Compute the normal vector
        if self.ndim == 2:
            dC = np.concatenate((dC, np.zeros((1, np.asarray(u).size))), axis=0)
            ddC = np.concatenate((ddC, np.zeros((1, np.asarray(u).size))), axis=0)
            numerator = np.cross(dC, np.cross(ddC, dC, axisa=0, axisb=0, axisc=0), axisa=0, axisb=0, axisc=0)
            denominator = (np.sum(numerator ** 2, axis=0)) ** (1 / 2)
            normal = (numerator / denominator)[[0, 1], ...]

        elif self.ndim == 3:
            numerator = np.cross(dC, np.cross(ddC, dC, axisa=0, axisb=0, axisc=0), axisa=0, axisb=0, axisc=0)
            denominator = (np.sum(numerator ** 2, axis=0)) ** (1 / 2)
            normal = numerator / denominator

        else: raise Exception("The number of dimensions must be 2 or 3")

        return normal

    def get_binormal(self, u):

        """ Evaluate the unitary binormal vector to the curve for the input u-parametrization

        The definition of the unitary binormal vector is given by equation 10.5 (Farin's textbook)

        Parameters
        ----------
        u : scalar or ndarray with shape (N,)
            Scalar or array containing the u-parameter used to evaluate the function

        Returns
        -------
        binormal : ndarray with shape (ndim, N)
            Array containing the unitary binormal vector
            The first dimension of ´binormal´ spans the ´(x,y,z)´ coordinates
            The second dimension of ´binormal´ spans the ´u´ parametrization sample points

        """

        # Compute the curve derivatives
        dC, ddC = self.compute_nurbs_derivatives(self.P, self.W, self.p, self.U, u, up_to_order=2)[[1, 2], ...]

        # Compute the binormal vector
        if self.ndim == 2:
            dC = np.concatenate((dC, np.zeros((1, np.asarray(u).size))), axis=0)
            ddC = np.concatenate((ddC, np.zeros((1, np.asarray(u).size))), axis=0)
            numerator = np.cross(dC, ddC, axisa=0, axisb=0, axisc=0)
            denominator = (np.sum(numerator ** 2, axis=0)) ** (1 / 2)
            binormal = (numerator / denominator)[[0, 1], ...]

        elif self.ndim == 3:
            numerator = np.cross(dC, ddC, axisa=0, axisb=0, axisc=0)
            denominator = (np.sum(numerator ** 2, axis=0)) ** (1 / 2)
            binormal = numerator / denominator

        else: raise Exception("The number of dimensions must be 2 or 3")

        return binormal

    def get_normal_2D(self, u):

        """ Evaluate the the unitary normal vector using the special formula 2D formula

        Parameters
        ----------
        u : scalar or ndarray with shape (N,)
            Scalar or array containing the u-parameter used to evaluate the function

        Returns
        -------
        normal : ndarray with shape (2, N)
            Array containing the unitary normal vector
            The first dimension of ´normal´ spans the ´(x,y,z)´ coordinates
            The second dimension of ´normal´ spans the ´u´ parametrization sample points

        """

        # Compute the curve derivatives
        u = np.asarray(u)
        dC = self.compute_nurbs_derivatives(self.P, self.W, self.p, self.U, u, up_to_order=1)[1, ...]

        # Compute the normal vector
        if self.ndim == 2:
            numerator = np.concatenate((-dC[[1], :], dC[[0], :]), axis=0)
            denominator = (np.sum(numerator ** 2, axis=0)) ** (1 / 2)
            normal = numerator / denominator

        else: raise Exception("The number of dimensions must be 2")

        return normal

    # ---------------------------------------------------------------------------------------------------------------- #
    # Compute the curvature, torsion, and arc-length
    # ---------------------------------------------------------------------------------------------------------------- #
    def get_curvature(self, u):

        """ Evaluate the curvature of the curve for the input u-parametrization

        The definition of the curvature is given by equation 10.7 (Farin's textbook)

        Parameters
        ----------
        u : scalar or ndarray with shape (N,)
            Scalar or array containing the u-parameter used to evaluate the curvature

        Returns
        -------
        curvature : scalar or ndarray with shape (N, )
            Scalar or array containing the curvature of the curve

        """

        # Compute the curve derivatives
        u = np.asarray(u)
        dC, ddC = self.compute_nurbs_derivatives(self.P, self.W, self.p, self.U, u, up_to_order=2)[[1, 2], ...]

        # Compute the curvature
        if self.ndim == 2:
            dC = np.concatenate((dC, np.zeros((1, np.asarray(u).size))), axis=0)
            ddC = np.concatenate((ddC, np.zeros((1, np.asarray(u).size))), axis=0)
            numerator = np.sum(np.cross(ddC, dC, axisa=0, axisb=0, axisc=0) ** 2, axis=0) ** (1 / 2)
            denominator = (np.sum(dC ** 2, axis=0)) ** (3 / 2)
            curvature = (numerator / denominator)

        elif self.ndim == 3:
            numerator = np.sum(np.cross(ddC, dC, axisa=0, axisb=0, axisc=0) ** 2, axis=0) ** (1 / 2)
            denominator = (np.sum(dC ** 2, axis=0)) ** (3 / 2)
            curvature = numerator / denominator

        else: raise Exception("The number of dimensions must be 2 or 3")

        return curvature

    def get_torsion(self, u):

        """ Evaluate the torsion of the curve for the input u-parametrization

        The definition of the torsion is given by equation 10.8 (Farin's textbook)

        Parameters
        ----------
        u : scalar or ndarray with shape (N,)
            Scalar or array containing the u-parameter used to evaluate the torsion

        Returns
        -------
        torsion : scalar or ndarray with shape (N, )
            Scalar or array containing the torsion of the curve

        """

        # Compute the curve derivatives
        u = np.asarray(u)
        dC, ddC, dddC = self.compute_nurbs_derivatives(self.P, self.W, self.p, self.U, u, up_to_order=3)[[1, 2, 3], ...]

        # Compute the torsion
        if self.ndim == 2:
            dC = np.concatenate((dC, np.zeros((1, np.asarray(u).size))), axis=0)
            ddC = np.concatenate((ddC, np.zeros((1, np.asarray(u).size))), axis=0)
            dddC = np.concatenate((dddC, np.zeros((1, np.asarray(u).size))), axis=0)
            numerator = np.sum(np.cross(dC, ddC, axisa=0, axisb=0, axisc=0) * dddC, axis=0)
            denominator = np.sum(np.cross(dC, ddC, axisa=0, axisb=0, axisc=0)**2, axis=0)
            torsion = (numerator / denominator)

        elif self.ndim == 3:
            numerator = np.sum(np.cross(dC, ddC, axisa=0, axisb=0, axisc=0) * dddC, axis=0)
            denominator = np.sum(np.cross(dC, ddC, axisa=0, axisb=0, axisc=0)**2, axis=0)
            torsion = numerator / denominator

        else: raise Exception("The number of dimensions must be 2 or 3")

        return torsion

    def get_arclength(self, u1=0.00, u2=1.00):

        """ Compute the arc length of a parametric curve in the interval [u1,u2] using numerical quadrature

        The definition of the arc length is given by equation 10.3 (Farin's textbook)

        Parameters
        ----------
        u1 : scalar
            Lower limit of integration for the arc length computation

        u2 : scalar
            Upper limit of integration for the arc length computation

        Returns
        -------
        L : scalar
            Arc length of NURBS curve in the interval [u1, u2]

        """

        # Compute the arc length differential analytically
        def get_arclegth_differential(u):
            dCdu = self.get_derivative(u, order=1)
            dLdu = np.sqrt(np.sum(dCdu ** 2, axis=0))  # dL/du = [(dx_0/du)^2 + ... + (dx_n/du)^2]^(1/2)
            return dLdu

        # Compute the arc length of C(t) in the interval [u1, u2] by numerical integration
        arclength = scipy.integrate.fixed_quad(get_arclegth_differential, u1, u2, n=10)[0]

        return arclength

    # ---------------------------------------------------------------------------------------------------------------- #
    # Plot the NURBS curve
    # ---------------------------------------------------------------------------------------------------------------- #
    def plot(self, fig=None, ax=None,
             curve=True, control_points=True, frenet_serret=False, axis_off=False, ticks_off=False):

        """ Create a plot and return the figure and axes handles """

        if fig is None:

            # One dimension (law of evolution)
            if self.ndim == 1:
                fig = plt.figure(figsize=(6, 5))
                ax = fig.add_subplot(111)
                ax.set_xlabel('$u$ parameter', fontsize=12, color='k', labelpad=12)
                ax.set_ylabel('NURBS curve value', fontsize=12, color='k', labelpad=12)
                # ax_xy.xaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
                # ax_xy.yaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
                for t in ax.xaxis.get_major_ticks(): t.label.set_fontsize(12)
                for t in ax.yaxis.get_major_ticks(): t.label.set_fontsize(12)
                if ticks_off:
                    ax.set_xticks([])
                    ax.set_yticks([])
                if axis_off:
                    ax.axis('off')

            # Two dimensions (plane curve)
            elif self.ndim == 2:
                fig = plt.figure(figsize=(6, 5))
                ax = fig.add_subplot(111)
                ax.set_xlabel('$x$ axis', fontsize=12, color='k', labelpad=12)
                ax.set_ylabel('$y$ axis', fontsize=12, color='k', labelpad=12)
                # ax_xy.xaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
                # ax_xy.yaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
                for t in ax.xaxis.get_major_ticks(): t.label.set_fontsize(12)
                for t in ax.yaxis.get_major_ticks(): t.label.set_fontsize(12)
                if ticks_off:
                    ax.set_xticks([])
                    ax.set_yticks([])
                if axis_off:
                    ax.axis('off')

            # Three dimensions (space curve)
            elif self.ndim == 3:
                fig = mpl.pyplot.figure(figsize=(6, 5))
                ax = fig.add_subplot(111, projection='3d')
                ax.view_init(azim=-120, elev=30)
                ax.grid(False)
                ax.xaxis.pane.fill = False
                ax.yaxis.pane.fill = False
                ax.zaxis.pane.fill = False
                ax.xaxis.pane.set_edgecolor('k')
                ax.yaxis.pane.set_edgecolor('k')
                ax.zaxis.pane.set_edgecolor('k')
                ax.xaxis.pane._alpha = 0.9
                ax.yaxis.pane._alpha = 0.9
                ax.zaxis.pane._alpha = 0.9
                ax.set_xlabel('$x$ axis', fontsize=12, color='k', labelpad=12)
                ax.set_ylabel('$y$ axis', fontsize=12, color='k', labelpad=12)
                ax.set_zlabel('$z$ axis', fontsize=12, color='k', labelpad=12)
                # ax_xy.xaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
                # ax_xy.yaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
                # ax_xy.zaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
                for t in ax.xaxis.get_major_ticks(): t.label.set_fontsize(8)
                for t in ax.yaxis.get_major_ticks(): t.label.set_fontsize(8)
                for t in ax.zaxis.get_major_ticks(): t.label.set_fontsize(8)
                ax.xaxis.set_rotate_label(False)
                ax.yaxis.set_rotate_label(False)
                ax.zaxis.set_rotate_label(False)
                if ticks_off:
                    ax.set_xticks([])
                    ax.set_yticks([])
                    ax.set_zticks([])
                if axis_off:
                    ax.axis('off')

            else: raise Exception('The number of dimensions must be 1, 2 or 3')

        # Add objects to the plot
        if curve:          self.plot_curve(fig, ax)
        if control_points: self.plot_control_points(fig, ax)
        if frenet_serret:  self.plot_frenet_serret(fig, ax)

        # Set the scaling of the axes
        self.rescale_plot(fig, ax)

        return fig, ax

    def plot_curve(self, fig, ax, linewidth=1.5, linestyle='-', color='black', u1=0.00, u2=1.00):

        """ Plot the coordinates of the NURBS curve """

        # One dimension (law of evolution)
        if self.ndim == 1:
            u = np.linspace(u1, u2, 501)
            X = np.real(self.get_value(u))
            line, = ax.plot(u, X[0,:])
            line.set_linewidth(linewidth)
            line.set_linestyle(linestyle)
            line.set_color(color)
            line.set_marker(' ')
            # line.set_label(' ')

        # Two dimensions (plane curve)
        elif self.ndim == 2:
            u = np.linspace(u1, u2, 501)
            X, Y = np.real(self.get_value(u))
            line, = ax.plot(X, Y)
            line.set_linewidth(linewidth)
            line.set_linestyle(linestyle)
            line.set_color(color)
            line.set_marker(' ')
            # line.set_label(' ')

        # Three dimensions (space curve)
        elif self.ndim == 3:
            u = np.linspace(u1, u2, 501)
            X, Y, Z = np.real(self.get_value(u))
            line, = ax.plot(X, Y, Z)
            line.set_linewidth(linewidth)
            line.set_linestyle(linestyle)
            line.set_color(color)
            line.set_marker(' ')
            # line.set_label(' ')

        else: raise Exception('The number of dimensions must be 1, 2 or 3')

        return fig, ax

    def plot_control_points(self, fig, ax, linewidth=1.25, linestyle='-.', color='red', markersize=5, markerstyle='o'):

        """ Plot the control points of the NURBS curve """

        # One dimension (law of evolution)
        if self.ndim == 1:
            Px = np.real(self.P)
            u = np.linspace(0, 1, Px.size)
            line, = ax.plot(u, Px[0,:])
            line.set_linewidth(linewidth)
            line.set_linestyle(linestyle)
            line.set_color(color)
            line.set_marker(markerstyle)
            line.set_markersize(markersize)
            line.set_markeredgewidth(linewidth)
            line.set_markeredgecolor(color)
            line.set_markerfacecolor('w')
            line.set_zorder(4)
            # line.set_label(' ')

        # Two dimensions (plane curve)
        elif self.ndim == 2:
            Px, Py = np.real(self.P)
            line, = ax.plot(Px, Py)
            line.set_linewidth(linewidth)
            line.set_linestyle(linestyle)
            line.set_color(color)
            line.set_marker(markerstyle)
            line.set_markersize(markersize)
            line.set_markeredgewidth(linewidth)
            line.set_markeredgecolor(color)
            line.set_markerfacecolor('w')
            line.set_zorder(4)
            # line.set_label(' ')

        # Three dimensions (space curve)
        elif self.ndim == 3:
            Px, Py, Pz = np.real(self.P)
            line, = ax.plot(Px, Py, Pz)
            line.set_linewidth(linewidth)
            line.set_linestyle(linestyle)
            line.set_color(color)
            line.set_marker(markerstyle)
            line.set_markersize(markersize)
            line.set_markeredgewidth(linewidth)
            line.set_markeredgecolor(color)
            line.set_markerfacecolor('w')
            line.set_zorder(4)
            # line.set_label(' ')

        else: raise Exception('The number of dimensions must be 2 or 3')

        return fig, ax

    def plot_frenet_serret(self, fig, ax, frame_number=5, frame_scale=0.10):

        """ Plot some Frenet-Serret reference frames along the NURBS curve """

        # Compute the tangent, normal and binormal unitary vectors
        h = 1e-12
        u = np.linspace(0+h, 1-h, frame_number)
        position = np.real(self.get_value(u))
        tangent = np.real(self.get_tangent(u))
        normal = np.real(self.get_normal(u))
        binormal = np.real(self.get_binormal(u))

        # Two dimensions (plane curve)
        if self.ndim == 2:

            # Plot the frames of reference
            for k in range(frame_number):

                # Plot the tangent vector
                x, y = position[:, k]
                u, v = tangent[:, k]
                ax.quiver(x, y, u, v, color='red', scale=7.5)

                # Plot the normal vector
                x, y = position[:, k]
                u, v = normal[:, k]
                ax.quiver(x, y, u, v, color='blue', scale=7.5)

            # Plot the origin of the vectors
            x, y = position
            points, = ax.plot(x, y)
            points.set_linestyle(' ')
            points.set_marker('o')
            points.set_markersize(5)
            points.set_markeredgewidth(1.25)
            points.set_markeredgecolor('k')
            points.set_markerfacecolor('w')
            points.set_zorder(4)
            # points.set_label(' ')

        # Three dimensions (space curve)
        elif self.ndim == 3:

            # Compute a length scale (fraction of the curve arc length)
            scale = frame_scale * self.get_arclength(0, 1)

            # Plot the frames of reference
            for k in range(frame_number):

                # Plot the tangent vector
                x, y, z = position[:, k]
                u, v, w = tangent[:, k]
                ax.quiver(x, y, z, u, v, w, color='red', length=scale, normalize=True)

                # Plot the norma vector
                x, y, z = position[:, k]
                u, v, w = normal[:, k]
                ax.quiver(x, y, z, u, v, w, color='blue', length=scale, normalize=True)

                # Plot the binormal vector
                x, y, z = position[:, k]
                u, v, w = binormal[:, k]
                ax.quiver(x, y, z, u, v, w, color='green', length=scale, normalize=True)

            # Plot the origin of the vectors
            x, y, z = position
            points, = ax.plot(x, y, z)
            points.set_linestyle(' ')
            points.set_marker('o')
            points.set_markersize(5)
            points.set_markeredgewidth(1.25)
            points.set_markeredgecolor('k')
            points.set_markerfacecolor('w')
            points.set_zorder(4)
            # points.set_label(' ')

        else: raise Exception('The number of dimensions must be 2 or 3')

        return fig, ax

    def rescale_plot(self, fig, ax):

        """ Adjust the aspect ratio of the figure """

        # Two dimensions (plane curve)
        if self.ndim == 2:

            # Set the aspect ratio of the data
            ax.set_aspect(1.0)

            # Adjust pad
            plt.tight_layout(pad=5.0, w_pad=None, h_pad=None)

        # Three dimensions (space curve)
        if self.ndim == 3:

            # Set axes aspect ratio
            ax.autoscale(enable=True)
            x_min, x_max = ax.get_xlim()
            y_min, y_max = ax.get_ylim()
            z_min, z_max = ax.get_zlim()
            x_mid = (x_min + x_max) / 2
            y_mid = (y_min + y_max) / 2
            z_mid = (z_min + z_max) / 2
            L = np.max((x_max - x_min, y_max - y_min, z_max - z_min)) / 2
            ax.set_xlim3d(x_mid - 1.0 * L, x_mid + 1.0 * L)
            ax.set_ylim3d(y_mid - 1.0 * L, y_mid + 1.0 * L)
            ax.set_zlim3d(z_mid - 1.0 * L, z_mid + 1.0 * L)

            # Adjust pad
            plt.tight_layout(pad=5.0, w_pad=None, h_pad=None)

    def plot_curvature(self, fig=None, ax=None, color='black', linestyle='-'):

        # Create the figure
        if fig is None:
            fig = plt.figure(figsize=(6, 5))
            ax = fig.add_subplot(111)
        ax.set_xlabel('$u$ parameter', fontsize=12, color='k', labelpad=12)
        ax.set_ylabel('Curvature', fontsize=12, color='k', labelpad=12)
        # ax_xy.xaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
        # ax_xy.yaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
        for t in ax.xaxis.get_major_ticks(): t.label.set_fontsize(12)
        for t in ax.yaxis.get_major_ticks(): t.label.set_fontsize(12)
        # ax_xy.set_xticks([])
        # ax_xy.set_yticks([])
        # ax_xy.axis('off')

        # Plot the curvature distribution
        u = np.linspace(0, 1, 1000)
        curvature = np.real(self.get_curvature(u))
        line, = ax.plot(u, curvature)
        line.set_linewidth(1.25)
        line.set_linestyle(linestyle)
        line.set_color(color)
        line.set_marker(" ")
        line.set_markersize(3.5)
        line.set_markeredgewidth(1)
        line.set_markeredgecolor("k")
        line.set_markerfacecolor("w")
        # line.set_label(' ')

        # Set the aspect ratio of the data
        # ax_xy.set_aspect(1.0)

        # # Set the aspect ratio of the figure
        # ratio = 1.00
        # x1, x2 = ax_xy.get_xlim()
        # y1, y2 = ax_xy.get_ylim()
        # ax_xy.set_aspect(np.abs((x2-x1)/(y2-y1))*ratio)

        # Adjust pad
        plt.tight_layout(pad=5.0, w_pad=None, h_pad=None)

        return fig, ax

    def plot_torsion(self, fig=None, ax=None, color='black', linestyle='-'):

        # Create the figure
        if fig is None:
            fig = plt.figure(figsize=(6, 5))
            ax = fig.add_subplot(111)
        ax.set_xlabel('$u$ parameter', fontsize=12, color='k', labelpad=12)
        ax.set_ylabel('Torsion', fontsize=12, color='k', labelpad=12)
        # ax_xy.xaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
        # ax_xy.yaxis.set_major_formatter(mpl.ticker.FormatStrFormatter('%.1f'))
        for t in ax.xaxis.get_major_ticks(): t.label.set_fontsize(12)
        for t in ax.yaxis.get_major_ticks(): t.label.set_fontsize(12)
        # ax_xy.set_xticks([])
        # ax_xy.set_yticks([])
        # ax_xy.axis('off')

        # Plot the curvature distribution
        u = np.linspace(0, 1, 1000)
        torsion = np.real(self.get_torsion(u))
        line, = ax.plot(u, torsion)
        line.set_linewidth(1.25)
        line.set_linestyle(linestyle)
        line.set_color(color)
        line.set_marker(" ")
        line.set_markersize(3.5)
        line.set_markeredgewidth(1)
        line.set_markeredgecolor("k")
        line.set_markerfacecolor("w")
        # line.set_label(' ')

        # Set the aspect ratio of the data
        # ax_xy.set_aspect(1.0)

        # Set the aspect ratio of the figure
        ratio = 1.00
        x1, x2 = ax.get_xlim()
        y1, y2 = ax.get_ylim()
        ax.set_aspect(np.abs((x2 - x1) / (y2 - y1)) * ratio)

        # Adjust pad
        plt.tight_layout(pad=5.0, w_pad=None, h_pad=None)

        return fig, ax

    # ---------------------------------------------------------------------------------------------------------------- #
    # Define the point projection problem class (Pygmo's user-defined problem)
    # ---------------------------------------------------------------------------------------------------------------- #
    def project_point_to_curve(self, P, algorithm_name='lbfgs'):

        """ Solve the point projection problem for the prescribed point `P` """

        # Import pygmo
        import pygmo as pg

        # Create the optimization algorithm
        myAlgorithm = pg.algorithm(pg.nlopt(algorithm_name))
        myAlgorithm.extract(pg.nlopt).xtol_rel = 1e-6
        myAlgorithm.extract(pg.nlopt).ftol_rel = 1e-6
        myAlgorithm.extract(pg.nlopt).xtol_abs = 1e-6
        myAlgorithm.extract(pg.nlopt).ftol_abs = 1e-6
        myAlgorithm.extract(pg.nlopt).maxeval = 100
        myAlgorithm.set_verbosity(0)

        # Create the optimization problem
        myProblem = pg.problem(self.PointToCurveProjectionProblem(self.get_value, self.get_derivative, P))

        # Create the population
        myPopulation = pg.population(prob=myProblem, size=0)

        # Create a list with the different starting points
        U0 = self.U[0:-1] + 1/2 * (self.U[1:] - self.U[0:-1])
        for u0 in U0:
            myPopulation.push_back([u0])

        # Solve the optimization problem (evolve the population in Pygmo's jargon)
        myPopulation = myAlgorithm.evolve(myPopulation)

        # Get the optimum
        u = myPopulation.champion_x[0]

        return u

    class PointToCurveProjectionProblem:

        def __init__(self, C, dC, P):
            """ Solve point inversion problem: min(u) ||C(u) - P|| """
            self.C_func = C
            self.dC_func = dC
            self.P = np.reshape(P, (P.shape[0], 1))

        @staticmethod
        def get_bounds():
            """ Set the bounds for the optimization problem """
            return [0.00], [1.00]

        def fitness(self, x):
            """ Evaluate the deviation between the prescribed point and the parametrized point """
            u = np.asarray([x[0]])
            C = self.C_func(u)
            P = self.P
            return np.asarray([np.sum(np.sum((C - P) ** 2, axis=0) ** (1 / 2))])

        def gradient(self, x):
            """ Compute the gradient of the fitness function analytically """
            u = np.asarray([x[0]])
            C = self.C_func(u)
            dC = self.dC_func(u, order=1)
            P = self.P
            numerator = np.sum((C - P) * dC, axis=0)
            denominator = np.sum(np.sum((C - P) ** 2, axis=0) ** (1 / 2))
            if np.abs(denominator) > 0:
                gradient = numerator/denominator
            else:
                gradient = np.asarray(0)[np.newaxis]
            return gradient


# Copyright 2021 Dice Finding Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
#Thanks to Anmol Kabra for orthogonalize function (https://gist.github.com/anmolkabra/b95b8e7fb7a6ff12ba5d120b6d9d1937)
"""Gets a set of orthonormal axes."""

import numpy as np
import numpy.linalg as la

def orthogonalize(U, eps=1e-15):
    """
    Orthogonalizes the matrix U (d x n) using Gram-Schmidt Orthogonalization.
    If the columns of U are linearly dependent with rank(U) = r, the last n-r columns
    will be 0.

    Args:
        U (numpy.array): A d x n matrix with columns that need to be orthogonalized.
        eps (float): Threshold value below which numbers are regarded as 0 (default=1e-15).

    Returns:
        (numpy.array): A d x n orthogonal matrix. If the input matrix U's cols were
            not linearly independent, then the last n-r cols are zeros.

    Examples:
    ```python
    >>> import numpy as np
    >>> import gram_schmidt as gs
    >>> gs.orthogonalize(np.array([[10., 3.], [7., 8.]]))
    array([[ 0.81923192, -0.57346234],
       [ 0.57346234,  0.81923192]])
    >>> gs.orthogonalize(np.array([[10., 3., 4., 8.], [7., 8., 6., 1.]]))
    array([[ 0.81923192 -0.57346234  0.          0.        ]
       [ 0.57346234  0.81923192  0.          0.        ]])
    ```
    """

    n = len(U[0])
    # numpy can readily reference rows using indices, but referencing full rows is a little
    # dirty. So, work with transpose(U)
    V = U.T
    for i in range(n):
        prev_basis = V[0:i]     # orthonormal basis before V[i]
        coeff_vec = np.dot(prev_basis, V[i].T)  # each entry is np.dot(V[j], V[i]) for all j < i
        # subtract projections of V[i] onto already determined basis V[0:i]
        V[i] -= np.dot(coeff_vec, prev_basis).T
        if la.norm(V[i]) < eps:
            V[i][V[i] < eps] = 0.   # set the small entries to 0
        else:
            V[i] /= la.norm(V[i])
    return V.T

def get_orthogonal_axes(direction):
    """Gets a set of N-1 vectors orthonormal to the given direction, so that all span the full N-dimensional space.
    N is the number of dimensions of the given direction vector."""
    N = len(direction)
    U = np.eye(N)
    coefficients = U @ direction
    abs_coefficients = np.abs(coefficients)
    direction_column_index = np.argmax(abs_coefficients)
    U[:,direction_column_index] = direction
    U_rolled = np.roll(U, -direction_column_index, axis = 1)#Start with important direction in first column
    new_basis = orthogonalize(U_rolled)
    other_columns = [new_basis[:,i] for i in range(N) if i > 0]
    return other_columns


import numpy as np

def confusion_matrix_init(y_gold: np.ndarray, y_prediction: np.ndarray, class_labels: np.ndarray = None) -> np.ndarray:
    """ Compute the confusion matrix.

    Args:
        y_gold (np.ndarray): the correct ground truth/gold standard labels
        y_prediction (np.ndarray): the predicted labels
        class_labels (np.ndarray): a list of unique class labels.
                               Defaults to the union of y_gold and y_prediction.

    Returns:
        np.array : shape (C, C), where C is the number of classes.
                   Rows are ground truth per class, columns are predictions
    """

    # if no class_labels are given, we obtain the set of unique class labels from
    # the union of the ground truth annotation and the prediction
    if not class_labels:
        class_labels = np.unique(np.concatenate((y_gold, y_prediction)))

    confusion = np.zeros(
        (len(class_labels), len(class_labels)), dtype=int)

    for i, actual in enumerate(class_labels):
        for j, predicted in enumerate(class_labels):
            # Find the number of predictions with label pair (actual, predicted)
            for gold, prediction in zip(y_gold, y_prediction):
                if gold == actual and predicted == prediction:
                    confusion[i, j] += 1

    return confusion

def accuracy(y_gold: np.ndarray, y_prediction: np.ndarray) -> float:
    """ Compute the accuracy given the ground truth and predictions

    Args:
        y_gold (np.ndarray): the correct ground truth/gold standard labels
        y_prediction (np.ndarray): the predicted labels

    Returns:
        float : the accuracy
    """

    assert len(y_gold) == len(y_prediction)

    try:
        return np.sum(y_gold == y_prediction) / len(y_gold)
    except ZeroDivisionError:
        return 0.

def accuracy_from_confusion(confusion: np.ndarray) -> float:
    """ Compute the accuracy given the confusion matrix

    Args:
        confusion (np.ndarray): shape (C, C), where C is the number of classes.
                    Rows are ground truth per class, columns are predictions

    Returns:
        float : the accuracy
    """
    try:
        return np.sum(np.diag(confusion)) / np.sum(confusion)
    except ZeroDivisionError:
        return 0.

def precision(y_gold: np.ndarray, y_prediction: np.ndarray) -> float:
    """ Compute the precision score per class given the ground truth and predictions

    Also return the macro-averaged precision across classes.

    Args:
        y_gold (np.ndarray): the correct ground truth/gold standard labels
        y_prediction (np.ndarray): the predicted labels

    Returns:
        tuple: returns a tuple (precisions, macro_precision) where
            - precisions is a np.ndarray of shape (C,), where each element is the
              precision for class c
            - macro-precision is macro-averaged precision (a float)
    """

    confusion = confusion_matrix_init(y_gold, y_prediction)

    # Compute the precision per class
    precisions = np.array([confusion[i, i] / np.sum(confusion[:, i])
                          for i in range(len(confusion))])

    # Compute the macro-averaged precision
    macro_p = np.average(precisions)

    return (precisions, macro_p)

def recall(y_gold: np.ndarray, y_prediction: np.ndarray) -> float:
    """ Compute the recall score per class given the ground truth and predictions

    Also return the macro-averaged recall across classes.

    Args:
        y_gold (np.ndarray): the correct ground truth/gold standard labels
        y_prediction (np.ndarray): the predicted labels

    Returns:
        tuple: returns a tuple (recalls, macro_recall) where
            - recalls is a np.ndarray of shape (C,), where each element is the
                recall for class c
            - macro-recall is macro-averaged recall (a float)
    """

    confusion = confusion_matrix_init(y_gold, y_prediction)

    # Compute the recall per class
    recalls = np.array([confusion[i, i] / np.sum(confusion[i])
                       for i in range(len(confusion))])

    # Compute the macro-averaged recall
    macro_r = np.average(recalls)

    return (recalls, macro_r)

def f1_score(y_gold: np.ndarray, y_prediction: np.ndarray) -> float:
    """ Compute the F1-score per class given the ground truth and predictions

    Also return the macro-averaged F1-score across classes.

    Args:
        y_gold (np.ndarray): the correct ground truth/gold standard labels
        y_prediction (np.ndarray): the predicted labels

    Returns:
        tuple: returns a tuple (f1s, macro_f1) where
            - f1s is a np.ndarray of shape (C,), where each element is the
              f1-score for class c
            - macro-f1 is macro-averaged f1-score (a float)
    """

    precisions, _ = precision(y_gold, y_prediction)
    recalls, _ = recall(y_gold, y_prediction)

    # just to make sure they are of the same length
    assert len(precisions) == len(recalls)

    # Compute the per-class F1
    sum_pr = precisions + recalls
    prod_pr = precisions * recalls
    f1_scores = 2 * prod_pr / sum_pr

    # Compute the macro-averaged F1
    macro_f = np.average(f1_scores)

    return (f1_scores, macro_f)


'''
How often do we get no-hitters?

The number of games played between each no-hitter in the modern era (1901-2015) of Major League Baseball
is stored in the array nohitter_times.

If you assume that no-hitters are described as a Poisson process, then the time between no-hitters is
Exponentially distributed. As you have seen, the Exponential distribution has a single parameter, which
we will call \tau
\tau, the typical interval time. The value of the parameter τ that makes the exponential distribution
best match the data is the mean interval time (where time is in units of number of games) between no-hitters.

Compute the value of this parameter from the data. Then, use np.random.exponential() to "repeat" the history
of Major League Baseball by drawing inter-no-hitter times from an exponential distribution with the τ you found
and plot the histogram as an approximation to the PDF.

NumPy, pandas, matlotlib.pyplot, and seaborn have been imported for you as np, pd, plt, and sns, respectively.

INSTRUCTIONS
100XP
-Seed the random number generator with 42.
-Compute the mean time (in units of number of games) between no-hitters.
-Draw 100,000 samples from an Exponential distribution with the parameter you computed from the mean of the inter-no-hitter times.
-Plot the theoretical PDF using plt.hist(). Remember to use keyword arguments bins=50, normed=True, and histtype='step'. Be sure to label your axes.
-Show your plot.
'''
import numpy as np
import matplotlib.pyplot as plt

nohitter_times = np.array([843, 1613, 1101,  215,  684,  814,  278,  324,  161,  219,  545,
        715,  966,  624,   29,  450,  107,   20,   91, 1325,  124, 1468,
        104, 1309,  429,   62, 1878, 1104,  123,  251,   93,  188,  983,
        166,   96,  702,   23,  524,   26,  299,   59,   39,   12,    2,
        308, 1114,  813,  887,  645, 2088,   42, 2090,   11,  886, 1665,
       1084, 2900, 2432,  750, 4021, 1070, 1765, 1322,   26,  548, 1525,
         77, 2181, 2752,  127, 2147,  211,   41, 1575,  151,  479,  697,
        557, 2267,  542,  392,   73,  603,  233,  255,  528,  397, 1529,
       1023, 1194,  462,  583,   37,  943,  996,  480, 1497,  717,  224,
        219, 1531,  498,   44,  288,  267,  600,   52,  269, 1086,  386,
        176, 2199,  216,   54,  675, 1243,  463,  650,  171,  327,  110,
        774,  509,    8,  197,  136,   12, 1124,   64,  380,  811,  232,
        192,  731,  715,  226,  605,  539, 1491,  323,  240,  179,  702,
        156,   82, 1397,  354,  778,  603, 1001,  385,  986,  203,  149,
        576,  445,  180, 1403,  252,  675, 1351, 2983, 1568,   45,  899,
       3260, 1025,   31,  100, 2055, 4043,   79,  238, 3931, 2351,  595,
        110,  215,    0,  563,  206,  660,  242,  577,  179,  157,  192,
        192, 1848,  792, 1693,   55,  388,  225, 1134, 1172, 1555,   31,
       1582, 1044,  378, 1687, 2915,  280,  765, 2819,  511, 1521,  745,
       2491,  580, 2072, 6450,  578,  745, 1075, 1103, 1549, 1520,  138,
       1202,  296,  277,  351,  391,  950,  459,   62, 1056, 1128,  139,
        420,   87,   71,  814,  603, 1349,  162, 1027,  783,  326,  101,
        876,  381,  905,  156,  419,  239,  119,  129,  467])

# Seed random number generator
np.random.seed(42)

# Compute mean no-hitter time: tau
tau = np.mean(nohitter_times)

# Draw out of an exponential distribution with parameter tau: inter_nohitter_time
inter_nohitter_time = np.random.exponential(tau, 100000)

# Plot the PDF and label axes
_ = plt.hist(inter_nohitter_time,
             bins=50, normed=True, histtype='step')
_ = plt.xlabel('Games between no-hitters')
_ = plt.ylabel('PDF')

# Show the plot
plt.show()


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

import numpy as np

# optimization functions from https://en.wikipedia.org/wiki/Test_functions_for_optimization

def schaffer(x, y):
    """constraints=100, minimum f(0,0)=0"""
    numer = np.square(np.sin(x ** 2 - y ** 2)) - 0.5
    denom = np.square(1.0 + (0.001 * (x ** 2 + y ** 2)))

    return 0.5 + (numer * (1.0 / denom))

def eggholder(x, y):
    """constraints=512, minimum f(512, 414.2319)=-959.6407"""
    y = y + 47.0
    a = (-1.0) * (y) * np.sin(np.sqrt(np.absolute((x / 2.0) + y)))
    b = (-1.0) * x * np.sin(np.sqrt(np.absolute(x - y)))
    return a + b

def booth(x, y):
    """constraints=10, minimum f(1, 3)=0"""
    return ((x) + (2.0 * y) - 7.0) ** 2 + ((2.0 * x) + (y) - 5.0) ** 2

def matyas(x, y):
    """constraints=10, minimum f(0, 0)=0"""
    return (0.26 * (x ** 2 + y ** 2)) - (0.48 * x * y)

def crossInTray(x, y):
    """constraints=10,
    minimum f(1.34941, -1.34941)=-2.06261
    minimum f(1.34941, 1.34941)=-2.06261
    minimum f(-1.34941, 1.34941)=-2.06261
    minimum f(-1.34941, -1.34941)=-2.06261
    """
    B = np.exp(np.absolute(100.0 - (np.sqrt(x ** 2 + y ** 2) / np.pi)))
    A = np.absolute(np.sin(x) * np.sin(y) * B) + 1
    return -0.0001 * (A ** 0.1)

def levi(x, y):
    """constraints=10,
    minimum f(1,1)=0.0
    """
    a = np.sin(3.0 * np.pi * x) ** 2
    b = ((x - 1) ** 2) * (1 + np.sin(3.0 * np.pi * y) ** 2)
    c = ((y - 1) ** 2) * (1 + np.sin(2.0 * np.pi * y) ** 2)
    return a + b + c


import networkx as nx

def pagerank(G, alpha=0.85, personalization=None, max_iter=100, tol=1.0e-6, nstart=None, weight='weight',  dangling=None):
  if len(G) == 0:
    return {}
  if not G.is_directed():
    D = G.to_directed()
  else:
    D = G
  W = nx.stochastic_graph(D, weight=weight)
  N = W.number_of_nodes()
  if nstart is None:
    x = dict.fromkeys(W, 1.0 / N)
  else:
    s = float(sum(nstart.values()))
    x = dict((k, v / s) for k, v in nstart.items())
  if personalization is None:
    p = dict.fromkeys(W, 1.0 / N)
  else:
    missing = set(G) - set(personalization)
    if missing:
      raise NetworkXError('Personalization dictionary '  'must have a value for every node. ' 'Missing nodes %s' % missing)
      s = float(sum(personalization.values()))
      p = dict((k, v / s) for k, v in personalization.items())
  if dangling is None:
    dangling_weights = p
  else:
    missing = set(G) - set(dangling)
    if missing:
      raise NetworkXError('Dangling node dictionary '  'must have a value for every node. '  'Missing nodes %s' % missing)
      s = float(sum(dangling.values()))
      dangling_weights = dict((k, v/s) for k, v in  dangling.items())
      dangling_nodes = [n for n in W if W.out_degree(n, weight=weight)  == 0.0]
  for _ in range(max_iter):
    xlast = x
    x = dict.fromkeys(xlast.keys(), 0)
    danglesum = alpha * sum(xlast[n] for n in dangling_nodes)
    for n in x:
      for nbr in W[n]:
        x[nbr] += alpha * xlast[n] * W[n][nbr][weight]
      x[n] += danglesum * dangling_weights[n] + (1.0 - alpha)  * p[n]
      err = sum([abs(x[n] - xlast[n]) for n in x])
      if err < N*tol:
        return x
        raise NetworkXError('pagerank: power iteration failed to converge ' 'in %d iterations.' % max_iter)

G = nx.DiGraph()
G.add_edges_from([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 1), (4, 1), (4, 3)])
pr = nx.pagerank(G,1)
print(pr)

# OUTPUT :
# {1: 0.38709615908859496, 2: 0.12903204605249047, 3: 0.29032302109901886, 4: 0.193548773759895}