from sympy import *
from numpy import *

x = Symbol('x')

ponto_x = 2
# Pn -> Polinomio de grau n
vet_x = array([ 1.2, 1.5])
vet_y = array([1.928, 3.875])

def L(vet_x, vet_y, ponto_x):
    l = list()
    aux_Numerado = 1
    aux_den = 1
    for k in range(len(vet_x)):
        for j in range(len(vet_x)):
            if k != j:
                aux_Numerado *= float(ponto_x - vet_x[j])
        for j in range(len(vet_x)):
            if k != j:
                aux_den *= float(vet_x[k] - vet_x[j])
        l.append(aux_Numerado / aux_den)
        aux_Numerado = 1
        aux_den = 1
    return l

l = L(vet_x, vet_y, ponto_x)
aux = 0
for k in range(len(vet_y)):
     aux += (vet_y[k] * l[k])

print(f"Resp: {aux}")

# qual e a funcao de cada L
aux_Numerado = 1
aux_den = 1
l = list()
for k in range(len(vet_x)):
    for j in range(len(vet_x)):
        if k != j:
            aux_Numerado *= (x - vet_x[j])
    for j in range(len(vet_x)):
        if k != j:
            aux_den *= (vet_x[k] - vet_x[j])
    l.append(aux_Numerado / aux_den)
    aux_Numerado = 1
    aux_den = 1

#print(l[1]) # Aqui da a funcao de cada L[n]


"""The 1D heat model."""

import numpy as np
import yaml

def solve_1d(temp, spacing, k=2e3, kappa=1e-6, Qm=0.05, time_step=1.0):
    """Solve the 1D Heat Equation in a column.

    Parameters
    ----------
    temp : ndarray
        Temperature.
    spacing : array_like
        Grid spacing in the row and column directions.
    k : float, optional
        Thermal conductivity. Default is 2e3.
    kappa : float, optional
        Thermal diffusivity. Default is 1e-6.
    Qm : float, optional
        Heat flux. Default is 0.05.
    time_step : float (optional)
        Time step.

    Returns
    -------
    result : ndarray
        The temperatures after time *time_step*.

    Examples
    --------
    >>> import numpy as np
    >>> from heat import solve_1d
    >>> spacing = [100]
    >>> dTdz = 0.01
    >>> temp = dTdz * np.arange(0, 1000, spacing[0])
    >>> temp
    array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
    >>> for i in range(10*365):
    ...     temp = solve_1d(temp, spacing, time_step=24*60*60)
    >>> temp
    array([0.        , 1.        , 2.        , 3.        , 4.        ,
           5.        , 5.99999996, 6.99999499, 7.99951939, 8.9690285 ])

    This is good, we expect it to cool.
    """
    Q = np.zeros_like(temp)
    dTdz = np.diff(temp) / spacing[0]

    Q[:-1] = - k * dTdz
    Q[-1] = - Qm
    dTdt = (-kappa/k) * np.diff(Q)/spacing[0]

    temp[1:] += dTdt * time_step

    return temp

class Heat(object):

    """Solve the Heat equation on a grid.

    Examples
    --------
    >>> heat = Heat()
    >>> heat.time
    0.0
    >>> heat.time_step
    250000.0
    >>> heat.advance_in_time()
    >>> heat.time
    250000.0

    >>> heat = Heat(shape=(5,))
    >>> heat.temperature = np.zeros_like(heat.temperature)
    >>> heat.temperature[2] = 1.
    >>> heat.advance_in_time()

    >>> heat = Heat(kappa=.5)
    >>> heat.time_step
    0.5
    >>> heat = Heat(kappa=.5, spacing=(2.,))
    >>> heat.time_step
    2.0
    """

    def __init__(
        self,
        shape=(110,),
        spacing=(1.0,),
        origin=(0.0,),
        kappa=1e-6,
        k=2e3,
        Qm=0.05,

    ):
        """Create a new heat model.

        Paramters
        ---------
        shape : array_like, optional
            The shape of the solution grid as (*rows*, *columns*).
        spacing : array_like, optional
            Spacing of grid rows and columns.
        origin : array_like, optional
            Coordinates of lower left corner of grid.
        k : float, optional
            Thermal conductivity. Default is 2e3.
        kappa : float, optional
            Thermal diffusivity. Default is 1e-6.
        Qm : float, optional
            Heat flux. Default is 0.05.
        """
        self._shape = shape
        self._spacing = spacing
        self._origin = origin
        self._time = 0.0
        self._kappa = kappa
        self._k = k
        self._Qm = Qm
        self._time_step = min(spacing) ** 2 / (4.0 * self._kappa)
        self._temperature = np.random.random(self._shape)

    @property
    def time(self):
        """Current model time."""
        return self._time

    @property
    def temperature(self):
        """Temperature of the plate."""
        return self._temperature

    @temperature.setter
    def temperature(self, new_temp):
        """Set the temperature of the plate.

        Parameters
        ----------
        new_temp : array_like
            The new temperatures.
        """
        self._temperature[:] = new_temp

    @property
    def time_step(self):
        """Model time step."""
        return self._time_step

    @time_step.setter
    def time_step(self, time_step):
        """Set model time step."""
        self._time_step = time_step

    @property
    def spacing(self):
        """Shape of the model grid."""
        return self._spacing

    @property
    def origin(self):
        """Origin coordinates of the model grid."""
        return self._origin

    @classmethod
    def from_file_like(cls, file_like):
        """Create a Heat object from a file-like object.

        Parameters
        ----------
        file_like : file_like
            Input parameter file.

        Returns
        -------
        Heat
            A new instance of a Heat object.
        """
        config = yaml.safe_load(file_like)
        return cls(**config)

    def advance_in_time(self):
        """Calculate new temperatures for the next time step."""
        self._temperature[:] = solve_1d(
            self._temperature,
            self._spacing,
            time_step=self._time_step,
            k=self._k, kappa=self._kappa, Qm=self._Qm,
        )
        self._time += self._time_step


import math
import numpy as np

def bound(value, lower, upper):
    """ Clips off a value which exceeds the lower or upper bounds. """
    if value < lower:
        return lower
    elif value > upper:
        return upper
    else:
        return value

def bound_vector(vect, maximum):
    """ Bounds a vector between a negative and positive maximum range. """
    xval = bound(vect[0], -maximum, maximum)
    yval = bound(vect[1], -maximum, maximum)
    return np.array([xval, yval])

def angle_difference(angle1, angle2):
    """ Computes the real difference between angles. """
    return norm_angle(angle1 - angle2)

def norm_angle(angle):
    """ Normalize the angle between -pi and pi. """
    while angle > np.pi:
        angle -= 2*np.pi
    while angle < -np.pi:
        angle += 2*np.pi
    return angle

def angle_close(angle1, angle2):
    """ Determines whether an angle1 is close to angle2. """
    return abs(angle_difference(angle1, angle2)) < np.pi/8

def angle_between(pos1, pos2):
    """ Computes the angle between two positions. """
    diff = pos2 - pos1
    # return np.arctan2(diff[1], diff[0])
    return math.atan2(diff[1], diff[0])  # faster than numpy

def angle_position(theta):
    """ Computes the position on a unit circle at angle theta. """
    return vector(np.cos(theta), np.sin(theta))

def vector(xvalue, yvalue):
    """ Returns a 2D numpy vector. """
    # return np.array([float(xvalue), float(yvalue)])
    return np.array([xvalue, yvalue], dtype=np.float64)

def vector_to_tuple(vect):
    """ Converts a numpy array to a tuple. """
    assert len(vect) == 2
    return (vect[0], vect[1])
    # return tuple(map(tuple, vect))


from typing import Union
from warnings import warn

import numpy as np

from .conventions import EulerAngleConvention, get_convention

def theta2rotx(theta: np.ndarray) -> np.ndarray:
    """
    Rx = [[1, 0, 0],
          [0, c(t), -s(t)],
          [0, s(t), c(t)]]
    :param theta: angle(s) in degrees, positive is counterclockwise
    :return: rotation_matrices
    """
    theta = np.deg2rad(np.asarray(theta).reshape(-1))
    rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=np.float)
    cos_theta = np.cos(theta)
    sin_theta = np.sin(theta)
    rotation_matrices[:, 0, 0] = 1
    rotation_matrices[:, (1, 2), (1, 2)] = cos_theta[:, np.newaxis]
    rotation_matrices[:, 1, 2] = -sin_theta
    rotation_matrices[:, 2, 1] = sin_theta
    return rotation_matrices

def theta2roty(theta: np.ndarray) -> np.ndarray:
    """
    Ry = [[c(t), 0, s(t)],
          [0, 1, 0],
          [-s(t), 0, c(t)]]
    :param theta: angle(s) in degrees, positive is counterclockwise
    :return: rotation_matrices
    """
    theta = np.deg2rad(np.asarray(theta).reshape(-1))
    rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=np.float)
    cos_theta = np.cos(theta)
    sin_theta = np.sin(theta)
    rotation_matrices[:, 1, 1] = 1
    rotation_matrices[:, (0, 2), (0, 2)] = cos_theta[:, np.newaxis]
    rotation_matrices[:, 0, 2] = sin_theta
    rotation_matrices[:, 2, 0] = -sin_theta
    return rotation_matrices

def theta2rotz(theta: np.ndarray) -> np.ndarray:
    """
    Rz = [[c(t), -s(t), 0],
          [s(t), c(t), 0],
          [0, 0, 1]]
    :param theta: angle(s) in degrees, positive is counterclockwise
    :return: rotation_matrices
    """
    theta = np.deg2rad(np.asarray(theta).reshape(-1))
    rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=np.float)
    cos_theta = np.cos(theta)
    sin_theta = np.sin(theta)
    rotation_matrices[:, 2, 2] = 1
    rotation_matrices[:, (0, 1), (0, 1)] = cos_theta[:, np.newaxis]
    rotation_matrices[:, 0, 1] = -sin_theta
    rotation_matrices[:, 1, 0] = sin_theta
    return rotation_matrices

def theta2rotm(theta: np.ndarray, axis: str):
    """
    Convert values for theta into rotation matrices around a given axis 'x', 'y' or 'z'
    :param theta: angle(s) in degrees, positive is counterclockwise
    :param axis: 'x', 'y' or 'z'
    :return: rotation_matrices
    """
    axis = axis.strip().lower()
    if axis not in ('x', 'y', 'z'):
        raise ValueError(f"Axis must be one of 'x', 'y' or 'z''")
    elif axis == 'x':
        rotation_matrices = theta2rotx(theta)
    elif axis == 'y':
        rotation_matrices = theta2roty(theta)
    elif axis == 'z':
        rotation_matrices = theta2rotz(theta)
    if rotation_matrices.shape[0] == 1:
        rotation_matrices = rotation_matrices.reshape((3, 3))
    return rotation_matrices

def euler2matrix(euler_angles: np.ndarray, axes: str, intrinsic: bool = None, extrinsic: bool = None,
                 positive_ccw: bool = None) -> np.ndarray:
    """

    :param positive_ccw:
    :param euler_angles:
    :param axes:
    :param intrinsic:
    :param extrinsic:
    :return: rotation_matrices (n, 3, 3)
    """
    # Check input
    euler_angles = np.asarray(euler_angles).reshape((-1, 3))
    axes = axes.strip().lower()
    if axes not in ('xyx', 'yzy', 'zxz', 'xzx', 'yxy', 'zyz', 'xyz', 'yzx', 'zxy', 'xzy', 'yxz', 'zyx'):
        raise ValueError(f'Axes {axes} are not a valid set of euler angle axes')

    # Calculate elemental rotation matrices from euler angles
    # Make sure your rotation angles have the correct sign
    if positive_ccw is None:
        warn('positive_ccw was not explicitly set, defaulting to considering positive angles as CCW rotations')
        positive_ccw = True
    if positive_ccw:
        # RotationMatrix object creates matrices representing ccw positive rotations looking against axis
        elemental_rotations = [theta2rotm(theta=euler_angles[:, ax_idx], axis=axes[ax_idx]) for ax_idx in range(3)]
    else:
        # While positive ccw looking against axis is standard in maths and physics, some people define positive to
        # mean a clockwise rotation when looking against the axis
        # The effect of this is we need to reverse the sign of the angle to use it with our rotation matrix
        # definitions which are positive ccw looking against the axis.
        elemental_rotations = [theta2rotm(theta=-1 * euler_angles[:, ax_idx], axis=axes[ax_idx]) for ax_idx in range(3)]

    # Compose final rotation matrices from elemental rotation matrices
    if intrinsic and extrinsic:
        raise ValueError(f"Only one of 'intrinsic': {intrinsic} and 'extrinsic': {extrinsic} can be set to True")

    elif intrinsic:
        # Intrinsic case,
        # axes = ax1, ax2, ax3
        # angles = a, b, c
        # R =  R1(a) * R2(b) * R3(c)
        rotation_matrices = elemental_rotations[0] @ elemental_rotations[1] @ elemental_rotations[2]

    elif extrinsic:
        # Extrinsic case,
        # axes = ax1, ax2, ax3
        # angles = a, b, c
        # R =  R3(c) * R2(b) * R1(a)
        rotation_matrices = elemental_rotations[2] @ elemental_rotations[1] @ elemental_rotations[0]
    else:
        raise ValueError(F"One of 'intrinsic': {intrinsic} and 'extrinsic': {extrinsic} must be set to True")

    return rotation_matrices

def matrix2xyx_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Rx(k3) @ Ry(k2) @ Rx(k1) = [[c2, s1s2, c1s2],
                                [s2s3, -s1c2s3+c1c3, -c1c2s3-s1c3],
                                [-s2c3, s1c2c3+c1s3, c1c2c3-s1s3]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = np.arccos(rotation_matrices[:, 0, 0])

    # Gimbal lock case (s2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 0, 2]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r23 = rotation_matrices[gimbal_idx, 1, 2]
    r22 = rotation_matrices[gimbal_idx, 1, 1]
    angles_radians[gimbal_idx, 0] = np.arctan2(-r23, r22)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r12 = rotation_matrices[idx, 0, 1]
    r13 = rotation_matrices[idx, 0, 2]
    r21 = rotation_matrices[idx, 1, 0]
    r31 = rotation_matrices[idx, 2, 0]
    angles_radians[idx, 0] = np.arctan2(r12, r13)
    angles_radians[idx, 2] = np.arctan2(r21, -r31)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2yzy_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Ry(k3) @ Rz(k2) @ Ry(k1) = [[c1c2c3-s1s3, -s2c3, s1c2c3+c1c3],
                                [c1s2, c2, s1s2],
                                [-c1c2s3, s2s3, -s1c2s3+c1c3]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = np.arccos(rotation_matrices[:, 1, 1])

    # Gimbal lock case (s2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 1, 0]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r31 = rotation_matrices[gimbal_idx, 2, 0]
    r33 = rotation_matrices[gimbal_idx, 2, 2]
    angles_radians[gimbal_idx, 0] = np.arctan2(-r31, r33)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r23 = rotation_matrices[idx, 1, 2]
    r21 = rotation_matrices[idx, 1, 0]
    r32 = rotation_matrices[idx, 2, 1]
    r12 = rotation_matrices[idx, 0, 1]
    angles_radians[idx, 0] = np.arctan2(r23, r21)
    angles_radians[idx, 2] = np.arctan2(r32, r12)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)
    return euler_angles

def matrix2zxz_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Rz(k3) @ Rx(k2) @ Rz(k1) = [[-s1c2s3+c1c3, -c1c2s3-s1c3, s2s3],
                                [s1c2c3+s1s3, c1c2c3-s1s3, -s2c3],
                                [s1s2, c1s2, c2]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles[:, 1] = np.arccos(rotation_matrices[:, 2, 2])

    # Gimbal lock case (s2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 0, 2]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r12 = rotation_matrices[gimbal_idx, 0, 1]
    r11 = rotation_matrices[gimbal_idx, 0, 0]
    angles[gimbal_idx, 0] = np.arctan2(-r12, r11)
    angles[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r31 = rotation_matrices[idx, 2, 0]
    r32 = rotation_matrices[idx, 2, 1]
    r13 = rotation_matrices[idx, 0, 2]
    r23 = rotation_matrices[idx, 1, 2]
    angles[idx, 0] = np.arctan2(r31, r32)
    angles[idx, 2] = np.arctan2(r13, -r23)

    # convert to degrees
    euler_angles = np.rad2deg(angles)

    return euler_angles

def matrix2xzx_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Rx(k3) @ Rz(k2) @ Rx(k1) = [[c2, -c1s2, s1s2],
                                [s2c3, c1c2c3-s3, -s1c2c3-c1s3],
                                [s2s3, c1c2s3+s1c3, -s1c2s3+c1c3]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = np.arccos(rotation_matrices[:, 0, 0])

    # Gimbal lock case (s2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 0, 2]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r32 = rotation_matrices[gimbal_idx, 2, 1]
    r33 = rotation_matrices[gimbal_idx, 2, 2]
    angles_radians[gimbal_idx, 0] = np.arctan2(r32, r33)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r13 = rotation_matrices[idx, 0, 2]
    r12 = rotation_matrices[idx, 0, 1]
    r31 = rotation_matrices[idx, 2, 0]
    r21 = rotation_matrices[idx, 1, 0]
    angles_radians[idx, 0] = np.arctan2(r13, -r12)
    angles_radians[idx, 2] = np.arctan2(r31, r21)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)
    return euler_angles

def matrix2yxy_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Ry(k3) @ Rx(k2) @ Ry(k1) = [[-s1c2s3+c1c3, s2s3, c1c2s3+s1c3],
                                [s1s2, c2, -c1s2],
                                [-s1c2c3-c1s3, s2c3, c1c2c3-s1s3]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = np.arccos(rotation_matrices[:, 1, 1])

    # Gimbal lock case (s2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 0, 1]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r13 = rotation_matrices[gimbal_idx, 0, 2]
    r11 = rotation_matrices[gimbal_idx, 0, 0]
    angles_radians[gimbal_idx, 0] = np.arctan2(r13, r11)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r21 = rotation_matrices[idx, 1, 0]
    r23 = rotation_matrices[idx, 1, 2]
    r12 = rotation_matrices[idx, 0, 1]
    r32 = rotation_matrices[idx, 2, 1]
    angles_radians[idx, 0] = np.arctan2(r21, -r23)
    angles_radians[idx, 2] = np.arctan2(r12, r32)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2zyz_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Rz(k3) @ Ry(k2) @ Rz(k1) = [[c1c2c3-s1s3, -s1c2c3-c1s3, s2c3],
                                [c1c2s3+s1c3, -s1c2s3+c1c3, s2s3],
                                [-c1s2, s1s2, c2]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = np.arccos(rotation_matrices[:, 2, 2])

    # Gimbal lock case (s2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 0, 2]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r21 = rotation_matrices[gimbal_idx, 1, 0]
    r22 = rotation_matrices[gimbal_idx, 1, 1]
    angles_radians[gimbal_idx, 0] = np.arctan2(r21, r22)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r32 = rotation_matrices[idx, 2, 1]
    r31 = rotation_matrices[idx, 2, 0]
    r23 = rotation_matrices[idx, 1, 2]
    r13 = rotation_matrices[idx, 0, 2]
    angles_radians[idx, 0] = np.arctan2(r32, -r31)
    angles_radians[idx, 2] = np.arctan2(r23, r13)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2xyz_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Rz(k3) @ Ry(k2) @ Rx(k1) = [[c2c3, s1s2c3-c1s3, c1s2c3+s1s3],
                                [c2s3, s1s2s3+c1c3, c1s2s3-s1c3],
                                [-s2, s1c2, c1c2]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = -np.arcsin(rotation_matrices[:, 2, 0])

    # Gimbal lock case (c2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 0, 0]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r23 = rotation_matrices[gimbal_idx, 1, 2]
    r22 = rotation_matrices[gimbal_idx, 1, 1]
    angles_radians[gimbal_idx, 0] = np.arctan2(-r23, r22)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r32 = rotation_matrices[idx, 2, 1]
    r33 = rotation_matrices[idx, 2, 2]
    r21 = rotation_matrices[idx, 2, 0]
    r11 = rotation_matrices[idx, 0, 0]
    angles_radians[idx, 0] = np.arctan2(r32, r33)
    angles_radians[idx, 2] = np.arctan2(r21, r11)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2yzx_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Rx(k3) @ Rz(k2) @ Ry(k1) = [[c1c2, -s2, s1c2],
                                [c1s2c3+s1s3, c2c3, s1s2c3-c1s3],
                                [c1s2s3-s1c3, c2s3, s1s2s3+c1c3]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = -np.arcsin(rotation_matrices[:, 0, 1])

    # Gimbal lock case (c2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 0, 0]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r31 = rotation_matrices[gimbal_idx, 2, 0]
    r33 = rotation_matrices[gimbal_idx, 2, 2]
    angles_radians[gimbal_idx, 0] = np.arctan2(-r31, r33)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r13 = rotation_matrices[idx, 0, 2]
    r11 = rotation_matrices[idx, 0, 0]
    r32 = rotation_matrices[idx, 2, 1]
    r22 = rotation_matrices[idx, 1, 1]
    angles_radians[idx, 0] = np.arctan2(r13, r11)
    angles_radians[idx, 2] = np.arctan2(r32, r22)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2zxy_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Ry(k3) @ Rx(k2) @ Rz(k1) = [[s1s2s3+c1c3, c1s2s3-s1c3, c2s3],
                                [s1c2, c1c2, -s2],
                                [s1s2c3-c1s3, c1s2c3+s1s3, c2c3]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = -np.arcsin(rotation_matrices[:, 1, 2])

    # Gimbal lock case (c2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 1, 0]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r12 = rotation_matrices[gimbal_idx, 0, 1]
    r11 = rotation_matrices[gimbal_idx, 0, 0]
    angles_radians[gimbal_idx, 0] = np.arctan2(-r12, r11)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r21 = rotation_matrices[idx, 1, 0]
    r22 = rotation_matrices[idx, 1, 1]
    r13 = rotation_matrices[idx, 0, 2]
    r33 = rotation_matrices[idx, 2, 0]
    angles_radians[idx, 0] = np.arctan2(r21, r22)
    angles_radians[idx, 2] = np.arctan2(r13, r33)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2xzy_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Ry(k3) @ Rz(k2) @ Rx(k1) = [[c2c3, -c1s2c3+s1s3, s1s2c3+c1s3],
                                [s2, c1c2, -s1c2],
                                [-c2s3, c1s2s3+s1c3, -s1s2s3+c1c3]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = np.arcsin(rotation_matrices[:, 1, 0])

    # Gimbal lock case (c2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 0, 0]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r32 = rotation_matrices[gimbal_idx, 2, 1]
    r33 = rotation_matrices[gimbal_idx, 2, 2]
    angles_radians[gimbal_idx, 0] = np.arctan2(r32, r33)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r23 = rotation_matrices[idx, 1, 2]
    r22 = rotation_matrices[idx, 1, 1]
    r31 = rotation_matrices[idx, 2, 0]
    r11 = rotation_matrices[idx, 0, 0]
    angles_radians[idx, 0] = np.arctan2(-r23, r22)
    angles_radians[idx, 2] = np.arctan2(-r31, r11)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2yxz_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Rz(k3) @ Rx(k2) @ Ry(k1) = [[-s1s2s3+c1c3, -c2s3, c1s2s3+s1c3],
                                [s1s2c3+c1s3, c2c3, -c1s2c3+s1s3],
                                [-s1c2, s2, c1c2]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = np.arcsin(rotation_matrices[:, 2, 1])

    # Gimbal lock case (c2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 1, 1]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r13 = rotation_matrices[gimbal_idx, 0, 2]
    r11 = rotation_matrices[gimbal_idx, 0, 0]
    angles_radians[gimbal_idx, 0] = np.arctan2(r13, r11)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r31 = rotation_matrices[idx, 2, 0]
    r33 = rotation_matrices[idx, 2, 2]
    r12 = rotation_matrices[idx, 0, 1]
    r22 = rotation_matrices[idx, 1, 1]
    angles_radians[idx, 0] = np.arctan2(-r31, r33)
    angles_radians[idx, 2] = np.arctan2(-r12, r22)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2zyx_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray:
    """
    Rx(k3) @ Ry(k2) @ Rz(k1) = [[c1c2, -s1c2, s2],
                                [c1s2s3+s1c3, -s1s2s3+c1c3, -c2s3],
                                [-c1s2c3+s1s3, s1s2c3+c1s3, c2c3]]
    :param rotation_matrices:
    :return: euler_angles
    """
    rotation_matrices = rotation_matrices.reshape((-1, 3, 3))
    angles_radians = np.zeros((rotation_matrices.shape[0], 3))

    # Angle 2 can be taken directly from matrices
    angles_radians[:, 1] = np.arcsin(rotation_matrices[:, 0, 2])

    # Gimbal lock case (c2 = 0)
    tolerance = 1e-4

    # Find indices where this is the case
    gimbal_idx = np.abs(rotation_matrices[:, 1, 1]) < tolerance

    # Calculate angle 1 and set angle 3 = 0 for those indices
    r21 = rotation_matrices[gimbal_idx, 1, 0]
    r22 = rotation_matrices[gimbal_idx, 1, 1]
    angles_radians[gimbal_idx, 0] = np.arctan2(r21, r22)
    angles_radians[gimbal_idx, 2] = 0

    # Normal case (s2 > 0)
    idx = np.invert(gimbal_idx)
    r12 = rotation_matrices[idx, 0, 1]
    r11 = rotation_matrices[idx, 0, 0]
    r23 = rotation_matrices[idx, 1, 2]
    r33 = rotation_matrices[idx, 2, 2]
    angles_radians[idx, 0] = np.arctan2(-r12, r11)
    angles_radians[idx, 2] = np.arctan2(-r23, r33)

    # convert to degrees
    euler_angles = np.rad2deg(angles_radians)

    return euler_angles

def matrix2euler_extrinsic(rotation_matrices: np.ndarray, axes: str):
    axes = axes.strip().lower()
    if axes not in ('xyx', 'yzy', 'zxz', 'xzx', 'yxy', 'zyz', 'xyz', 'yzx', 'zxy', 'xzy', 'yxz', 'zyx'):
        raise ValueError(f'Axes {axes} are not a valid set of euler angle axes')
    elif axes == 'xyx':
        return matrix2xyx_extrinsic(rotation_matrices)
    elif axes == 'yzy':
        return matrix2yzy_extrinsic(rotation_matrices)
    elif axes == 'zxz':
        return matrix2zxz_extrinsic(rotation_matrices)
    elif axes == 'xzx':
        return matrix2xzx_extrinsic(rotation_matrices)
    elif axes == 'yxy':
        return matrix2yzy_extrinsic(rotation_matrices)
    elif axes == 'zyz':
        return matrix2zyz_extrinsic(rotation_matrices)
    elif axes == 'xyz':
        return matrix2xyz_extrinsic(rotation_matrices)
    elif axes == 'yzx':
        return matrix2yzx_extrinsic(rotation_matrices)
    elif axes == 'zxy':
        return matrix2zxy_extrinsic(rotation_matrices)
    elif axes == 'xzy':
        return matrix2xzy_extrinsic(rotation_matrices)
    elif axes == 'yxz':
        return matrix2xzy_extrinsic(rotation_matrices)
    elif axes == 'zyx':
        return matrix2yxz_extrinsic(rotation_matrices)
    raise ValueError('A problem occurred')

def matrix2euler_intrinsic(rotation_matrices: np.ndarray, axes: str):
    """
    It can be shown that a set of intrinsic rotations about axes x then y then z through angles α, β, γ is equivalent
    to a set of extrinsic rotations about axes z then y then x by angles γ, β, α. :param rotation_matrices: :param
    axes: :return:
    """
    extrinsic_axes = axes[::-1]
    extrinsic_eulers = matrix2euler_extrinsic(rotation_matrices, extrinsic_axes)
    intrinsic_eulers = extrinsic_eulers[:, ::-1]
    return intrinsic_eulers

def matrix2euler_positive_ccw(rotation_matrices: np.ndarray, axes: str, intrinsic: bool = None, extrinsic: bool = None):
    """
    Calculates intrinsic or extrinsic euler angles (positive angles are ccw rotations) around a set of three axes from
    a set of rotation matrices
    :param rotation_matrices: rotation matrix (n,3,3) numpy array
    :param axes: 'zxz', 'zyz' or similar, three nonconsecutive axes
    :param intrinsic: fixed object, mobile coordinate system
    :param extrinsic: fixed coordinate system, mobile object
    :return:
    """
    if intrinsic and extrinsic:
        raise ValueError(f"Only one of 'intrinsic' and 'extrinsic' can be True")
    elif intrinsic:
        return matrix2euler_intrinsic(rotation_matrices, axes)
    elif extrinsic:
        return matrix2euler_extrinsic(rotation_matrices, axes)
    raise ValueError(f"Problem, could not interpret intrinsic or extrinsic as True or False")

def matrix2euler(rotation_matrices: np.ndarray, target_axes: str, target_positive_ccw: bool,
                 target_intrinsic: bool = None, target_extrinsic: bool = None) -> np.ndarray:
    if target_positive_ccw:
        euler_angles = matrix2euler_positive_ccw(rotation_matrices, target_axes, target_intrinsic, target_extrinsic)
    elif target_positive_ccw is False:
        euler_angles = matrix2euler_positive_ccw(rotation_matrices, target_axes, target_intrinsic,
                                                 target_extrinsic) * -1

    try:
        if euler_angles.shape[0] == 1:
            euler_angles = euler_angles.reshape(-1)
        return euler_angles

    except NameError:
        raise ValueError(f"Problem, could not interpret positive_ccw as True or False")

def euler2euler(euler_angles: np.ndarray, source_convention: Union[str, EulerAngleConvention],
                target_convention: Union[str, EulerAngleConvention]):
    # Parse conventions
    if isinstance(target_convention, str):
        target_convention = get_convention(target_convention)
    tc = target_convention
    if isinstance(source_convention, str):
        source_convention = get_convention(source_convention)
    sc = source_convention

    # Calculate rotation matrices from euler angles
    rotation_matrices = euler2matrix(euler_angles, sc.axes, sc.intrinsic,
                                     sc.extrinsic, sc.positive_ccw)

    # Invert matrices if one set of eulers describe rotation of the reference and the other
    # rotation of the particles
    # For rotation matrices, inversion is equivalent to the transpose
    if getattr(sc, 'reference_frame', None) != getattr(tc, 'reference_frame', None):
        rotation_matrices = rotation_matrices.transpose((0, 2, 1))

    # Calculate euler angles in the target convention
    euler_angles = matrix2euler(rotation_matrices, tc.axes, tc.positive_ccw, tc.intrinsic, tc.extrinsic)
    return euler_angles


# -*- coding: utf-8 -*-
"""
Created on Wed Dec 23 09:06:55 2015

Functions to find point of intersection and angle of intersection between
two lines
"""
import math
import random
import numpy as np
import matplotlib.pyplot as plt

#number of significant digits (rounding)
SIG_DIGITS = 6

def sci_round(x, sig=SIG_DIGITS):
    """Scientific rounding

       x:                 float to be rounded
       sig = SIG_DIGITS:  Number of significant digits """

    if x == 0:  # special case, prevent taking log of zero
        return x
    else:    # round
        return round(x, sig-int(math.floor(math.log10(abs(x))))-1)

def intersect_line(L0, L2):
    """ calculates the intersects of two lines

        L0 and L1: Lines specified by ((x0, y0), (x1, y1))

        - function returns the coordinataes of the intersection
          between both lines (px, py)
        - function returns an empty tuple if no intersection is found
        function returns (NaN, NaN) if lines are parallel
    """

    # unwrap lines
    x = [L0[0][0], L0[1][0], L2[0][0], L2[1][0]]
    y = [L0[0][1], L0[1][1], L2[0][1], L2[1][1]]

    #calculate denominator
    s1_x = x[1] - x[0]
    s1_y = y[1] - y[0]

    s2_x = x[3] - x[2]
    s2_y = y[3] - y[2]

    den = (-s2_x * s1_y + s1_x * s2_y)

    # if denominator is zero lines are parallel return NaN
    if sci_round(den) == 0:
        return (np.NAN, np.NAN)

    # parameterize line
    s = (-s1_y * (x[0] - x[2]) + s1_x * (y[0] - y[2])) / den
    t = (s2_x * (y[0] - y[2]) - s2_y * (x[0] - x[2])) / den

    # check if intersection lies on line pieces
    if (s >= 0) & (s <= 1) & (t >= 0) & (t <= 1):
        # calculate the intersection and return
        i_x = x[0] + (t * s1_x)
        i_y = y[0] + (t * s1_y)

        return (sci_round(i_x), sci_round(i_y))

    # lines not parallel but no intersection found on finite lines
    return (None, None)

def angle_between_lines(L0, L1):
    """ Angle in radians between two lines

        L0 and L1: Lines specified by ((x0, y0), (x1, y1))

        Even if both lines don't intersect the angle is returned.
    """
    angle1 = line_angle(L0)
    angle2 = line_angle(L1)
    return sci_round(abs(angle2-angle1))

def line_angle(L):
    """ Angle in radians between a line and the x-axis

        L = ((x0, y0), (x1, y1)) is a line defined by two points

       """

    #define vector direction
    v0 = abs(L[1]-L[0])
    v1 = [0, 1]

    #normalize vectors
    n0 = v0/np.linalg.norm(v0)
    n1 = v1/np.linalg.norm(v1)

    #prevent rounding errors, acos returns an error for values > 1
    inprod = sci_round(np.inner(n0, n1))

    #calculate angles
    angle = math.acos(inprod)

    return sci_round(angle)

def intersect_lines(lines):
    """ Calculate all intersections of a  list of lines

        lines:  A list of lines. Each line is defined by two points
                line = ((x0, y0), (x1, y1))"""

    intersect = []
    for i in range(0, len(lines)):
        for j in range(i+1, len(lines)):
            p = intersect_line(lines[i], lines[j])
            if not None in p:
                if not np.any(np.isnan(p)):
                    intersect.append(p)

    return tuple(intersect)

def plot_lines(lines):
    """ Plot a list of lines with points of intersection

        lines:  A list of lines. Each line is defined by two points
                line = ((x0, y0), (x1, y1))"""

    for line in lines:
        plt.plot(line[:, 0], line[:, 1])

    intersects = intersect_lines(lines)

    for intersect in intersects:
        plt.plot(*intersect, ' ro')
    return

def rand_line(bounds=((0, 0), (1, 1))):
    """ Return a random line

         bounds: ((xmin, ymin), (xmax, ymax)) the line must be within these
                  bounds."""
    return np.array((rand_point(bounds), rand_point(bounds)))

def rand_point(bounds=((0, 0), (1, 1))):
    """ Return a random point.

        bounds:  ((xmin, ymin), (xmax, ymax)) the line point be within these
                  bounds."""
    xi = random.random() * (bounds[1][0]-bounds[0][0]) + bounds[0][0]
    yi = random.random() * (bounds[1][0]-bounds[0][0]) + bounds[0][0]
    return (xi, yi)

if __name__ == "__main__":
    # testing
    NLINES = 10
    BOX = ((-10, -10), (10, 10))

    random_lines = [rand_line(BOX) for i in range(NLINES)]

    plot_lines(random_lines)


"""This module plots periodic B-spline quadratic basis functions.

Example:
> cd ~/sibl/geo/doc
> conda activate siblenv
> python plot_periodic_bspline_basis_p2.py
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
from matplotlib.ticker import MultipleLocator
from pathlib import Path

display = True
dpi = 100  # dots per inch
latex = True  # False
n_bisections = 6  # 2^n_bisections
serialize = True

# colors = ("tab:blue", "tab:orange", "tab:green", "tab:red", "tab:purple", "tab:cyan")
colors = ("tab:orange", "tab:green", "tab:red", "tab:purple", "tab:cyan", "tab:blue")
# linestyles = ("solid", "dashed", "dashdot")
linestyles = ("dashed", "dashdot", "solid")

if latex:
    rc("font", **{"family": "serif", "serif": ["Computer Modern Roman"]})
    rc("text", usetex=True)

# bases = (0, 1, 2)  # three basis functions
t = np.linspace(0, 1, 2 ** n_bisections + 1)
# b = tuple(t)
# a = tuple(map(lambda x: 1.0 - x, b))
# a = np.array(tuple(map(lambda t: 1.0 - t, t)))
# b = t

# n0 = 0.5 * a * a * a
# n1 = 1.5 * (a * a * b + 5 * a * b * b) + 0.5
# n2 = 0.5 * b * b * b

N0 = np.array(tuple(map(lambda t: 0.5 * (1.0 - t) ** 2, t)))
N1 = np.array(
    tuple(map(lambda t: 0.5 * (1.0 - t) ** 2 + 2 * t * (1.0 - t) + 0.5 * t ** 2, t))
)
N2 = np.array(tuple(map(lambda t: 0.5 * t ** 2, t)))

bases = (N0, N1, N2)
labels = (r"$\hat{N}_0^2$", r"$\hat{N}_1^2$", r"$\hat{N}_2^2$")

# fig = plt.figure(figsize=plt.figaspect(1.0 / 1.0), dpi=dpi)
fig = plt.figure()
ax = fig.gca()

for i, base in enumerate(bases):
    # y = bases[i]
    label = labels[i]
    ax.plot(
        t,
        base,
        "-",
        linewidth=2,
        label=label,
        color=colors[np.remainder(i, len(linestyles))],
        linestyle=linestyles[np.remainder(i, len(linestyles))],
    )

ax.grid(True, which="major", linestyle="-")
ax.grid(True, which="minor", linestyle=":")

ax.set_xlabel(r"$t$")
ax.set_ylabel(r"$\hat{N}^{p=2}_i(t)$")
# ax.legend(loc="upper right")
ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0)

_eps = 0.1
ax.set_xlim([0.0 - 2 * _eps, 1.0 + 2 * _eps])
ax.set_ylim([0.0 - 2 * _eps, 1.0 + 2 * _eps])

ax.set_aspect("equal")

ax.xaxis.set_major_locator(MultipleLocator(1.0))
ax.xaxis.set_minor_locator(MultipleLocator(0.25))
ax.yaxis.set_major_locator(MultipleLocator(1.0))
ax.yaxis.set_minor_locator(MultipleLocator(0.25))

if display:
    plt.show()

if serialize:
    extension = ".pdf"  # or '.svg'
    bstring = Path(__file__).stem + extension
    # fig.savefig(bstring, bbox_inches="tight")
    fig.savefig(bstring, bbox_inches="tight", pad_inches=0)


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: Carlos M. Alaíz

Color changes made by: José Antonio Álvarez
"""
import numpy as np
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score, accuracy_score
from matplotlib import pyplot as plt, colors, lines
from sklearn.linear_model import LinearRegression, Perceptron, Ridge

# For coloring (https://coolors.co/4effef-f6ae2d-f26419-6f8f72-504b43)
c1 = '#4EFFEF'
c2 = '#F6AE2D'

def plot_dataset(x, y):
    if (len(x.shape) == 1):
        plt.plot(x, y, "*")
        plt.xlabel("$x$")
        plt.ylabel("$y$")
        plt.title("Data")
    else:
        n_plot = x.shape[1]
        fig, axs = plt.subplots(ncols=n_plot, sharey=True)
        for i in range(n_plot):
            ax = axs[i]
            ax.plot(x[:, i], y, "*")
            ax.set_xlabel("$x_{%d}$" % (i + 1))
            if (i == 0):
                ax.set_ylabel("$y$")
        plt.suptitle("Data")

def plot_linear_model(x, y_r, w_e, b_e, w_r=None, b_r=None):
    if (np.isscalar(w_e) or (len(w_e) == 1)):
        y_p = w_e * x + b_e

        plt.plot(x, y_r, "*", label="Obs.")
        plt.plot(x, y_p, "-", label="Pred")

        for i in range(len(x)):
            plt.plot([x[i].item(), x[i].item()], [y_p[i].item(), y_r[i].item()], ":k")

        if (w_r is not None) and (b_r is not None):
            plt.plot(x, w_r * x + b_r, "--k", label="Real")
            plt.legend()

        plt.xlabel("$x$")
        plt.ylabel("$y$")
        plt.title("$y = %.2f x + %.2f$ (MSE: %.2f, MAE: %.2f, R2: %.2f)" % (w_e, b_e, mean_squared_error(y_r, y_p), mean_absolute_error(y_r, y_p), r2_score(y_r, y_p)))
    else:
        y_p = x @ w_e + b_e
        pos = np.arange(len(w_e) + 1)
        plt.bar(pos, np.append(w_e, b_e), alpha=0.5, label="Est.")

        if (w_r is not None) and (b_r is not None):
            plt.bar(pos, np.append(w_r, b_r), alpha=0.5, label="Real")
            plt.legend()

        plt.grid()
        labels = []
        for i in range(len(w_e)):
            labels.append("$w_%d$" % (i + 1))
        labels.append("$b$")
        plt.xticks(pos, labels)
        plt.title("MSE: %.2f, MAE: %.2f, R2: %.2f" % (mean_squared_error(y_r, y_p), mean_absolute_error(y_r, y_p), r2_score(y_r, y_p)))

def evaluate_linear_model(x_tr, y_tr_r, x_te, y_te_r, w, b, plot=False):
    if (np.isscalar(w)):
        y_tr_p = w * x_tr + b
        y_te_p = w * x_te + b
    else:
        y_tr_p = x_tr @ w.ravel() + b
        y_te_p = x_te @ w.ravel() + b

    er_tr = [mean_squared_error(y_tr_r, y_tr_p), mean_absolute_error(y_tr_r, y_tr_p), r2_score(y_tr_r, y_tr_p)]
    er_te = [mean_squared_error(y_te_r, y_te_p), mean_absolute_error(y_te_r, y_te_p), r2_score(y_te_r, y_te_p)]

    ers = [er_tr, er_te]
    headers=["MSE", "MAE", "R2"]

    print("%10s" % "", end="")
    for h in headers:
        print("%10s" % h, end="")
    print("")

    headersc = ["Train", "Test"]

    cnt = 0
    for er in ers:
        hc = headersc[cnt]
        cnt = cnt + 1
        print("%10s" % hc, end="")

        for e in er:
            print("%10.2f" % e, end="")
        print("")

    if plot:
        plot_linear_model(x_te, y_te_r, w.ravel(), b)

def plot_dataset_clas(x, y):
    if (len(x.shape) == 1):
        plt.plot(x, y, "*")
        plt.xlabel("$x$")
        clas = np.unique(y)
        plt.yticks(clas)
    else:
        if (len(np.unique(y) == 2)):
            ind = y == 1
            plt.scatter(x[ind, 0], x[ind, 1], c=c1, zorder=100)
            ind = y != 1
            plt.scatter(x[ind, 0], x[ind, 1], c=c2, zorder=100)
        plt.xlabel("$x_1$")
        plt.ylabel("$x_2$")

    plt.axis("equal")
    plt.title("Data")

def order_points(points):
    if len(points) == 0:
        return []
    centre = points.mean(axis=0);
    angles = np.arctan2(points[:, 0] - centre[0], points[:, 1] - centre[1])
    o_points = points[np.argsort(angles), :]

    return np.vstack((o_points, o_points[0, :]))

def plot_linear_model_clas(x, y_r, w, b):
    if (len(x.shape) == 1) or (x.shape[1] != 2):
        raise ValueError("only_r two-dimensional problems can be represented")

    y_p = np.sign(x @ w + b)

    plot_dataset_clas(x, y_r)
    ax = plt.axis("equal")
    lims = np.array([ax[0] - 100, ax[1] + 100, ax[2] - 100, ax[3] + 100])

    if (w[1] != 0):
        x1 = lims[0:2]
        x2 = - (w[0] * x1 + b) / w[1]
    else:
        x2 = lims[2:]
        x1 = - (w[1] * x2 + b) / w[0]

    points = np.column_stack((np.append(x1, [lims[0], lims[1], lims[0], lims[1]]), np.append(x2, [lims[2], lims[3], lims[3], lims[2]])))

    points_p = order_points(points[points @ w + b >= - 1e-2])
    if (len(points_p) > 0):
        plt.fill(points_p[:, 0], points_p[:, 1], "b", alpha=0.3)

    points_n = order_points(points[points @ w + b <= + 1e-2])
    if (len(points_n) > 0):
        plt.fill(points_n[:, 0], points_n[:, 1], "r", alpha=0.3)

    plt.plot(x1, x2, "-k")
    plot_dataset_clas(x, y_r)
    plt.axis(ax)

    plt.title("$y = %.2f x_1 + %.2f x_2 + %.2f$ (Acc: %.2f%%)" % (w[0], w[1], b, 100 * accuracy_score(y_r, y_p)))

def fun_cross_entropy(X, y, w):
    y_b = y.copy()
    y_b[y_b == -1] = 0

    y_p = 1 / (1 + np.exp(- X @ w))

    return (- (1 - y_b) * np.log(1 - y_p) - y_b * np.log(y_p)).sum()

def grad_cross_entropy(X, y, w):
    y_b = y.copy()
    y_b[y_b == -1] = 0

    y_p = 1 / (1 + np.exp(- X @ w))

    return X.T @ (y_p - y_b)

def fit_polylinear_regression(x, y, deg=1):
    X = np.power(np.reshape(x, (len(x), 1)), np.arange(1, deg + 1))
    model = LinearRegression()
    model.fit(X, y)

    return model

def pred_polylinear_regression(model, x):
    X = np.power(np.reshape(x, (len(x), 1)), np.arange(1, len(model.coef_) + 1))
    return model.predict(X)

def plot_polylinear_model(x, y_r, model):
    xv = np.linspace(x.min(), x.max())
    plt.plot(x, y_r, "*", label="Obs.")
    plt.plot(xv, pred_polylinear_regression(model, xv), "-", label="Pred")

    y_p = pred_polylinear_regression(model, x)

    for i in range(len(x)):
        plt.plot([x[i].item(), x[i].item()], [y_p[i].item(), y_r[i].item()], ":k")

        plt.xlabel("$x$")
        plt.ylabel("$y$")
        plt.title("Degree: %d (MSE: %.2f, MAE: %.2f, R2: %.2f)" % (len(model.coef_), mean_squared_error(y_r, y_p), mean_absolute_error(y_r, y_p), r2_score(y_r, y_p)))

def norm_p(w, p):
    if (p == 0):
        return np.count_nonzero(w)

    if (p == np.inf):
        return np.max(np.abs(w))

    nw = np.sum(np.power(np.abs(w), p))
    if (p > 1):
        nw = np.power(nw, 1 / p)
    return nw

def plot_contour_lp(p, mini=-3, maxi=3, npoi = 21):
    x = np.linspace(mini, maxi, npoi)
    y = np.linspace(mini, maxi, npoi)
    x, y = np.meshgrid(x, y)

    z = np.apply_along_axis(norm_p, 2, np.stack([x, y], axis = 2), p)
    plt.contour(x, y, z)

    plt.xlabel("$x_1$")
    plt.ylabel("$x_2$")
    plt.gca().set_aspect("equal", "box")
    plt.title("Norm $\ell_{%g}$" % p)

    plt.grid()
    plt.show()

def plot_contour_l1_l2(l1_ratio=0.5, mini=-3, maxi=3, npoi = 21):
    x = np.linspace(mini, maxi, npoi)
    y = np.linspace(mini, maxi, npoi)
    x, y = np.meshgrid(x, y)

    z = l1_ratio * np.apply_along_axis(norm_p, 2, np.stack([x, y], axis = 2), 1) + (1 - l1_ratio) * np.apply_along_axis(norm_p, 2, np.stack([x, y], axis = 2), 2)
    plt.contour(x, y, z)

    plt.xlabel("$x_1$")
    plt.ylabel("$x_2$")
    plt.gca().set_aspect("equal", "box")
    plt.title("%g * Norm $\ell_1$ + %g * Norm $\ell_2$" % (l1_ratio, 1 - l1_ratio))

    plt.grid()
    plt.show()

def plot_contour_linear_lp(X, y, p=None, mini=-3, maxi=3, npoi=51):
    def mse_linear(w):
        return mean_squared_error(y, X @ w)

    x1 = np.linspace(mini, maxi, npoi)
    x2 = np.linspace(mini, maxi, npoi)
    x1, x2 = np.meshgrid(x1, x2)

    z = np.apply_along_axis(mse_linear, 2, np.stack([x1, x2], axis = 2))
    plt.contour(x1, x2, z, 30)

    if p is not None:
        x = np.linspace(-1, 1, 101)
        if (p == 0):
            plt.plot([-1, 1], [0, 0], "-k")
            plt.plot([0, 0], [-1, 1], "-k")
            ball = np.abs(x1) + np.abs(x2) <= 1
        elif (p == np.inf):
            plt.plot([-1, 1, 1, -1, -1], [1, 1, -1, -1, 1, ], "-k")
            plt.fill([-1, 1, 1, -1, -1], [1, 1, -1, -1, 1, ], "k")
            ball = np.maximum(x1, x2) <= 1
        else:
            y = np.power(1 - np.power(np.abs(x), p), 1 / p)
            plt.plot(np.concatenate((x, np.flip(x))), np.concatenate((y, np.flip(-y))), "-k")
            plt.fill(np.concatenate((x, np.flip(x))), np.concatenate((y, np.flip(-y))), "k")
            ball = np.power(np.abs(x1), p) + np.power(np.abs(x2), p) <= 1

        obj = z
        obj[ball == False] = np.inf
    else:
        obj = z

    ind = np.unravel_index(np.argmin(obj), obj.shape)
    plt.plot(x1[ind[0], ind[1]], x2[ind[0], ind[1]], "r*")

    plt.xlabel("Feature 1")
    plt.ylabel("Feature 2")
    plt.gca().set_aspect("equal", "box")
    if (p is not None):
        plt.title("Norm $\ell_{%g}$ + MSE" % p)
    else:
        plt.title("MSE")

    plt.grid()
    plt.show()

def generate_bv_example(n_rep=250, n_mod=15, n_dim=10, noise=3e-1, seed=1234):
    n_pat = n_dim
    alpha_v = np.logspace(-4, 4, n_mod)

    np.random.seed(seed)

    w = np.random.randn(n_dim)
    x_te = np.random.randn(n_pat, n_dim)
    y_te = x_te @ w + noise * np.random.randn(n_pat)

    distances = np.zeros((n_mod, n_rep))
    predictions = np.zeros((n_mod, n_rep, n_pat))
    for i, alpha in enumerate(alpha_v):
        for j in range(n_rep):
            x_tr = np.random.randn(n_pat, n_dim)
            y_tr = x_tr @ w + noise * np.random.randn(n_pat)
            y_te_p = Ridge(alpha=alpha, fit_intercept=False).fit(x_tr, y_tr).predict(x_te)
            predictions[i, j, :] = y_te_p
            distances[i, j] = mean_squared_error(y_te, y_te_p)

    return distances, predictions, y_te

def plot_perceptron_evo_epochs(x, y, max_epochs=5):
    import warnings
    warnings.filterwarnings("ignore", category=Warning)

    fig, ax = plt.subplots(nrows=1, ncols=max_iters)
    for i in range(max_epochs):
        model = Perceptron(tol=-1, max_iter=i + 1)
        model.fit(x, y)

        plt.sca(ax[i])
        plot_linear_model_clas(x, y, model.coef_[0], model.intercept_)
        if (i > 0):
            ax[i].set_yticklabels("")
            ax[i].set_ylabel("")
        plt.title("Epoch %d (Acc: %.2f%%)" % (i + 1, 100 * accuracy_score(y, model.predict(x))))
    plt.tight_layout()

def plot_perceptron_evo_iter(x, y, max_iters=5):
    import warnings
    warnings.filterwarnings("ignore", category=Warning)

    n_pat = x.shape[0]
    n_dim = x.shape[1]
    w = np.zeros(n_dim + 1)
    w[0] = 1e-1
    x_b = np.column_stack((np.ones(n_pat), x))

    nrows = int(np.ceil(max_iters / 5))
    ncols = 5
    fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(15, 3 * nrows))
    ax = ax.ravel()
    for i in range(max_iters):
        x_i = x_b[i % n_pat, :]
        y_i = y[i % n_pat]

        pred = np.sign(w @ x_i)

        plt.sca(ax[i])
        plot_linear_model_clas(x, y, w[1:], w[0])
        plt.scatter(x_i[1], x_i[2], s=200, linewidth=4, facecolors="none", edgecolors="k", zorder=100)
        if (i % ncols > 0):
            ax[i].set_yticklabels("")
            ax[i].set_ylabel("")
        if (i < (nrows - 1) * ncols):
            ax[i].set_xticklabels("")
            ax[i].set_xlabel("")
        plt.title("Iter. %d (Acc: %.2f%%)" % (i + 1, 100 * accuracy_score(y, np.sign(x_b @ w))))

        w += (y_i - pred) / 2 * x_i

    plt.tight_layout()

def plot_nonlinear_model(x, y_r, model, phi=None):
    if phi is None:
        phi = lambda x: np.reshape(x, (-1, 1))
    y_p = model.predict(phi(x))

    plt.plot(x, y_r, "*", label="Obs.")
    plt.plot(x, y_p, "-", label="Pred")

    for i in range(len(x)):
        plt.plot([x[i].item(), x[i].item()], [y_p[i].item(), y_r[i].item()], ":k")

    plt.xlabel("$x$")
    plt.ylabel("$y$")
    plt.title("(MSE: %.2f, MAE: %.2f, R2: %.2f)" % (mean_squared_error(y_r, y_p), mean_absolute_error(y_r, y_p), r2_score(y_r, y_p)))

def plot_nonlinear_model_clas(x, y_r, model, phi=None, n_points=31):
    if phi is None:
        phi = lambda x: np.reshape(x, (-1, 1))

    alpha = 0.3
    col_1 = np.array([31, 119, 180]) / 255
    col_2 = np.array([214, 39, 40]) / 255

    y_p = model.predict(phi(x))

    ind = y_r < 0
    plt.scatter(x[ind, 0], x[ind, 1], c=[col_1], zorder=100)
    ind = y_r >= 0
    plt.scatter(x[ind, 0], x[ind, 1], c=[col_2], zorder=100)
    ax = plt.axis("equal")

    x_1 = np.linspace(plt.xlim()[0], plt.xlim()[1], n_points)
    x_1 = np.hstack((x_1[0] - 100, x_1, x_1[-1] + 100))
    x_2 = np.linspace(plt.ylim()[0], plt.ylim()[1], n_points)
    x_2 = np.hstack((x_2[0] - 100, x_2, x_2[-1] + 100))

    x_1, x_2 = np.meshgrid(x_1, x_2, indexing="ij")

    plt.pcolormesh(x_1, x_2, np.reshape(model.predict(phi(np.column_stack((x_1.ravel(), x_2.ravel())))), x_1.shape), shading="auto", cmap=colors.ListedColormap([alpha * col_1 + 1 - alpha, alpha * col_2 + 1 - alpha]))

    plt.axis(ax)
    plt.xlabel("$x_1$")
    plt.ylabel("$x_2$")

    plt.title("(Acc: %.2f%%)" % (100 * accuracy_score(y_r, y_p)))

def polynomial_basis(X, deg):
    def phi(x):
        return np.power(x, np.arange(0, deg + 1))
    return np.array([phi(x) for x in X])

def gaussian_basis(X, mu, sigma):
    sigma_2 = np.power(sigma, 2)
    def phi(x):
        return np.exp(- np.power(x - mu, 2) / sigma_2)
    return np.array([phi(x) for x in X])

def sigmoidal_basis(X, a, b):
    def phi(x):
        return 1 / (1 + np.exp(- (a * x - b)))
    return np.array([phi(x) for x in X])

def plot_krr_coefficients(model, label_gap=5):
    coef = model.dual_coef_
    pos = np.arange(len(coef))
    plt.bar(pos, coef, alpha=0.5)

    plt.grid()
    labels = []
    for i in range(len(coef)):
        labels.append("$\\alpha_{%d}$" % (i + 1))
    plt.xticks(pos[::label_gap], labels[::label_gap])
    plt.title("Dual Coefficients")

def plot_svc(x, y, model, n_points=151, plot_slack=False,
             plot_support_vectors=True):
    alpha = 0.2
    col_2 = np.array([78, 255, 239]) / 255
    col_1 = np.array([246, 174, 45]) / 255

    ind = y != 1
    plt.scatter(x[ind, 0], x[ind, 1], c=c1, s=30, zorder=100)
    ind = y == 1
    plt.scatter(x[ind, 0], x[ind, 1], c=c2, s=30, zorder=100)

    lims = plt.axis("equal")

    xx = np.linspace(lims[0] - 1.1 * (lims[1] - lims[0]), lims[1] + 1.1 * (lims[1] - lims[0]), n_points)
    yy = np.linspace(lims[2], lims[3], n_points)
    yy, xx = np.meshgrid(yy, xx)
    xy = np.vstack([xx.ravel(), yy.ravel()]).T
    zz = model.decision_function(xy).reshape(xx.shape)

    plt.pcolormesh(xx, yy, np.sign(zz), shading="auto",
                   cmap=colors.ListedColormap([alpha * col_2 + 1 - alpha, alpha * col_1 + 1 - alpha]))
    plt.contour(xx, yy, zz, colors=[c1, "k", c2], levels=[-1, 0, 1], linestyles=["--", "-", "--"], linewidths=[2, 4, 2])
    plt.legend(handles=[
        lines.Line2D([], [], color=c1, linestyle="--", label="Support Hyp. $-1$"),
        lines.Line2D([], [], color="k", linestyle="-", label="Sepparating Hyp."),
        lines.Line2D([], [], color=c2, linestyle="--", label="Support Hyp. $+1$")
    ])

    # Plot black circles around the support vectors
    if plot_support_vectors:
        plt.scatter(model.support_vectors_[:, 0],
                    model.support_vectors_[:, 1],
                    s=100, linewidth=3, facecolors="none", edgecolors="k")

    if (plot_slack):
        w = model.coef_[0]
        b = model.intercept_
        nws = np.linalg.norm(w)**2
        for i in model.support_:
            p = x[i, :] - (w @ x[i, :] + b - y[i]) / nws * w
            c = c2 if y[i] == 1 else c1
            plt.plot([p[0], x[i, 0]], [p[1], x[i, 1]], ":", color=c)

    plt.axis(lims)
    plt.xlabel("$x_1$")
    plt.ylabel("$x_2$")
    plt.title("SVM (%s, C=%.2g)" % (model.kernel, model.C))

def plot_svc_animation_frame(x, y, ax, model, lims, subtitle, n_points=151, alpha=0.2):
    alpha = 0.2
    col_2 = np.array([78, 255, 239]) / 255
    col_1 = np.array([246, 174, 45]) / 255

    ind = y != 1
    ax.scatter(x[ind, 0], x[ind, 1], c=c1, s=30, zorder=100)
    ind = y == 1
    ax.scatter(x[ind, 0], x[ind, 1], c=c2, s=30, zorder=100)

    if lims == None:
        lims = ax.axis("equal")

    xx = np.linspace(lims[0] - 1.1 * (lims[1] - lims[0]),
                     lims[1] + 1.1 * (lims[1] - lims[0]),
                     n_points)
    yy = np.linspace(lims[2], lims[3], n_points)
    yy, xx = np.meshgrid(yy, xx)
    xy = np.vstack([xx.ravel(), yy.ravel()]).T
    zz = model.decision_function(xy).reshape(xx.shape)

    ax.pcolormesh(xx, yy, np.sign(zz), shading="auto",
                   cmap=colors.ListedColormap([alpha * col_2 + 1 - alpha,
                                               alpha * col_1 + 1 - alpha]))
    ax.contour(xx, yy, zz, colors=[c1, "k", c2], levels=[-1, 0, 1],
               linestyles=["--", "-", "--"], linewidths=[2, 4, 2])

    ax.legend(handles=[
        lines.Line2D([], [], color=c1, linestyle="--", label="Support Hyp. $-1$"),
        lines.Line2D([], [], color="k", linestyle="-", label="Sepparating Hyp."),
        lines.Line2D([], [], color=c2, linestyle="--", label="Support Hyp. $+1$")
    ])

    # Plot black circles around the support vectors
    ax.scatter(model.support_vectors_[:, 0],
                model.support_vectors_[:, 1],
                s=100, linewidth=3, facecolors="none", edgecolors="k")

    ax.axis(lims)
    ax.set_xlabel("$x_1$")
    ax.set_ylabel("$x_2$")
    ax.set_title(subtitle)
    return lims

def plot_all_linear_separators(x, y, plot_best=False, n_points=51):
    ang_vec = np.linspace(0, 2 * np.pi, n_points)
    b_vec = np.linspace(-5, 5, n_points)

    ang_mat, b_mat = np.meshgrid(ang_vec, b_vec, indexing="ij")
    ws = []
    bs = []
    ms = []
    svs = []

    for i_ang in range(len(ang_vec)):
        ang = ang_vec[i_ang]
        for i_b in range(len(b_vec)):
            b = b_vec[i_b]
            w = np.array([np.sin(ang), np.cos(ang)])
            d = (np.abs(x @ w + b) / np.linalg.norm(w))
            m = d.min()
            sv = np.argsort(d)[:3]
            y_p = np.sign(x @ w + b)
            if (accuracy_score(y, y_p) == 1):
                ws.append(w)
                bs.append(b)
                ms.append(m)
                svs.append(sv)

    plot_dataset_clas(x, y)
    lims = plt.axis()

    max_m = np.array(ms).max()
    for w, b, m, sv in zip(ws, bs, ms, svs):
        if (w[1] != 0):
            x1 = np.asarray(lims[0:2])
            x1[0] -= 1.1 * (lims[1] - lims[0])
            x1[1] += 1.1 * (lims[1] - lims[0])
            x2 = - (w[0] * x1 + b) / w[1]
        else:
            x2 = lims[2:]
            x1 = - (w[1] * x2 + b) / w[0]

        if (plot_best):
            if (m == max_m):
                plt.plot(x1, x2, "-k", alpha=1.0)
                plt.scatter(x[sv, 0], x[sv, 1], s=100, linewidth=3, facecolors="none", edgecolors="k")
            else:
                plt.plot(x1, x2, "-k", alpha=0.3)
        else:
            plt.plot(x1, x2, "-k", alpha=0.3)

    plt.axis(lims)
    plt.xlabel("$x_1$")
    plt.ylabel("$x_2$")
    plt.title("Linear Classifiers")

def plot_svr(x, y, model, n_points=151, plot_slack=False):
    x_e = np.linspace(x.min(), x.max(), n_points)
    np.append(x_e, x)
    x_e.sort()

    y_p = model.predict(x_e.reshape(-1, 1))
    y_pi = model.predict(x.reshape(-1, 1))

    plt.plot(x, y, "*", label="Obs.")
    plt.plot(x_e, y_p, "-", label="Model")
    plt.plot(x_e, y_p + model.epsilon, "--k")
    plt.plot(x_e, y_p - model.epsilon, "--k")

    plt.scatter(x[model.support_], y[model.support_], s=100, linewidth=3, facecolors="none", edgecolors="k")

    if (plot_slack):
        for i in range(len(x)):
            if (y_pi[i] > y[i] + model.epsilon):
                plt.plot([x[i].item(), x[i].item()], [y_pi[i].item() - model.epsilon, y[i].item()], ":k")
            if (y_pi[i] < y[i] - model.epsilon):
                plt.plot([x[i].item(), x[i].item()], [y_pi[i].item() + model.epsilon, y[i].item()], ":k")

    plt.xlabel("$x$")
    plt.ylabel("$y$")
    plt.title("SVM (%s, C=%.2g)" % (model.kernel, model.C))

def evaluate_nonlinear_model(x_tr, y_tr_r, x_te, y_te_r, model):
    y_tr_p = model.predict(x_tr)
    y_te_p = model.predict(x_te)

    er_tr = [mean_squared_error(y_tr_r, y_tr_p), mean_absolute_error(y_tr_r, y_tr_p), r2_score(y_tr_r, y_tr_p)]
    er_te = [mean_squared_error(y_te_r, y_te_p), mean_absolute_error(y_te_r, y_te_p), r2_score(y_te_r, y_te_p)]

    ers = [er_tr, er_te]
    headers=["MSE", "MAE", "R2"]

    print("%10s" % "", end="")
    for h in headers:
        print("%10s" % h, end="")
    print("")

    headersc = ["Train", "Test"]

    cnt = 0
    for er in ers:
        hc = headersc[cnt]
        cnt = cnt + 1
        print("%10s" % hc, end="")

        for e in er:
            print("%10.2f" % e, end="")
        print("")

def generate_sequence(env, early_stop=True, model=None, n_steps=500):
    state = env.reset()
    for i in range(n_steps):
        env.render()
        if model is None:
            action = env.action_space.sample()
        else:
            action_probs, _ = model.predict(tf.expand_dims(tf.convert_to_tensor(state), 0))
            p = np.squeeze(action_probs)
            action = np.random.choice(len(p), p=p)
        state, _, done, _ = env.step(action)
        if early_stop and done:
            print("Finished after %d steps" % i)
            break


"""Conduct auxiliary operations throughout the project."""
import numba as nb
import numpy as np

def gini(pop, val, make_plot=False):
    """Computes Gini coefficient and Lorentz curve.

    The vectors pop and val must be equally long and must contain
    only positive values (zeros are also acceptable). A typical application
    would be that pop represents population sizes of some subgroups (e.g.
    different countries or states), and val represents the income per
    capita in this different subgroups. The Gini coefficient is a measure
    of how unequally income is distributed between these classes. A
    coefficient of zero means that all subgroups have exactly the same
    income per capital, so there is no dispesion of income; A very large
    coefficient would result if all the income accrues only to one subgroup
    and all the remaining groups have zero income. In the limit, when the
    total population size approaches infinity, but all the income accrues
    only to one individual, the Gini coefficient approaches unity.

    The Lorenz curve is a graphical representation of the distribution. If
    (x,y) is a point on the Lorenz curve, then the poorest x-share of the
    population has the y-share of total income. By definition, (0,0) and
    (1,1) are points on the Lorentz curve (the poorest 0% have 0% of total
    income, and the poorest 100% [ie, everyone] have 100% of total income).
    Equal distribution implies that the Lorentz curve is the 45 degree line.
    Any inequality manifests itself as deviation of the Lorentz curve from
    the 45 degree line. By construction, the Lorenz curve is weakly convex
    and increasing.

    The two concepts are related as follows: The Gini coefficient is twice
    the area between the 45 degree line and the Lorentz curve.

    Author : Yvan Lengwiler
    Release: $1.0$
    Date   : $2010-06-27$

    Arguments:
        pop: np.array(length)
            Population sizes of the different classes
        val: np.array(length)
            measurement variable (e.g. income per capita) in the different classes.
        make_plot: bool
            indicator whether a figure of the Lorentz curve should be produced
            or not. Default is false.

    Returns:
        gini: np.float64
            Gini coefficient
        lorentz_rel: np.array(num_classes)
            Lorentz curve (relative): two-column array, with the left column
            representing cumulative population shares of the different classes,
            sorted according to val, and the right column representing the cumulative
            value share that belongs to the population up to the given class. The
            Lorentz curve is a scatter plot of the left vs the right column.
        lorentz_abs: np.array(num_classes)
            Lorentz curve (absolute): Same as lorentz_rel, except that the
            components are not normalized to range in the unit interval. Thus,
            the left column of a is the absolute cumulative population sizes of
            the classes, and the right column is the absolute cumulative value of
            all classes up to the given one.
    """
    # check arguments
    assert np.prod(pop.shape) == np.prod(
        val.shape
    ), "gini expects two equally long vectors (#d ~= #d).".format(
        np.prod(pop.shape), np.prod(val.shape)
    )

    # pre-append a zero
    pop = np.pad(pop.T, (1, 0), "constant", constant_values=0)
    val = np.pad(val.T, (1, 0), "constant", constant_values=0)

    # filter out NaNs
    pop = pop[~np.isnan(pop) & ~np.isnan(val)]
    val = val[~np.isnan(pop) & ~np.isnan(val)]

    if len(pop) < 2:
        print("gini:lacking_data", "not enough data")
        gini_coefficient = np.nan
        lorentz_rel = np.nan(1, 4)
        lorentz_abs = np.nan(1, 4)
        return gini_coefficient, lorentz_rel, lorentz_abs

    assert np.all(pop >= 0) and np.all(
        val >= 0
    ), "gini expects non-negative vectors (neg elements in pop = {}, in val = {}).".format(
        sum(pop < 0), sum(val < 0)
    )

    # process input
    z = val * pop
    val_index = val.argsort()
    pop = pop[val_index]
    z = z[val_index]
    pop = np.cumsum(pop)
    z = np.cumsum(z)
    rel_pop = pop / pop[-1]
    rel_z = z / z[-1]

    # Gini coefficient

    # We compute the area below the Lorentz curve. We do this by
    # computing the average of the left and right Riemann-like sums.
    # (I say Riemann-'like' because we evaluate not on a uniform grid, but
    # on the points given by the pop data). These are the two Riemann-like sums:
    #    left_sum  = sum(rel_z[:-1] * diff(rel_pop))
    #    right_sum = sum(rel_z[1:] * diff(rel_pop))
    # The Gini coefficient is one minus twice the average of left_sum and
    # right_sum. We can put all of this into one line.
    gini_coefficient = 1 - sum((rel_z[:-1] + rel_z[1:]) * np.diff(rel_pop))

    # Lorentz curve
    lorentz_rel = [rel_pop, rel_z]
    lorentz_abs = [pop, z]
    if make_plot:
        # area(relpop, relz, 'FaceColor', [0.5, 0.5, 1.0])  # the Lorentz curve
        # plot([0, 1], [0, 1], '--k')  # 45 degree line
        # axis tight  # ranges of abscissa and ordinate are by definition exactly [0,1]
        # axis square  # both axes should be equally long
        # set(gca, 'XTick', get(gca, 'YTick'))  # ensure equal ticking
        # set(gca, 'Layer', 'top')  # grid above the shaded area
        # title(['\bfGini coefficient = ', str(g)])
        # xlabel('share of population')
        # ylabel('share of value')
        pass

    return gini_coefficient, lorentz_rel, lorentz_abs

def reshape_as_vector(in_1, in_2):
    """Cast input matrices in single vector for function "gini".

    Arguments:
        in_1: np.array(n_gridpoints_dim_1, n_gridpoints_dim_2, length_1)
            Input for working age agents
        in_2: np.array(n_gridpoints_dim_1, length_2)
            Input for retired agents
    Returns:
        out: np.array(np.prod(in_1.shape) + np.prod(in_2.shape))
            Combined input in vector shape
    """
    out = np.zeros(np.prod(in_1.shape) + np.prod(in_2.shape))
    out[: np.prod(in_1.shape)] = in_1.reshape((np.prod(in_1.shape)), order="F")
    out[np.prod(in_1.shape) :] = in_2.reshape((np.prod(in_2.shape)), order="F")

    return out

def get_average_hours_worked(policy, mass_distribution):
    """Compute average hours worked from labor input policy and mass distribution of
        working age households.

    Arguments:
        policy: np.array(n_gridpoints_dim_1, n_gridpoints_dim_2, length_1)
            Labor input policy function
        mass_distribution: np.array(n_gridpoints_dim_1, n_gridpoints_dim_2, length_1)
            Mass distribution of working age households
    Returns:
        out: np.array(length)
            Average hours worked by age
    """
    hours = np.multiply(policy, mass_distribution)
    hours_by_age = np.sum(np.sum(hours, axis=1), axis=0)
    hours_average = hours_by_age / np.sum(np.sum(mass_distribution, axis=1), axis=0)

    return hours_average

def get_income(
    interest_rate,
    capital_grid,
    pension_benefit,
    duration_retired,
    n_gridpoints_capital,
    duration_working,
    n_gridpoints_hc,
    hc_grid,
    efficiency,
    policy_labor_working,
):
    """Compute household income during working age and during retirement.

    Arguments:
        interest_rate: np.float64
            Current interest rate on capital holdings
        capital_grid: np.array(n_gridpoints_capital)
            Asset grid
        pension_benefit: np.float64
            Income from pension benefits
        duration_retired: np.int32
            Length of retirement period
        n_gridpoints_capital: np.int32
            Number of grid points of capital grid
        duration_working: np.int32
            Length of working age
        n_gridpoints_hc: np.int32
            Number of grid points of human capital grid
        hc_grid: np.array(n_gridpoints_capital)
            Human capital grid
        efficiency: np.array(age_max)
            Vector of age-dependent labor efficiency multipliers
        policy_labor_working: np.array(n_gridpoints_capital, n_gridpoints_hc, duration_working)
            Labor supply policy function for agents of given age (storing optimal
            hours worked as np.float64)
    Returns:
        income_retired: np.array(n_gridpoints_capital, duration_retired)
            Total household income during retirement by current asset level and age
        income_working: np.array(n_gridpoints_capital, n_gridpoints_hc, duration_working)
            Total household income during working age by current assets, current human capital
            level and age
    """
    # Repeat retirement income for all ages in retirement period
    income_retired = interest_rate * capital_grid + pension_benefit
    income_retired = np.repeat(income_retired[:, np.newaxis], duration_retired, axis=1)

    # Calculate working age income from states and labor input policy
    assets_this_period, hc_this_period = np.meshgrid(capital_grid, hc_grid)
    income_working = np.zeros((n_gridpoints_capital, n_gridpoints_hc, duration_working))

    for age_idx in range(duration_working):
        labor_input = policy_labor_working[:, :, age_idx].T
        income_working[:, :, age_idx] = (
            interest_rate * assets_this_period
            + hc_this_period * efficiency[age_idx] * labor_input
        ).T

    return income_retired, income_working

@nb.njit
def set_continuous_point_on_grid(value, grid, gp_ids, gp_weights):
    """Linearly interpolate a continuous value on a given grid.

    Linearly interpolate a continuous value on a discrete grid by first finding
    the indices of the values on *grid* circumjacent to *value* and then calculating
    linear interpolation weights.

    Arguments:
        value: np.float64
            Value to be interpolated
        grid: np.array(n_gridpoints)
            Interpolation grid
        gp_ids: np.array(2)
            Indices of interpolation nodes
        gp_weights: np.array(2)
            Interpolation weights
    """
    n = grid.size
    if value < grid[0]:
        gp_ids[0] = 0
        gp_ids[1] = 1
    elif value < grid[n - 1]:
        for i in range(1, n):
            if value < grid[i]:
                gp_ids[0] = i - 1
                gp_ids[1] = i
                break
    else:
        gp_ids[0] = n - 2
        gp_ids[1] = n - 1

    gp_weights[0] = (grid[gp_ids[1]] - value) / (grid[gp_ids[1]] - grid[gp_ids[0]])
    gp_weights[1] = np.float64(1.0) - gp_weights[0]


"""
POLINOMIO DE INTERPOLACIÓN DE NEWTON

Dada una lista de nodos, devuelve su polinomio de
interpolación de newton

Ejemplo de uso abajo del fichero

Blanca Cano Camarero
Granada 3/v/20

"""

import numpy as np
import sympy as sp

def polinomioNewton (x,y=[]):
    """
    Devuelve el polinomio de interpolación de newton.
    Argumentos:
        x: lista de nodos simbólicos, ejmplo [a-h,a,a+h] donde  a,h = sp.symbols('a,h').
        y: valores interpolatorios, se pasa para ahorrar cálculos, DEJAR VACÍO.
    """
    f = sp.Function('f')
    n = len(x)-1
    z = sp.Symbol('z')

    if n == 0:
        return f(x[0])
    else:
        # valores interpolatorios

        if(y == []):
            y = [ f(i) for i in x]

        #polinomio anterior
        p = polinomioNewton(x[0:-1],y)

        #D = f[x0,x1..x_{n}]
        D = sp.Symbol('D')

        #polinomio sucesor (el que vamos a devolver)
        ps = p + np.prod([z - x[i] for i in range(n)])*D

        ## Despejamos D para que garantice que p(xn) = yn
        D = (sp.solve(ps.subs({z:x[n]})-y[n],D))[0]
        ps = p +  np.prod([z - x[i] for i in range(n)])*D

        return ps

## ejemplo de ejecución

if __name__ == '__main__':
    f = sp.Function('f')
    a,h,z = sp.symbols('a,h,z')
    x = [a-h, a, a+h]

    print(polinomioNewton(x))
## esto imprime en pantalla
## f(a - h) + (f(a) - f(a - h))*(-a + h + z)/h + (-a + z)*(-a + h + z)*(-2*f(a) + f(a - h) + f(a + h))/(2*h**2)


import superimport

import numpy as np
import matplotlib.pyplot as plt

def gaussProb(X, mu, Sigma):
    d = 1
    X = np.reshape(X, newshape=(-1, d), order='F')
    X = X - mu.T
    logp = -0.5 * np.sum(X*X/Sigma, axis=1)
    logZ = 0.5 * d * np.log(2*np.pi) + 0.5 * np.log(Sigma)
    logp = logp - logZ
    p = np.exp(logp)

    return p

k0s = np.arange(4)
xrange = np.arange(-1, 2.55, 0.05)
n=5
thetaTrue = 1
sigmaTrue = 1
thetaPrior = 0
colors = ['b', 'r', 'k', 'g', 'c', 'y', 'm', 'r', 'b', 'k', 'g', 'c', 'y', 'm']
styles = ['-', ':', '-.', '--', '-', ':', '-.', '--', '-', ':', '-.', '--', '-', ':', '-.', '--']

plt.figure(figsize=(12, 7))
names = []
for ki in range(len(k0s)):
    k0 = k0s[ki]
    w = n / (n + k0)
    v = w**2 * sigmaTrue**2 / n
    thetaEst = w*thetaTrue + (1-w)*thetaPrior
    names.append('postMean{0:02d}'.format(k0s[ki]))
    plt.plot(xrange, gaussProb(xrange, thetaEst, np.sqrt(v)), color=colors[ki], linestyle=styles[ki], linewidth=3)
plt.title('sampling distribution, truth = {}, prior = {}, n = {}'.format(thetaTrue, thetaPrior, n), fontweight="bold")
plt.legend(names)
plt.show()

ns = np.arange(1, 50, 2)
mseThetaE = np.zeros((len(ns), len(k0s)))
mseThetaB = np.zeros((len(ns), len(k0s)))

for ki in range(len(k0s)):
    k0 = k0s[ki]
    ws = ns / (ns + k0)
    mseThetaE[:, ki] = sigmaTrue**2/ns
    mseThetaB[:, ki] = ws**2 * sigmaTrue**2 / ns + (1-ws)**2 * (thetaPrior-thetaTrue)**2

ratio = mseThetaB / mseThetaE

plt.figure(figsize=(12, 7))
for ki in range(len(k0s)):
    plt.plot(ns, ratio[:, ki],  color=colors[ki], linestyle=styles[ki], linewidth=3)

plt.legend(names)
plt.ylabel('relative MSE');
plt.xlabel('sample size')
plt.title('MSE of postmean / MSE of MLE', fontweight="bold")
plt.tight_layout()
plt.show()


import numpy as np

class RandomGaussian2D:
    """
    Generates random samples from randomized 2D Gaussian distribution.

    Parameters
    ----------
    xmin : float, default=0
        Minimum x value of mean to be randomized.
    xmax : float, default=10
        Maximum x value of mean to be randomized.
    ymin : flaot, default=0
        Minimum y value of mean to be randomized.
    ymax : float, default=10
        Maximum y value of mean to be randomized.
    """

    def __init__(self, xmin=0, xmax=10, ymin=0, ymax=10):
        ranges = np.array([xmax - xmin, ymax - ymin])
        offsets = np.array([xmin, ymin])
        self._mean = np.random.random_sample(2) * ranges + offsets

        # covariance matrix eigen values
        D = np.diag(np.random.random_sample(2))
        # rotation angle
        phi = np.random.random_sample() * 2*np.pi
        R = np.array([
            [np.cos(phi), -np.sin(phi)],
            [np.sin(phi),  np.cos(phi)]
        ])
        self._cov = R.T @ D @ R

    def get_sample(self, N):
        """
        Generates a random sample from this distribution.

        Parameters
        ----------
        N : int
            Number of samples to generate.

        Returns
        -------
        X : ndarray of shape (N, 2)
            Matrix with N samples. Every row in returned matrix is one
            generated random sample.
        """
        return np.random.multivariate_normal(self._mean, self._cov, N)

def sample_gaussian_2d(C, N, xmin=0, xmax=10, ymin=0, ymax=10):
    """
    Generates random Gaussian samples from multiple classes.

    Parameters
    ----------
    c : int
        Number of classes. For each class, samples are drawn from a
        random 2D Gaussian distribution created for the corresponding
        class.
    N : int
        Number of samples to generate from each class.
    xmin : float, default=0
        Minimum x value of Gaussian means to be randomized.
    xmax : float, default=10
        Maximum x value of Gaussian means to be randomized.
    ymin : float, default=0
        Minimum y value of Gaussian means to be randomized.
    ymax : float, default=10
        Maximum x value of Gaussian means to be randomized.

    Returns
    -------
    (X, y) : (ndarray of shape (C * N, 2), ndarray of shape (C * N,))
        X is an array of datapoints. Each row represents a datapoint
        and y[i] is the class of datapoint X[i].
    """
    X = np.empty((C * N, 2))
    y = np.empty(C * N, dtype=np.int)
    for i in range(C):
        g = RandomGaussian2D()
        X_ = g.get_sample(N)
        y_ = i * np.ones(N)
        X[i*N:(i+1)*N, :] = X_
        y[i*N:(i+1)*N] = y_
    return X, y

def as_one_hot(y):
    """
    Returns a one-hot encoding of given vector of classes.

    Parameters
    ----------
    y : ndarray of shape (N,)
        A vector of classes. Classes should be encoded by value from
        0 to C-1 (both included), where C is the number of classes. N
        is the number of samples in y.

    Returns
    -------
    y_one_hot : ndarray of shape (N, C)
        One-hot encoding of given vector. y[i] represents a one-hot
        encoding of y[i]. N is the number of samples and C is the
        number of classes.
    """
    k = np.max(y) + 1
    Y_oh = np.zeros((y.shape[0], k), dtype=np.int)
    for i, c in enumerate(y):
        Y_oh[i, c] = 1
    return Y_oh


# Erro de integrais
import numpy as np
import matplotlib.pyplot as plt

def int_trap(func, a, b, n):
    '''Calcula a integral de uma função entre os
    limites a e b pela área de n trapézios.
    func: função analítica
    a, b: limites inferior e superior
    n: número de secções da área sob a função
    return: o valor da integral
    '''
    dx = (b - a)/n
    sdx = ((func(a) + func(b))/2)
    for i in range(1, n):
        sdx += func(a + i*dx)
    return sdx*dx

def int_simpson(fx, a, b, n):
    '''Integração pelo Método de Simpson.
    fx: função a ser integrada
    a, b: limites inferior e superior para a integração
    n: número de secções da área integrada
    return: o valor da integral definida de fx
    '''
    if n % 2 != 0:
        n += 1
    h = (b-a)/n
    x = list(a+(j*h) for j in range(n+1))
    si = 4*sum(map(fx, x[1:n:2]))
    sp = 2*sum(map(fx, x[2:n-1:2]))
    return (h/3)*(fx(a) + fx(b) + si + sp)

def erro_int(ia, inum):
    '''
    Função que calcula o erro entre o método analítico
    e algum numérico de integral.
    :param ia: valor analítico da integral
    :param inum: valor numérico da integral
    :return: módulo do erro entre eles
    '''
    return abs(ia - inum)

def aj_lin(xi, yi):
    '''
    Realiza o ajuste linear de pontos em uma reta de
    ajuste no formato "y = ax + b".
    :param xi: coordenadas x dos pontos
    :param yi: coordenadas y dos pontos
    :return: coeficiente angular "a" e coeficiente
             linear "b" da reta de ajuste
    '''
    n = len(xi)
    mxy = sum(xi*yi)/n
    mx = sum(xi)/n
    my = sum(yi)/n
    mqx = sum(xi**2)/n
    a = (mxy - (mx*my))/(mqx - (mx**2))
    b = ((mqx*my) - (mx*mxy))/(mqx - (mx**2))
    return a, b

fx = lambda x: 1/((x**2) + 1)   # Função dada pela atividade
n = 2**np.arange(2, 21, 2)
ia = np.arctan(3)-np.arctan(-3)   # Valor analítico da integral de fx
logdx = np.log10(6/n)   # array com os valores de dx dimensionados pelo log

# || Método do Trapézio ||
# função que retorna a integral de fx com dependência
# com o valor de intervalos
trap = lambda nn: int_trap(fx, -3, 3, nn)
# lista com os valores das interais com cada numero de intervalos
itrap = list(map(trap, n))

# log da função erro entre o método do trapézio e analítico
logetpz = np.log10(erro_int(ia, itrap))
# coeficientes da reta que melhor aproxima os
# dados de (logdx, logetpz)
parstpz = aj_lin(logdx, logetpz)
# função que representa essa reta
ftpz = lambda delx: parstpz[0]*delx + parstpz[1]

# || Método de Simpson ||
# função que retorna o valor da integral por simspon
# dependendo do n
simp = lambda nn: int_simpson(fx, -3, 3, nn)
# lista com todos os valores correspondentes a cada n
isimp = list(map(simp, n))

# log da função do erro entre o método de Simpson e analítico
logesimp = np.log10(erro_int(ia, isimp))
# coeficientes da reta que melhor aproxima
# a parte linear dos dados (logdx, logesimp)
parssimp = aj_lin(logdx[:6], logesimp[:6])
# função que representa essa erta
fsimp = lambda delx: parssimp[0]*delx + parssimp[1]

plt.figure(figsize=(20, 5))
plt.subplot(131)
plt.scatter(logdx, logetpz, s=30, c='r', marker='.', label='Dados')
plt.plot(logdx, ftpz(logdx), 'b', label='Reta de ajuste')
plt.ylim(-13, 0)
plt.title('Erro pelo método do trapézio')
plt.ylabel('log(|erro|)')
plt.xlabel(r'log($\Delta x$)')
plt.text(-5, -4, f'a = {parstpz[0]:.3f}\nb = {parstpz[1]:.3f}',
         bbox=dict(boxstyle='square', ec='k', color='white'))
plt.legend()
plt.grid()
#plt.savefig('errtpz.jpeg')
#plt.show()

plt.subplot(132)
plt.scatter(logdx, logesimp, s=30, c='k', marker='.', label='Dados')
plt.plot(logdx[:6], fsimp(logdx[:6]), 'magenta', label='Reta de ajuste')
plt.text(-5, -4.5, f'a = {parssimp[0]:.3f}\nb = {parssimp[1]:.3f}',
         bbox=dict(boxstyle='square', ec='k', color='white'))
plt.title('Erro pelo método de Simpson')
plt.ylim(-15.5, 0)
plt.ylabel('log(|erro|)')
plt.xlabel(r'log($\Delta x$)')
plt.legend()
plt.grid()
#plt.show()
#plt.savefig('errsimp.jpeg')

plt.subplot(133)
plt.scatter(logdx, logetpz, s=30, c='r', marker='.', label='Trapézio')
plt.plot(logdx, ftpz(logdx), 'b', label='Ajuste do trapézio')
plt.scatter(logdx, logesimp, s=30, c='k', marker='.', label='Simpson')
plt.plot(logdx[:6], fsimp(logdx[:6]), 'magenta', label='Ajuste de simpson')
plt.title('Comparação dos métodos de Simpson e do Trapézio')
plt.ylim(-15.5, 0)
plt.ylabel('log(|erro|)')
plt.xlabel(r'log($\Delta x$)')
plt.grid()
plt.legend()
plt.show()
#plt.savefig('errcomp.jpeg')


import numpy as np

def step(z: float) -> float:
    return 1.0 if z >= 0.0 else 0.0

def sigmoid(z: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Calculate the sigmoid activation function

    The sigmoid function take any range real number
    and returns the output value which falls in the range of 0 to 1.

    When the input z is a vector or Numpy array,
    Numpy automatically applies the function
    element wise, that is, in vectorized form.
    The output will thus be:  np.exp(x)=(e^x1, e^x2,...,e^xn)

    Args:
        z -- Output of the linear layer, of any shape
    Returns:
        s -- a numpy matrix equal to the sigmoid of x, of shape (n,m)
        cache -- returns Z as well, useful during backpropagation

    Examples:
        >>> data = np.arange(-10, 10, 1)
        >>> sigmoid(data)

        >>> [sigmoid(x) for x in data]
    """
    return 1.0 / (1.0 + np.exp(-z)), z

def tanh(z: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Calculate the tangent activation function

    Takes any range real number and returns the output value
    which falls in the range of 0 to 1.

    When the input z is a vector or Numpy array,
    Numpy automatically applies the function
    element wise, that is, in vectorized form.

    Args:
        z -- Output of the linear layer, of any shape
    Returns:
        h -- a numpy matrix equal to the tanh of x, of shape (n,m)
        cache -- returns Z as well, useful during backpropagation

    Examples:
        >>> data = np.arange(-10, 10, 1)
        >>> tanh(data)

        >>> [tanh(x) for x in data]

    """
    # tanh(z) = 2σ(2z) − 1
    return 2 * np.array(sigmoid(z)) - 1, z

def hypertang(z: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Calculate the hyper tangent activation function

    Takes any range real number and returns the output value
    which falls in the range of 0 to 1.

    When the input z is a vector or Numpy array,
    Numpy automatically applies the function
    element wise, that is, in vectorized form.

    Args:
        z -- Output of the linear layer, of any shape
    Returns:
        h -- a numpy matrix equal to the hyperbolic tangent of x, of shape (n,m)
        cache -- returns Z as well, useful during backpropagation
    Examples:
        >>> data = np.arange(-10, 10, 1)
        >>> hypertang(data)

        >>> [hypertang(x) for x in data]

    """
    exp = np.exp(2 * z)
    return (exp - 1) / (exp + 1), z

def softmax(z: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """ "Calculate the softmax activation function

    Takes any range real number and returns
    the probabilities range with values between 0 and 1,
    and the sum of all the probabilities will be equal to one.

    When the input z is a vector or Numpy array,
    Numpy automatically applies the function
    element wise, that is, in vectorized form.

    Args:
        z -- Output of the linear layer, of any shape
    Returns:
        s -- a numpy matrix equal to the softmax of x, of shape (n,m)
        cache -- returns Z as well, useful during backpropagation

    Examples:
        >>> data = np.arange(-10, 10, 1)
        >>> softmax(data)

        >>> [softmax(x) for x in data]
    """
    z_exp = np.exp(z)
    z_sum = np.sum(z_exp, axis=1, keepdims=True)
    return z_exp / z_sum, z

def relu(z: np.ndarray) -> np.ndarray:
    """Computes the Rectified Linear Unit activation function

    Arguments:
        z -- Output of the linear layer, of any shape

    Returns:
        A -- Post-activation parameter, of the same shape as Z
        cache -- returns Z as well, useful during backpropagation
    """
    return np.maximum(0, z), z

def relu_backward(dA, cache):
    """Implement the backward propagation for a single RELU unit.

    Arguments:
        dA -- post-activation gradient, of any shape
        cache -- 'Z' where we store for computing backward propagation efficiently

    Returns:
        dZ -- Gradient of the cost with respect to Z
    """
    Z = cache
    dZ = np.array(dA, copy=True)  # just converting dz to a correct object.
    # When z <= 0, you should set dz to 0 as well.
    dZ[Z <= 0] = 0
    return dZ

def sigmoid_backward(dA, cache):
    """Implement the backward propagation for a single SIGMOID unit.

    Arguments:
        dA -- post-activation gradient, of any shape
        cache -- 'Z' where we store for computing backward propagation efficiently

    Returns:
        dZ -- Gradient of the cost with respect to Z
    """
    Z = cache
    s = 1 / (1 + np.exp(-Z))
    dZ = dA * s * (1 - s)
    return dZ

if __name__ == "__main__":
    a = np.random.rand(3, 2)
    print(sigmoid(a))

    a = np.random.random((3, 2)) - 0.5
    print("a")
    print(a)
    print("Relu")

    print(relu(a))


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

# # Graph
# Listing all paths from source to sink in an undirected weighted graph.
# Jason Miller.

# In[1]:

import numpy as np

# ## Define the Graph
# Represent the graph on page 377, Fig 5.3, of Kurose & Ross 7th ed.

# In[2]:

G=np.zeros( (6,6), dtype=np.int8)
NODES=['u','v','w','x','y','z']
nodecount = len(NODES)
u,v,w,x,y,z=0,1,2,3,4,5

# In[3]:

def add_edge(graph,node1,node2,weight):
    graph[node1,node2]=graph[node2,node1]=weight
add_edge(G,u,v,2)
add_edge(G,u,x,1)
add_edge(G,u,w,5)
add_edge(G,x,v,2)
add_edge(G,x,w,3)
add_edge(G,x,y,1)
add_edge(G,w,v,3)
add_edge(G,w,y,1)
add_edge(G,w,z,5)
add_edge(G,y,z,2)

# In[4]:

print("Node count:",len(G))
print("Edge count:",np.count_nonzero(G)//2)
print(G)

# ## Graph utility functions

# In[11]:

# Recursive DFS
def list_paths_from(graph,this_path,all_paths):
    this_node=this_path[-1]
    for i in range(0,len(graph)):
        if i not in this_path: # avoid cycles
            if graph[this_node,i]>0: # positive weight => adjacency
                next_path = this_path.copy()
                next_path.append(i)
                #print("this_path",this_path)
                #print("next_path",next_path)
                all_paths.append(next_path)
                all_paths = list_paths_from(graph,next_path,all_paths)
    return all_paths
# Convert node names from ints to letters
def get_path_name(one_path):
    pname=''
    for i in one_path:
        pname = pname + NODES[i]
    return pname
# Print a list of all paths
def show_all_paths(paths):
    c = 0
    for p in paths:
        c = c+1
        print(c,get_path_name(p))
# Restrict a list based on last node
def list_paths_a_to_b(graph,a,b):
    paths_from_a = [[a]]
    paths_from_a = list_paths_from(G,[a],paths_from_a)
    paths_a_to_b = []
    for p in paths_from_a:
        if p[-1]==b:
            paths_a_to_b.append(p)
    show_all_paths(paths_a_to_b)

# ## Graph exploration

# In[12]:

# Long output, was used for debugging
paths_from_Y = [[y]]
paths_from_Y = list_paths_from(G,[y],paths_from_Y)
# show_all_paths(paths_from_Y)

# In[7]:

list_paths_a_to_b(G,y,u)

# In[8]:

list_paths_a_to_b(G,x,z)

# In[9]:

list_paths_a_to_b(G,z,u)

# In[10]:

list_paths_a_to_b(G,z,w)

# In[ ]:


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

# Smallest constant c1 that satisfies the
# inequality f(x)<=c1.g(x)
c1 = 2.1094
c2 = 2.5
c3 = 3.0
# Number of samples
sampleSize = [100, 1000, 10000, 100000, 1000000]

def f(x):
	return 20.00*x*((1-x)**3.00)

def plotHistogram(rand_sample, c, N):
	hist, bins, _ = plt.hist(rand_sample, density=True, bins=100)
	xAxis = (bins[1:]+bins[:-1])/2
	yAxis = 20.00*xAxis*((1-xAxis)**3.00)

	plt.plot(xAxis, yAxis, label = "f(x)")
	plt.legend(loc = "upper right")
	plt.xlabel('x')
	plt.ylabel('y')
	plt.title('c = ' + str(c) + ' samples = ' + str(N))

	plt.show()

def generate(c):
	count = 0
	while True:
		count += 1
		x = np.random.uniform(0.00, 1.00)
		u = np.random.uniform(0.00, 1.00)
		# g(x) = 1 for U[0.00, 1.00]
		if u <= f(x)/c:
			return x, count

def findAverage(rand_sample, iterations):
	avg_rand = 0.00
	avg_itr = 0.00

	for i in rand_sample:
		avg_rand += i
	avg_rand /= len(rand_sample)

	for i in iterations:
		avg_itr += i
	avg_itr /= len(iterations)

	return avg_rand, avg_itr

def execute(curr, N):
	c = curr
	iterations = []
	rand_sample = []
	for i in range(N):
		x, itr = generate(c)
		rand_sample.append(x)
		iterations.append(itr)

	avg_rand, avg_itr = findAverage(rand_sample, iterations)

	print('For c = ' + str(c) +' and sample size = ' + str(N) + ' : ', end = ' ')
	print('|c - avg_itr| = ' + str(abs(c-avg_itr)))

	if c == 2.1094:
		plotHistogram(rand_sample, c, N)

def main():
	for i in sampleSize:
		N = i
		execute(c1, N)

	for i in sampleSize:
		N = i
		execute(c2, N)

	for i in sampleSize:
		N = i
		execute(c3, N)

if __name__ == "__main__":
	main()


import numpy
import pandas
import scipy
import sympy

import statsmodels.api as stats

# A function that returns the columnwise product of two dataframes (must have same number of rows)
def create_interaction (inDF1, inDF2):
    name1 = inDF1.columns
    name2 = inDF2.columns
    outDF = pandas.DataFrame()
    for col1 in name1:
        for col2 in name2:
            outName = col1 + " * " + col2
            outDF[outName] = inDF1[col1] * inDF2[col2]
    return(outDF)

# A function that find the non-aliased columns, fit a logistic model, and return the full parameter estimates
def build_mnlogit (fullX, y, debug = 'N'):
    # Number of all parameters
    nFullParam = fullX.shape[1]

    # Number of target categories
    y_category = y.cat.categories
    nYCat = len(y_category)

    # Find the non-redundant columns in the design matrix fullX
    reduced_form, inds = sympy.Matrix(fullX.values).rref()

    # These are the column numbers of the non-redundant columns
    if (debug == 'Y'):
        print('Column Numbers of the Non-redundant Columns:')
        print(inds)

    # Extract only the non-redundant columns for modeling
    X = fullX.iloc[:, list(inds)]

    # The number of free parameters
    thisDF = len(inds) * (nYCat - 1)

    # Build a multionomial logistic model
    logit = stats.MNLogit(y, X)
    thisFit = logit.fit(method='newton', full_output = True, maxiter = 100, tol = 1e-8)
    thisParameter = thisFit.params
    thisLLK = logit.loglike(thisParameter.values)

    if (debug == 'Y'):
        print(thisFit.summary())
        print("Model Parameter Estimates:\n", thisParameter)
        print("Model Log-Likelihood Value =", thisLLK)
        print("Number of Free Parameters =", thisDF)

    # Recreat the estimates of the full parameters
    workParams = pandas.DataFrame(numpy.zeros(shape = (nFullParam, (nYCat - 1))))
    workParams = workParams.set_index(keys = fullX.columns)
    fullParams = pandas.merge(workParams, thisParameter, how = "left", left_index = True, right_index = True)
    fullParams = fullParams.drop(columns = '0_x').fillna(0.0)

    # Return model statistics
    return (thisLLK, thisDF, fullParams)

hmeq = pandas.read_csv('C:\\Users\\minlam\\Documents\\IIT\\Machine Learning\\Data\\hmeq.csv',
                       delimiter=',', usecols = ['BAD', 'JOB', 'REASON'])

hmeq = hmeq.dropna()

# Specify Origin as a categorical variable
y = hmeq['BAD'].astype('category')

# Specify JOB and REASON as categorical variables
xR = pandas.get_dummies(hmeq[['REASON']].astype('category'))
xJ = pandas.get_dummies(hmeq[['JOB']].astype('category'))

# Intercept only model
designX = pandas.DataFrame(y.where(y.isnull(), 1))
LLK0, DF0, fullParams0 = build_mnlogit (designX, y, debug = 'Y')

# Intercept + REASON
designX = stats.add_constant(xR, prepend=True)
LLK_1R, DF_1R, fullParams_1R = build_mnlogit (designX, y, debug = 'N')
testDev = 2 * (LLK_1R - LLK0)
testDF = DF_1R - DF0
testPValue = scipy.stats.chi2.sf(testDev, testDF)
print('Deviance Chi=Square Test')
print('Chi-Square Statistic = ', testDev)
print('  Degreee of Freedom = ', testDF)
print('        Significance = ', testPValue)

# Intercept + REASON + JOB
designX = xR
designX = designX.join(xJ)
designX = stats.add_constant(designX, prepend=True)
LLK_1R_1J, DF_1R_1J, fullParams_1R_1J = build_mnlogit (designX, y, debug = 'N')
testDev = 2 * (LLK_1R_1J - LLK_1R)
testDF = DF_1R_1J - DF_1R
testPValue = scipy.stats.chi2.sf(testDev, testDF)
print('Deviance Chi=Square Test')
print('Chi-Square Statistic = ', testDev)
print('  Degreee of Freedom = ', testDF)
print('        Significance = ', testPValue)

# Intercept + REASON + JOB + REASON * JOB
designX = xR
designX = designX.join(xJ)

# Create the columns for the JOB * REASON interaction effect
xRJ = create_interaction(xR, xJ)
designX = designX.join(xRJ)

designX = stats.add_constant(designX, prepend=True)
LLK_2RJ, DF_2RJ, fullParams_2RJ = build_mnlogit (designX, y, debug = 'N')
testDev = 2 * (LLK_2RJ - LLK_1R_1J)
testDF = DF_2RJ - DF_1R_1J
testPValue = scipy.stats.chi2.sf(testDev, testDF)
print('Deviance Chi=Square Test')
print('Chi-Square Statistic = ', testDev)
print('  Degreee of Freedom = ', testDF)
print('        Significance = ', testPValue)

in-class/Week 8 HMEQ Logistic with Interaction.py

import numpy,import scipy,import sympy,import statsmodels