#%%
import numpy as np
from kdg.utils import generate_gaussian_parity, generate_ellipse, generate_spirals, generate_sinewave, generate_polynomial
from kdg.utils import plot_2dsim
from kdg import kdf
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from scipy.io import savemat, loadmat
# %%
n_samples = 1e4
X, y = {}, {}
#%%
X['gxor'], y['gxor'] = generate_gaussian_parity(n_samples)
X['spiral'], y['spiral'] = generate_spirals(n_samples)
X['circle'], y['circle'] = generate_ellipse(n_samples)
X['sine'], y['sine'] = generate_sinewave(n_samples)
X['poly'], y['poly'] = generate_polynomial(n_samples, a=[1,3])
#%%
sns.set_context('talk')
fig, ax = plt.subplots(6,5, figsize=(40,48), sharex=True)
title_size = 45
ticksize = 30

plot_2dsim(X['gxor'], y['gxor'], ax=ax[0][0])
ax[0][0].set_ylabel('Simulation Data', fontsize=title_size-5)
ax[0][0].set_xlim([-2,2])
ax[0][0].set_ylim([-2,2])
ax[0][0].set_xticks([])
ax[0][0].set_yticks([-2,-1,0,1,2])
ax[0][0].tick_params(labelsize=ticksize)
ax[0][0].set_title('Gaussian XOR', fontsize=title_size)

plot_2dsim(X['spiral'], y['spiral'], ax=ax[0][1])
ax[0][1].set_xlim([-2,2])
ax[0][1].set_ylim([-2,2])
ax[0][1].set_xticks([])
ax[0][1].set_yticks([])
ax[0][1].tick_params(labelsize=ticksize)
ax[0][1].set_title('Spiral', fontsize=title_size)

plot_2dsim(X['circle'], y['circle'], ax=ax[0][2])
ax[0][2].set_xlim([-2,2])
ax[0][2].set_ylim([-2,2])
ax[0][2].set_xticks([])
ax[0][2].set_yticks([])
ax[0][2].tick_params(labelsize=ticksize)
ax[0][2].set_title('Circle', fontsize=title_size)

plot_2dsim(X['sine'], y['sine'], ax=ax[0][3])
ax[0][3].set_xlim([-2,2])
ax[0][3].set_ylim([-2,2])
ax[0][3].set_xticks([])
ax[0][3].set_yticks([])
ax[0][3].tick_params(labelsize=ticksize)
ax[0][3].set_title('Sinewave', fontsize=title_size)

plot_2dsim(X['poly'], y['poly'], ax=ax[0][4])
ax[0][4].set_xlim([-2,2])
ax[0][4].set_ylim([-2,2])
ax[0][4].set_xticks([])
ax[0][4].set_yticks([])
ax[0][4].tick_params(labelsize=ticksize)
ax[0][4].set_title('Polynomial', fontsize=title_size)

################################################
#define grids
p = np.arange(-2, 2, step=0.01)
q = np.arange(-2, 2, step=0.01)
xx, yy = np.meshgrid(p, q)

# get true posterior
tp_df = pd.read_csv("true_posterior/Gaussian_xor_pdf.csv")
proba_true = 0.5*np.ones((400, 400))
tmp = np.array([tp_df["posterior"][x] for x in range(40000)])
tmp = tmp.reshape(200, 200)
proba_true[100:300, 100:300] = tmp

ax0 = ax[1][0].imshow(
    proba_true,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
#ax[1][0].set_title("True Class Posteriors", fontsize=24)
ax[1][0].set_aspect("equal")
ax[1][0].tick_params(labelsize=ticksize)
ax[1][0].set_yticks([-2,-1,0,1,2])
ax[1][0].set_xticks([])
ax[1][0].set_ylabel('True Posteriors',fontsize=title_size-5)

tp_df = pd.read_csv("true_posterior/spiral_pdf.csv")
proba_true = 0.5*np.ones((400, 400))
tmp = np.array([tp_df["posterior"][x] for x in range(40000)])
tmp = tmp.reshape(200, 200)
proba_true[100:300, 100:300] = 1 - tmp

ax0 = ax[1][1].imshow(
    np.flip(proba_true, axis=0),
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
#ax[1][1].set_title("True Class Posteriors", fontsize=24)
ax[1][1].set_aspect("equal")
ax[1][1].tick_params(labelsize=ticksize)
ax[1][1].set_yticks([])
ax[1][1].set_xticks([])

tp_df = pd.read_csv("true_posterior/ellipse_pdf.csv")
proba_true = 0.5*np.ones((400, 400))
tmp = np.array([tp_df["posterior"][x] for x in range(40000)])
tmp = tmp.reshape(200, 200)
proba_true[100:300, 100:300] = tmp

ax0 = ax[1][2].imshow(
    proba_true,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
#ax[1][2].set_title("True Class Posteriors", fontsize=24)
ax[1][2].set_aspect("equal")
ax[1][2].tick_params(labelsize=ticksize)
ax[1][2].set_yticks([])
ax[1][2].set_xticks([])

tp_df = pd.read_csv("true_posterior/sinewave_pdf.csv")
proba_true = 0.5*np.ones((400, 400))
tmp = np.array([tp_df["posterior"][x] for x in range(40000)])
tmp = np.flip(tmp.reshape(200, 200),axis=0)
proba_true[100:300, 100:300] = tmp

ax0 = ax[1][3].imshow(
    proba_true,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
#ax[1][3].set_title("True Class Posteriors", fontsize=24)
ax[1][3].set_aspect("equal")
ax[1][3].tick_params(labelsize=ticksize)
ax[1][3].set_yticks([])
ax[1][3].set_xticks([])

tp_df = pd.read_csv("true_posterior/polynomial_pdf.csv")
proba_true = 0.5*np.ones((400, 400))
tmp = np.array([tp_df["posterior"][x] for x in range(40000)])
tmp = np.flip(tmp.reshape(200, 200),axis=0)
proba_true[100:300, 100:300] = tmp

ax0 = ax[1][4].imshow(
    proba_true,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
#ax[1][4].set_title("True Class Posteriors", fontsize=24)
ax[1][4].set_aspect("equal")
ax[1][4].tick_params(labelsize=ticksize)
ax[1][4].set_yticks([])
ax[1][4].set_xticks([])

#########################################################
df = loadmat('kdf_experiments/results/gxor_plot_data.mat')
ax1 = ax[2][0].imshow(
    df['posterior_rf'],
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[2][0].set_ylabel("RF Posteriors", fontsize=title_size-5)
ax[2][0].set_aspect("equal")
ax[2][0].tick_params(labelsize=ticksize)
ax[2][0].set_yticks([-2,-1,0,1,2])
ax[2][0].set_xticks([])

ax1 = ax[3][0].imshow(
    df['posterior_kdf'],
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[3][0].set_ylabel('KDF Posteriors', fontsize=title_size-5)
ax[3][0].set_aspect("equal")
ax[3][0].tick_params(labelsize=ticksize)
ax[3][0].set_yticks([-2,-1,0,1,2])
ax[3][0].set_xticks([])

############################################
df = loadmat('kdf_experiments/results/spiral_plot_data.mat')
ax1 = ax[2][1].imshow(
    1-np.flip(df['posterior_rf'],axis=0),
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[2][1].set_aspect("equal")
ax[2][1].tick_params(labelsize=ticksize)
ax[2][1].set_yticks([])
ax[2][1].set_xticks([])

ax1 = ax[3][1].imshow(
    1-np.flip(df['posterior_kdf'],axis=0),
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[3][1].set_aspect("equal")
ax[3][1].tick_params(labelsize=ticksize)
ax[3][1].set_yticks([])
ax[3][1].set_xticks([])

#############################################
df = loadmat('kdf_experiments/results/circle_plot_data.mat')
ax1 = ax[2][2].imshow(
    df['posterior_rf'],
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[2][2].set_aspect("equal")
ax[2][2].tick_params(labelsize=ticksize)
ax[2][2].set_yticks([])
ax[2][2].set_xticks([])

ax1 = ax[3][2].imshow(
    df['posterior_kdf'],
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[3][2].set_aspect("equal")
ax[3][2].tick_params(labelsize=ticksize)
ax[3][2].set_yticks([])
ax[3][2].set_xticks([])

##################################################
df = loadmat('kdf_experiments/results/sinewave_plot_data.mat')
ax1 = ax[2][3].imshow(
    np.flip(df['posterior_rf'],axis=0),
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[2][3].set_aspect("equal")
ax[2][3].tick_params(labelsize=ticksize)
ax[2][3].set_yticks([])
ax[2][3].set_xticks([])

ax1 = ax[3][3].imshow(
    np.flip(df['posterior_kdf'], axis=0),
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[3][3].set_aspect("equal")
ax[3][3].tick_params(labelsize=ticksize)
ax[3][3].set_yticks([])
ax[3][3].set_xticks([])

###################################################
df = loadmat('kdf_experiments/results/polynomial_plot_data.mat')
ax1 = ax[2][4].imshow(
    np.flip(df['posterior_rf'],axis=0),
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[2][4].set_aspect("equal")
ax[2][4].tick_params(labelsize=ticksize)
ax[2][4].set_yticks([])
ax[2][4].set_xticks([])

ax1 = ax[3][4].imshow(
    np.flip(df['posterior_kdf'],axis=0),
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[3][4].set_aspect("equal")
ax[3][4].tick_params(labelsize=ticksize)
ax[3][4].set_yticks([])
ax[3][4].set_xticks([])

##############################################
##############################################
df = loadmat('kdn_experiments/results/gxor_plot_data.mat')
proba_nn = 1-np.flip(df["nn_proba"][:, 0].reshape(400, 400), axis=1)
proba_kdn = 1-np.flip(df["kdn_proba"][:, 0].reshape(400, 400), axis=1)

ax1 = ax[4][0].imshow(
    proba_nn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[4][0].set_aspect("equal")
ax[4][0].tick_params(labelsize=ticksize)
ax[4][0].set_ylabel('NN Posteriors',fontsize=title_size-5)
ax[4][0].set_yticks([-2,-1,0,1,2])
ax[4][0].set_xticks([])

ax1 = ax[5][0].imshow(
    proba_kdn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[5][0].set_aspect("equal")
ax[5][0].set_ylabel('KDN Posteriors',fontsize=title_size-5)
ax[5][0].tick_params(labelsize=ticksize)
ax[5][0].set_yticks([-2,-1,0,1,2])
ax[5][0].set_xticks([-2,-1,0,1,2])

########################################
df = loadmat('kdn_experiments/results/spiral_plot_data.mat')
proba_nn = np.flip(df["nn_proba"][:, 0].reshape(400, 400), axis=1)
proba_kdn = np.flip(df["kdn_proba"][:, 0].reshape(400, 400), axis=1)

ax1 = ax[4][1].imshow(
    proba_nn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[4][1].set_aspect("equal")
ax[4][1].tick_params(labelsize=ticksize)
ax[4][1].set_yticks([])
ax[4][1].set_xticks([])

ax1 = ax[5][1].imshow(
    proba_kdn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[5][1].set_aspect("equal")
ax[5][1].tick_params(labelsize=ticksize)
ax[5][1].set_yticks([])
ax[5][1].set_xticks([-2,-1,0,1,2])

########################################################
df = loadmat('kdn_experiments/results/circle_plot_data.mat')
proba_nn = np.flip(df["nn_proba"][:, 0].reshape(400, 400), axis=1)
proba_kdn = np.flip(df["kdn_proba"][:, 0].reshape(400, 400), axis=1)

ax1 = ax[4][2].imshow(
    proba_nn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[4][2].set_aspect("equal")
ax[4][2].tick_params(labelsize=ticksize)
ax[4][2].set_yticks([])
ax[4][2].set_xticks([])

ax1 = ax[5][2].imshow(
    proba_kdn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[5][2].set_aspect("equal")
ax[5][2].tick_params(labelsize=ticksize)
ax[5][2].set_yticks([])
ax[5][2].set_xticks([-2,-1,0,1,2])

####################################################
df = loadmat('kdn_experiments/results/sinewave_plot_data.mat')
proba_nn = np.flip(df["nn_proba"][:, 0].reshape(400, 400), axis=0)
proba_kdn = np.flip(df["kdn_proba"][:, 0].reshape(400, 400), axis=0)

ax1 = ax[4][3].imshow(
    proba_nn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[4][3].set_aspect("equal")
ax[4][3].tick_params(labelsize=ticksize)
ax[4][3].set_yticks([])
ax[4][3].set_xticks([])

ax1 = ax[5][3].imshow(
    proba_kdn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
ax[5][3].set_aspect("equal")
ax[5][3].tick_params(labelsize=ticksize)
ax[5][3].set_yticks([])
ax[5][3].set_xticks([-2,-1,0,1,2])

#######################################################
df = loadmat('kdn_experiments/results/polynomial_plot_data.mat')
proba_nn = 1-np.flip(df["nn_proba"][:, 0].reshape(400, 400), axis=1)
proba_kdn = np.flip(df["kdn_proba"][:, 0].reshape(400, 400), axis=1)

ax1 = ax[4][4].imshow(
    proba_nn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
#fig.colorbar(ax1, ax=ax[4][4], anchor=(0, 0.3), shrink=0.85)
ax[4][4].set_aspect("equal")
ax[4][4].tick_params(labelsize=ticksize)
ax[4][4].set_yticks([])
ax[4][4].set_xticks([])

ax1 = ax[5][4].imshow(
    proba_kdn,
    extent=[xx.min(), xx.max(), yy.min(), yy.max()],
    cmap="bwr",
    vmin=0,
    vmax=1,
    interpolation="nearest",
    aspect="auto",
)
#fig.colorbar(ax1, anchor=(0, 0.3), shrink=0.85)

ax[5][4].set_aspect("equal")
ax[5][4].tick_params(labelsize=ticksize)
ax[5][4].set_yticks([])
ax[5][4].set_xticks([-2,-1,0,1,2])

#plt.savefig('plots/simulations.pdf')
# %%
def calc_stat(a, reps=45):
    a_med = []
    a_25 = []
    a_75 = []
    a = a.reshape(-1,reps)
    return np.median(a,axis=1), np.quantile(a,[.25], axis=1)[0], np.quantile(a,[.75], axis=1)[0]
# %%
sns.set_context('talk')
sample_size = [50, 100, 500, 1000, 5000, 10000]

fig, ax = plt.subplots(5,4, figsize=(45,40))
title_size = 45
ticksize = 30

for ax_ in ax:
    for ax__ in ax_:
        ax__.tick_params(labelsize=ticksize)

df = loadmat('kdn_experiments/results/graphs/gxor.mat')

med, a_25, a_75 = calc_stat(1-df['kdn_acc'])
med_nn, nn_25, nn_75 = calc_stat(1-df['nn_acc'])

ax[0][0].plot(sample_size, med[1:], c="b", label='KDN')
ax[0][0].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[0][0].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[0][0].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[0][0].set_xscale('log')
#ax[0][0].set_xlabel('Sample size')
ax[0][0].set_xticks([])
ax[0][0].set_ylabel('Generalization Error', fontsize=ticksize)

right_side = ax[0][0].spines["right"]
right_side.set_visible(False)
top_side = ax[0][0].spines["top"]
top_side.set_visible(False)

df_ = loadmat('kdf_experiments/results/gxor_plot_data.mat')
ax[0][0].plot(sample_size, df_['error_kdf_med'].ravel(), c="r", label='KDF')
ax[0][0].plot(sample_size, df_['error_rf_med'].ravel(), c="k", label='RF')

ax[0][0].fill_between(sample_size, df_["error_kdf_25"].ravel(), df_["error_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[0][0].fill_between(sample_size, df_["error_rf_25"].ravel(), df_["error_rf_75"].ravel(), facecolor='k', alpha=.3)

ax[0][0].legend(fontsize=ticksize, frameon=False)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_hd'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_hd'])

ax[0][1].plot(sample_size, med[1:], c="b", label='KDN')
ax[0][1].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[0][1].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[0][1].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[0][1].set_xscale('log')
ax[0][1].set_xticks([])
ax[0][1].set_ylabel('Hellinger Distance', fontsize=ticksize)

right_side = ax[0][1].spines["right"]
right_side.set_visible(False)
top_side = ax[0][1].spines["top"]
top_side.set_visible(False)

ax[0][1].plot(sample_size, df_['hellinger_kdf_med'].ravel(), c="r", label='KDF')
ax[0][1].plot(sample_size, df_['hellinger_rf_med'].ravel(), c="k", label='RF')

ax[0][1].fill_between(sample_size, df_["hellinger_kdf_25"].ravel(), df_["hellinger_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[0][1].fill_between(sample_size, df_["hellinger_rf_25"].ravel(), df_["hellinger_rf_75"].ravel(), facecolor='k', alpha=.3)
ax[0][1].set_title('Gaussian XOR', fontsize=title_size)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcIn'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcIn'])

ax[0][2].plot(sample_size, med[1:], c="b", label='KDN')
ax[0][2].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[0][2].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[0][2].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[0][2].set_xscale('log')
ax[0][2].set_xticks([])
ax[0][2].set_ylabel('Mean Max Confidence\n (In Distribution)', fontsize=ticksize)

right_side = ax[0][2].spines["right"]
right_side.set_visible(False)
top_side = ax[0][2].spines["top"]
top_side.set_visible(False)

ax[0][2].plot(sample_size, df_['mmcIn_kdf_med'].ravel(), c="r", label='KDF')
ax[0][2].plot(sample_size, df_['mmcIn_rf_med'].ravel(), c="k", label='RF')

ax[0][2].fill_between(sample_size, df_["mmcIn_kdf_25"].ravel(), df_["mmcIn_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[0][2].fill_between(sample_size, df_["mmcIn_rf_25"].ravel(), df_["mmcIn_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcOut'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcOut'])

ax[0][3].plot(sample_size, med[1:], c="b", label='KDN')
ax[0][3].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[0][3].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[0][3].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[0][3].set_xscale('log')
ax[0][3].set_xticks([])
ax[0][3].set_ylabel('Mean Max Confidence\n (Out Distribution)', fontsize=ticksize)

right_side = ax[0][3].spines["right"]
right_side.set_visible(False)
top_side = ax[0][3].spines["top"]
top_side.set_visible(False)

ax[0][3].plot(sample_size, df_['mmcOut_kdf_med'].ravel(), c="r", label='KDF')
ax[0][3].plot(sample_size, df_['mmcOut_rf_med'].ravel(), c="k", label='RF')

ax[0][3].fill_between(sample_size, df_["mmcOut_kdf_25"].ravel(), df_["mmcOut_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[0][3].fill_between(sample_size, df_["mmcOut_rf_25"].ravel(), df_["mmcOut_rf_75"].ravel(), facecolor='k', alpha=.3)

#########################################################
#########################################################
df = loadmat('kdn_experiments/results/graphs/spiral.mat')

med, a_25, a_75 = calc_stat(1-df['kdn_acc'])
med_nn, nn_25, nn_75 = calc_stat(1-df['nn_acc'])

ax[1][0].plot(sample_size, med[1:], c="b", label='KDN')
ax[1][0].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[1][0].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[1][0].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[1][0].set_xscale('log')
ax[1][0].set_xticks([])
ax[1][0].set_ylabel('Generalization Error', fontsize=ticksize)

right_side = ax[1][0].spines["right"]
right_side.set_visible(False)
top_side = ax[1][0].spines["top"]
top_side.set_visible(False)

df_ = loadmat('kdf_experiments/results/spiral_plot_data.mat')
ax[1][0].plot(sample_size, df_['error_kdf_med'].ravel(), c="r", label='KDF')
ax[1][0].plot(sample_size, df_['error_rf_med'].ravel(), c="k", label='RF')

ax[1][0].fill_between(sample_size, df_["error_kdf_25"].ravel(), df_["error_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[1][0].fill_between(sample_size, df_["error_rf_25"].ravel(), df_["error_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_hd'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_hd'])

ax[1][1].plot(sample_size, med[1:], c="b", label='KDN')
ax[1][1].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[1][1].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[1][1].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[1][1].set_xscale('log')
ax[1][1].set_xticks([])
ax[1][1].set_ylabel('Hellinger Distance', fontsize=ticksize)

right_side = ax[1][1].spines["right"]
right_side.set_visible(False)
top_side = ax[1][1].spines["top"]
top_side.set_visible(False)

ax[1][1].plot(sample_size, df_['hellinger_kdf_med'].ravel(), c="r", label='KDF')
ax[1][1].plot(sample_size, df_['hellinger_rf_med'].ravel(), c="k", label='RF')

ax[1][1].fill_between(sample_size, df_["hellinger_kdf_25"].ravel(), df_["hellinger_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[1][1].fill_between(sample_size, df_["hellinger_rf_25"].ravel(), df_["hellinger_rf_75"].ravel(), facecolor='k', alpha=.3)
ax[1][1].set_title('Spiral', fontsize=title_size)
##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcIn'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcIn'])

ax[1][2].plot(sample_size, med[1:], c="b", label='KDN')
ax[1][2].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[1][2].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[1][2].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[1][2].set_xscale('log')
ax[1][2].set_xticks([])
ax[1][2].set_ylabel('Mean Max Confidence\n (In Distribution)', fontsize=ticksize)

right_side = ax[1][2].spines["right"]
right_side.set_visible(False)
top_side = ax[1][2].spines["top"]
top_side.set_visible(False)

ax[1][2].plot(sample_size, df_['mmcIn_kdf_med'].ravel(), c="r", label='KDF')
ax[1][2].plot(sample_size, df_['mmcIn_rf_med'].ravel(), c="k", label='RF')

ax[1][2].fill_between(sample_size, df_["mmcIn_kdf_25"].ravel(), df_["mmcIn_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[1][2].fill_between(sample_size, df_["mmcIn_rf_25"].ravel(), df_["mmcIn_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcOut'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcOut'])

ax[1][3].plot(sample_size, med[1:], c="b", label='KDN')
ax[1][3].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[1][3].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[1][3].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[1][3].set_xscale('log')
ax[1][3].set_xticks([])
ax[1][3].set_ylabel('Mean Max Confidence\n (Out Distribution)', fontsize=ticksize)

right_side = ax[1][3].spines["right"]
right_side.set_visible(False)
top_side = ax[1][3].spines["top"]
top_side.set_visible(False)

ax[1][3].plot(sample_size, df_['mmcOut_kdf_med'].ravel(), c="r", label='KDF')
ax[1][3].plot(sample_size, df_['mmcOut_rf_med'].ravel(), c="k", label='RF')

ax[1][3].fill_between(sample_size, df_["mmcOut_kdf_25"].ravel(), df_["mmcOut_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[1][3].fill_between(sample_size, df_["mmcOut_rf_25"].ravel(), df_["mmcOut_rf_75"].ravel(), facecolor='k', alpha=.3)

#########################################################
#########################################################
df = loadmat('kdn_experiments/results/graphs/circle.mat')

med, a_25, a_75 = calc_stat(1-df['kdn_acc'])
med_nn, nn_25, nn_75 = calc_stat(1-df['nn_acc'])

ax[2][0].plot(sample_size, med[1:], c="b", label='KDN')
ax[2][0].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[2][0].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[2][0].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[2][0].set_xscale('log')
ax[2][0].set_xticks([])
ax[2][0].set_ylabel('Generalization Error', fontsize=ticksize)

right_side = ax[2][0].spines["right"]
right_side.set_visible(False)
top_side = ax[2][0].spines["top"]
top_side.set_visible(False)

df_ = loadmat('kdf_experiments/results/circle_plot_data.mat')
ax[2][0].plot(sample_size, df_['error_kdf_med'].ravel(), c="r", label='KDF')
ax[2][0].plot(sample_size, df_['error_rf_med'].ravel(), c="k", label='RF')

ax[2][0].fill_between(sample_size, df_["error_kdf_25"].ravel(), df_["error_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[2][0].fill_between(sample_size, df_["error_rf_25"].ravel(), df_["error_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_hd'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_hd'])

ax[2][1].plot(sample_size, med[1:], c="b", label='KDN')
ax[2][1].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[2][1].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[2][1].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[2][1].set_xscale('log')
ax[2][1].set_xticks([])
ax[2][1].set_ylabel('Hellinger Distance', fontsize=ticksize)

right_side = ax[2][1].spines["right"]
right_side.set_visible(False)
top_side = ax[2][1].spines["top"]
top_side.set_visible(False)

ax[2][1].plot(sample_size, df_['hellinger_kdf_med'].ravel(), c="r", label='KDF')
ax[2][1].plot(sample_size, df_['hellinger_rf_med'].ravel(), c="k", label='RF')

ax[2][1].fill_between(sample_size, df_["hellinger_kdf_25"].ravel(), df_["hellinger_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[2][1].fill_between(sample_size, df_["hellinger_rf_25"].ravel(), df_["hellinger_rf_75"].ravel(), facecolor='k', alpha=.3)
ax[2][1].set_title('Circle', fontsize=title_size)
##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcIn'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcIn'])

ax[2][2].plot(sample_size, med[1:], c="b", label='KDN')
ax[2][2].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[2][2].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[2][2].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[2][2].set_xscale('log')
ax[2][2].set_xticks([])
ax[2][2].set_ylabel('Mean Max Confidence\n (In Distribution)', fontsize=ticksize)

right_side = ax[2][2].spines["right"]
right_side.set_visible(False)
top_side = ax[2][2].spines["top"]
top_side.set_visible(False)

ax[2][2].plot(sample_size, df_['mmcIn_kdf_med'].ravel(), c="r", label='KDF')
ax[2][2].plot(sample_size, df_['mmcIn_rf_med'].ravel(), c="k", label='RF')

ax[2][2].fill_between(sample_size, df_["mmcIn_kdf_25"].ravel(), df_["mmcIn_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[2][2].fill_between(sample_size, df_["mmcIn_rf_25"].ravel(), df_["mmcIn_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcOut'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcOut'])

ax[2][3].plot(sample_size, med[1:], c="b", label='KDN')
ax[2][3].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[2][3].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[2][3].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[2][3].set_xscale('log')
ax[2][3].set_xticks([])
ax[2][3].set_ylabel('Mean Max Confidence\n (Out Distribution)', fontsize=ticksize)

right_side = ax[2][3].spines["right"]
right_side.set_visible(False)
top_side = ax[2][3].spines["top"]
top_side.set_visible(False)

ax[2][3].plot(sample_size, df_['mmcOut_kdf_med'].ravel(), c="r", label='KDF')
ax[2][3].plot(sample_size, df_['mmcOut_rf_med'].ravel(), c="k", label='RF')

ax[2][3].fill_between(sample_size, df_["mmcOut_kdf_25"].ravel(), df_["mmcOut_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[2][3].fill_between(sample_size, df_["mmcOut_rf_25"].ravel(), df_["mmcOut_rf_75"].ravel(), facecolor='k', alpha=.3)

#########################################################
#########################################################
df = loadmat('kdn_experiments/results/graphs/sinewave.mat')

med, a_25, a_75 = calc_stat(1-df['kdn_acc'])
med_nn, nn_25, nn_75 = calc_stat(1-df['nn_acc'])

ax[3][0].plot(sample_size, med[1:], c="b", label='KDN')
ax[3][0].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[3][0].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[3][0].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[3][0].set_xscale('log')
ax[3][0].set_xticks([])
ax[3][0].set_ylabel('Generalization Error', fontsize=ticksize)

right_side = ax[3][0].spines["right"]
right_side.set_visible(False)
top_side = ax[3][0].spines["top"]
top_side.set_visible(False)

df_ = loadmat('kdf_experiments/results/sinewave_plot_data.mat')
ax[3][0].plot(sample_size, df_['error_kdf_med'].ravel(), c="r", label='KDF')
ax[3][0].plot(sample_size, df_['error_rf_med'].ravel(), c="k", label='RF')

ax[3][0].fill_between(sample_size, df_["error_kdf_25"].ravel(), df_["error_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[3][0].fill_between(sample_size, df_["error_rf_25"].ravel(), df_["error_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_hd'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_hd'])

ax[3][1].plot(sample_size, med[1:], c="b", label='KDN')
ax[3][1].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[3][1].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[3][1].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[3][1].set_xscale('log')
ax[3][1].set_xticks([])
ax[3][1].set_ylabel('Hellinger Distance', fontsize=ticksize)

right_side = ax[3][1].spines["right"]
right_side.set_visible(False)
top_side = ax[3][1].spines["top"]
top_side.set_visible(False)

ax[3][1].plot(sample_size, df_['hellinger_kdf_med'].ravel(), c="r", label='KDF')
ax[3][1].plot(sample_size, df_['hellinger_rf_med'].ravel(), c="k", label='RF')

ax[3][1].fill_between(sample_size, df_["hellinger_kdf_25"].ravel(), df_["hellinger_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[3][1].fill_between(sample_size, df_["hellinger_rf_25"].ravel(), df_["hellinger_rf_75"].ravel(), facecolor='k', alpha=.3)
ax[3][1].set_title('Sinewave', fontsize=title_size)
##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcIn'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcIn'])

ax[3][2].plot(sample_size, med[1:], c="b", label='KDN')
ax[3][2].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[3][2].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[3][2].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[3][2].set_xscale('log')
ax[3][2].set_xticks([])
ax[3][2].set_ylabel('Mean Max Confidence\n (In Distribution)', fontsize=ticksize)

right_side = ax[3][2].spines["right"]
right_side.set_visible(False)
top_side = ax[3][2].spines["top"]
top_side.set_visible(False)

ax[3][2].plot(sample_size, df_['mmcIn_kdf_med'].ravel(), c="r", label='KDF')
ax[3][2].plot(sample_size, df_['mmcIn_rf_med'].ravel(), c="k", label='RF')

ax[3][2].fill_between(sample_size, df_["mmcIn_kdf_25"].ravel(), df_["mmcIn_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[3][2].fill_between(sample_size, df_["mmcIn_rf_25"].ravel(), df_["mmcIn_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcOut'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcOut'])

ax[3][3].plot(sample_size, med[1:], c="b", label='KDN')
ax[3][3].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[3][3].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[3][3].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[3][3].set_xscale('log')
ax[3][3].set_xticks([])
ax[3][3].set_ylabel('Mean Max Confidence\n (Out Distribution)', fontsize=ticksize)

right_side = ax[3][3].spines["right"]
right_side.set_visible(False)
top_side = ax[3][3].spines["top"]
top_side.set_visible(False)

ax[3][3].plot(sample_size, df_['mmcOut_kdf_med'].ravel(), c="r", label='KDF')
ax[3][3].plot(sample_size, df_['mmcOut_rf_med'].ravel(), c="k", label='RF')

ax[3][3].fill_between(sample_size, df_["mmcOut_kdf_25"].ravel(), df_["mmcOut_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[3][3].fill_between(sample_size, df_["mmcOut_rf_25"].ravel(), df_["mmcOut_rf_75"].ravel(), facecolor='k', alpha=.3)

#########################################################
#########################################################
df = loadmat('kdn_experiments/results/graphs/polynomial.mat')

med, a_25, a_75 = calc_stat(1-df['kdn_acc'])
med_nn, nn_25, nn_75 = calc_stat(1-df['nn_acc'])

ax[4][0].plot(sample_size, med[1:], c="b", label='KDN')
ax[4][0].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[4][0].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[4][0].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[4][0].set_xscale('log')
ax[4][0].set_xlabel('Sample size', fontsize=ticksize)
ax[4][0].set_ylabel('Generalization Error', fontsize=ticksize)

right_side = ax[4][0].spines["right"]
right_side.set_visible(False)
top_side = ax[4][0].spines["top"]
top_side.set_visible(False)

df_ = loadmat('kdf_experiments/results/polynomial_plot_data.mat')
ax[4][0].plot(sample_size, df_['error_kdf_med'].ravel(), c="r", label='KDF')
ax[4][0].plot(sample_size, df_['error_rf_med'].ravel(), c="k", label='RF')

ax[4][0].fill_between(sample_size, df_["error_kdf_25"].ravel(), df_["error_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[4][0].fill_between(sample_size, df_["error_rf_25"].ravel(), df_["error_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_hd'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_hd'])

ax[4][1].plot(sample_size, med[1:], c="b", label='KDN')
ax[4][1].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[4][1].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[4][1].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[4][1].set_xscale('log')
ax[4][1].set_xlabel('Sample size', fontsize=ticksize)
ax[4][1].set_ylabel('Hellinger Distance', fontsize=ticksize)

right_side = ax[4][1].spines["right"]
right_side.set_visible(False)
top_side = ax[4][1].spines["top"]
top_side.set_visible(False)

ax[4][1].plot(sample_size, df_['hellinger_kdf_med'].ravel(), c="r", label='KDF')
ax[4][1].plot(sample_size, df_['hellinger_rf_med'].ravel(), c="k", label='RF')

ax[4][1].fill_between(sample_size, df_["hellinger_kdf_25"].ravel(), df_["hellinger_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[4][1].fill_between(sample_size, df_["hellinger_rf_25"].ravel(), df_["hellinger_rf_75"].ravel(), facecolor='k', alpha=.3)
ax[4][1].set_title('Polynomial', fontsize=title_size)
##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcIn'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcIn'])

ax[4][2].plot(sample_size, med[1:], c="b", label='KDN')
ax[4][2].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[4][2].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[4][2].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[4][2].set_xscale('log')
ax[4][2].set_xlabel('Sample size', fontsize=ticksize)
ax[4][2].set_ylabel('Mean Max Confidence\n (In Distribution)', fontsize=ticksize)

right_side = ax[4][2].spines["right"]
right_side.set_visible(False)
top_side = ax[4][2].spines["top"]
top_side.set_visible(False)

ax[4][2].plot(sample_size, df_['mmcIn_kdf_med'].ravel(), c="r", label='KDF')
ax[4][2].plot(sample_size, df_['mmcIn_rf_med'].ravel(), c="k", label='RF')

ax[4][2].fill_between(sample_size, df_["mmcIn_kdf_25"].ravel(), df_["mmcIn_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[4][2].fill_between(sample_size, df_["mmcIn_rf_25"].ravel(), df_["mmcIn_rf_75"].ravel(), facecolor='k', alpha=.3)

##################################################################################################
med, a_25, a_75 = calc_stat(df['kdn_mmcOut'])
med_nn, nn_25, nn_75 = calc_stat(df['nn_mmcOut'])

ax[4][3].plot(sample_size, med[1:], c="b", label='KDN')
ax[4][3].plot(sample_size, med_nn[1:], c="c", label='NN')

ax[4][3].fill_between(sample_size, a_25[1:], a_75[1:], facecolor='b', alpha=.3)
ax[4][3].fill_between(sample_size, nn_25[1:], nn_75[1:], facecolor='c', alpha=.3)

ax[4][3].set_xscale('log')
ax[4][3].set_xlabel('Sample size', fontsize=ticksize)
ax[4][3].set_ylabel('Mean Max Confidence\n (Out Distribution)', fontsize=ticksize)

right_side = ax[4][3].spines["right"]
right_side.set_visible(False)
top_side = ax[4][3].spines["top"]
top_side.set_visible(False)

ax[4][3].plot(sample_size, df_['mmcOut_kdf_med'].ravel(), c="r", label='KDF')
ax[4][3].plot(sample_size, df_['mmcOut_rf_med'].ravel(), c="k", label='RF')

ax[4][3].fill_between(sample_size, df_["mmcOut_kdf_25"].ravel(), df_["mmcOut_kdf_75"].ravel(), facecolor='r', alpha=.3)
ax[4][3].fill_between(sample_size, df_["mmcOut_rf_25"].ravel(), df_["mmcOut_rf_75"].ravel(), facecolor='k', alpha=.3)

plt.savefig('plots/simulation_res.pdf')
# %%


#%%
import sympy as sp
import numpy as np

# Setting up the symbolic variables
x, nu, t = sp.symbols('x nu t')
phi = sp.exp(-(x-4*t)**2/(4*nu*(t+1))) + sp.exp(-(x-4*t-2*np.pi)**2/(4*nu*(t+1)))

# Evaluate the partial derivative dphi/dx using sympy
phiprime = phi.diff(x)
#print phiprime

# Create the initial conditions function
u = -2*nu*(phiprime/phi)+4
#print u

# Transform the symbolic equation into a function using lambdify
from sympy.utilities.lambdify import lambdify
ufunc = lambdify ((t, x, nu), u)

nu = 0.01/np.pi
vxn = 256
vtn = 100
x = np.linspace(-1.0, 1, vxn)
t = np.linspace(0, 1, vtn)
u = np.zeros((vxn, vtn))
for i, x_i in enumerate(x):
    for j, t_j in enumerate(t):
        u[i, j] = ufunc(t_j, x_i, nu)

print(u)

np.save("1d-burgers/data/burgers_x", x)
np.save("1d-burgers/data/burgers_t", t)
np.save("1d-burgers/data/burgers_u", u)

import matplotlib.pyplot as plt
from scipy.interpolate import griddata
import scipy.io
import matplotlib.gridspec as gridspec

X, T = np.meshgrid(x,t)

# Preparing the inputs x and t (meshed as X, T) for predictions in one single array, as X_star
X_star = np.hstack((X.flatten()[:,None], T.flatten()[:,None]))
U_pred = griddata(X_star, u.T.flatten(), (X, T), method='cubic')

####### Row 0: u(t,x) ##################
gs0 = gridspec.GridSpec(1, 2)
gs0.update(top=1-0.06, bottom=1-1.0/3.0+0.06, left=0.15, right=0.85, wspace=0)
ax = plt.subplot(gs0[:, :])

h = ax.imshow(U_pred.T, interpolation='nearest', cmap='rainbow',
            extent=[t.min(), t.max(), x.min(), x.max()],
                  origin='lower', aspect='auto')
plt.show()

import numpy,import scipy,from scipy,import sympy,from sympy


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans

# Dataset oluşturma 3 farklı class olacak
x1 = np.random.normal(25, 5, 1000) # 25 ortalama 5 sigmaya(25+5,25-5) sahip 1000 değer üret
y1 = np.random.normal(25, 5, 1000)

x2 = np.random.normal(55, 5, 1000)
y2 = np.random.normal(60, 5, 1000)

x3 = np.random.normal(55, 5, 1000)
y3 = np.random.normal(15, 5, 1000)

# X ve Y leri birleştirdik
x = np.concatenate((x1,x2,x3), axis = 0)
y = np.concatenate((y1,y2,y3), axis = 0)

# X ve Y yi birleştirdik
dictionary = {"x":x, "y":y}

# Dataframe oluşturma
data = pd.DataFrame(dictionary)
data.describe()

# Labellarımız olamdğığ için kmeans alg. datayı böyle görür
plt.scatter(x1,y1, color = "black")
plt.scatter(x2,y2, color = "black")
plt.scatter(x3,y3, color = "black")
plt.show()

# kmeans clustering
wcss = []

for k in range(1,20):
    kmeans = KMeans(n_clusters = k)
    kmeans.fit(data)
    wcss.append(kmeans.inertia_)

plt.plot(range(1,20), wcss)
plt.xlabel("Number of k ")
plt.ylabel("WCSS")
plt.show()

# En iyi k 3 oalrak gördük. k=3 içn model
kmeans2 = KMeans(n_clusters = 3)
clusters = kmeans2.fit_predict(data)

data["label"] = clusters

plt.scatter(data.x[data.label == 0], data.y[data.label == 0], color = "red")
plt.scatter(data.x[data.label == 1], data.y[data.label == 1], color = "green")
plt.scatter(data.x[data.label == 2], data.y[data.label == 2], color = "blue")
plt.scatter(kmeans2.cluster_centers_[:,0], kmeans2.cluster_centers_[:,1], color = "yellow")


import numpy as np
import matplotlib.pyplot as plt
from time import time

def createKDTree(dataSet, depth):
    n = np.shape(dataSet)[0]
    treeNode = {}
    if n == 0:
        return None
    else:
        n, m = np.shape(dataSet)
        split_axis = depth % m
        depth += 1
        treeNode['split'] = split_axis
        dataSet = sorted(dataSet, key=lambda a: a[split_axis])
        num = n // 2
        treeNode['median'] = dataSet[num]
        treeNode['left'] = createKDTree(dataSet[:num], depth)
        treeNode['right'] = createKDTree(dataSet[num + 1:], depth)
        return treeNode

def searchTree(tree, data):
    k = len(data)
    if tree is None:
        return [0] * k, float('inf')
    split_axis = tree['split']
    median_point = tree['median']
    if data[split_axis] <= median_point[split_axis]:
        nearestPoint, nearestDistance = searchTree(tree['left'], data)
    else:
        nearestPoint, nearestDistance = searchTree(tree['right'], data)
    nowDistance = np.linalg.norm(data - median_point)  # the distance between data to current point
    if 0 < nowDistance < nearestDistance:
        nearestDistance = nowDistance
        nearestPoint = median_point.copy()
    splitDistance = abs(data[split_axis] - median_point[split_axis])  # the distance between hyperplane
    if splitDistance > nearestDistance > 0:
        return nearestPoint, nearestDistance
    else:
        if data[split_axis] <= median_point[split_axis]:
            nextTree = tree['right']
        else:
            nextTree = tree['left']
        nearPoint, nearDistance = searchTree(nextTree, data)
        if nearestDistance > nearDistance > 0:
            nearestDistance = nearDistance
            nearestPoint = nearPoint.copy()
        return nearestPoint, nearestDistance


from functools import lru_cache
import inspect
import math

import numpy as np

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

@lru_cache(maxsize=10)
def choose(n, k):
    return math.comb(n, k)

def gen_choose(n, r):
    return np.prod(np.arange(n, n - r, -1)) / math.factorial(r)

def get_num_args(function):
    return len(get_parameters(function))

def get_parameters(function):
    return inspect.signature(function).parameters

# Just to have a less heavyweight name for this extremely common operation
#
# We may wish to have more fine-grained control over division by zero behavior
# in the future (separate specifiable values for 0/0 and x/0 with x != 0),
# but for now, we just allow the option to handle indeterminate 0/0.

def clip(a, min_a, max_a):
    if a < min_a:
        return min_a
    elif a > max_a:
        return max_a
    return a

def fdiv(a, b, zero_over_zero_value=None):
    if zero_over_zero_value is not None:
        out = np.full_like(a, zero_over_zero_value)
        where = np.logical_or(a != 0, b != 0)
    else:
        out = None
        where = True

    return np.true_divide(a, b, out=out, where=where)

def binary_search(function,
                  target,
                  lower_bound,
                  upper_bound,
                  tolerance=1e-4):
    lh = lower_bound
    rh = upper_bound
    while abs(rh - lh) > tolerance:
        mh = np.mean([lh, rh])
        lx, mx, rx = [function(h) for h in (lh, mh, rh)]
        if lx == target:
            return lx
        if rx == target:
            return rx

        if lx <= target and rx >= target:
            if mx > target:
                rh = mh
            else:
                lh = mh
        elif lx > target and rx < target:
            lh, rh = rh, lh
        else:
            return None
    return mh


# -*- coding: utf-8 -*-
"""
Created on Mon Feb 24 11:37:13 2020

@author: Sumanshu
"""

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

r=3
fig_sz=(3,3)
fig1=plt.figure(figsize=fig_sz)
ax1=plt.axes([0.125,0.125,0.75,0.75])
d_tot=1000
d_circ=0
area_square=(2*r)**2
pi=np.pi
plt.xlim([-r,r])
plt.ylim([-r,r])
plt.axis('square')
k=0
theta=np.arange(0,2*pi,0.01)

for jj in range(d_tot):
    k +=1
    x=rnd.uniform(-r,r)
    y=rnd.uniform(-r,r)
    dist=np.sqrt(x**2+y**2)

    if dist<=r:
        d_circ += 1
        ax1.plot(x,y,'r.')
    else:
        ax1.plot(x,y,'b.')
    pr=d_circ/k
    area=pr*area_square
    # print(area)

plt.xticks(np.linspace(-r, r,2*r+1))
plt.yticks(np.linspace(-r, r,2*r+1))
ax1.plot(r*np.cos(theta),r*np.sin(theta),'k-',linewidth=2.0)

print('Calculated area of circle from simulation:', area)
area_circ=pi*r**2
print('Calculated area of circle using equation:',area_circ)
fig1.savefig('monte-carlo.png',dpi=300)


import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import matplotlib.cm as cm

DIMENSIONS = 2
CLUSTERS = 10
DATA_POINTS = 10000

TRAINING_STEPS = 1000
TOLERANCE = 0

def generate_gmm_data(points, components, dimensions):
    """Generates synthetic data of a given size from a random GMM"""
    np.random.seed(10)

    c_means = np.random.normal(size=[components, dimensions]) * 10
    c_variances = np.abs(np.random.normal(size=[components, dimensions]))
    c_weights = np.abs(np.random.normal(size=[components]))
    c_weights /= np.sum(c_weights)

    result = np.zeros((points, dimensions), dtype=np.float32)

    for i in range(points):
        comp = np.random.choice(np.array(range(10)), p=c_weights)
        result[i] = np.random.multivariate_normal(
            c_means[comp], np.diag(c_variances[comp])
        )

    np.random.seed()

    return result, c_means, c_variances, c_weights

def plot_clustered_data(points, c_means, c_assignments):
    """Plots the cluster-colored data and the cluster means"""
    colors = cm.rainbow(np.linspace(0, 1, CLUSTERS))

    for cluster, color in zip(range(CLUSTERS), colors):
        c_points = points[c_assignments == cluster]
        plt.plot(c_points[:, 0], c_points[:, 1], ".", color=color, zorder=0)
        plt.plot(c_means[cluster, 0], c_means[cluster, 1], ".", color="black", zorder=1)

    plt.show()

# PREPARING DATA

# generating DATA_POINTS points from a GMM with CLUSTERS components
data, true_means, true_variances, true_weights = generate_gmm_data(DATA_POINTS, CLUSTERS, DIMENSIONS)

# BUILDING COMPUTATIONAL GRAPH

# model inputs: generated data points
input = tf.placeholder(tf.float32, [None, DIMENSIONS])

# trainable variables: clusters means
random_point_ids = tf.squeeze(tf.multinomial(tf.ones([1, tf.shape(input)[0]]), CLUSTERS))
means = tf.Variable(tf.gather(input, random_point_ids), dtype=tf.float32)

# E-step: recomputing cluster assignments according to the current means
inputs_ex, means_ex = tf.expand_dims(input, 0), tf.expand_dims(means, 1)
distances = tf.reduce_sum(tf.squared_difference(inputs_ex, means_ex), 2)
assignments = tf.argmin(distances, 0)

# M-step: relocating cluster means according to the computed assignments
sums = tf.unsorted_segment_sum(input, assignments, CLUSTERS)
counts = tf.reduce_sum(tf.one_hot(assignments, CLUSTERS), 0)
means_ = tf.divide(sums, tf.expand_dims(counts, 1))

# distortion measure: sum of squared distances
# from each point to the closest cluster mean
distortion = tf.reduce_sum(tf.reduce_min(distances, 0))

# updating the means by new values
train_step = means.assign(means_)

# RUNNING COMPUTATIONAL GRAPH

# creating session
sess = tf.InteractiveSession()

# initializing trainable variables
sess.run(tf.global_variables_initializer(), feed_dict={input: data})

previous_assignments = None

# training loop
for step in range(TRAINING_STEPS):
    # executing a training step and
    # fetching evaluation information
    distortion_measure, current_means, current_assignments, _ = sess.run(
        [distortion, means_, assignments, train_step],
        feed_dict={input: data}
    )

    if step > 0:
        # computing the number of re-assignments during the step
        re_assignments = (current_assignments != previous_assignments).sum()
        print("{0}:\tdistortion {1:.2f}\tre-assignments {2}".format(
            step, distortion_measure, re_assignments))

        # stopping if no re-assignments occurred
        if re_assignments <= TOLERANCE:
            break
    else:
        print("{0}:\tdistortion {1:.2f}".format(
            step, distortion_measure))

    previous_assignments = current_assignments

plot_clustered_data(data, current_means, current_assignments)


import numpy as np
import scipy as sp
from scipy import integrate, special

from .function_fast import function_1d, function_2d

## One-dimensional functions
def linear(x, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    return p[0] * x + p[1]

def quad_1d(x, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    A, x0, C = p
    xc = x - x0

    return A * xc**2 + C

def gaussian_1d(x, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]

    Returns:
        [type] -- [description]
    """

    return function_1d.gaussian(x, *p)

def ThomasFermi_1d(x, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    A, x0, r0, C = p
    xc = x - x0

    y = 1 - (xc / r0)**2
    y[y<0] = 0

    return A * y + C

def bimodal_1d(x, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    ATF, xTF, rTF, Ag, x0, sigma, C = p

    # Thomas Fermi distribution
    pTF = [ATF, xTF, rTF, 0]
    yTF = ThomasFermi_1d(x, *pTF)

    # Gaussian distribution
    pg = [Ag, x0, sigma, 0]
    yg = gaussian_1d(x, *pg)

    return yTF + yg + C

def double_gauss_1d(x, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]
    # Parameters
    # 0: amplitude - 1, 1: center - 1, 2: 1/e width - 1,
    # 3: amplitude - 2, 4: center - 2, 5: 1/e width - 2,
    # 6: offset

    Returns:
        [type] -- [description]
    """
    A1, x01, sigma1, A2, x02, sigma2, C = p

    # Gaussian - 1
    xc1 = x - x01
    y1 = A1 * np.exp(-xc1**2 / sigma1**2)

    # Gaussian - 2
    xc2 = x - x02
    y2 = A2 * np.exp(-xc2**2 / sigma2**2)

    return y1 + y2 + C

def lorentzian_1d(x, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    A, x0, sigma, C = p

    x_shift = x - x0

    return A * sigma**2 / (x_shift**2 + sigma**2) + C

def psf_1d(x, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    A, x0, alpha, C = p

    r = (x - x0) * alpha
    y = np.zeros(r.shape)

    y[r!=0] = A * (2 * special.j1(r[r!=0]) / r[r!=0])**2
    y[r==0] = A

    return y + C

## Two-dimensional functions
def gaussian_2d(xy_mesh, *p):
    """[summary]

    Arguments:
        xy_mesh {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    # unpack 1D list into 2D x and y coords
    (x, y) = xy_mesh

    return function_2d.gaussian(x, y, *p)

def gaussian_2d_tmp(xy_mesh, *p):
    """[summary]

    Arguments:
        xy_mesh {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    # unpack 1D list into 2D x and y coords
    (x, y) = xy_mesh

    # # Parameters
    A, x0, sigmax, y0, sigmay, C = p

    xc = x - x0
    yc = y - y0

    return A * np.exp(-xc**2/sigmax**2 - yc**2/sigmay**2) + C

def gaussian_2dr(xy_mesh, *p):
    """[summary]

    Arguments:
        x {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    # unpack 1D list into 2D x and y coords
    (x, y) = xy_mesh

    # Parameters
    A, x0, sigmax, y0, sigmay, theta, C = p

    xc = x - x0
    yc = y - y0

    xr = xc * np.cos(theta) - yc * np.sin(theta)
    yr = xc * np.sin(theta) + yc * np.cos(theta)

    return A * np.exp(-xr**2/sigmax**2 - yr**2/sigmay**2) + C

def psf_2d(xy_mesh, *p):
    """[summary]

    Arguments:
        xy_mesh {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    # unpack 1D list into 2D x and y coords
    (x, y) = xy_mesh

    # Parameters
    A, x0, y0, alpha, C = p

    xc = x - x0
    yc = y - y0

    r = np.sqrt(xc**2 + yc**2) * alpha

    z = np.zeros(r.shape)

    z[r!=0] = A * (2 * special.j1(r[r!=0]) / r[r!=0])**2
    z[r==0] = A

    return z + C

def psf_2d_tmp(xy_mesh, *p):
    """[summary]

    Arguments:
        xy_mesh {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    # unpack 1D list into 2D x and y coords
    (x, y) = xy_mesh

    z = function_2d.psf(x, y, *p)
    return z.reshape(x.shape)

def psf_2d_int(xy_mesh, *p):
    """[summary]

    Arguments:
        xy_mesh {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    # unpack 1D list into 2D x and y coords
    (x, y) = xy_mesh

    A, x0, y0, alpha, C = p
    xc = x - x0
    yc = y - y0

    s_eff = x[1, 0] - x[0, 0]
    z = np.zeros(x0.shape)
    z_err = np.zeros(x0.shape)

    def f(x, y):
        if x == 0:
            val = 1
        else:
            r = alpha * np.sqrt(xc**2 + yc**2)
            val = (2 * special.j1(r) / r)**2

        return alpha**2 / (4 * np.pi) * val

    for i in range(x.shape[0]):
        for j in range(x.shape[1]):
            z[i, j], z_err[i, j] = integrate.dblquad(f, xc[i, j], xc[i, j] - s_eff,
                                                                            lambda x: yc[i, j] + s_eff, lambda x: yc[i, j])

    z = C + A * z

    return z

def psf_2d_gaussian(xy_mesh, *p):
    """[summary]

    Arguments:
        xy_mesh {[type]} -- [description]

    Returns:
        [type] -- [description]
    """
    # unpack 1D list into 2D x and y coords
    (x, y) = xy_mesh

    # # Parameters
    # A, x0, y0, sigma, C = p

    # r2 = (x - x0)**2 + (y - y0)**2

    # return A * np.exp(-r2/sigma**2) + C

    return function_2d.gaussian_iso(x, y, *p)


import numpy as np

#reproducibility
np.random.seed(0)

class OneHiddenLayerNetwork:

    #
    # activation functions
    #

    @staticmethod
    def tang(y):
        return np.tanh(y)

    @staticmethod
    def derivative_tang(y):
        return 1.0 - y ** 2

    @staticmethod
    def sigmoid(y):
        return 1 / (1 + np.exp(-y))

    @staticmethod
    def derivative_sigmoid(y):
        return y * (1 - y)

    def __init__(self, learning_rate=0.1):

        """
         neural network architecture
         simple 2 x 2 x 1 that is enough for XOR example
         input x hidden x output
        """
        self.learning_rate = learning_rate
        self.output = None

        # weights with random values
        self.weights = [
            np.random.uniform(low=-0.2, high=0.2, size=(2, 2)),  # input layer
            np.random.uniform(low=-2, high=2, size=(2, 1))  # hidden layer
        ]

    def activation(self, activation_type, y):
        if activation_type == 'sigmoid':
            return self.sigmoid(y)
        if activation_type == 'tang':
            return self.tang(y)

        raise ValueError('Undefined activation function: {}'.format(activation_type))

    def derivative_activation(self, activation_type, y):
        if activation_type == 'sigmoid':
            return self.derivative_sigmoid(y)
        if activation_type == 'tang':
            return self.derivative_tang(y)

        raise ValueError('Undefined derivative activation function: {}'.format(activation_type))

    #
    # forward pass
    # layer by layer
    #
    def feed_forward_pass(self, x_values):

        # forward pass
        input_layer = x_values
        hidden_layer = self.activation('tang', np.dot(input_layer, self.weights[0]))
        output_layer = self.activation('tang', np.dot(hidden_layer, self.weights[1]))

        self.layers = [
            input_layer,
            hidden_layer,
            output_layer
        ]

        # last layer is an output
        return self.layers[2]

    #
    # back propagation error through the network layers
    #
    def backward_pass(self, target_output, actual_output):

        # divergence of network output
        err = (target_output - actual_output)

        # backward from output to input layer
        # propagate gradients using chain rule
        for backward in range(2, 0, -1):
            err_delta = err * self.derivative_activation('tang', self.layers[backward])

            # update weights using computed gradient
            self.weights[backward - 1] += self.learning_rate * np.dot(self.layers[backward - 1].T, err_delta)

            # propagate error using updated weights of previous layer
            err = np.dot(err_delta, self.weights[backward - 1].T)

    def train(self, x_values, target):
        self.output = self.feed_forward_pass(x_values)
        self.backward_pass(target, self.output)

    def predict(self, x_values):
        return self.feed_forward_pass(x_values)

#XOR gate i/o, simple small dataset ;)
X = np.array(([0, 0], [0, 1], [1, 0], [1, 1]), dtype=float)
y = np.array(([0], [1], [1], [0]), dtype=float)

network = OneHiddenLayerNetwork(learning_rate=0.1)
iterations = 5000

# training loop
for i in range(iterations):
    network.train(X, y)

    ten = iterations // 10
    if i % ten == 0:
        print('-' * 10)
        print("Iteration number: " + str(i) + ' / ' +
              "Squared loss: " + str(np.mean(np.square(y - network.output))))

# predict
for i in range(len(X)):
    print('-' * 10)
    print('Input value: ' + str(X[i]))
    print('Predicted target: ' + str(network.predict(X[i])))
    print('Actual target: ' + str(y[i]))


from __future__ import annotations
import logging
import numpy as np

"""
PICARD Visual inspection of the Picard condition.

 eta = picard(U,s,b,d)
 eta = picard(U,sm,b,d)  ,  sm = [sigma,mu]

 Plots the singular values, s(i), the abs. value of the Fourier
 coefficients, |U(:,i)'*b|, and a (possibly smoothed) curve of
 the solution coefficients eta(i) = |U(:,i)'*b|/s(i).

 If s = [sigma,mu], where gamma = sigma./mu are the generalized
 singular values, then this routine plots gamma(i), |U(:,i)'*b|,
 and (smoothed) eta(i) = |U(:,i)'*b|/gamma(i).

 The smoothing is a geometric mean over 2*d+1 points, centered
 at point # i. If nargin = 3, then d = 0 (i.e, no smothing).

 Reference: P. C. Hansen, "The discrete Picard condition for
 discrete ill-posed problems", BIT 30 (1990), 658-672.

 Per Christian Hansen, IMM, April 14, 2001.
 ported to Python by Michael Hirsch
"""

def picard(U, s, b, d=0) -> tuple[np.ndarray, int, np.ndarray, np.ndarray, np.ndarray, int]:

    n, ps = np.atleast_2d(s).T.shape

    beta = np.abs(np.asfortranarray(U[:, :n]).T.dot(b))
    eta = np.zeros(n, order="F")

    if ps == 2:
        s = s[:, 0] / s[:, 1]

    d21 = 2 * d + 1
    keta = np.arange(d, n - d)

    if (s == 0).any():  # 10**-14 is OK?
        logging.warning("** picard: Division by zero: singular values")

    for i in keta:
        es = np.s_[i - d : i + d + 1]
        eta[i] = (beta[es].prod() ** (1 / d21)) / s[i]

    return eta, n, s, beta, keta, ps

def plotpicard(
    n: int, s: np.ndarray, beta: np.ndarray, eta: np.ndarray, keta: np.ndarray, ps: int
):
    from matplotlib.pyplot import figure

    ni = np.arange(n)
    ax = figure().gca()
    ax.semilogy(ni, s, ".-")  # breaks for inf
    ax.semilogy(ni, beta, "x")  # breaks for inf
    ax.semilogy(keta, eta[keta], "o")
    ax.set_xlabel("i")
    ax.set_title("Picard plot")
    # ax.autoscale(True,tight=True)
    if ps == 1:
        ax.legend((r"$\sigma_i$", r"$|u_i^T b|$", r"$|u_i^T b|/\sigma_i$"), loc="lower left")
    else:
        ax.legend(
            (r"$\sigma_i/\mu_i$", r"$|u_i^T b|$", r"$|u_i^T b|/ (\sigma_i/\mu_i)$"),
            loc="lower left",
        )


import numpy as np
from sympy import Matrix, Transpose, expand, latex

def expandir_matriz(matriz):
    fila = matriz.shape[0]
    for i in range(fila):
        for j in range(fila):
            matriz[i, j] = expand(matriz[i, j])
    return matriz

def lagrangiana_1D(x, n):
    puntos = np.linspace(-1, 1, n)
    resultado = []
    for i in range(np.size(puntos)):
        b = puntos[puntos != puntos[i]]
        polinomio = 1.0
        for j in range(np.size(b)):
            polinomio *= (x - b[j]) / (puntos[i] - b[j])
        resultado.append(expand(polinomio))
    return resultado

def lagrangiana_2D(x, y, n):
    Lx = Matrix(lagrangiana_1D(x, n))
    Ly = Transpose(Matrix(lagrangiana_1D(y, n)))
    Lxy = expandir_matriz((Lx*Ly))
    return Lxy

def lagrangiana_3D(x, y, z, n):
    Lx = Matrix(lagrangiana_1D(x, n))
    Ly = Transpose(Matrix(lagrangiana_1D(y, n)))
    Lz = Transpose(Matrix(lagrangiana_1D(z, n)))
    Lxy = expandir_matriz((Lx*Ly))
    filas = Lxy.shape[0]
    Lxyz = []
    for i in range(filas):
        resultado = expandir_matriz((Lxy[:, i]*Lz))
        Lxyz = np.append(Lxyz, resultado)
    return Lxyz.reshape(filas, filas, filas)

def recorrer_matriz_espiral(M):
    filas_M = M.shape[0]
    iteraciones = filas_M - (filas_M/2)
    for i in range(iteraciones):
        m = M[i:filas_M-i, i:filas_M-i]
        filas_m = m.shape[0]
        n = filas_m - 1
        if n == 0:
            print m[0,0]
        else:
            for j in range(n):
                print m[j,0]
            for j in range(n):
                print m[n,j]
            for j in range(n,0,-1):
                print m[j,n]
            for j in range(n,0,-1):
                print m[0,j]

def guardar_matriz_espiral(M):
    resultado = []
    filas_M = M.shape[0]
    iteraciones = filas_M - (filas_M/2)
    for i in range(iteraciones):
        m = M[i:filas_M-i, i:filas_M-i]
        filas_m = m.shape[0]
        n = filas_m - 1
        if n == 0:
            resultado.append(m[0,0])
        else:
            for j in range(n):
                resultado.append(m[j,0])
            for j in range(n):
                resultado.append(m[n,j])
            for j in range(n,0,-1):
                resultado.append(m[j,n])
            for j in range(n,0,-1):
                resultado.append(m[0,j])
    return resultado

def recorrer_matriz_3d_espiral(N):
    filas = N.shape[0]
    for i in range(filas):
        recorrer_matriz_espiral(N[i])

def guardar_matriz_3d_espiral(N):
    resultado1 = []
    filas = N.shape[0]
    for i in range(filas):
        resultado1.append(guardar_matriz_espiral(N[i]))
    return reduce(lambda x,y: x+y,resultado1)

def hermitiana_1D(x, n):
    resultado1 = lagrangiana_1D(x, n)
    resultado2 = []
    puntos2 = np.linspace(-1, 1, n)
    for i in range(np.size(puntos2)):
        A = x - puntos2[i]
        B = (resultado1[i].diff(x)).subs(x, puntos2[i])
        C = resultado1[i]**2
        D = (1 - (A*B))*C
        resultado2.append(expand(D))
        resultado2.append(expand(A*C))
    return resultado2


# -*- coding: utf-8 -*-
"""
Created on Mon May 24 06:47:31 2021

@author: Pramesh Kumar
"""

import numpy as np
from gurobipy import *
import time, math

def GenerateCuttingStockData():
    n=12;
    d=[20,13,17,18,20,45,13,50,20,35,20,30];
    w=[29,75,58,32,18,47,24,62,70,33,21,41];
    W=100;

    u=np.zeros(n) # no. of rolls of type i from one big roll
    for i in range(n):
        u[i]=math.floor(W/w[i]);

    nraw=0; # total big rolls needed
    for i in range(n):
        nraw+=math.ceil(d[i]/math.floor(W/w[i]))

    return n,d,w,u,W,nraw;

def SolveCuttingStockModel(n,d,w,u,W,nraw,types):

    val=-float("inf");
    # Step 0: start clock
    ts = time.time();

    # Step 1: Create the model
    m = Model()
    if (types=="IP"):
        x = {(i,j): m.addVar(lb = 0.0, vtype = GRB.INTEGER, name = str(i) + '_' + str(j)) for i in range(n) for j in range(nraw)}
        y = {j: m.addVar(lb = 0.0, vtype = GRB.BINARY, name = str(j)) for j in range(nraw)}
    elif (types=="LP"):
        x = {(i,j): m.addVar(lb = 0.0, vtype = GRB.CONTINUOUS, name = str(i) + '_' + str(j)) for i in range(n) for j in range(nraw)}
        y = {j: m.addVar(lb = 0.0, ub = 1.0, vtype = GRB.CONTINUOUS, name = str(j)) for j in range(nraw)}
    else:
        print("\nModel type ({:s}) is not recognized \n".format(types));
        val=-float("inf");
        return val

    obj = sum([y[k] for k in y])

    for i in range(n):
        m.addConstr(sum([x[i, j] for j in range(nraw)]) == d[i])

    for j in range(nraw):
        m.addConstr(sum([w[i] * x[i, j] for i in range(n)]) <= W*y[j])

    m.setObjective(obj, sense=GRB.MINIMIZE); m.update(); m.Params.OutputFlag = 1; m.Params.InfUnbdInfo = 1; m.Params.DualReductions = 0

    # Step 2: Solve the model
    m.optimize()

    # Step 3: Display solution if required
    if m.status == 2:
        print("---------------------------------------\n")
        print("Natural Formulation:",types)
        print(" {:6.2f} \n".format(m.objVal))
        print("---------------------------------------\n")

        if (type=="IP"):
            for j in range(nraw):
                valy=y[j].x
                if (valy>1e-4):
                    print("* Pattern : [")
                    for i in range(n):
                        print("{:3d}, ".format(round(x[i,j].x)));
                    print("   .......   used   {:3d} times\n".format(round(y[j].x)))

        else:
            for j in range(nraw):
                valy=y[j].x
                if (valy>1e-4):
                    print("* Pattern : [")
                    for i in range(n):
                        print("{:3.2f}, ".format(x[i,j].x));
                    print("   .......   used   {:3f} times\n".format(round(y[j].x)))

    # Step 4: Calculate solution time
    te = time.time();
    Deltat = (te-ts);

    print("-----> Run time:      {:6.4f}\n\n".format(Deltat))

    # Step 5: output optimal value;
    return m.objVal

def SolveCuttingStockModelCG(n,d,w,u,W,nraw,type, verbsol):

    # Step 0: start clock
    ts = time.time();

    # Step 1: Initialize variables
    t=1;

    # Step 2: Display solution
    print("---------------------------------------\n")
    print("Column Generation Algorithm: \n")
    print("---------------------------------------\n")

    # Step 3: Generate a first collection of simple patterns
    A=np.zeros((n,n)) # pttern of type i rom pattern j
    for i in range(n):
        A[i,i]=u[i]

    # Step 4: Create and solve initial master problem
    m = Model()
    x = {i: m.addVar(lb = 0.0, vtype = GRB.CONTINUOUS, name = str(i)) for i in range(n)}
    m.update()
    demConstrs = {i: m.addConstr(sum([A[i,j]*x[j] for j in range(n)]) >= d[i]) for i in range(n)}
    obj = sum([x[j] for j in range(n)])
    m.setObjective(obj, sense=GRB.MINIMIZE); m.update(); m.Params.OutputFlag = verbsol; m.Params.InfUnbdInfo = 1; m.Params.DualReductions = 0
    m.optimize()
    if m.status == 2:
        val = m.objVal
        print("* Iteration {:3.1f}: {:7.3f}       ".format(t,val))

    # Step 5: Create and solve initial subproblem
    subm = Model()
    y = {i: subm.addVar(lb = 0.0, vtype = GRB.INTEGER, name = str(i)) for i in range(n)}
    for j in range(n):
        subm.addConstr(sum([w[i]*y[i] for i in range(n)]) <= W)

    subObj = sum([demConstrs[i].pi * y[i] for i in range(n)])
    subm.setObjective(subObj, sense=GRB.MAXIMIZE); m.update(); m.Params.OutputFlag = verbsol; m.Params.InfUnbdInfo = 1; m.Params.DualReductions = 0

    subm.optimize()
    if subm.status == 2:
        rc=subm.objVal;
        print("// |rc| = {:7.3f} //       Pattern = [".format(rc-1))
        for i in range(n):
            print("{:3.0f},".format(round(y[i].x)))

    # Step 6: Create names and matrices for new columns generated
    newcols = [];
    newpatterns =np.zeros((0,n), int)

    # Step 7: Perform column generation
    while (rc>1+1e-5):
        # Step 7.1: Increase number of patterns added
        t+=1;

        # Step 7.2: Add variable to master and record generated pattern
        # Column is used for the added varaibles that are used to modify the constraints
        c = Column()
        for i in demConstrs:
            c.addTerms(y[i].x, demConstrs[i])

        z = m.addVar(lb = 0.0, vtype = GRB.CONTINUOUS, name = str(len(x)), column = c)
        m.update()

        newcols.append(z)
        newpatterns = np.append(newpatterns, np.array([[y[k].x for k in y]]), axis=0)

        # Step 7.3: Solve master model
        m.optimize()
        if m.status == 2:
            val=m.objVal
        print("* Iteration {:3.1f}: {:7.3f}       ".format(t,val))

        # Step 7.4: Set the objective and solve new subproblem
        subObj = sum([demConstrs[i].pi * y[i] for i in range(n)])
        subm.setObjective(subObj, sense=GRB.MAXIMIZE); m.update()

        subm.optimize()
        if subm.status == 2:
            rc=subm.objVal;
            print("// |rc| = {:7.3f} //       Pattern = [".format(rc-1))
            for i in range(n):
                print("{:3.0f},".format(round(y[i].x)))

    val=m.objVal;

    # Step 8: Write lp model to a file
    m.write("CGmodel.lp")

    # Step 9: Display solution obtained at the end of the CG algorithm
    print("---------------------------------------\n")
    print("Solution (Relaxation): {:7.3f} \n".format(val))
    print("---------------------------------------\n")

    for i in range(n):
        newpat = x[i].x;
        if (newpat>1e-4):
            print("* Pattern {:3.0f}: [".format(i));
            for j in range(n-1):
                print("{:3.0f}, ".format(A[i,j]))
            print("{:3.0f}]   .......   used {:7.3f} times \n".format(A[i,n-1],newpat))

    for i in range(len(newcols)):
        newpat= newcols[i].x;
        if (newpat>1e-4):
            print("* Pattern {:3.0f}: [".format(i+n));
            for j in range(n-1):
                print("{:3.0f}, ".format(round(newpatterns[i,j])))
            print("{:3.0f}]   .......   used {:7.3f} times \n".format(round(newpatterns[i,n-1]),newpat))

    print("---------------------------------------\n")

    # Step 11: Calculate solution time
    te = time.time();
    Deltat = (te-ts);
    if (verbsol>=1):
        print("-----> Run time:      {:6.4f}\n\n".format(Deltat))

    # Step 12: return values
    return(val)

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

# Step 1: Define dimensions and parameters

n,d,w,u,W,nraw = GenerateCuttingStockData();

# Step 2: Solve column generation
vallp3 = SolveCuttingStockModelCG(n,d,w,u,W,nraw,"IP", 0)

# Step 3:
#vallp1 = SolveCuttingStockModel(n,d,w,u,W,nraw,"LP");
#valip1 = SolveCuttingStockModel(n,d,w,u,W,nraw,"IP");


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: alejandro
"""

import numpy as np
from math import exp, sin, cos, pi
import matplotlib.pyplot as plt
import plotly.express as px
import plotly.graph_objects as go
from plotly.offline import init_notebook_mode, plot
import os
np.random.seed(1)

#init_notebook_mode()

#Establece un punto de parada
def esperar():
  input("Pulsa una tecla para continuar...")
  plt.close('all')

###Funciones para el ejercicio 1 (GD)
def E(u,v):
    return (u**3*exp(v-2)-2*v**2*exp(-u))**2

#Derivada parcial de E con respecto a u
def dEu(u,v):
    return 2*(u**3*exp(v-2)-2*v**2*exp(-u))*(3*u**2*exp(v-2)+2*v**2*exp(-u))

#Derivada parcial de E con respecto a v
def dEv(u,v):
    return 2*(u**3*exp(v-2)-2*v**2*exp(-u))*(u**3*exp(v-2)-4*v*exp(-u))

#Gradiente de E
def gradE(u,v):
    return np.array([dEu(u,v), dEv(u,v)])

#GD
def gradient_descent(loss, gradLoss, lr=0.01, initial_point=np.array([0.0, 0.0], dtype=np.float64), max_iter=1000000,
                     error2get=float('-inf'), decay=1.0):
    hist = [initial_point]
    prevL = loss(*initial_point)
    hist_loss = [prevL]
    n_iters = 0
    while n_iters < max_iter:
      n_iters += 1
      hist.append( hist[-1]-lr*gradLoss(*hist[-1]) )
      currLoss = loss(*hist[-1])
      hist_loss.append(currLoss)
      if currLoss < error2get:
        break;
      else:
        prevL = currLoss
        lr *= decay
    return hist, n_iters, hist_loss

def f(x,y):
    return (x + 2)**2 + 2*(y - 2)**2 + 2*sin(2*pi*x)*sin(2*pi*y)

def f_arr(x,y):
    return (x + 2)**2 + 2*(y - 2)**2 + 2*np.sin(2*pi*x)*np.sin(2*pi*y)

def dfx(x,y):
    return 2*(x+2)+4*pi*cos(2*pi*x)*sin(2*pi*y)

def dfy(x,y):
    return 4*(y-2) + 4*pi*sin(2*pi*x)*cos(2*pi*y)

def gradf(x,y):
    return np.array([dfx(x,y), dfy(x,y)])

#Función para mostrar la gráfica de una función 2D junto con los puntos seguidos
# por el proceso de optimización con GD. Las llamadas a esta función se encuentran
# comentadas
def display_gd(loss, hist, title, x_min=-3, x_max=3, y_min=-3, y_max=3):
    import plotly.graph_objects as go
    x = np.linspace(x_min, x_max, 50)
    y = np.linspace(y_min, y_max, 50)
    X, Y = np.meshgrid(x, y)
    Z = loss(X,Y)

    x_traj = [ point[0] for point in hist ]
    y_traj = [ point[1] for point in hist ]
    z_traj = [ loss(point[0], point[1]) for point in hist ]

    fig = go.Figure(
                    data=[go.Surface(x=x, y=y, z=Z)],
                    layout=go.Layout(
                              title=go.layout.Title(text=title),
                              autosize=False,
                              scene=go.layout.Scene(
                                  xaxis_title="x",
                                  yaxis_title="y",
                                  zaxis_title="f(x,y)",
                              ),
                              width=600, height=600,
                              margin=dict(l=20, r=20, b=30, t=30)
                          )
    )
    fig.add_traces(data=[go.Scatter3d(x=x_traj, y=y_traj, z=z_traj)])

    #fig.show()
    plot(fig)

###Funciones para el ejercicio 2 (sobre regresión lineal)
def pseudoinverse(A):
      return np.linalg.inv(A.T@A)@A.T

def sgd(X, y, minibatch_size, lr=0.1, epochs=50, decay=1.0):
    data = np.copy(X)
    data = np.insert(data, 0, np.ones((len(data))), axis=1)
    w = np.random.uniform(low=0.0, high=1.0, size=data.shape[1])
    n_iters = 0
    while n_iters < epochs:
        n_iters += 1
        ind_chks = chunks(np.random.permutation(len(data)), minibatch_size)
        for indices in ind_chks:
            data_minibatch = data[indices]
            y_minibatch = y[indices]
            stoch_gradient = 2/minibatch_size*np.array([
                                  np.sum( data_minibatch[:,j]*(data_minibatch@w-y_minibatch) )
                                  for j in range(len(w))
                              ])
            w -= lr * stoch_gradient
        lr *= decay
    return w

def chunks(lst, n):
    """Yield successive n-sized chunks from lst."""
    for i in range(0, len(lst), n):
        yield lst[i:i + n]

def sign(x):
    if x >= 0:
      return 1
    return -1

def line_from_w(w):
    return [0,0.8], [-w[0]/w[2], (-w[0]-0.8*w[1])/w[2]]

def error(pred, truth):
    return 1-np.sum(np.equal(pred,truth))/len(truth)

def erm(X,y,w):
    return np.mean((X@w-y)**2)

def simula_unif(N, d, size):
	  return np.random.uniform(-size,size,(N,d))

def scatter_data(data, ax=None, label=None, title=None):
    if ax==None:
        _, ax = plt.subplots()
    if title!=None:
        ax.set_title(title)
    ax.scatter(data[:,0], data[:,1], label=label)
    return ax

def muestra_dataset(X, y, label1, label2, title=None):
    ax = scatter_data(X[y==label1], label=str(label1), title=title)
    _ = scatter_data(X[y==label2], ax=ax, label=str(label2))
    ax.legend(loc='upper right')
    plt.show()
    return ax

def muestra_preds(X, y, label1, label2, title=None):
    ax = scatter_data(X[y>0], label=str(label1), title=title)
    _ = scatter_data(X[y<0], ax=ax, label=str(label2))
    ax.legend(loc='upper right')
    return ax

#Cambia aleatoriamente el signo de un 10% de las etiquetas
def random_sign_change(foX, perc):
    new_foX = np.copy(foX)
    to_change = np.random.choice( np.arange(len(foX)), int(len(foX)*perc) )
    new_foX[to_change] = -new_foX[to_change]
    return new_foX

#Encapsula el experimento de repetir el muestreo uniforme de train y test, la etiquetación de
# los elementos y la clasificación mediante SGD, tantas veces como se indique
# por el parámetro reps (para esta práctica, 1000 repeticiones)
def test_experimento(reps, f):
    f_vec = np.vectorize(f)
    sum_error_clasif_tr = 0.0
    sum_error_clasif_te = 0.0
    sum_Ein = 0.0
    sum_Eout = 0.0
    for i in range(reps):
        X = simula_unif(1000, 2, 1)
        X_test = simula_unif(1000, 2, 1)

        foX = f_vec(X[:,0], X[:,1])
        foX = random_sign_change(foX, 0.1)
        foX_te = f_vec(X_test[:,0], X_test[:,1])
        foX_te = random_sign_change(foX_te, 0.1)

        w_sgd = sgd(X, foX, 32, lr=0.01, epochs=10)

        X_tr_mat = np.insert( np.copy(X), 0, np.ones((len(X))), axis=1 )
        X_te_mat = np.insert( np.copy(X_test), 0, np.ones((len(X_test))), axis=1 )

        preds_sgd_trainset = X_tr_mat @ w_sgd
        preds_sgd_trainset = np.array(list(map(sign, preds_sgd_trainset)))
        preds_sgd_testset = X_te_mat @ w_sgd
        preds_sgd_testset = np.array(list(map(sign, preds_sgd_testset)))

        sum_error_clasif_tr += error(preds_sgd_trainset, foX)
        sum_error_clasif_te += error(preds_sgd_testset, foX_te)

        sum_Ein += erm(X_tr_mat, foX, w_sgd)
        sum_Eout += erm(X_te_mat, foX_te, w_sgd)

    avg_error_clasif_tr = sum_error_clasif_tr/reps
    avg_error_clasif_te = sum_error_clasif_te/reps
    avg_Ein = sum_Ein/reps
    avg_Eout = sum_Eout/reps

    return avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout

#Devuelve la lista de transformaciones no lineales que aplicar a los elementos
# del dataset
def transformations():
    return [
              (lambda x1, x2: x1),
              (lambda x1, x2: x2),
              (lambda x1, x2: x1*x2),
              (lambda x1, x2: x1*x1),
              (lambda x1, x2: x2*x2)
           ]

#Genera un nuevo conjunto de datos a partir de X, formado por una columna de 1s
# y otra columna por cada transformación en la lista transformations
def transform_characteristics(X, transformations):
    # Aplicamos las transformaciones deseadas sobre las características originales
    trans_data = np.array([
                np.array([ f(*x_row) for f in transformations ])
                for x_row in X
    ])
    return trans_data

#Encapsula el experimento de repetir el muestreo uniforme de train y test, la etiquetación de
# los elementos y la clasificación mediante SGD, tantas veces como se indique
# por el parámetro reps (para esta práctica, 1000 repeticiones). Añade el cálculo
# de las características no lineales
def test_NLChars(reps, f, batch=32):
    f_vec = np.vectorize(f)
    sum_error_clasif_tr = 0.0
    sum_error_clasif_te = 0.0
    sum_Ein = 0.0
    sum_Eout = 0.0
    for i in range(reps):
        X = simula_unif(1000, 2, 1)
        X_test = simula_unif(1000, 2, 1)

        foX = f_vec(X[:,0], X[:,1])
        foX = random_sign_change(foX, 0.1)
        foX_te = f_vec(X_test[:,0], X_test[:,1])
        foX_te = random_sign_change(foX_te, 0.1)

        NL_X = transform_characteristics(X, transformations())
        w_sgd = sgd(NL_X, foX, batch, lr=0.01, epochs=10)

        X_tr_mat = np.insert( np.copy(NL_X), 0, np.ones((len(X))), axis=1 )
        NL_X_test = transform_characteristics(X_test, transformations())
        X_te_mat = np.insert( np.copy(NL_X_test), 0, np.ones((len(NL_X_test))), axis=1 )

        preds_sgd_trainset = X_tr_mat @ w_sgd
        preds_sgd_trainset = np.array(list(map(sign, preds_sgd_trainset)))
        preds_sgd_testset = X_te_mat @ w_sgd
        preds_sgd_testset = np.array(list(map(sign, preds_sgd_testset)))

        sum_error_clasif_tr += error(preds_sgd_trainset, foX)
        sum_error_clasif_te += error(preds_sgd_testset, foX_te)

        sum_Ein += erm(X_tr_mat, foX, w_sgd)
        sum_Eout += erm(X_te_mat, foX_te, w_sgd)

    avg_error_clasif_tr = sum_error_clasif_tr/reps
    avg_error_clasif_te = sum_error_clasif_te/reps
    avg_Ein = sum_Ein/reps
    avg_Eout = sum_Eout/reps

    return avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout

###Funciones relacionadas con el BONUS sobre el método de Newton
#Segundas derivadas parciales
def d2fxx(x,y):
    return 2 - 8*(pi**2)*sin(2*pi*x)*sin(2*pi*y)

def d2fxy(x,y):                                       # Igual a d2fyx
    return 8*(pi**2)*cos(2*pi*x)*cos(2*pi*y)

def d2fyy(x,y):
    return 4 - 8*(pi**2)*sin(2*pi*x)*sin(2*pi*y)

def hessianf(x,y):
    return np.array(
                    [[d2fxx(x,y), d2fxy(x,y)],
                     [d2fxy(x,y), d2fyy(x,y)]]
                   )
#Newton
def NR_method1(loss, gradLoss, hessianLoss, lr=0.1, initial_point=np.array([0.0, 0.0], dtype=np.float64),
              max_iter=1000000, decay=1.0):
    hist = [initial_point]
    hist_loss = [loss(*initial_point)]
    n_iters = 0
    while n_iters < max_iter:
        n_iters += 1
        invH = np.linalg.inv(hessianLoss(*hist[-1]))
        nextP = hist[-1] - lr*invH@gradLoss(*hist[-1])
        nextLoss = loss(*nextP)
        hist.append( nextP )
        hist_loss.append( nextLoss )
        lr *= decay
    return hist, n_iters, hist_loss

#Añade el cambio de signo para corregir la convergencia a puntos críticos
# no mínimos
def NR_method2(loss, gradLoss, hessianLoss, lr=0.1, initial_point=np.array([0.0, 0.0], dtype=np.float64),
              max_iter=1000000, decay=1.0):
    hist = [initial_point]
    hist_loss = [loss(*initial_point)]
    n_iters = 0
    while n_iters < max_iter:
      n_iters += 1
      invH = np.linalg.inv(hessianLoss(*hist[-1]))
      s = -1 if np.linalg.det(invH) < 0 or np.linalg.det(invH) > 0 and np.trace(invH) < 0 else 1
      nextP = hist[-1] - lr*invH@gradLoss(*hist[-1])*s
      nextLoss = loss(*nextP)
      hist.append( nextP )
      hist_loss.append( nextLoss )
      lr *= decay
    return hist, n_iters, hist_loss

class P1:

    def __init__(self):
        self.hist_loss1 = None
        self.hist_loss2 = None

    def apartado11y12(self):
        hist, it, hist_loss = gradient_descent(loss=E, gradLoss=gradE, lr=0.1, initial_point = np.array([1.0,1.0]), error2get=1e-14)

        print ('Número de iteraciones: ', it)
        print ('Coordenadas obtenidas: (', hist[-1][0], ', ', hist[-1][1],')')
        print ('Valor de la función objetivo: (', hist_loss[-1],')')

    def apartado13a(self):
        hist, it, self.hist_loss1 = gradient_descent(loss=f, gradLoss=gradf, lr=0.01, initial_point = np.array([-1.0,1.0]), max_iter=50)

        print('eta=0.01')
        print ('Número de iteraciones: ', it)
        print ('Coordenadas obtenidas: (', hist[-1][0], ', ', hist[-1][1],')')
        print ('Valor de la función objetivo: (', self.hist_loss1[-1],')')

        #display_gd(f_arr, hist, title="Ejercicio 1.3.a: eta = 0.01", x_min=-2, x_max=0, y_min=0, y_max=2)

        esperar()

        hist, it, self.hist_loss2 = gradient_descent(loss=f, gradLoss=gradf, lr=0.1, initial_point = np.array([-1.0,1.0]), max_iter=50)

        print('eta=0.1')
        print ('Número de iteraciones: ', it)
        print ('Coordenadas obtenidas: (', hist[-1][0], ', ', hist[-1][1],')')
        print ('Valor de la función objetivo: (', self.hist_loss2[-1],')')

        #display_gd(f_arr, hist, title="Ejercicio 1.3.a: eta = 0.1", x_min=-5, x_max=0, y_min=0, y_max=4)

        esperar()

        ###Mostramos en una gráfica la evolución de la función de pérdida a lo largo de
        ### cada experimento. Observamos que la gráfica correspondiente al GD con un lr
        ### más bajo presenta una evolución mucho más estable.
        plt.plot(np.arange( max(len(self.hist_loss1), len(self.hist_loss2)) ),
                 self.hist_loss1, label="lr = 0.01")
        plt.plot(np.arange( max(len(self.hist_loss1), len(self.hist_loss2)) ),
                 self.hist_loss2, label="lr = 0.1")
        plt.legend()
        plt.show()

    def apartado13b(self):
        h1, _, hl1 = gradient_descent(loss=f, gradLoss=gradf, lr=0.01, initial_point = np.array([-0.5,-0.5]), max_iter=50)
        h2, _, hl2 = gradient_descent(loss=f, gradLoss=gradf, lr=0.01, initial_point = np.array([1.0,1.0]), max_iter=50)
        h3, _, hl3 = gradient_descent(loss=f, gradLoss=gradf, lr=0.01, initial_point = np.array([2.1,-2.1]), max_iter=50)
        h4, _, hl4 = gradient_descent(loss=f, gradLoss=gradf, lr=0.01, initial_point = np.array([-3.0,3.0]), max_iter=50)
        h5, _, hl5 = gradient_descent(loss=f, gradLoss=gradf, lr=0.01, initial_point = np.array([-2.0,2.0]), max_iter=50)

        print ('Coordenadas (-0.5, -0.5): (%.4f, %.4f)' % (h1[-1][0], h1[-1][1]))
        print ('Función objetivo (-0.5, -0.5): %.4f' % hl1[-1])

        print ('Coordenadas (1.0,1.0): (%.4f, %.4f)' % (h2[-1][0], h2[-1][1]))
        print ('Función objetivo (1.0,1.0): %.4f' % hl2[-1])

        print ('Coordenadas (2.1,-2.1): (%.4f, %.4f)' % (h3[-1][0], h3[-1][1]))
        print ('Función objetivo (2.1,-2.1): %.4f' % hl3[-1])

        print ('Coordenadas (-3.0,3.0):(%.4f, %.4f)' % (h4[-1][0], h4[-1][1]))
        print ('Función objetivo (-3.0,3.0): %.4f' % hl4[-1])

        print ('Coordenadas (-2.0,2.0): (%.4f, %.4f)' % (h5[-1][0], h5[-1][1]))
        print ('Función objetivo (-2.0,2.0): %.4f' % hl5[-1])

        esperar()

        #Calculamos la desviación típica de las soluciones obtenidas con cada punto inicial
        solutions = np.array([h1[-1], h2[-1], h3[-1], h4[-1], h5[-1]])
        std_solutions = np.std(solutions, axis=0)
        print('Desviación en las soluciones:', std_solutions)

        f_solutions = np.array([hl1[-1], hl2[-1], hl3[-1], hl4[-1], hl5[-1]])
        std_f_solutions = np.std(f_solutions, axis=0)
        print('Desviación en el valor objetivo:', std_f_solutions)

    def apartado13(self):
        self.apartado13a()
        esperar()

        self.apartado13b()

    def apartado1(self):
        self.apartado11y12()
        esperar()

        self.apartado13()

    def apartado21(self):
        #Cargamos los datos de entrenamiento y test
        X_train = np.load('datos/X_train.npy')
        y_train = np.load('datos/y_train.npy')
        X_test = np.load('datos/X_test.npy')
        y_test = np.load('datos/y_test.npy')

        #Eliminamos aquellas instancias de las muestras de entrenamiento y de test
        # que no estén etiquetadas como 1 ó 5
        X_train = X_train[np.logical_or(y_train==1, y_train==5)]
        y_train = y_train[np.logical_or(y_train==1, y_train==5)]
        X_test = X_test[np.logical_or(y_test==1, y_test==5)]
        y_test = y_test[np.logical_or(y_test==1, y_test==5)]

        print('Tamaño del conjunto de entrenamiento:', len(y_train))
        print('Tamaño del conjunto de test:', len(y_test))

        y_train_labels = np.array(list(map(lambda x: 1 if x==1 else -1, y_train)), dtype=np.float64)
        y_test_labels = np.array(list(map(lambda x: 1 if x==1 else -1, y_test)), dtype=np.float64)

        #Mostramos un scatter plot con las muestras de entrenamiento y test
        muestra_dataset(X_train, y_train, 1, 5, title="Conjunto de entrenamiento")
        muestra_dataset(X_test, y_test, 1, 5, title="Conjunto de test")
        esperar()

        #Cálculo de los pesos óptimos con la pseudoinversa
        X = np.insert( X_train, 0, np.ones((len(X_train))), axis=1 )
        w = pseudoinverse(X)@y_train_labels

        #X_tr_mat y X_te_mat son las matrices con los datos de entrenamiento
        # y test a las que se le añade una columna de 1s al principio
        X_tr_mat = np.insert( X_train, 0, np.ones((len(X_train))), axis=1 )
        preds_trainset = X_tr_mat @ w
        preds_trainset = np.array(list(map(sign, preds_trainset)))

        X_te_mat = np.insert( X_test, 0, np.ones((len(X_test))), axis=1 )
        preds = X_te_mat @ w
        preds = np.array(list(map(sign, preds)))

        print("Errores obtenidos con la pseudoinversa:")

        err_clasf_tr=error(preds_trainset, y_train_labels)
        err_clasf_te=error(preds, y_test_labels)

        print("Error de clasificación en train:", err_clasf_tr)
        print("Error de clasificación en test:", err_clasf_te)

        Ein = erm(X_tr_mat, y_train_labels, w)
        Eout = erm(X_te_mat, y_test_labels, w)

        print("Ein:", Ein)
        print("Eout:", Eout)
        esperar()

        #Plot de las predicciones
        line = line_from_w(w)
        ax_tr = muestra_preds(X_train, preds_trainset, 1, 5, "Predicciones de entrenamiento")
        ax_tr.plot(line[0], line[1], color='g')
        ax_te = muestra_preds(X_test, preds, 1, 5, "Predicciones de test")
        ax_te.plot(line[0], line[1], color='g')
        esperar()

        #Cálculo de los pesos óptimos con SGD
        w_sgd = sgd(X_train, y_train_labels, 32, lr=0.01, epochs=10)

        preds_sgd_trainset = np.insert(X_train, 0, np.ones((len(X_train))), axis=1 ) @ w_sgd
        preds_sgd_trainset = np.array(list(map(sign, preds_sgd_trainset)))

        preds_sgd = np.insert( X_test, 0, np.ones((len(X_test))), axis=1 ) @ w_sgd
        preds_sgd = np.array(list(map(sign, preds_sgd)))

        print("Errores obtenidos con SGD:")
        err_clasf_tr=error(preds_sgd_trainset, y_train_labels)
        err_clasf_te=error(preds_sgd, y_test_labels)

        print("Error de clasificación en train:", err_clasf_tr)
        print("Error de clasificación en test:", err_clasf_te)

        X_tr_mat = np.insert( np.copy(X_train), 0, np.ones((len(X_train))), axis=1 )
        X_te_mat = np.insert( np.copy(X_test), 0, np.ones((len(X_test))), axis=1 )
        Ein = erm(X_tr_mat, y_train_labels, w_sgd)
        Eout = erm(X_te_mat, y_test_labels, w_sgd)

        print("Ein:", Ein)
        print("Eout:", Eout)
        esperar()

        #Plot de las predicciones
        line_sgd = line_from_w(w_sgd)
        ax_tr = muestra_preds(X_train, preds_sgd_trainset, 1, 5, "Predicciones de entrenamiento")
        ax_tr.plot(line_sgd[0], line_sgd[1], color='g')
        ax_te = muestra_preds(X_test, preds_sgd, 1, 5, "Predicciones de test")
        ax_te.plot(line_sgd[0], line_sgd[1], color='g')

    def apartado22(self):
        X = simula_unif(1000, 2, 1)
        _ = scatter_data(X)

        def f(x1, x2):
            return sign((x1-0.2)**2 + x2**2-0.6)

        f_vec = np.vectorize(f)
        foX = f_vec(X[:,0], X[:,1])
        foX = random_sign_change(foX, 0.1)

        ax = scatter_data(X[foX==-1], label="f = -1")
        _ = scatter_data(X[foX==1], ax, label="f = 1")
        ax.legend()
        plt.show()
        esperar()

        ##USO DE CARACTERÍSTICAS LINEALES
        w_sgd = sgd(X, foX, 32, lr=0.01, epochs=10)

        preds_sgd_trainset = np.insert( np.copy(X), 0, np.ones((len(X))), axis=1 ) @ w_sgd
        preds_sgd_trainset = np.array(list(map(sign, preds_sgd_trainset)))

        print("Características lineales:")
        error_clasif = error(preds_sgd_trainset, foX)
        print("Error de clasificación:", error_clasif)

        X_tr_mat = np.insert( np.copy(X), 0, np.ones((len(X))), axis=1 )
        Ein = erm(X_tr_mat, foX, w_sgd)
        print("Ein:", Ein)
        esperar()

        #Plot de las predicciones
        line_sgd = line_from_w(w_sgd)
        ax_tr = muestra_preds(X, preds_sgd_trainset, -1, 1, "Modelo ajustado")
        ax_te = muestra_preds(X, foX, -1, 1, "Modelo ajustado (etiquetas originales)")
        esperar()

        avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout = test_experimento(reps=1000, f=f)
        print("Errores medios (ECin, ECout, Ein, Eout):", avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout)
        esperar()

        X = simula_unif(1000, 2, 1)
        foX = f_vec(X[:,0], X[:,1])
        foX = random_sign_change(foX, 0.1)

        ##USO DE CARACTERÍSTICAS NO LINEALES
        NL_X = transform_characteristics(X, transformations())
        w_sgd = sgd(NL_X, foX, 16, lr=0.01, epochs=10)

        X_tr_mat = np.insert( np.copy(NL_X), 0, np.ones((len(X))), axis=1 )
        preds_sgd_trainset = X_tr_mat @ w_sgd
        preds_sgd_trainset = np.array(list(map(sign, preds_sgd_trainset)))

        print("Características no lineales:")
        error_clasif = error(preds_sgd_trainset, foX)
        print("Error de clasificación:", error_clasif)

        Ein = erm(X_tr_mat, foX, w_sgd)
        print("Ein:", Ein)
        esperar()

        line_sgd = line_from_w(w_sgd)
        ax_tr = muestra_preds(X, preds_sgd_trainset, -1, 1, "Modelo ajustado")
        ax_te = muestra_preds(X, foX, -1, 1, "Modelo ajustado (etiquetas originales)")
        esperar()

        print("Errores medios (ECin, ECout, Ein, Eout) según el tamaño de batch:")
        avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout = test_NLChars(reps=1000, f=f, batch=256)
        print("256->", avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout)

        avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout = test_NLChars(reps=1000, f=f, batch=128)
        print("128->", avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout)

        avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout = test_NLChars(reps=1000, f=f, batch=64)
        print("64->", avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout)

        avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout = test_NLChars(reps=1000, f=f, batch=32)
        print("32->", avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout)

        avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout = test_NLChars(reps=1000, f=f, batch=16)
        print("16->", avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout)

        avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout = test_NLChars(reps=1000, f=f, batch=8)
        print("8->", avg_error_clasif_tr, avg_error_clasif_te, avg_Ein, avg_Eout)

    def apartado2(self):
        self.apartado21()
        esperar()

        self.apartado22()

    def BONUS(self):
        print("Optimización de f con el método de Newton:")
        hist, it, hist_loss_NR = NR_method1(loss=f, gradLoss=gradf, hessianLoss=hessianf,
                                           lr=0.1, initial_point = np.array([-1.0,1.0]), max_iter=50,
                                           decay=1.0)

        print ('Número de iteraciones: ', it)
        print ('Coordenadas obtenidas: (', hist[-1][0], ', ', hist[-1][1],')')
        print ('Valor de la función objetivo: (', hist_loss_NR[-1],')')

        #display_gd(f_arr, hist, title="BONUS: método de Newton-Raphson 1", x_min=-2, x_max=0, y_min=0, y_max=2)
        esperar()

        #Mostramos en una gráfica la evolución de la función de pérdida a lo largo de
        # cada experimento.
        plt.plot(np.arange( max(len(hist_loss_NR), len(hist_loss_NR)) ),
                 hist_loss_NR, label="NR")
        plt.legend()
        plt.show()
        esperar()

        print("Optimización usando versión modificada de Newton:")
        hist, it, hist_loss_NR = NR_method2(loss=f, gradLoss=gradf, hessianLoss=hessianf,
                                           lr=0.1, initial_point = np.array([-1.0,1.0]), max_iter=50,
                                           decay=1.0)

        print ('Número de iteraciones: ', it)
        print ('Coordenadas obtenidas: (', hist[-1][0], ', ', hist[-1][1],')')
        print ('Valor de la función objetivo: (', hist_loss_NR[-1],')')

        #display_gd(f_arr, hist, title="BONUS: método de Newton-Raphson 2", x_min=-2, x_max=0, y_min=0, y_max=2)
        esperar()

        plt.plot(np.arange( max(len(hist_loss_NR), len(hist_loss_NR)) ),
                 hist_loss_NR, label="NR; lr = 0.1")
        plt.plot(np.arange( max(len(self.hist_loss1), len(self.hist_loss2)) ),
                 self.hist_loss1, label="lr = 0.01")
        plt.plot(np.arange( max(len(self.hist_loss1), len(self.hist_loss2)) ),
                 self.hist_loss2, label="lr = 0.1")
        plt.legend()
        plt.show()
        esperar()

        print("Prueba con distintos puntos iniciales:")
        h1, _, hl1 = NR_method2(loss=f, gradLoss=gradf, hessianLoss=hessianf, lr=0.1, initial_point = np.array([-0.5,-0.5]), max_iter=50)
        h2, _, hl2 = NR_method2(loss=f, gradLoss=gradf, hessianLoss=hessianf, lr=0.1, initial_point = np.array([1.0,1.0]), max_iter=50)
        h3, _, hl3 = NR_method2(loss=f, gradLoss=gradf, hessianLoss=hessianf, lr=0.1, initial_point = np.array([2.1,-2.1]), max_iter=50)
        h4, _, hl4 = NR_method2(loss=f, gradLoss=gradf, hessianLoss=hessianf, lr=0.1, initial_point = np.array([-3.0,3.0]), max_iter=50)
        h5, _, hl5 = NR_method2(loss=f, gradLoss=gradf, hessianLoss=hessianf, lr=0.1, initial_point = np.array([-2.0,2.0]), max_iter=50)

        print ('Coordenadas (-0.5, -0.5): (%.4f, %.4f)' % (h1[-1][0], h1[-1][1]))
        print ('Función objetivo (-0.5, -0.5): %.4f' % hl1[-1])

        print ('Coordenadas (1.0,1.0): (%.4f, %.4f)' % (h2[-1][0], h2[-1][1]))
        print ('Función objetivo (1.0,1.0): %.4f' % hl2[-1])

        print ('Coordenadas (2.1,-2.1): (%.4f, %.4f)' % (h3[-1][0], h3[-1][1]))
        print ('Función objetivo (2.1,-2.1): %.4f' % hl3[-1])

        print ('Coordenadas (-3.0,3.0):(%.4f, %.4f)' % (h4[-1][0], h4[-1][1]))
        print ('Función objetivo (-3.0,3.0): %.4f' % hl4[-1])

        print ('Coordenadas (-2.0,2.0): (%.4f, %.4f)' % (h5[-1][0], h5[-1][1]))
        print ('Función objetivo (-2.0,2.0): %.4f' % hl5[-1])

def main():
    p1 = P1()
    p1.apartado1()
    esperar()

    p1.apartado2()
    esperar()

    p1.BONUS()
    esperar()

    if os.path.exists("temp-plot.html"):
        os.remove("temp-plot.html") # one file at a time

if __name__ == "__main__":
    main()