import math
from numpy.core.umath_tests import inner1d
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
import numpy as np
from shapely.geometry.polygon import Polygon
import pygplates
import pandas as pd

########## BASIC CALCULATIONS ##########

def lat_lon_2_cart(lat, lon):
    """
    Convert lat/lon coordinates to cartesian.

    Parameters
    ----------
    lat : array-like
        latitude (-90 to 90)
    lon : array-like
        longitude (-180 to 180)

    Returns
    -------
    cart : tuple
        (x, y, z) cartesian coordinates on the unit sphere
    """
    # convert to radians
    lat = math.radians(lat)
    lon = math.radians(lon)
    # calculations
    x = math.cos(lon) * math.cos(lat)
    y = math.sin(lon) * math.cos(lat)
    z = math.sin(lat)
    cart = (x, y, z)

    return cart

def cart_2_lat_lon(cart):
    """
    Convert cartesian coordinates to lat/lon.

    Parameters
    ----------
    cart : tuple
        (x, y, z) cartesian coordinates on the unit sphere

    Returns
    -------
    lat : array-like
        latitude (-90 to 90)
    lon : array-like
        longitude (-180 to 180)
    """
    # calculations
    lon = math.atan2(cart[1], cart[0])
    lat = math.atan2(cart[2], math.sqrt(cart[0]**2 + cart[1]**2))
    # convert to degrees
    lat = math.degrees(lat)
    lon = math.degrees(lon)

    return lat, lon

def fast_cross(a, b):
    """
    3D matrix cross multiplication.
    source: http://ssb.stsci.edu/doc/stsci_python_x/stsci.sphere.doc/html/_modules/stsci/sphere/great_circle_arc.html

    Parameters
    ----------
    a, b : numpy arrays
        matrices to be cross multiplied

    Returns
    -------
    cp : numpy array
        cross product
    """
    cp = np.empty(np.broadcast(a, b).shape)
    aT = a.T
    bT = b.T
    cpT = cp.T
    cpT[0] = aT[1]*bT[2] - aT[2]*bT[1]
    cpT[1] = aT[2]*bT[0] - aT[0]*bT[2]
    cpT[2] = aT[0]*bT[1] - aT[1]*bT[0]

    return cp

def cross_and_normalize(A, B):
    """
    3D matrix cross multiplication and normalized.
    source: http://ssb.stsci.edu/doc/stsci_python_x/stsci.sphere.doc/html/_modules/stsci/sphere/great_circle_arc.html

    Parameters
    ----------
    A, B : numpy arrays
        matrices to be cross multiplied

    Returns
    -------
    TN : numpy array
        normalized cross product
    """
    T = fast_cross(A, B)
    # normalization
    l = np.sqrt(np.sum(T ** 2, axis=-1))
    l = np.expand_dims(l, 2)
    # might get some divide-by-zeros, but we don't care
    with np.errstate(invalid='ignore'):
        TN = T / l

    return TN

def intersection(A, B, C, D):
    """
    Point of intersection between two great circle arcs.
    source: http://ssb.stsci.edu/doc/stsci_python_x/stsci.sphere.doc/html/_modules/stsci/sphere/great_circle_arc.html

    Parameters
    ----------
    A, B : (*x*, *y*, *z*) triples or Nx3 arrays of triples
        Endpoints of the first great circle arc.
    C, D : (*x*, *y*, *z*) triples or Nx3 arrays of triples
        Endpoints of the second great circle arc.

    Returns
    -------
    T : (*x*, *y*, *z*) triples or Nx3 arrays of triples
        If the given arcs intersect, the intersection is returned. If the arcs do not intersect,
        the triple is set to all NaNs.
    """
    A = np.asanyarray(A)
    B = np.asanyarray(B)
    C = np.asanyarray(C)
    D = np.asanyarray(D)

    A, B = np.broadcast_arrays(A, B)
    C, D = np.broadcast_arrays(C, D)

    ABX = fast_cross(A, B)
    CDX = fast_cross(C, D)
    T = cross_and_normalize(ABX, CDX)
    T_ndim = len(T.shape)

    if T_ndim > 1:
        s = np.zeros(T.shape[0])
    else:
        s = np.zeros(1)
    s += np.sign(inner1d(fast_cross(ABX, A), T))
    s += np.sign(inner1d(fast_cross(B, ABX), T))
    s += np.sign(inner1d(fast_cross(CDX, C), T))
    s += np.sign(inner1d(fast_cross(D, CDX), T))
    if T_ndim > 1:
        s = np.expand_dims(s, 2)

    cross = np.where(s == -4, -T, np.where(s == 4, T, np.nan))

    # If they share a common point, it's not an intersection.  This
    # gets around some rounding-error/numerical problems with the
    # above.
    equals = (np.all(A == C, axis=-1) |
              np.all(A == D, axis=-1) |
              np.all(B == C, axis=-1) |
              np.all(B == D, axis=-1))

    equals = np.expand_dims(equals, 2)

    T = np.where(equals, np.nan, cross)

    return T

########## ZONAL CALCULATIONS ##########

def check_polygon_in_band(polygon, lat_min, lat_max):
    """
    Check to see whether any part of a given polygon is inside a given latitude band.

    Parameters
    ----------
    polygon : pygpplates polygon
    lat_min : float
        the minimum latitude of the latitude band
    lat_max : float
        the maximum latitude of the latitude band

    Returns
    -------
    in_band : boolean
        True if inside, False if outside
    """
    # pull out lat/lon vertices
    lat_lon_array = polygon.to_lat_lon_array()
    lats = lat_lon_array[:,0]

    # check to see if any part of the polygon falls into our latitude band
    in_band = False
    for j in range(len(lats)):
        if lats[j]>lat_min and lats[j]<lat_max:
            in_band = True
            break

    return in_band

def get_area_in_band(polygon, lat_min, lat_max):
    """
    Calculate the area of a given polygon inside a given latitude band.

    Parameters
    ----------
    polygon : pygpplates polygon
    lat_min : float
        the minimum latitude of the latitude band
    lat_max : float
        the maximum latitude of the latitude band

    Returns
    -------
    area : float
        the area of the polygon within the latitude band (in km^2)
    band_polygon : pygplates polygon
        with the parts outside of the latitude band removed
    """
    # pull out lat/lon vertices
    lat_lon_array = polygon.to_lat_lon_array()
    lats = lat_lon_array[:,0]
    lons = lat_lon_array[:,1]

    # storage lists
    bookmarks = []
    lat_add_list = []
    lon_add_list = []

    # iterate through the points
    for i in range(1,len(lats)):
        top_cross = False
        bot_cross = False

        # case where we move into the band from above
        if lats[i-1]>lat_max and lats[i]<lat_max:
            top_cross = True
        # case where we move out of the band from below
        if lats[i-1]<lat_max and lats[i]>lat_max:
            top_cross = True
        # case where we move out of the band from above
        if lats[i-1]>lat_min and lats[i]<lat_min:
            bot_cross = True
        # case where we move into the band from below
        if lats[i-1]<lat_min and lats[i]>lat_min:
            bot_cross = True

        # do calculations if we cross
        if top_cross or bot_cross:

            # convert the endpoints of the polygon segment into cartesian
            A = lat_lon_2_cart(lats[i-1], lons[i-1])
            B = lat_lon_2_cart(lats[i]  , lons[i])

            # get the intersection point (for the top and bottom cases), and convert back to lat/lon
            if top_cross:
                C_top = lat_lon_2_cart(lat_max, min([lons[i],lons[i-1]]))
                D_top = lat_lon_2_cart(lat_max, max([lons[i],lons[i-1]]))
                T = intersection(A, B, C_top, D_top)
            else:
                C_bot = lat_lon_2_cart(lat_min, min([lons[i],lons[i-1]]))
                D_bot = lat_lon_2_cart(lat_min, max([lons[i],lons[i-1]]))
                T = intersection(A, B, C_bot, D_bot)
            lat_add, lon_add = cart_2_lat_lon(T)

            # add to the storage lists
            bookmarks.append(i)
            lat_add_list.append(lat_add)
            lon_add_list.append(lon_add)

    # now insert the stored values into the original arrays
    new_lats = np.insert(lats, bookmarks, lat_add_list)
    new_lons = np.insert(lons, bookmarks, lon_add_list)

    # only keep values below the maximum latitude (with small buffer)
    mask = np.less(new_lats, lat_max+0.1)
    new_lats = new_lats[mask]
    new_lons = new_lons[mask]

    # only keep values above the minimum latitude
    mask = np.greater(new_lats, lat_min-0.1)
    new_lats = new_lats[mask]
    new_lons = new_lons[mask]

    # create a Polygon, if we are left with enough points
    if len(new_lats) >= 3:
        band_polygon = pygplates.PolygonOnSphere(zip(new_lats,new_lons))

        # get the area in km2
        area = band_polygon.get_area() * 6371.009**2

    # if we don't...
    else:
        area = 0
        band_polygon = None

    return area, band_polygon

def get_areas_in_bands(reconstructed_feature_geometries, lat_mins, lat_maxs):
    """
    Get the area of all features in each latitude band.

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums

    Returns
    -------
    areas : array
        list of total area in each latitude band
    area_polygons : list
        list of all polygons for which areas were calculated
    """
    # storage vectors
    areas = np.array([])
    area_polygons = []

    # iterate over each latitude band
    for i in range(len(lat_mins)):

        accumulated_area = 0

        # iterate over each polygon
        for j in range(len(reconstructed_feature_geometries)):

            current_polygon = reconstructed_feature_geometries[j].get_reconstructed_geometry()

            # check if the polygon is in the band
            in_band = check_polygon_in_band(current_polygon, lat_mins[i], lat_maxs[i])

            if in_band:
                # do the calculation
                area, band_polygon = get_area_in_band(current_polygon, lat_mins[i], lat_maxs[i])

                # store results
                accumulated_area = accumulated_area + area
                area_polygons.append(band_polygon)

        # store total area for the band
        areas = np.append(areas, accumulated_area)

    return areas, area_polygons

def get_length_in_band(polyline, lat_min, lat_max):
    """
    Calculate the length of a given polyline inside a given latitude band.

    Parameters
    ----------
    polyline : pygpplates polyline
    lat_min : float
        the minimum latitude of the latitude band
    lat_max : float
        the maximum latitude of the latitude band

    Returns
    -------
    length : float
        the length of the polyline within the latitude band (in km)
    band_polylines : list
        pygplates polylines with the parts outside of the latitude band removed
    """
    # pull out lat/lon vertices
    lat_lon_array = polyline.to_lat_lon_array()
    lats = lat_lon_array[:,0]
    lons = lat_lon_array[:,1]

    # storage lists
    bookmarks = []
    lat_add_list = []
    lon_add_list = []

    # iterate through the points
    for i in range(1,len(lats)):
        top_cross = False
        bot_cross = False

        # case where we move into the band from above
        if lats[i-1]>lat_max and lats[i]<lat_max:
            top_cross = True
        # case where we move out of the band from below
        if lats[i-1]<lat_max and lats[i]>lat_max:
            top_cross = True
        # case where we move out of the band from above
        if lats[i-1]>lat_min and lats[i]<lat_min:
            bot_cross = True
        # case where we move into the band from below
        if lats[i-1]<lat_min and lats[i]>lat_min:
            bot_cross = True

        # do calculations if we cross
        if top_cross or bot_cross:

            # convert the endpoints of the polygon segment into cartesian
            A = lat_lon_2_cart(lats[i-1], lons[i-1])
            B = lat_lon_2_cart(lats[i]  , lons[i])

            # get the intersection point (for the top and bottom cases), and convert back to lat/lon
            if top_cross:
                C_top = lat_lon_2_cart(lat_max, min([lons[i],lons[i-1]]))
                D_top = lat_lon_2_cart(lat_max, max([lons[i],lons[i-1]]))
                T = intersection(A, B, C_top, D_top)
            else:
                C_bot = lat_lon_2_cart(lat_min, min([lons[i],lons[i-1]]))
                D_bot = lat_lon_2_cart(lat_min, max([lons[i],lons[i-1]]))
                T = intersection(A, B, C_bot, D_bot)
            lat_add, lon_add = cart_2_lat_lon(T)

            # add to the storage lists
            bookmarks.append(i)
            lat_add_list.append(lat_add)
            lon_add_list.append(lon_add)

    # now insert the stored values into the original arrays
    new_lats = np.insert(lats, bookmarks, lat_add_list)
    new_lons = np.insert(lons, bookmarks, lon_add_list)

    # mask points above and below the latitude band (with small buffer)
    top_mask = np.less(new_lats, lat_max+0.1)
    bot_mask = np.greater(new_lats, lat_min-0.1)
    mask = top_mask & bot_mask

    # initiate final output variabiles
    length = 0
    band_polylines = []

    # initiate lat lon arrays for the current polyline that is being split out
    split_lats = np.array([])
    split_lons = np.array([])
    hold = False

    # iterate through the mask
    for i in range(len(mask)):
        if mask[i]:
            # begin holding, and store points
            hold = True
            split_lats = np.append(split_lats, new_lats[i])
            split_lons = np.append(split_lons, new_lons[i])

        # i.e. the first time we reach a point outside of the band
        elif hold:
            # create a polyline, if we are left with enough points
            if len(split_lats) >= 2:
                band_polyline = pygplates.PolylineOnSphere(zip(split_lats,split_lons))

                # get the length in km
                length_split = band_polyline.get_arc_length() * pygplates.Earth.mean_radius_in_kms

                # store
                length = length + length_split
                band_polylines.append(band_polyline)

            # reset stuff
            split_lats = np.array([])
            split_lons = np.array([])
            hold = False

    # if the last point was within the band
    if hold:
        # create a polyline, if we are left with enough points
        if len(split_lats) >= 2:
            band_polyline = pygplates.PolylineOnSphere(zip(split_lats,split_lons))

            # get the length in km
            length_split = band_polyline.get_arc_length() * pygplates.Earth.mean_radius_in_kms

            # store
            length = length + length_split
            band_polylines.append(band_polyline)

    return length, band_polylines

def get_lengths_in_bands(reconstructed_feature_geometries, lat_mins, lat_maxs):
    """
    Get the area of all features in each latitude band.

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums

    Returns
    -------
    accumulated_lengths : array
        array of total area in each latitude band
    length_polylines : polylines
        list of all polylines for which areas were calculated
    polyline_attributes : Pandas Dataframe
        Dataframe with names, lengths, latitudes and recon_times for each polyline
    """
    # storage vectors
    lengths = np.array([])
    accumulated_lengths = np.array([])
    length_polylines = []
    names = []
    polyline_lat_min = []
    polyline_lat_max = []
    recon_times = []

    # iterate over each latitude band
    for i in range(len(lat_mins)):

        accumulated_length = 0

        # iterate over each polygon
        for j in range(len(reconstructed_feature_geometries)):

            current_polyline = reconstructed_feature_geometries[j].get_reconstructed_geometry()

            # check if the polygon is in the band
            in_band = check_polygon_in_band(current_polyline, lat_mins[i], lat_maxs[i])

            if in_band:
                # do the calculation
                length, band_polyline = get_length_in_band(current_polyline, lat_mins[i], lat_maxs[i])
                name = reconstructed_feature_geometries[j].get_feature().get_name()
                recon_time = reconstructed_feature_geometries[j].get_reconstruction_time()

                # store results
                accumulated_length = accumulated_length + length
                length_polylines.append(band_polyline)
                names.append(name)
                lengths = np.append(lengths, length)
                polyline_lat_min.append(lat_mins[i])
                polyline_lat_max.append(lat_maxs[i])
                recon_times.append(recon_time)

        # store total area for the band
        accumulated_lengths = np.append(accumulated_lengths, accumulated_length)

        polyline_attributes = pd.DataFrame({'name':names,'length':lengths,'polyline_lat_min':polyline_lat_min,'polyline_lat_max':polyline_lat_max,'recon_time':recon_times})

    return accumulated_lengths, length_polylines, polyline_attributes

def get_points_in_bands(reconstructed_feature_geometries, lat_mins, lat_maxs):
    """
    Get the number of all features in each latitude band.

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums

    Returns
    -------
    points : array
        list of total number of points in each latitude band
    """
    # storage vector
    points = np.zeros(len(lat_mins))

    # iterate over each point
    for i in range(len(reconstructed_feature_geometries)):

        # get the latitude of the point
        current_point = reconstructed_feature_geometries[i].get_reconstructed_geometry()
        current_lat = current_point.to_lat_lon_array()[:,0]

        # put the point where it belongs
        for j in range(len(points)):
            if current_lat>lat_mins[j] and current_lat<lat_maxs[j]:
                points[j] = points[j] + 1

    return points

########## PLOTTING FUNCTIONS ##########

def plot_reconstructed_features(ax, reconstructed_feature, **kwargs):
    """
    Plot reconstructed features output by pygplates.

    Parameters
    ----------
    ax : axis handle
        axis set up by cartopy on which to plot

    reconstructed_feature : reconstructed features
        reconstructed features output by pygplates

    Other Parameters
    ----------------
    **kwargs passed to plt.add_geometries

    Returns
    -------
    None.
    """
    for n in range(len(reconstructed_feature)):

        # pull out lat/lon vertices
        lat_lon_array = reconstructed_feature[n].get_reconstructed_geometry().to_lat_lon_array()
        lats = lat_lon_array[:,0]
        lons = lat_lon_array[:,1]

        # polygons need to be drawn counter clockwise to be drawn properly if they cross the dateline
        if reconstructed_feature[n].get_reconstructed_geometry().get_orientation() == pygplates.PolygonOnSphere.Orientation.clockwise:
            lats = np.flip(lats)
            lons = np.flip(lons)

        # create the polygon and add it to the axis
        # note that ccrs.Geodetic() must be used to make sure that polygons that cross the dateline are drawn properly
        poly = Polygon(zip(lons,lats))
        ax.add_geometries([poly], ccrs.Geodetic(), **kwargs)

def plot_reconstructed_points(ax, reconstructed_feature, **kwargs):
    """
    Plot reconstructed points output by pygplates.

    Parameters
    ----------
    ax : axis handle
        axis set up by cartopy on which to plot

    reconstructed_feature : reconstructed features
        reconstructed features output by pygplates

    Other Parameters
    ----------------
    **kwargs passed to plt.add_geometries

    Returns
    -------
    None.
    """
    for n in range(len(reconstructed_feature)):

        # pull out lat/lon vertices
        lat_lon_array = reconstructed_feature[n].get_reconstructed_geometry().to_lat_lon_array()
        lats = lat_lon_array[:,0]
        lons = lat_lon_array[:,1]

        ax.scatter(lons, lats, transform=ccrs.Geodetic(), **kwargs)

########## OLD FUNCTIONS ##########

def weather_LIPs_OLD(reconstructed_feature_geometries, LIP_fracs, Ts, Rs, t_step, thickness, density):
    """
    A model for weathering LIPs, using weathering equation from Dessert et al. (2003).

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    LIP_fracs : dictionary
        with keys = LIP names, values = LIP fraction remaining
    Ts : dataframe
        of zonal temperature
    Rs : dataframe
        of zonal runoff
    t_step : float
        time since last weathering calculation (million years)
    thickness : float
        LIP thickness (km)
    density : float
        LIP density (kg/km^3)

    Returns
    -------
    None.

    Notes
    -----
    modifies LIP_fracs in place
    """
    # only calculate if there are geometries
    if len(reconstructed_feature_geometries) > 0:
        # the time
        t = int(reconstructed_feature_geometries[0].get_reconstruction_time())

        # only calculate if the t is in our climate model
        T_string = 'T_C_'+str(t)
        if T_string in Ts.columns:

            # iterate over each polygon
            for i in range(len(reconstructed_feature_geometries)):

                # get the name
                current_name = reconstructed_feature_geometries[i].get_feature().get_name()

                # only calculate if there is some LIP left
                if LIP_fracs[current_name] > 0:

                    # get the polygon, ID, area, mass, and centre latitude
                    current_polygon = reconstructed_feature_geometries[i].get_reconstructed_geometry()

                    og_mass = current_polygon.get_area() * (6371.009**2) * thickness * density

                    current_area = (current_polygon.get_area() * 6371.009**2) * LIP_fracs[current_name]
                    current_mass = current_area * thickness * density

                    centre_point = current_polygon.get_interior_centroid()
                    centre_lat = centre_point.to_lat_lon_array()[0][0]

                    # get the T and R at this time and latitude
                    for j in range(len(Ts.index)):
                        if abs(centre_lat)<Ts['lat_maxs'][j] and abs(centre_lat)>Ts['lat_mins'][j]:
                            T = Ts['T_C_'+str(t)][j]
                    for j in range(len(Rs.index)):
                        if centre_lat<Rs['lat_maxs'][j] and centre_lat>Rs['lat_mins'][j]:
                            R = Rs['R_m/yr_'+str(t)][j]

                    # get the weathering rate (t/km^2/yr) - from Dessert et al. (2003)
                    fw = (R * 18.41 * np.exp(0.0533 * T)) / 0.649

                    # weather (kg)
                    weather_mass = fw * current_area * abs(t_step)*1e6 * 1000

                    # update the mass
                    current_mass = current_mass - weather_mass

                    # update the remaining fraction in place
                    if current_mass > 0:
                        LIP_fracs[current_name] = current_mass / og_mass
                    else:
                        LIP_fracs[current_name] = 0

        # if the t is not in our climate model, fill the dictionary with nans
        else:
            # if the dictionary is already filled with nans, we can move on
            if math.isnan(LIP_fracs[reconstructed_feature_geometries[0].get_feature().get_name()]):
                pass
            else:
                for key, value in LIP_fracs.iteritems():
                    LIP_fracs[key] = float('NaN')

def get_LIP_areas_in_bands_OLD(reconstructed_feature_geometries, lat_mins, lat_maxs, LIP_fracs):
    """
    Get the area of all LIP features in each latitude band, accounting for weathering.

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums
    LIP_fracs : dictionary
        with keys = LIP names, values = LIP fraction remaining

    Returns
    -------
    areas : list
        list of total area in each latitude band
    area_polygons : list
        list of all polygons for which areas were calculated
    """
    # only calculate if there are geometries
    if len(reconstructed_feature_geometries) > 0:
        # if our LIP_fracs has nan's
        if math.isnan(LIP_fracs[reconstructed_feature_geometries[0].get_feature().get_name()]):
            areas = [np.nan]*len(lat_mins)
            area_polygons = None

        # if our LIP_fracs has values
        else:
            # storage vectors
            areas = []
            area_polygons = []

            # iterate over each latitude band
            for i in range(len(lat_mins)):

                accumulated_area = 0

                # iterate over each polygon
                for j in range(len(reconstructed_feature_geometries)):

                    current_polygon = reconstructed_feature_geometries[j].get_reconstructed_geometry()
                    current_name = reconstructed_feature_geometries[j].get_feature().get_name()

                    # check if the polygon is in the band
                    in_band = check_polygon_in_band(current_polygon, lat_mins[i], lat_maxs[i])

                    if in_band:
                        # do the calculation
                        area, band_polygon = get_area_in_band(current_polygon, lat_mins[i], lat_maxs[i])

                        # scale the area appropriately
                        area = area * LIP_fracs[current_name]

                        # store results
                        accumulated_area = accumulated_area + area
                        area_polygons.append(band_polygon)

                # store total area for the band
                areas.append(accumulated_area)

    else:
        areas = [0]*len(lat_mins)
        area_polygons = []

    return areas, area_polygons

########## OLD WEATHERING CALCULATIONS ##########

def get_LIP_areas_in_bands(reconstructed_feature_geometries, lat_mins, lat_maxs, thresh=None, halflife=None):
    """
    Get the area of all features in each latitude band, with optional calculations for:
    - features instantly disappearing after `thresh` years
    - features decaying exponentially

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums
    thresh : array-like (optional)
        after this many Myrs, the feature will instantly disappear
    halflife : array-like (optional)
        half life of exponential decay

    Returns
    -------
    areas : array
        list of total area in each latitude band
    area_polygons : list
        list of all polygons for which areas were calculated

    Optional Returns
    ----------------
    areas_thresh : list of arrays
        list of total area in each latitude band, accounting for thresholding method
    areas_decay : list of arrays
        list of total area in each latitude band, accounting for exponential decay method
    """
    # storage vectors
    areas = np.array([])
    area_polygons = []

    # optional storage vectors
    if thresh != None:
        areas_thresh_temp = []
    if halflife != None:
        areas_decay_temp = []
        # convert halflife to decay constant
        lamb = np.log(2)/halflife

    # iterate over each latitude band
    for i in range(len(lat_mins)):

        accumulated_area = 0

        if thresh != None:
            accumulated_area_thresh = np.zeros(len(thresh))
        if halflife != None:
            accumulated_area_decay = np.zeros(len(halflife))

        # iterate over each polygon
        for j in range(len(reconstructed_feature_geometries)):

            if thresh != None or halflife != None:
                # get the begin date, reconstruction date, and feature age
                begin_date, end_date = reconstructed_feature_geometries[j].get_feature().get_valid_time()
                now_date = reconstructed_feature_geometries[j].get_reconstruction_time()
                feature_age = begin_date - now_date

            # get the actual polygon
            current_polygon = reconstructed_feature_geometries[j].get_reconstructed_geometry()

            # check if the polygon is in the band
            in_band = check_polygon_in_band(current_polygon, lat_mins[i], lat_maxs[i])

            if in_band:
                # do the calculation
                area, band_polygon = get_area_in_band(current_polygon, lat_mins[i], lat_maxs[i])

                # store results
                accumulated_area = accumulated_area + area
                area_polygons.append(band_polygon)

                if thresh != None:
                    for k in range(len(thresh)):
                        # check if the polygon is past its due by date
                        if begin_date < (now_date+thresh[k]):
                            still_alive = True
                        else:
                            still_alive = False

                        if still_alive:
                            accumulated_area_thresh[k] = accumulated_area_thresh[k] + area

                if halflife != None:
                    for k in range(len(halflife)):
                        # scale the area based on the exponential decay equation
                        decay_area = area * np.exp(-lamb[k]*feature_age)

                        # store results
                        accumulated_area_decay[k] = accumulated_area_decay[k] + decay_area

        # store total area for the band
        areas = np.append(areas, accumulated_area)

        if thresh != None:
            areas_thresh_temp.append(accumulated_area_thresh)
        if halflife != None:
            areas_decay_temp.append(accumulated_area_decay)

    # basically, flip our outputs so that each array in our list is for a given input thresh/halflife
    if thresh != None:
        areas_thresh = []
        for i in range(len(thresh)):
            this_array = np.array([])
            for j in range(len(areas_thresh_temp)):
                this_array = np.append(this_array, areas_thresh_temp[j][i])
            areas_thresh.append(this_array)
    if halflife != None:
        areas_decay = []
        for i in range(len(halflife)):
            this_array = np.array([])
            for j in range(len(areas_decay_temp)):
                this_array = np.append(this_array, areas_decay_temp[j][i])
            areas_decay.append(this_array)

    # returns
    if thresh == None and halflife == None:
        return areas, area_polygons
    elif thresh != None and halflife == None:
        return areas, area_polygons, areas_thresh
    elif thresh == None and halflife != None:
        return areas, area_polygons, areas_decay
    elif thresh != None and halflife != None:
        return areas, area_polygons, areas_thresh, areas_decay

def initialize_LIP_dict(LIP_feature_collection):
    """
    Initialize the dictionary which contains the LIP fraction remaining for all LIPs.

    Parameters
    ----------
    LIP_feature_collection : feature collection
        feature collection of LIPs

    Returns
    -------
    LIP_fracs : dictionary
        with keys = LIP Ids, values = LIP fraction remaining
    """
    # get the unique ID associated with each LIP geometry
    LIP_Ids = []
    for feature in LIP_feature_collection:
        LIP_Id = feature.get_feature_id().get_string()
        LIP_Ids.append(LIP_Id)

    # create a dictionary: key = LIP Id, value = LIP fraction remaining
    ones = [1]*len(LIP_Ids)
    LIP_fracs = dict(zip(LIP_Ids, ones))

    return LIP_fracs

def weather_LIPs(reconstructed_feature_geometries, LIP_fracs, climate_df, t_step, thickness, density):
    """
    A model for weathering LIPs, using weathering equation from Dessert et al. (2003).

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    LIP_fracs : dictionary
        with keys = LIP Ids, values = LIP fraction remaining
    climate_df : dataframe
        with columns: "lat_mins", "lat_maxs", "lat_mids", "T" (C), "R" (m/yr)
    t_step : float
        time since last weathering calculation (million years)
    thickness : float
        LIP thickness (km)
    density : float
        LIP density (kg/km^3)

    Returns
    -------
    None.

    Notes
    -----
    modifies LIP_fracs in place
    """
    # only calculate if there are geometries
    if len(reconstructed_feature_geometries) > 0:
        # the time
        t = int(reconstructed_feature_geometries[0].get_reconstruction_time())

        # iterate over each polygon
        for i in range(len(reconstructed_feature_geometries)):

            # get the Id
            current_Id = reconstructed_feature_geometries[i].get_feature().get_feature_id().get_string()

            # only calculate if there is some LIP left
            if LIP_fracs[current_Id] > 0:

                # get the polygon, ID, area, mass, and centre latitude
                current_polygon = reconstructed_feature_geometries[i].get_reconstructed_geometry()

                og_mass = current_polygon.get_area() * (6371.009**2) * thickness * density

                current_area = (current_polygon.get_area() * (6371.009**2)) * LIP_fracs[current_Id]
                current_mass = current_area * thickness * density

                centre_point = current_polygon.get_interior_centroid()
                centre_lat = centre_point.to_lat_lon_array()[0][0]

                # get the T and R at this latitude
                for j in range(len(climate_df.index)):
                    if abs(centre_lat)<climate_df['lat_maxs'][j] and abs(centre_lat)>climate_df['lat_mins'][j]:
                        T = climate_df['T'][j]
                        R = climate_df['R'][j]

                # get the weathering rate (t/km^2/yr) - from Dessert et al. (2003)
                fw = (R * 18.41 * np.exp(0.0533 * T)) / 0.649

                # weather (kg)
                weather_mass = fw * current_area * abs(t_step)*1e6 * 1000

                # update the mass
                current_mass = current_mass - weather_mass

                # update the remaining fraction in place
                if current_mass > 0:
                    LIP_fracs[current_Id] = current_mass / og_mass
                else:
                    LIP_fracs[current_Id] = 0


import math
import numpy as np
from utils import normalize_vector, rotate

class Torus:
	def __init__(self, R=1.0, r=0.4, theta_step=75, phi_step=75):
		self.R = R
		self.r = r
		self.theta_step = theta_step
		self.phi_step = phi_step
		self.thetas = np.arange(0, 2 * math.pi, 2 * math.pi / self.theta_step)
		self.phis = np.arange(0, 2 * math.pi, 2 * math.pi / self.phi_step)
		self.points = np.array([self.point_equation(theta, phi) for theta in self.thetas for phi in self.phis])
		self.normals = np.array([self.normal_equation(theta, phi) for theta in self.thetas for phi in self.phis])

	def point_equation(self, theta, phi):
		return np.array([
				(self.R + self.r * math.cos(phi)) * math.cos(theta),
				(self.R + self.r * math.cos(phi)) * math.sin(theta),
				self.r * math.sin(phi)
			])

	def normal_equation(self, theta, phi):
		return normalize_vector(np.array([
				math.cos(theta) * math.cos(phi),
				math.sin(theta) * math.cos(phi),
				math.sin(phi)
			]))

	def rotate(self, rotation_matrix_func, theta):
		self.points, self.normals = rotate(rotation_matrix_func, theta, self.points, self.normals)


import numpy as np
from scipy import signal

def myconv2d(image, filterr):
    m = filterr.shape[0]
    y, x = image.shape
    y = y - m + 1
    x = x - m + 1
    conv = np.zeros((y,x))
    for i in range(y):
        for j in range(x):
            conv[i][j] = np.sum(image[i:i+m, j:j+m]*filterr)
    return conv

image = np.array([[164,188,164,161,195],
                  [178,201,197,150,137],
                  [174,168,181,190,184],
                  [131,179,176,185,198],
                  [92,185,179,133,167]
                 ])
filter1 = np.array([[1,1,1],[1,1,1],[1,1,1]])
filter2 = np.array([[-1,-2,-1],[0,0,0],[1,2,1]])
filter3 = np.array([[-1,-1,-1],[-1,9,-1],[-1,-1,-1]])
#conv1 = signal.convolve2d(image, filter1, mode='valid')
#print(conv1)

conv_f1 = myconv2d(image, filter1)
conv_f2 = myconv2d(image, filter2)
conv_f3 = myconv2d(image, filter3)
print(conv_f1)
print(conv_f2)
print(conv_f3)


#2D Transformations are regularly used in Linear Algebra.

#I have added the codes for reflection, projection, scaling and rotation matrices.

import numpy as np

def scaling(scaling_factor):
	return(scaling_factor*(np.identity(2))) #This returns a scaling matrix

def rotation(angle):
	arr = np.empty([2, 2])
	c = np.cos(angle)
	s = np.sin(angle)
	arr[0][0] = c
	arr[0][1] = -s
	arr[1][0] = s
	arr[1][1] = c

	return arr #This returns a rotation matrix

def projection(angle):
	arr = np.empty([2, 2])
	c = np.cos(angle)
	s = np.sin(angle)
	arr[0][0] = c*c
	arr[0][1] = c*s
	arr[1][0] = c*s
	arr[1][1] = s*s

	return arr #This returns a rotation matrix

def reflection(angle):
	arr = np.empty([2, 2])
	c = np.cos(angle)
	s = np.sin(angle)
	arr[0][0] = (2*c) -1
	arr[0][1] = 2*s*c
	arr[1][0] = 2*s*c
	arr[1][1] = (2*s) -1

	return arr #This returns a reflection matrix


import numpy as np

def estimate_n_trials_needed(inlier_ratio: float, n_sample: int, probability: float = 0.95) -> int:
    """
    Estimate number of trials needed to select only true inliers with RANSAC.

    Parameters
    ----------
    inlier_ratio : number of inliers / number of all points
    n_sample : number of samples needed to fit model
    probability : desired probability of success.

    Returns
    -------
    Estimated number of trials needed.
    """
    return int(np.log(1 - probability) / np.log(1 - inlier_ratio ** n_sample))


# код (n, k) n -- символов в код. слове, k -- символов в сообщении
# пор. мат. G размера k×n
# пров. мат. H размер (n-k)×n
G = [
    [1, 0, 0, 0, 1, 1, 0],
    [0, 1, 0, 0, 1, 0, 1],
    [0, 0, 1, 0, 0, 1, 1],
    [0, 0, 0, 1, 1, 1, 1],
] # (7, 4)

import numpy as np

G = np.array(G)
print(G.T)

# In[0]
H = np.array([
    [1, 1, 0, 1, 1, 0, 0],
    [1, 0, 1, 1, 0, 1, 0],
    [0, 1, 1, 1, 0, 0, 1],
])

# H×G(T) = 0 (mod q)

np.matmul(H,G.T) % 2

# In[0]

def getH(G):
    k, n = G.shape
    H = np.zeros(shape=(n-k, n))
    for i in range(k, n):
        H[i-k, i] = 1
    for i in range(k):
        for j in range(k, n):
            H[j-k, i] = G[i, j]
    return H

# In[0]

def lincode(G, p=2):
    k,n = G.shape
    H = getH(G)
    tbl = {}
    for i in range(n):
        ei = np.zeros((n, 1))
        ei[i, 0] = 1
        si = np.reshape(np.matmul(H, ei) % p, n-k)
        tbl[str(si)] = np.reshape(ei, n)

    def encode(G, m):
        return np.reshape(np.matmul(G.T, np.reshape(np.array(m), (k, 1))) % p, n)

    def decode(G, c):
        s = np.reshape(np.matmul(H, c) % p, n-k)
        if sum(s) == 0:
            return c[:k]
        e = tbl[str(s)]
        c1 = c - e
        return c1[:k]

    return (encode, decode)

encode, decode = lincode(G = np.array([
    [1, 0, 0, 0, 1, 1, 0],
    [0, 1, 0, 0, 0, 1, 1],
    [0, 0, 1, 0, 1, 0, 1],
    [0, 0, 0, 1, 1, 1, 1],
])) # (7, 4)
decode(G,encode(G, [1,0,1,0])+np.array([0,0,0,1,0,0,0]))


import pandas as pd
import numpy as np
import scipy.stats as ss
from dython.nominal import theils_u, cramers_v
from sklearn.metrics import mean_squared_error
from scipy.spatial.distance import jensenshannon
from joblib import Parallel, delayed
from typing import Dict, Any, List
from scipy.stats import ks_2samp

def mean_absolute_error(y_true: np.ndarray, y_pred: np.ndarray):
    """
    Returns the mean absolute error between y_true and y_pred.

    :param y_true: NumPy.ndarray with the ground truth values.
    :param y_pred: NumPy.ndarray with the ground predicted values.
    :return: Mean absolute error (float).
    """
    return np.mean(np.abs(np.subtract(y_true, y_pred)))

def euclidean_distance(y_true: np.ndarray, y_pred: np.ndarray):
    """
    Returns the euclidean distance between y_true and y_pred.

    :param y_true: NumPy.ndarray with the ground truth values.
    :param y_pred: NumPy.ndarray with the ground predicted values.
    :returns: Mean absolute error (float).
    """
    return np.sqrt(np.sum(np.power(np.subtract(y_true, y_pred), 2)))

def mean_absolute_percentage_error(y_true: np.ndarray, y_pred: np.ndarray):
    """
    Returns the mean absolute percentage error between y_true and y_pred. Throws ValueError if y_true contains zero values.

    :param y_true: NumPy.ndarray with the ground truth values.
    :param y_pred: NumPy.ndarray with the ground predicted values.
    :return: Mean absolute percentage error (float).
    """
    y_true, y_pred = np.array(y_true), np.array(y_pred)
    return np.mean(np.abs((y_true - y_pred) / y_true))

def rmse(y_true: np.ndarray, y_pred: np.ndarray):
    """
    Returns the root mean squared error between y_true and y_pred.

    :param y_true: NumPy.ndarray with the ground truth values.
    :param y_pred: NumPy.ndarray with the ground predicted values.
    :return: root mean squared error (float).
    """
    return np.sqrt(mean_squared_error(y_true, y_pred))

def cosine_similarity(y_true: np.ndarray, y_pred: np.ndarray):
    y_true, y_pred = y_true.reshape(-1), y_pred.reshape(-1)
    return np.sum(y_true * y_pred) / (np.sqrt(np.sum(y_true ** 2)) * np.sqrt(np.sum(y_pred ** 2)))

def column_correlations(dataset_a, dataset_b, categorical_columns, theil_u=True):
    """
    Column-wise correlation calculation between ``dataset_a`` and ``dataset_b``.

    :param dataset_a: First DataFrame
    :param dataset_b: Second DataFrame
    :param categorical_columns: The columns containing categorical values
    :param theil_u: Whether to use Theil's U. If False, use Cramer's V.
    :return: Mean correlation between all columns.
    """
    if categorical_columns is None:
        categorical_columns = list()
    elif categorical_columns == 'all':
        categorical_columns = dataset_a.columns
    assert dataset_a.columns.tolist() == dataset_b.columns.tolist()
    corr = pd.DataFrame(columns=dataset_a.columns, index=['correlation'])

    for column in dataset_a.columns.tolist():
        if column in categorical_columns:
            if theil_u:
                corr[column] = theils_u(dataset_a[column].sort_values(), dataset_b[column].sort_values())
            else:
                corr[column] = cramers_v(dataset_a[column].sort_values(), dataset_b[column].sort_vaues())
        else:
            corr[column], _ = ss.pearsonr(dataset_a[column].sort_values(), dataset_b[column].sort_values())
    corr.fillna(value=np.nan, inplace=True)
    correlation = np.mean(corr.values.flatten())
    return correlation

def js_distance_df(real: pd.DataFrame, fake: pd.DataFrame, numerical_columns: List) -> pd.DataFrame:
    assert real.columns.tolist() == fake.columns.tolist(), f'Colums are not identical between `real` and `fake`. '
    real_iter = real[numerical_columns].iteritems()
    fake_iter = fake[numerical_columns].iteritems()
    distances = Parallel(n_jobs=-1)(
        delayed(jensenshannon_distance)
        (colname, real_col, fake_col) for (colname, real_col), (_, fake_col) in zip(real_iter, fake_iter))

    distances_df = pd.DataFrame(distances)
    # distances_df = distances_df.append({'colname': 'mean', 'js_distance': distances_df.js_distance.mean()})
    return distances_df.set_index('col_name')

def jensenshannon_distance(colname: str, real_col: pd.Series, fake_col: pd.Series, bins=25) -> Dict[str, Any]:
    binned_values_real, bins = pd.cut(real_col, bins=bins, retbins=True)
    binned_probs_real = binned_values_real.value_counts(normalize=True, sort=False)
    binned_probs_fake = pd.cut(fake_col, bins=bins).value_counts(normalize=True, sort=False)
    js_distance = jensenshannon(binned_probs_real, binned_probs_fake)
    return {'col_name': colname, 'js_distance': js_distance}

def kolmogorov_smirnov_test(col_name, real_col, fake_col):
    statistic, p_value = ks_2samp(real_col, fake_col)
    equality = 'identical' if p_value > 0.01 else 'different'
    return {'col_name': col_name, 'statistic': statistic, 'p-value': p_value, 'equality': equality}

def kolmogorov_smirnov_df(real: pd.DataFrame, fake: pd.DataFrame, numerical_columns: List) -> List[Dict[str, Any]]:
    assert real.columns.tolist() == fake.columns.tolist(), f'Colums are not identical between `real` and `fake`. '
    real_iter = real[numerical_columns].iteritems()
    fake_iter = fake[numerical_columns].iteritems()
    distances = Parallel(n_jobs=-1)(
        delayed(kolmogorov_smirnov_test)
        (colname, real_col, fake_col) for (colname, real_col), (_, fake_col) in zip(real_iter, fake_iter))
    distances_df = pd.DataFrame(distances)
    return distances_df.set_index('col_name')


from scipy.special import comb

def independent_alleles(k, N):
    prob_N=0.0
    total_offspring=2**k

    for i in range(N, total_offspring):
        #calculating the success of at least N in among 2**k trials-->binomial distribution
        prob_N=prob_N+comb(total_offspring,i)*((0.25)**i)*((0.75)**(total_offspring-i))

    prob_N=round(prob_N,4)
    return prob_N

def main():
    with open('datasets/rosalind_lia.txt') as input_file:
        k, N = input_file.read().strip().split()

    prob_N=independent_alleles(int(k),int(N))

    print(prob_N)

    with open('solutions/rosalind_lia.txt', 'w') as output_file:
        output_file.write(str(prob_N))

if(__name__=='__main__'):
    main()


# assignment 4 for Watkins

# from numpy import *
import numpy as np
from scipy.interpolate import lagrange

# **************** problem #1 **********************
# Find the polynomial of degree 2 that interpolates these points

# x = np.array([.8, .9, 1.0])
# f = np.array([.696707, .621610, .540302])
#
# polynomial_2 = lagrange(x, f)
#
# a = polynomial_2(0.85)
#
# print a
#
# print polynomial_2

# ******** problem #4 ***********

def f(x):
    return 3.0*np.power(x, 2) - 6.0*x + 5

x = np.array([-3, -2, -1, 0, 1, 2, 3, 4])
y = np.array([f(-3), f(-2), f(-1), f(0), f(1), f(2), f(3), f(4)])

print y

polynomial_2 = lagrange(x, y)

print polynomial_2

#
# # **************** problem #2 *************************
# from pylab import *
#
#
# def divided_difference( x, y ):
#     """Compute interpolated polynomial coefficients using divided differences.
#
#     USAGE:
#         a = divided_difference( x, y )
#
#     INPUT:
#         x      - integer or float array of x coordinates
#         y      - integer or float array of y coordiantes
#
#     OUTPUT:
#         float array - array of interpolating polynomial coefficients
#
#     NOTES:
#         This function computes the divided differences that can be used to
#         construct an interpolating polynomial for the given set of data.
#
#         This function accepts two vectors and returns a vector whose elements
#         are the divided difference values.
#
#         The value of the polynomial p (degree will be at most n) evaluated at
#         r is given by:
#
#             s = a[n]
#             for i in range( n, 0, -1 ):
#                 s = s * ( r - x[i-1] ) + a[i-1]
#
#         using the array a returned by this function.
#
#     AUTHOR:
#         Jonathan R. Senning <jonathan.senning@gordon.edu>
#         Gordon College
#         February 7, 1999
#         Converted to Python May 2008
#     """
#
#     nx = shape( x )
#     ny = shape( y )
#
#     if max( nx ) != max( ny ):
#         print 'both input variables must be vectors of the same length'
#         return
#     elif len( nx ) != 1 or len( ny ) != 1:
#         print 'both input variables must be vectors, not matrices'
#         return
#
#     n = max( nx ) - 1
#     a = y.copy()
#
#     for i in xrange( 1, n + 1 ):
#         for j in xrange( n, i - 1 , -1 ):
#             a[j] = float( a[j] - a[j-1] ) / float( x[j] - x[j-i] )
#
#     return a
#
#
# def evaluate(r, x, a):
#     """Evaluate divided difference polynomial at r
#
#     USAGE:
#         a = evaluate( r, x, a )
#
#     INPUT:
#         r      - integer or float scalar or numpy array of evaluation points
#         x      - integer or float array of interpolation point x coordinates
#         a      - coefficient array returned by divided_difference()
#
#     OUTPUT:
#         float array - float scalar or array of y-values corresponding to r
#
#     """
#
#     n = len( a ) - 1
#     s = a[n]
#     for i in xrange( n - 1, -1, -1 ):
#         s = s * ( r - x[i] ) + a[i]
#     return s
#
# b = divided_difference(x, f)
#
# c = evaluate(x, f, b)
#
# print c


import numpy as np

def gaussian_work_example(N_F=200, N_R=200, mu_F=2.0, DeltaF=None, sigma_F=1.0, seed=None):
    """Generate samples from forward and reverse Gaussian work distributions.

    Parameters
    ----------
    N_F : int, optional
        number of forward measurements (default: 200)
    N_R : float, optional
        number of reverse measurements (default: 200)
    mu_F : float, optional
        mean of forward work distribution (default: 2.0)
    DeltaF : float, optional
        the free energy difference, which can be specified instead of mu_F (default: None)
    sigma_F : float, optional
        variance of the forward work distribution (default: 1.0)
    seed : int, optional
        If not None, specify the numpy random number seed. Old state is restored after completion.

    Returns
    -------
    w_F : np.ndarray, dtype=float
        forward work values
    w_R : np.ndarray, dtype=float
        reverse work values

    Notes
    -----
    By the Crooks fluctuation theorem (CFT), the forward and backward work distributions are related by

    P_R(-w) = P_F(w) \exp[DeltaF - w]

    If the forward distribution is Gaussian with mean \mu_F and std dev \sigma_F, then

    P_F(w) = (2 \pi)^{-1/2} \sigma_F^{-1} \exp[-(w - \mu_F)^2 / (2 \sigma_F^2)]

    With some algebra, we then find the corresponding mean and std dev of the reverse distribution are

    \mu_R = - \mu_F + \sigma_F^2
    \sigma_R = \sigma_F \exp[\mu_F - \sigma_F^2 / 2 + \Delta F]

    where all quantities are in reduced units (e.g. divided by kT).

    Note that \mu_F and \Delta F are not independent!  By the Zwanzig relation,

    E_F[exp(-w)] = \int dw \exp(-w) P_F(w) = \exp[-\Delta F]

    which, with some integration, gives

    \Delta F = \mu_F + \sigma_F^2/2

    which can be used to determine either \mu_F or \DeltaF.

    Examples
    --------

    Generate work values with default parameters.

    >>> [w_F, w_R] = gaussian_work_example()

    Generate 50 forward work values and 70 reverse work values.

    >>> [w_F, w_R] = gaussian_work_example(N_F=50, N_R=70)

    Generate work values specifying the work distribution parameters.

    >>> [w_F, w_R] = gaussian_work_example(mu_F=3.0, sigma_F=2.0)

    Generate work values specifying the work distribution parameters, specifying free energy difference instead of mu_F.

    >>> [w_F, w_R] = gaussian_work_example(mu_F=None, DeltaF=3.0, sigma_F=2.0)

    Generate work values with known seed to ensure reproducibility for testing.

    >>> [w_F, w_R] = gaussian_work_example(seed=0)

    """

    # Make sure either mu_F or DeltaF, but not both, are specified.
    if (mu_F is not None) and (DeltaF is not None):
        raise ValueError("mu_F and DeltaF are not independent, and cannot both be specified; one must be set to None.")
    if (mu_F is None) and (DeltaF is None):
        raise ValueError("Either mu_F or DeltaF must be specified.")
    if (mu_F is None):
        mu_F = DeltaF + sigma_F ** 2 / 2.0
    if (DeltaF is None):
        DeltaF = mu_F - sigma_F ** 2 / 2.0

    # Set random number generator into a known state for reproducibility.
    random = np.random.RandomState(seed)

    # Determine mean and variance of reverse work distribution by Crooks
    # fluctuation theorem (CFT).
    mu_R = - mu_F + sigma_F ** 2
    sigma_R = sigma_F * np.exp(mu_F - sigma_F ** 2 / 2.0 - DeltaF)

    # Draw samples from forward and reverse distributions.
    w_F = random.randn(N_F) * sigma_F + mu_F
    w_R = random.randn(N_R) * sigma_R + mu_R

    return [w_F, w_R]


import numpy as np
import scipy.spatial as spspacial
import scipy.sparse as spsparse

import sys

def distance_matrix(X):
    distance_matrix = spspacial.distance.squareform(spspacial.distance.pdist(X))
    return distance_matrix

def node_degrees(W):
    if isinstance(W, np.ndarray):
        D = W.sum(axis=1) - np.diag(W)
    else:
        D = W.sum(axis=1).A1 - W.diagonal()
    return D

def kernel_exponential(a, eps=1.0, d=1, cutoff=None):
    result = (eps ** -d) * np.exp(-((a / eps) ** 2))
    if not cutoff is None:
        result[result < cutoff] = 0.0
    result = spsparse.csr_matrix(result)

    return result

def kernel_indicator(a, eps=1.0, d=1, radius=1.0):
    result = a.copy()
    result[result >= radius * eps] = 0.0
    result[result > 1e-10] = 1.0
    np.fill_diagonal(result, 1.0)  # inplace operation
    result = (eps ** -d) * spsparse.csr_matrix(result)

    return result

def epsilon_ball(data, epsilon, kernel="gaussian", eta=None):
    """Epsilon ball weight matrix

    This is an adaptation of Jeff Calders implementation licensed
    under the MIT licence. For the original source code see
    `https://github.com/jwcalder/GraphLearning/blob/master/graphlearning/ssl.py`.
    ======

    General function for constructing a sparse epsilon-ball weight matrix, whose weights have the form
    \\[ w_{i,j} = \\eta\\left(\\frac{\\|x_i - x_j\\|^2}{\\varepsilon^2} \\right), \\]
    when \\(\\|x_i - x_j\\|\\leq \\varepsilon\\), and \\(w_{i,j}=0\\) otherwise.
    This type of weight matrix is only feasible in relatively low dimensions.

    Parameters
    ----------
    data : (n,m) numpy array
        n data points, each of dimension m
    epsilon : float
        Connectivity radius
    kernel : string (optional), {'uniform','gaussian','singular','distance'}, default='gaussian'
        The choice of kernel in computing the weights between \\(x_i\\) and \\(x_j\\) when
        \\(\\|x_i-x_j\\|\\leq \\varepsilon\\). The choice 'uniform' corresponds to \\(w_{i,j}=1\\)
        and constitutes an unweighted graph, 'gaussian' corresponds to
        \\[ w_{i,j} = \\exp\\left(\\frac{-4\\|x_i - x_j\\|^2}{\\varepsilon^2} \\right), \\]
        'distance' corresponds to
        \\[ w_{i,j} = \\|x_i - x_j\\|, \\]
        and 'singular' corresponds to
        \\[ w_{i,j} = \\frac{1}{\\|x_i - x_j\\|}, \\]
        when \\(i\\neq j\\) and \\(w_{i,i}=1\\).
    eta : python function handle (optional)
        If provided, this overrides the kernel option and instead uses the weights
        \\[ w_{i,j} = \\eta\\left(\\frac{\\|x_i - x_j\\|^2}{\\varepsilon^2} \\right). \\]

    Returns
    -------
    W : (n,n) scipy sparse matrix, float
        Sparse weight matrix.
    """
    n = data.shape[0]  # Number of points

    # Rangesearch to find nearest neighbors
    Xtree = spspacial.cKDTree(data)
    M = Xtree.query_pairs(epsilon, output_type="ndarray")

    # Differences between points and neighbors
    V = data[M[:, 0], :] - data[M[:, 1], :]
    dists = np.sum(V * V, axis=1)

    # If eta is None, use kernel keyword
    if eta is None:

        if kernel == "uniform":
            weights = np.ones_like(dists)
            fzero = 1
        elif kernel == "gaussian":
            weights = np.exp(-4 * dists / (epsilon * epsilon))
            fzero = 1
        elif kernel == "distance":
            weights = np.sqrt(dists)
            fzero = 0
        elif kernel == "singular":
            weights = np.sqrt(dists)
            weights[dists == 0] = 1
            weights = 1 / weights
            fzero = 1
        else:
            sys.exit("Invalid choice of kernel: " + kernel)

    # Else use user-defined eta
    else:
        weights = eta(dists / (epsilon * epsilon))
        fzero = eta(0)

    # Weights

    # Symmetrize weights and add diagonal entries
    weights = np.concatenate((weights, weights, fzero * np.ones(n,)))
    M1 = np.concatenate((M[:, 0], M[:, 1], np.arange(0, n)))
    M2 = np.concatenate((M[:, 1], M[:, 0], np.arange(0, n)))

    # Construct sparse matrix and convert to Compressed Sparse Row (CSR) format
    W = spsparse.coo_matrix((weights, (M1, M2)), shape=(n, n))

    return W.tocsr()


import torch
import numpy as np

def np_one_hot(array, num_classes):
    return np.eye(num_classes, dtype=int)[np.array(array, dtype=int)]

def one_hot(array, num_classes):
    return torch.eye(num_classes, dtype=torch.int)[array]

def np_det_nd(array):
    # support nd array det op.
    return np.reshape(
        np.linalg.det(np.reshape(array, (-1, *array.shape[-2:]))),
        array.shape[:-2]
    )

def det_nd(array):
    # support nd array det op.
    return torch.reshape(
        torch.det(torch.reshape(array, (-1, *array.shape[-2:]))),
        array.shape[:-2]
    )

if __name__ == '__main__':
    # inputs = torch.randn(1, 2, 4, 4)
    inputs = torch.randn((4, 4))
    out = det_nd(inputs)
    print(out)
    print(torch.det(inputs))

    # inputs = np.random.randn(1, 2, 4, 4)
    inputs = np.random.randn(4, 4)
    out = np_det_nd(inputs)
    print(out)
    print(np.linalg.det(inputs))


# batch version

import numpy as np

def cross_entropy_error(y, t):
    delta = 1e-7  # to avoid log(0)

    # if y is not a matrix
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    # if labels are one-hot vectors
    if t.size == y.size:
        t = t.argmax(axis=1)

    batch_size = y.shape[0]
    return - np.sum(np.log(y[np.arange(batch_size), t] + delta)) / batch_size

if __name__ == '__main__':
    t = np.array([0, 1, 0, 0, 0, 0, 0, 0, 0, 0])

    y1 = np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    y2 = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    y3 = np.array([0.1, 0.9, 0.3, 0.3, 0, 0, 0.2, 0, 0, 0.6])

    print('t = {0}'.format(t))
    print('y1 = {0}, cross_entropy_error = {1}'.format(
        y1, cross_entropy_error(y1, t)))
    print('y2 = {0}, cross_entropy_error = {1}'.format(
        y2, cross_entropy_error(y2, t)))
    print('y3 = {0}, cross_entropy_error = {1}'.format(
        y3, cross_entropy_error(y3, t)))
    print('t = {0}, cross_entropy_error = {1}'.format(
        t, cross_entropy_error(t, t)))

    ts = np.array([[0, 0, 1, 0, 0],
                   [1, 0, 0, 0, 0],
                   [0, 0, 0, 1, 0]])
    ys = np.array([[0.1, 0.4, 0.6, 0, 0.3],
                   [0.3, 0.5, 0.1, 0.7, 0],
                   [0, 0, 0, 1, 0]])

    print('ts: {0}, ys: {1}'.format(ts.shape, ys.shape))
    print('ys = {0}, cross_entropy_error = {1}'.format(
        ys, cross_entropy_error(ys, ts)))


import numpy as np
from context import gpytoolbox

# Choose grid size
gs = np.array([19,15])
h = 1.0/(gs-1)

# Build a grid
x, y = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0,1,gs[1]))
V = np.concatenate((np.reshape(x,(-1, 1)),np.reshape(y,(-1, 1))),axis=1)

# Build staggered grid in x direction
x, y = np.meshgrid(np.linspace(0+0.5*h[0],1-0.5*h[0],gs[0]-1),np.linspace(0,1,gs[1]))
Vx = np.concatenate((np.reshape(x,(-1, 1)),np.reshape(y,(-1, 1))),axis=1)
# Build staggered grid in y direction
x, y = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0+0.5*h[1],1-0.5*h[1],gs[1]-1))
Vy = np.concatenate((np.reshape(x,(-1, 1)),np.reshape(y,(-1, 1))),axis=1)

# Build partial derivative matrices
Dx = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=0)
Dy = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=1)

# all rows must sum up to zero (i.e. a constant function has zero derivative)
assert((np.isclose(Dx.sum(axis=1),np.zeros((Dx.shape[0],1)))).all())
assert((np.isclose(Dy.sum(axis=1),np.zeros((Dy.shape[0],1)))).all())

# Build linear function
f = 2*V[:,0] + 5*V[:,1]
computed_derivative_x = Dx*f
computed_derivative_y = Dy*f
# Derivatives must be 2.0 and 5.0, respectively
assert((np.isclose(computed_derivative_x,2.0*np.ones((computed_derivative_x.shape[0])))).all())
assert((np.isclose(computed_derivative_y,5.0*np.ones((computed_derivative_y.shape[0])))).all())

# Convergence test
linf_norm_x = 100.0
linf_norm_y = 100.0
print("This experiment should print a set of decreasing values, converging")
print("towards zero and decreasing roughly by half in each iteration")
for power in range(3,13,1):
    gs = np.array([2**power,2**power - 2])
    h = 1.0/(gs-1)
    # Build a grid
    x, y = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0,1,gs[1]))
    V = np.concatenate((np.reshape(x,(-1, 1)),np.reshape(y,(-1, 1))),axis=1)

    # Build staggered grid in x direction
    x, y = np.meshgrid(np.linspace(0+0.5*h[0],1-0.5*h[0],gs[0]-1),np.linspace(0,1,gs[1]))
    Vx = np.concatenate((np.reshape(x,(-1, 1)),np.reshape(y,(-1, 1))),axis=1)
    # Build staggered grid in y direction
    x, y = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0+0.5*h[1],1-0.5*h[1],gs[1]-1))
    Vy = np.concatenate((np.reshape(x,(-1, 1)),np.reshape(y,(-1, 1))),axis=1)

    # Build partial derivative matrices
    Dx = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=0)
    Dy = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=1)
    # Build non-linear function
    f = np.cos(V[:,0]) + np.sin(V[:,1])
    # Derivatives on staggered grids
    fx = -np.sin(Vx[:,0])
    fy =  np.cos(Vy[:,1])
    # Computed derivatives using our matrices
    computed_derivative_x = Dx*f
    computed_derivative_y = Dy*f
    # Print L infinity norm of difference
    # Make sure norm is decreasing
    assert(linf_norm_x>np.max(np.abs(computed_derivative_x - fx)))
    assert(linf_norm_y>np.max(np.abs(computed_derivative_y - fy)))
    linf_norm_x = np.max(np.abs(computed_derivative_x - fx))
    linf_norm_y = np.max(np.abs(computed_derivative_y - fy))
    # Print L infinity norm of difference
    print(np.array([np.max(np.abs(computed_derivative_x - fx)),np.max(np.abs(computed_derivative_y - fy))]))

# 3D Unit test
# Choose grid size
gs = np.array([19,15,23])
h = 1.0/(gs-1)

# Build a grid
x, y, z = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0,1,gs[1]),np.linspace(0,1,gs[2]),indexing='ij')
V = np.concatenate((np.reshape(x,(-1, 1),order='F'),np.reshape(y,(-1, 1),order='F'),np.reshape(z,(-1, 1),order='F')),axis=1)

# Build staggered grid in x direction
x, y, z = np.meshgrid(np.linspace(0+0.5*h[0],1-0.5*h[0],gs[0]-1),np.linspace(0,1,gs[1]),np.linspace(0,1,gs[2]),indexing='ij')
Vx = np.concatenate((np.reshape(x,(-1, 1),order='F'),np.reshape(y,(-1, 1),order='F'),np.reshape(z,(-1, 1),order='F')),axis=1)
# Build staggered grid in y direction
x, y, z = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0+0.5*h[1],1-0.5*h[1],gs[1]-1),np.linspace(0,1,gs[2]),indexing='ij')
Vy = np.concatenate((np.reshape(x,(-1, 1),order='F'),np.reshape(y,(-1, 1),order='F'),np.reshape(z,(-1, 1),order='F')),axis=1)
# Build staggered grid in z direction
x, y, z = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0,1,gs[1]),np.linspace(0+0.5*h[2],1-0.5*h[2],gs[2]-1),indexing='ij')
Vz = np.concatenate((np.reshape(x,(-1, 1),order='F'),np.reshape(y,(-1, 1),order='F'),np.reshape(z,(-1, 1),order='F')),axis=1)

# Build partial derivative matrices
Dx = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=0)
Dy = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=1)
Dz = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=2)

# all rows must sum up to zero (i.e. a constant function has zero derivative)
assert((np.isclose(Dx.sum(axis=1),np.zeros((Dx.shape[0],1)))).all())
assert((np.isclose(Dy.sum(axis=1),np.zeros((Dy.shape[0],1)))).all())
assert((np.isclose(Dz.sum(axis=1),np.zeros((Dz.shape[0],1)))).all())

# Build linear function
f = 2*V[:,0] + 5*V[:,1] - 4*V[:,2]
computed_derivative_x = Dx @ f
computed_derivative_y = Dy @ f
computed_derivative_z = Dz @ f
# Derivatives must be 2.0 and 5.0, respectively
assert((np.isclose(computed_derivative_x,2.0*np.ones((computed_derivative_x.shape[0])))).all())
assert((np.isclose(computed_derivative_y,5.0*np.ones((computed_derivative_y.shape[0])))).all())
assert((np.isclose(computed_derivative_z,-4.0*np.ones((computed_derivative_z.shape[0])))).all())

# Convergence test
linf_norm_x = 100.0
linf_norm_y = 100.0
linf_norm_z = 100.0
print("This experiment should print a set of decreasing values, converging")
print("towards zero and decreasing roughly by half in each iteration")
for power in range(3,9,1):
    gs = np.array([2**power,2**power - 2,2**power - 1])
    h = 1.0/(gs-1)
    # Build a grid
    x, y, z = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0,1,gs[1]),np.linspace(0,1,gs[2]),indexing='ij')
    V = np.concatenate((np.reshape(x,(-1, 1),order='F'),np.reshape(y,(-1, 1),order='F'),np.reshape(z,(-1, 1),order='F')),axis=1)

    # Build staggered grid in x direction
    x, y, z = np.meshgrid(np.linspace(0+0.5*h[0],1-0.5*h[0],gs[0]-1),np.linspace(0,1,gs[1]),np.linspace(0,1,gs[2]),indexing='ij')
    Vx = np.concatenate((np.reshape(x,(-1, 1),order='F'),np.reshape(y,(-1, 1),order='F'),np.reshape(z,(-1, 1),order='F')),axis=1)
    # Build staggered grid in y direction
    x, y, z = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0+0.5*h[1],1-0.5*h[1],gs[1]-1),np.linspace(0,1,gs[2]),indexing='ij')
    Vy = np.concatenate((np.reshape(x,(-1, 1),order='F'),np.reshape(y,(-1, 1),order='F'),np.reshape(z,(-1, 1),order='F')),axis=1)
    # Build staggered grid in z direction
    x, y, z = np.meshgrid(np.linspace(0,1,gs[0]),np.linspace(0,1,gs[1]),np.linspace(0+0.5*h[2],1-0.5*h[2],gs[2]-1),indexing='ij')
    Vz = np.concatenate((np.reshape(x,(-1, 1),order='F'),np.reshape(y,(-1, 1),order='F'),np.reshape(z,(-1, 1),order='F')),axis=1)

    # Build partial derivative matrices
    Dx = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=0)
    Dy = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=1)
    Dz = gpytoolbox.fd_partial_derivative(gs=gs,h=h,direction=2)

    # Build non-linear function
    f = np.cos(V[:,0]) + np.sin(V[:,1]) + 3*np.cos(V[:,2])
    # Derivatives on staggered grids
    fx = -np.sin(Vx[:,0])
    fy =  np.cos(Vy[:,1])
    fz =-3*np.sin(Vz[:,2])
    computed_derivative_x = Dx @ f
    computed_derivative_y = Dy @ f
    computed_derivative_z = Dz @ f
    assert(linf_norm_x>np.max(np.abs(computed_derivative_x - fx)))
    assert(linf_norm_y>np.max(np.abs(computed_derivative_y - fy)))
    assert(linf_norm_z>np.max(np.abs(computed_derivative_z - fz)))
    linf_norm_x = np.max(np.abs(computed_derivative_x - fx))
    linf_norm_y = np.max(np.abs(computed_derivative_y - fy))
    linf_norm_z = np.max(np.abs(computed_derivative_z - fz))
    # Print L infinity norm of difference
    print(np.array([np.max(np.abs(computed_derivative_x - fx)),np.max(np.abs(computed_derivative_y - fy)),np.max(np.abs(computed_derivative_z - fz))]))

print("Unit test passed, all asserts passed")


import numpy as np
from numpy.fft import fft, ifft, fftshift, ifftshift
from .FFTBase import FFTBase

class T1FFT(FFTBase):
    """
    The Fast Fourier Transform on the Circle / 1-Torus / 1-Sphere.
    """

    @staticmethod
    def analyze(f, axis=0):
        """
        Compute the Fourier Transform of the discretely sampled function f : T^1 -> C.

        Let f : T^1 -> C be a band-limited function on the circle.
        The samples f(theta_k) correspond to points on a regular grid on the circle, as returned by spaces.T1.linspace:
        theta_k = 2 pi k / N
        for k = 0, ..., N - 1

        This function computes
        \hat{f}_n = (1/N) \sum_{k=0}^{N-1} f(theta_k) e^{-i n theta_k}
        which, if f has band-limit less than N, is equal to:
        \hat{f}_n = \int_0^{2pi} f(theta) e^{-i n theta} dtheta / 2pi,
                  = <f(theta), e^{i n theta}>
        where dtheta / 2pi is the normalized Haar measure on T^1, and < , > denotes the inner product on Hilbert space,
        with respect to which this transform is unitary.

        The range of frequencies n is -floor(N/2) <= n <= ceil(N/2) - 1

        :param f:
        :param axis:
        :return:
        """
        # The numpy FFT returns coefficients in a different order than we want them,
        # and using a different normalization.
        fhat = fft(f, axis=axis)
        fhat = fftshift(fhat, axes=axis)
        return fhat / f.shape[axis]

    @staticmethod
    def synthesize(f_hat, axis=0):
        """
        Compute the inverse / synthesis Fourier transform of the function f_hat : Z -> C.
        The function f_hat(n) is sampled at points in a limited range -floor(N/2) <= n <= ceil(N/2) - 1

        This function returns
        f[k] = f(theta_k) = sum_{n=-floor(N/2)}^{ceil(N/2)-1} f_hat(n) exp(i n theta_k)
        where theta_k = 2 pi k / N
        for k = 0, ..., N - 1

        :param f_hat:
        :param axis:
        :return:
        """

        f_hat = ifftshift(f_hat * f_hat.shape[axis], axes=axis)
        f = ifft(f_hat, axis=axis)
        return f

    @staticmethod
    def analyze_naive(f):
        f_hat = np.zeros_like(f)
        for n in range(f.size):
            for k in range(f.size):
                theta_k = k * 2 * np.pi / f.size
                f_hat[n] += f[k] * np.exp(-1j * n * theta_k)
        return fftshift(f_hat / f.size, axes=0)


#Antonio Diaz UIN 327003625 TAMU 2022
#Numerical Simulations 430
#Hmwk 1 graphing and plotting exat solutions

# -*- coding: utf-8 -*
import sys
import numpy as np
import matplotlib.pyplot as plt

#attempt to create more general method for smaller delta x
def case_1_produce_FDE_matrix(n):
    # creating 2**n - 1 rows / columns
    zero_array = np.zeros((2**n-1, 2**n-1))
    FDE_matrix = zero_array
    #print(zero_array)
    # alpha_sum const open to change
    alpha = 4
    delta_x = 1.0/(2**n)
    # second order k
    # k = 2+alpha**2*delta_x**2
    # fourth order k
    # k = 2+alpha**2*delta_x**2*(1 + delta_x**2/12*alpha**2)
    # sixth order k
    # k = 2+alpha**2*delta_x**2*(1 + delta_x**2/12*alpha**2+delta_x**4/360*alpha**4)
    # eighth order k
    # k = 2+alpha**2*delta_x**2*(1 + delta_x**2/12*alpha**2+delta_x**4/360*alpha**4+delta_x**6/20160*alpha**6)
    # tenth order k
    k = 2+alpha**2*delta_x**2*(1 + delta_x**2/12*alpha**2+delta_x**4/360*alpha**4+delta_x**6/20160*alpha**6+delta_x**8/1814400*alpha**8)
    # print("K value used in matrix")
    # print(k)
    case_1_row_triple = [1, -k, 1]

    FDE_matrix[0][0:2] = case_1_row_triple[1:3]

    for row_num in range(1, 2**n-2):
        FDE_matrix[row_num][row_num-1:row_num+2] = case_1_row_triple

    FDE_matrix[2**n-2][2**n-3:2**n-1] = case_1_row_triple[0:2]

    #print(FDE_matrix)
    return(FDE_matrix)

def analytical_sol_case1(n):
    # Bar Parameters ----------------------------------------------------------------------------------------------------------------------------------
    k = .5              # thermal conductivity of material
    R = .1              # cross section radius
    Ac = np.pi * R**2   # cross sectional area
    L = 1               # length

    # Case Parameters -----------------------------------------------------------------------------------------------------------------------------
    Tb = 0              # T(0), base temperature
    T_l = 100            # T(L), tip temperature
    Ta = 0              # ambient temperature

    # Processing & Output ----------------------------------------------------------------------------------------------------------------------------
    x = np.linspace(0, 1, 2**n+1)
    a=4
    h = a**2 * k * R / 2
    C = (T_l - Ta - (Tb-Ta)*np.cosh(a*L))/np.sinh(a*L)
    D = 0
    T = C*np.sinh(a*x) + D*np.cosh(a*x) + Ta
    analytical_sol_list = []
    for i in np.nditer(T):
        analytical_sol_list.append(i.tolist())
    return analytical_sol_list

#creating resultant matrix

log_error_list = []
log_deltax_list = []

for n in range(2, 14):
    print('results for n = {:.2f}'.format(n))
    print('\u0394X = 1/2^({:.2f})'.format(n))
    resultant_matrix = np.zeros((2**n-1,1))
    resultant_matrix[2**n-2,0] = -100
    # print("FDE matrix:")
    # print(case_1_produce_FDE_matrix(n))

    # print("Resultant matrix")
    # print(resultant_matrix)

    a_matrix_inverse = np.linalg.inv(case_1_produce_FDE_matrix(n))
    solution_matrix = np.dot(a_matrix_inverse, resultant_matrix)
    pos_along_rod_list_cm = np.linspace(0, 1, num=2**n+1)
    temp_solutions_list = [0]

    for i in np.nditer(solution_matrix):
        temp_solutions_list.append(i.tolist())

    temp_solutions_list.append(100)

    # print("Temp FDM outputs:")
    # print(temp_solutions_list)

    halfway = int(len(temp_solutions_list)/2)
    print("FDM output at halfway point:")
    print('{:.10f} \xb0C'.format(temp_solutions_list[halfway]))
    print("True analytical solution list at halfway point: ")
    print('{:.10f} \xb0C'.format(analytical_sol_case1(n)[halfway]))
    # print(u'{:.12f} cm : T_FDM = {:.12f} \xb0C,  T_analytical = {:.12f} \xb0C, error = {:.12f}'.format(pos_along_rod_list_cm[halfway], temp_solutions_list[halfway], analytical_sol_case1(n)[halfway],  (temp_solutions_list[halfway]-analytical_sol_case1(n)[halfway])/analytical_sol_case1(n)[halfway]*100))

    log_deltax_list.append(np.log(1.0/2**n))
    # print(log_deltax_list)
    log_error_list.append(np.log(np.abs((-temp_solutions_list[halfway]+analytical_sol_case1(n)[halfway])/analytical_sol_case1(n)[halfway]*100)))
    # print(log_error_list)

    if (n > 3):
        print("Beta Value:")
        last_index = len(log_error_list)-1
        print((log_error_list[last_index]-log_error_list[last_index-1])/(log_deltax_list[last_index]-log_deltax_list[last_index-1]))

    print(" ")
# print("log(DeltaX):")
# print(log_deltax_list)
# print("log(Error):")
# print(log_error_list)
# print("Beta value:")
#last_index = len(log_error_list)-1
#print((log_error_list[last_index]-log_error_list[last_index-1])/(log_deltax_list[last_index]-log_deltax_list[last_index-1]))
plt.title("10th order FDM Convergence of Temperature at .5 cm")
plt.plot(log_deltax_list,log_error_list)
plt.xlabel('log DeltaX')
plt.ylabel('log Error')

plt.show()

"""
print("Case 1, 10th order FDM Results, DeltaX = 1/2**4:")
for i in range(0, len(temp_solutions_list)):
    percenterror = (temp_solutions_list[i]-analytical_sol_case1(n)[i])/(analytical_sol_case1(n)[i]+.000000001)
    print(u'{:.7f} cm : T_(FDM) = {:.7f} \xb0C,  T_analytical = {:.7f} \xb0C,  Error = {:.7f}'.format(pos_along_rod_list_cm[i], temp_solutions_list[i], analytical_sol_case1(n)[i], percenterror*100))

plt.title("10th order FDM Temp Solutions vs Analytical, DeltaX = 1/2**4")
plt.plot(np.linspace(0, 1, 2**n+1), analytical_sol_case1(n), 'r', np.linspace(0, 1, 2**n+1), temp_solutions_list, 'b')
plt.xlabel('Position Along Rod (cm)')
plt.ylabel('Temperature (C)')

plt.show()
"""


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

# In[ ]:

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

def fact(n):
    if n==0:
        return 1
    else:
        return n*fact(n-1)

# Taylor series for cosine function
x = np.arange(-5*math.pi, 5*math.pi, 0.0001)

y1 = np.cos(x)

k = int(input('Enter the degree upto which taylor series will be taken = '))

y2=0
for i in range(k):
    y2 += ((-1)**i)*(x**(2*i)) / (fact(2*i))

plt.plot(x, y1)
plt.plot(x, y2)
plt.grid()
plt.ylabel('cos(x)')
plt.xlabel('x')
plt.legend(['cosine', 'taylor'])
plt.ylim(-3,3)
plt.show()

Taylor Series Visualization for Cosine Function.py


'''
objective: 2 layers NN learns XOR
also demo the implementation of back propagation in numpy
'''
print(__doc__)

import numpy as np

sigmoid = lambda x: 1 / (1 + np.exp(-x))
dsigmoid = lambda x: x * (1 - x)

X = np.array([[0, 0, 1], [0, 1, 1], [1, 0, 1], [1, 1, 1]])
y = np.array([[0, 1, 1, 0]]).T

n_sample, input_dim = X.shape
n_sample, output_dim = y.shape
n_neuron = 4  # design param

w0 = np.random.random((input_dim, n_neuron))
w2 = np.random.random((n_neuron, output_dim))

while True:
    l1 = np.dot(X, w0)  # Nxd = Nx3 * 3xd # N: number of sample, d = n_neuron, design param, 3 = input_dim
    l2 = sigmoid(l1)    # Nxd
    l3 = np.dot(l2, w2) # Nx1 = Nxd * dx1 , 1 = output_dim
    l4 = sigmoid(l3)    # Nx1

    if all((l4 > 0.5).astype(bool) == y.astype(bool)): break

    l4_error = l4 - y  # Nx1
    l3_error = l4_error * dsigmoid(l4)  # Nx1 = Nx1 .* Nx1
    l2_error = l3_error.dot(w2.T)  # Nxd = Nx1 * 1xd
    l1_error = l2_error * dsigmoid(l2)  # Nxd

    w2 -= l2.T.dot(l3_error)  # dx1 = dxN * Nx1
    w0 -= X.T.dot(l1_error)  # 3xd = 3xN * Nxd

print('last layer output, required by XOR', l4)


import numpy as np
import random as rd
import matplotlib.pyplot as plt
import math
import seaborn as sns
def test(sampleSize, s):
    #f =
    d = []
    x = s
    for i in range(sampleSize):
        y = np.random.normal(x, 1)
        #y = np.random.rand()
        xx = func(x)
        yy = func(y)
        acpt = min(1, yy/xx)
        r = rd.random()
        if r < acpt:
            x = y
        d.append(x)

    print(d)
    #return d
    sns.distplot(d)
    plt.show()

def func(x):
    return 0.3 * math.exp((-0.2) * x * x) + 0.7 * math.exp((-0.2) * pow(x-10, 2))
    #return np.sqrt(1/(2*np.pi))*np.power(np.e,(-0.5)*(x-10)**2)

if __name__ == "__main__":
    test(5000,0)


import numpy as np

def sigmoid(x):
    """
    Calculate sigmoid
    """
    return 1 / (1 + np.exp(-x))

x = np.array([0.5, 0.1, -0.2])
target = 0.6
learnrate = 0.5

weights_input_hidden = np.array([[0.5, -0.6],
                                 [0.1, -0.2],
                                 [0.1, 0.7]])

weights_hidden_output = np.array([0.1, -0.3])

# Forward pass
hidden_layer_input = np.dot(x, weights_input_hidden)
hidden_layer_output = sigmoid(hidden_layer_input)
print('HO = ', hidden_layer_output)

output_layer_in = np.dot(hidden_layer_output, weights_hidden_output)
output = sigmoid(output_layer_in)
print('OO = ', output)

# Backwards pass
# TODO: Calculate output error
error = target - output
print('Output Error = ', error)

# TODO: Calculate error term for output layer
output_error_term = error * output * (1 - output)
print('dO = ', output_error_term)

# TODO: Calculate error term for hidden layer
hidden_error_term = np.dot(weights_hidden_output, output_error_term) * \
    hidden_layer_output * (1 - hidden_layer_output)
print('dH = ', hidden_error_term)

# TODO: Calculate change in weights for hidden layer to output layer
delta_w_h_o = learnrate * output_error_term * hidden_layer_output
print('delta w H->O = ', delta_w_h_o)

# TODO: Calculate change in weights for input layer to hidden layer
delta_w_i_h = learnrate * hidden_error_term * x[:, None]
print('delta w I->H = ', delta_w_i_h)

print('Change in weights for hidden layer to output layer:')
print(delta_w_h_o)
print('Change in weights for input layer to hidden layer:')
print(delta_w_i_h)


# Illustration of gibbs sampling for 2-dim Gaussian
# Author: Gerardo Durán-Martín
# Translated from gibbsGaussDemo.m

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import multivariate_normal, norm
import pyprobml_utils as pml

μ = np.zeros(2)
Σ = np.array([[1, 0.99], [0.99, 1]])
mvn = multivariate_normal(μ, Σ)

# Plot one contour of the multivariate normal
X = np.mgrid[-3:3:0.01, -3:3:0.01]
density = np.apply_along_axis(mvn.pdf, 0, X)

# Gibbs-Sampling path
blue_seq = np.array([
    [-1/2,-1],
    [-1/2,0],
    [1,0],
    [1,1],
    [-1/2,1],
    [-1/2,1/2],
    [1.5,1/2],
    [1.5,1.5],
]) / 3

# Compute marginal parameters x0|x1
x0_range = np.arange(-2, 2, 0.01)
x0_obs = 0.7
Σ0_cond = Σ[0,0] - Σ[0, 1] * Σ[1, 0] / Σ[1, 1]
μ0_cond = μ[0] + Σ[0, 1] * (x0_obs - μ[1]) / Σ[1, 1]

plt.plot(*blue_seq.T)
plt.contour(*X, density, levels=[0.07], colors="tab:red")
plt.xlim(-3, 3)
plt.ylim(-3, 3)
plt.scatter(0, 0, marker="x", c="tab:red", s=300)
plt.text(0, 1.7, "L", size=11)
plt.text(1.2, -2, "l", size=11)
plt.annotate("", xy=(-2.5, 1.5), xytext=(2.5, 1.5), arrowprops=dict(arrowstyle='<->'))
plt.annotate("", xy=(0.5, -2), xytext=(1, -2), arrowprops=dict(arrowstyle='<->'))
# Scaled down and shifted marginal gaussian
plt.plot(x0_range, norm(μ0_cond, np.sqrt(Σ0_cond)).pdf(x0_range)* 0.3 - 3, c="tab:green")
plt.tight_layout()
pml.savefig('gmm_singularity.pdf')
plt.show()


import numpy as np

def perceptron_train(X, Y):
  w = np.zeros(len(X[0]))
  bias = 1.0
  running = True
  count = 0
  a = 0.0

  while (running):

    for i in range(len(X)):
      for j in range(len(X[0])):
        a += w[j]*X[i][j]
      a += bias
      if not (a*Y[i] > 0):
        for k in range(len(X[0])):
          w[k] += X[i][k]*Y[i]
        bias += Y[i]
      else:
        count += 1
      a = 0
    # check the update of weight in an epoch.
    if (count == len(X)):
      running = False
    count = 0

  print("The returned weights ", w, " and bias: ", bias)
  return w, bias

def perceptron_test(X_test, Y_test, w, b):

  accuracy = 0.0
  correct = 0.0
  a = 0.0

  for i in range(len(X_test)):
    for j in range(len(w)):
      a += w[j]*X_test[i][j]
    a += b
    if (a*Y_test[i] > 0):
      correct += 1.0
    a = 0.0

  accuracy = float(correct)/float(len(Y_test))

  return accuracy

if __name__ == '__main__':

  X = np.array([[-2,1], [1,1], [1.5,-0.5], [-2,-1], [-1,-1.5], [2,-2],[-2,1], [1,1], [1.5,-0.5]])
  Y = np.array([[1],[1],[1],[-1],[-1],[-1],[1],[1],[1]])

  W = perceptron_train(X,Y)
  test_acc = perceptron_test(X,Y,W[0],W[1])
  print("The accuracy: ", test_acc*100, "%")


# Smallest multiple
# Problem 5
# 2520 is the smallest number that can be divided by each of the numbers from 1 to 10 without any remainder.
#
# What is the smallest positive number that is evenly divisible by all of the numbers from 1 to 20?

import numpy as np
import math

# is_prime
def is_prime(n):
    if n % 2 == 0 and n > 2:
        return False
    return all(n % i for i in range(3, int(math.sqrt(n)) + 1, 2))

N = 20
Nl = np.arange(2, N+1, dtype=np.int64)
Nl = Nl[:, np.newaxis]
is_vprime = np.vectorize(is_prime)
pl = np.extract(is_vprime(Nl),Nl)
pl = pl[np.newaxis, :]

emax = 1
while 2**(emax+1) < N:
    emax += 1

ml = np.zeros([emax, N-1, pl.size], dtype=np.int64);
for i in range(emax):
     ml[i,:,:] = (Nl % (pl**(i+1)))  < 1

pel = np.max(np.sum(ml,axis=0),axis=0)
np.power(pl,pel)
ans = np.prod(np.power(pl,pel))
print(ans)


import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
plt.rcParams['figure.figsize']=[8,7]

k0s = np.array([0,1,2,3])
xrange = np.arange(-1,2.5,0.05)
n = 5
theta_true = 1
sigma_true = 1
theta_prior = 0
markers = ['*','o','v','^']
colors = ['black','blue','green','red']
legends = ['postMean0','postMean1','postMean2','postMean3']

for i, ki in enumerate(k0s):
    w = n/(n+ki)
    v = (w**2)*(sigma_true**2)/n
    theta_post = w*theta_true + (1-w)*theta_prior
    plt.plot(xrange, norm.pdf(xrange, theta_post, v**0.5), color=colors[i],
            marker=markers[i], markersize=5, linewidth=2,label=legends[i])
plt.legend()
plt.ylim([0,1.5])
plt.show()

n_list = np.arange(1,50,2)

for i, ki in enumerate(k0s):
    w = n_list/(n_list+ki)
    mse_map = ((1-w)*(theta_prior-theta_true))**2 + (w**2)*sigma_true/n_list
    mse_mean = sigma_true/n_list
    mse_ratio = mse_map/mse_mean
    plt.plot(n_list, mse_ratio, color=colors[i],
            marker=markers[i], markersize=5, linewidth=2,label=legends[i])
plt.legend()
plt.ylim([0.5,1.3])
plt.show()


from numba import njit
import numba
import numpy as np
import hvwfg as hv

from desdeo_tools.scalarization import SimpleASF

@njit()
def epsilon_indicator(s1: np.ndarray, s2: np.ndarray) -> float:
    """ Computes the additive epsilon-indicator between two solutions.

    Args:
        s1 (np.ndarray): Solution 1. Should be an one-dimensional array.
        s2 (np.ndarray): Solution 2. Should be an one-dimensional array.

    Returns:
        float: The maximum distance between the values in s1 and s2.
    """
    eps = 0.0
    for i in range(s1.size):
        value = s2[i] - s1[i]
        if value > eps:
            eps = value
    return eps

@njit()
def epsilon_indicator_ndims(front: np.ndarray, reference_point: np.ndarray) -> list:
    """ Computes the additive epsilon-indicator between reference point and current one-dimensional vector of front.

    Args:
        front (np.ndarray): The front that the current reference point is being compared to.
            Should be set of arrays, where the rows are the solutions and the columns are the objective dimensions.
        reference_point (np.ndarray): The reference point that is compared. Should be one-dimensional array.

    Returns:
        list: The list of indicator values.
    """
    min_eps = 0.0
    eps_list = np.zeros((front.shape[0]), dtype=numba.float64)
    for i in np.arange(front.shape[0]):
        value = np.max(reference_point - front[i])
        if value > min_eps:
            eps_list[i] = value
    return eps_list

def preference_indicator(s1: np.ndarray, s2: np.ndarray, min_asf_value: float, ref_point: np.ndarray, delta: float) -> float:
    """ Computes the preference-based quality indicator.

    Args:
        s1 (np.ndarray): Solution 1. Should be an one-dimensional array.
        s2 (np.ndarray): Solution 2. Should be an one-dimensional array.
        ref_point (np.ndarray): The reference point should be same shape as front.
        min_asf_value (float): Minimum value of achievement scalarization of the reference_front. Used in normalization.
        delta (float): The spesifity delta allows to set the amplification of the indicator to be closer or farther
            from the reference point. Smaller delta means that all solutions are in smaller range around the reference
            point.

    Returns:
        float: The maximum distance between the values in s1 and s2 taking into account
            the reference point and spesifity.
    """
    s2_asf = SimpleASF(np.ones_like(s2))
    norm = s2_asf(s2, reference_point=ref_point) + delta - min_asf_value
    return epsilon_indicator(s1, s2) / norm

def hypervolume_indicator(front: np.ndarray, reference_point: np.ndarray) -> float:
    """ Computes the hypervolume-indicator between reference front and current approximating point.

    Args:
        front (np.ndarray): The front that is compared. Should be set of arrays, where the rows are the solutions and
            the columns are the objective dimensions.
        reference_point (np.ndarray): The reference point that the current front is being compared to. Should be 1D array.

    Returns:
        float: Measures the volume of the objective space dominated by an approximation set.
    """
    ref = np.asarray(reference_point, dtype='double') # hv.wfg needs datatype to be double
    fr = np.asarray(front, dtype='double')
    return hv.wfg(fr, ref)

if __name__=="__main__":

    po_front = np.asarray([[1.0,0],[0.5,0.5], [0,1.0], [2, -1], [0,0]])
    sol1 = [4, 4] # cant be better than po front, min is zero
    sol = np.asarray(sol1)
    ref = np.asarray([0.7, 0.3])

    print("eps indi value")
    print(epsilon_indicator(po_front[0], sol))
    print(epsilon_indicator(po_front[1], sol))
    print(epsilon_indicator(po_front[2], sol))
    print(epsilon_indicator(po_front[3], sol))
    print(epsilon_indicator(po_front[4], sol))
    print("ndims")
    print(epsilon_indicator_ndims(po_front, sol))
    print(hypervolume_indicator(po_front, sol))
    print("pref")
    print(preference_indicator(po_front[1], sol, 0.1, ref, 0.1))


import numpy as np
import matplotlib.pyplot as plt
import math
from scipy.stats import triang
from scipy.stats import beta

## Integrand function
def f(x): return x**5+x**4

x=np.linspace(-1,1,endpoint=True,dtype=float)
plt.plot(x,f(x),'.')

## 1st Interval: Triangular-approximation
s=np.linspace(-1,-0.25,50)
plt.plot(s,0.04*triang.pdf(s,0.2/0.7,a,b-a), '--')
a=-1
b=-0.25
n1=50
x1=np.random.triangular(a,-0.8,b,n1)
eval_funct1=f(x1)
y1=triang.pdf(x1,0.2/0.7,a,b-a)
n1=n1*1.0

i1=(1/n1)*np.sum(eval_funct1/y1)

mu_camp1=(np.sum(eval_funct1/y1))/n1
var_camp1=1/(n1-1)*np.sum((eval_funct1/y1-mu_camp1)**2)
var1=(1/n1)*var_camp1

## 2nd Interval: Beta-approximation
k=np.linspace(-0.25,1,100)
plt.plot(k,0.4*beta.pdf(k,5,1), '-.')

n2=9950
x2=beta.rvs(5,1,size=n2)
eval_funct2=f(x2)
y2=beta.pdf(x2,5,1)

n2=n2*1.0
i2=(1/n2)*np.sum(eval_funct2/y2)

mu_camp2=(np.sum(eval_funct2/y2))/n2
var_camp2=1/(n2-1)*np.sum((eval_funct2/y2-mu_camp2)**2)
var2=(1/n2)*var_camp2

## Results
print(i1+i2,'Integral Mean with Importance Sampling')
print(var1+var2,'Total Variance with Importance Sampling')
print(sqrt(var1+var2), 'Total Standard Deviation with Importance Sampling')
plt.xlabel('x')
plt.ylabel('y')
plt.show()


"""
Discrete logarithm algorithms
"""
from sage.all import *

def discrete_log_brute(gen, alpha):
    """
    Brute force discrete logarithm computing.

    Return a discrete logarithm of alpha using gen as a generator of the finite field.

    Parameters
    ----------
    gen : Generator of the finite field.
    alpha : Element of the field.

    Returns
    -------
    A discrete logarithm of alpha.
    """

    base_field = gen.parent()

    if not gen.parent() == alpha.parent():
        raise ValueError("Both elements must belong to the same finite field")

    if not base_field.is_field() or not base_field.is_finite():
        raise ValueError("The base field must be a finite field")

    order = base_field.order()
    b = base_field.one()
    i = 0
    while b != alpha and i < order:
        b = gen * b
        i += 1

    if i == order:
        raise ValueError("No discrete log of %s found to base %s using gen %s" % (alpha, order, gen))

    return i

def discrete_log_bsgs(gen, alpha):
    """
    Baby step / giant step discrete logarithm computing.

    Return a discrete logarithm of alpha using gen as a generator of the finite field.

    Parameters
    ----------
    gen : Generator of the finite field.
    alpha : Element of the field.

    Returns
    -------
    A discrete logarithm of alpha.
    """

    base_field = gen.parent()

    if not gen.parent() == alpha.parent():
        raise ValueError("Both elements must belong to the same finite field")

    if not base_field.is_field() or not base_field.is_finite():
        raise ValueError("The base field must be a finite field")

    order = base_field.order()
    m = isqrt(order) + 1
    table = dict()

    # Baby steps, fill the table
    b = base_field.one()
    for i in xrange(m):
        table[b] = i
        b = b * gen

    # Giant steps
    genp = gen ** -m
    b = alpha
    for i in xrange(m):
        j = table.get(b)
        if j is not None:
            return i*m + j
        b = b * genp

    raise ValueError("No discrete log of %s found to base %s using gen %s" % (alpha, order, gen))

def main():
    K = GF(137)
    print discrete_log_bsgs(K(7), K(11))  # Expected 45

    K = GF(3 ** 6, 'b')
    b = K.gen()
    a = b ** 210
    print discrete_log_bsgs(b, a)  # Expected 210

    try:
        K = GF(37)
        print discrete_log_bsgs(K(1), K(2))  # No discrete log of 2 found to base 37 using gen 1
    except ValueError as err:
        print err

if __name__ == '__main__':
    main()


"""Is the matrix idempotent."""
import numpy as np
from toqito.matrix_props import is_square

def is_idempotent(mat: np.ndarray, rtol: float = 1e-05, atol: float = 1e-8) -> bool:
    r"""
    Check if matrix is the idempotent matrix [WikIdempotent]_.

    An *idempotent matrix* is a square matrix, which, when multiplied by itself, yields itself.
    That is, the matrix :math:`A` is idempotent if and only if :math:`A^2 = A`.

    Examples
    ==========

    The following is an example of a :math:`2 x 2` idempotent matrix:

    .. math::
        A = \begin{pmatrix}
            3 & -6 \\
            1 & -2
        \end{pmatrix}

    >>> from toqito.matrix_props import is_idempotent
    >>> import numpy as np
    >>> mat = np.array([[3, -6], [1, -2]])
    >>> is_idempotent(mat)

    Alternatively, the following matrix

    .. math::
        B = \begin{pmatrix}
                1 & 2 & 3 \\
                4 & 5 & 6 \\
                7 & 8 & 9
            \end{pmatrix}

    is not idempotent.

    >>> from toqito.matrix_props import is_idempotent
    >>> import numpy as np
    >>> mat = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    >>> is_idempotent(mat)
    False

    References
    ==========
    .. [WikIdempotent] Wikipedia: Idempotent matrix
        https://en.wikipedia.org/wiki/Idempotent_matrix

    :param mat: Matrix to check.
    :param rtol: The relative tolerance parameter (default 1e-05).
    :param atol: The absolute tolerance parameter (default 1e-08).
    :return: Return :code:`True` if matrix is the idempotent matrix, and
            :code:`False` otherwise.
    """
    if not is_square(mat):
        return False
    return np.allclose(mat, mat @ mat, rtol=rtol, atol=atol)


#!/usr/bin/python
"""
This is a mixture of two gaussians example

In part the model was derived from:
http://stats.stackexchange.com/questions/46626/fitting-model-for-two-normal-distributions-in-pymc

The data for this example are the same as the EM example in the book:
"The Elements of Statistical Learning" (pp 272)

http://www-stat.stanford.edu/~tibs/ElemStatLearn/
"""

import shutil,os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
import pymc as mc

__author__ = 'ajrichards'

## variables
book_mu1 = 4.62
book_mu2 = 1.06
book_var1 = 0.87
book_var2 = 0.77
book_pi = 0.546

x1 = np.array([-0.39,0.12,0.94,1.67,1.76,2.44,3.72,4.28,4.92,5.53])
x2 = np.array([ 0.06,0.48,1.01,1.68,1.80,3.25,4.12,4.60,5.28,6.22])
x  = np.hstack([x1,x2])

## the mixing parameter
pi = mc.Uniform('pi',lower=0,upper=1)
delta = mc.Bernoulli( "delta",p=pi,size=x.size) # produces 1 with proportion p.

## other model parameters
sigma = mc.Uniform("sigma", 0.0,0.5)
var1 = mc.Uniform('var1',0,1)
var2 = mc.Uniform('var2',0,1)
mode1 = mc.Normal("mode1",mu=0,tau=var1**-2)
mode2 = mc.Normal("mode2",mu=0,tau=var2**-2)

@mc.deterministic(plot=False)
def mix_model(delta=delta,mode1=mode1,mode2=mode2):
    return (1.0-delta)*mode1 + delta*mode2

obs = mc.Normal("obs",mu=mix_model,tau=sigma**-2,value=x,observed=True)
model = mc.Model({"mode1":mode1,"mode2":mode2,"sigma":sigma,"pi":pi,
                  "var1":var1,"var2":var2,"obs":obs})

M = mc.MCMC(model)
M.sample(100000,50000)

## relabel the estimates if necessary
if M.trace('mode1')[:].mean() > M.trace('mode2')[:].mean():
    est_mu1  = M.trace('mode1')[:].mean()
    est_var1 = M.trace('var1')[:].mean()
    est_mu2  = M.trace('mode2')[:].mean()
    est_var2 = M.trace('var2')[:].mean()
    est_pi   = M.trace('pi')[:].mean()
else:
    est_mu1  = M.trace('mode2')[:].mean()
    est_var1 = M.trace('var2')[:].mean()
    est_mu2  = M.trace('mode1')[:].mean()
    est_var2 = M.trace('var1')[:].mean()
    est_pi   = 1.0 - M.trace('pi')[:].mean()

print 'book,model'
print 'mu1', book_mu1,est_mu1
print 'mu2', book_mu2,est_mu2
print 'var1', book_var1,est_var1
print 'var2', book_var2,est_var2
print 'pi', book_pi, est_pi
print 'DIC',M.dic

## plot the distribution and the MCMC fit
fig = plt.figure(1)
ax = fig.add_subplot(111)
n,bins,patches = ax.hist(x,15,normed=1,facecolor='gray',alpha=0.75)

## add the book and best fit lines
p1 = mlab.normpdf(bins, book_mu1, np.sqrt(book_var1))
p2 = mlab.normpdf(bins, book_mu2, np.sqrt(book_var2))
l1 = plt.plot(bins, p1, 'r--', linewidth=1)
l2 = plt.plot(bins, p2, 'r--', linewidth=1)

p3 = mlab.normpdf(bins, est_mu1, np.sqrt(est_var1))
p4 = mlab.normpdf(bins, est_mu2, np.sqrt(est_var2))
l3 = plt.plot(bins, p3, 'k-', linewidth=1)
l4 = plt.plot(bins, p4, 'k-', linewidth=1)

plt.xlabel('x')
plt.ylabel('freq')
plt.ylim([0,0.8])

ax.legend((l1[0], l3[0]), ('Book Estimate', 'PyMC Estimate') )

## create pymc plots and save all
baseDir =  os.path.join("..",os.path.realpath(os.path.dirname(__file__)))
dirPath = os.path.join(baseDir,'plots')
if os.path.isdir(dirPath):
    shutil.rmtree(dirPath)
os.mkdir(dirPath)

plt.savefig(os.path.join(dirPath,'TwoComponentGauss.png'))
mc.Matplot.plot(M,path=dirPath)
print 'done.'


import numpy as np
import numpy.linalg as lin

MAX_ITERS = 20
EPSILON   = 1.0e-7

def h(theta, X, y):
    margins = y * X.dot(theta)

    return 1 / (1 + np.exp(-margins))

def J(theta, X, y):
    probs = h(theta, X, y)
    mm = probs.size

    return (-1 / mm) * np.sum(np.log(probs))

def gradJ(theta, X, y):
    probs = h(theta, X, -y) * y
    mm = probs.size

    return (-1 / mm) * X.T.dot(probs)

def hessJ(theta, X, y):
    d     = h(theta, X, y)
    probs = d * (1 - d)
    mm    = probs.size
    prob_vec = np.diag(probs)
    hessJ = (1 / mm) * (X.T.dot(prob_vec).dot(X))

    return hessJ

def logistic_regression(X, y, epsilon, max_iters):
    mm = X.shape[0]
    nn = X.shape[1]

    # The cost of the ith iteration of Newton-Raphson.
    cost = np.zeros(max_iters)
    theta = np.zeros(nn)

    for i in range(0, max_iters):
        cost[i] = J(theta, X, y)
        grad    = gradJ(theta, X, y)
        H       = hessJ(theta, X, y)
        Hinv    = lin.inv(H)
        theta   = theta - Hinv.dot(grad)

    return (theta, cost)


import numpy

def mandelbrot(width, height, x_range, y_range, n_iterations, c=None):
    """Computes heightmap for a mandelbrot or julia set fractal

    Each iteration, the new value of a pixel is calculated as z = z^2 + c, where for a mandelbrot set (default)
    c is a function of the position in the complex plane (c = x + iy) or a optionally for a julia set, c is a constant

    :parameter width: Integer width of the final fractal image in pixels
    :type width: int

    :parameter height: Integer height of the final fractal image in pixels
    :type height: int

    :parameter x_range: The range of x [min, max] for which the fractal will be drawn
    :type x_range: Union[list of float, numpy.ndarray]

    :parameter y_range: The range of y [min, max] for which the fractal will be drawn
    :type y_range: Union[list of float, numpy.ndarray]

    :parameter n_iterations: the number of iterations limit used to compute the fractal
    :type n_iterations: int

    :parameter c: Optional 2-tuple (or other iterable) containing real and complex parts of constant coefficient c.
    Giving this argument will result in creation of a julia set, not the default mandelbrot set
    :type c: Union[None, (float, float)]

    :return: x, y, z values of pixel locations in the x, y complex plane and a corresponding heightmap z, with which
    you can lot a fancy looking 3d fractal
    :rtype: (numpy.ndarray, numpy.ndarray, numpy.ndarray)
    """

    # Create a linearly spaced 2d grid
    [x, y] = numpy.meshgrid(
        numpy.linspace(x_range[0], x_range[1], width), numpy.linspace(y_range[0], y_range[1], height)
    )

    # Preallocate output array
    z = numpy.zeros((height, width))

    # Simple loop to render the fractal set. This is not efficient python and would be vectorised in production, but the
    # purpose here is just to provide a simple demo.
    for idx, a in numpy.ndenumerate(x):

        # Get default constant c (Mandelbrot sets use spatial coordinates as constants)
        if c is None:
            b = y[idx]
            c = (a, b)

        x_old = 0.0
        y_old = 0.0
        k = 1
        while (k <= n_iterations) and ((x_old ** 2.0 + y_old ** 2.0) < 4.0):
            x_new = x_old ** 2.0 - y_old ** 2.0 + c[0]
            y_new = 2.0 * x_old * y_old + c[1]
            x_old = x_new
            y_old = y_new
            k += 1

        z[idx] = k

    return x, y, z