diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0e020b230f6d06f224446a553d8882298cdcd299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,156 @@ +# Added Tiz +tb_logs/ +*.npy + + +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject +### VirtualEnv template +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +pip-selfcheck.json +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +.idea/ + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties diff --git a/example.py b/example.py new file mode 100644 index 0000000000000000000000000000000000000000..42062e526c9ee3f26ff8f965426f5423122b776e --- /dev/null +++ b/example.py @@ -0,0 +1,73 @@ +# Example file to demonstrate the use of the VKOGA-PDE code: +# We use \Omega = [0, 1]^2, L = -LaPlace and the Gaussian kernel. + + +# Some imports +import numpy as np +from matplotlib import pyplot as plt + +from vkoga_pde.kernels_PDE import Gaussian_laplace +from vkoga_pde.vkoga_PDE import VKOGA_PDE + +np.random.seed(1) + +# Create data set: Square domain +dim = 2 + +scale_domain = 1 +X1 = scale_domain * np.random.rand(int(1e4), dim) +X2 = scale_domain * np.random.rand(400, dim) +X2[:100, 0] = 0 +X2[100:200, 0] = scale_domain +X2[200:300, 1] = 0 +X2[300:400, 1] = scale_domain + +# Define some functions +u = lambda x: x[:, [1]] * x[:, [0]]**3 +f = lambda x: -6 * x[:, [0]] * x[:, [1]] + +# Define right hand side values +y1 = f(X1) +y2 = u(X2) + +# Initialize and run models +kernel = Gaussian_laplace(dim=2) +maxIter = 200 + +model_f = VKOGA_PDE(kernel=kernel, greedy_type='f_greedy') +model_p = VKOGA_PDE(kernel=kernel, greedy_type='p_greedy') + +_ = model_f.fit(X1, y1, X2, y2, maxIter=maxIter) +_ = model_p.fit(X1, y1, X2, y2, maxIter=maxIter) + +# Plot some results +plt.figure(1) +plt.clf() +plt.plot(np.array(model_f.train_hist['f'])**.5, 'r') +plt.plot(np.array(model_p.train_hist['f'])**.5, 'b') +plt.yscale('log') +plt.xscale('log') +plt.legend(['f-greedy', 'p-greedy']) +plt.title('residuals') +plt.draw() + +plt.figure(2) +plt.clf() +plt.plot(model_f.ctrs_[model_f.ind_ctrs1, 0], model_f.ctrs_[model_f.ind_ctrs1, 1], 'rx') +plt.plot(model_f.ctrs_[model_f.ind_ctrs2, 0], model_f.ctrs_[model_f.ind_ctrs2, 1], 'ro') +plt.title('Selected centers of f-greedy') +plt.draw() + +plt.figure(3) +plt.clf() +plt.plot(model_p.ctrs_[model_p.ind_ctrs1, 0], model_p.ctrs_[model_p.ind_ctrs1, 1], 'bx') +plt.plot(model_p.ctrs_[model_p.ind_ctrs2, 0], model_p.ctrs_[model_p.ind_ctrs2, 1], 'bo') +plt.title('Selected centers of P-greedy') +plt.draw() + +plt.figure(4) +plt.clf() +plt.plot(X1[:, 0], X1[:, 1], 'm.') +plt.plot(X2[:, 0], X2[:, 1], 'r.') + + diff --git a/kernels_PDE.py b/kernels_PDE.py new file mode 100644 index 0000000000000000000000000000000000000000..5caac657bbc141b1f45ff04f796fae119c2f4b3b --- /dev/null +++ b/kernels_PDE.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 + +from abc import ABC, abstractmethod +from scipy.spatial import distance_matrix +import numpy as np + + +# Abstract kernel +class Kernel(ABC): + @abstractmethod + def __init__(self): + super().__init__() + + @abstractmethod + def eval(self, x, y): + pass + + def eval_prod(self, x, y, v, batch_size=100): + N = x.shape[0] + num_batches = int(np.ceil(N / batch_size)) + mat_vec_prod = np.zeros((N, 1)) + for idx in range(num_batches): + idx_begin = idx * batch_size + idx_end = (idx + 1) * batch_size + A = self.eval(x[idx_begin:idx_end, :], y) + mat_vec_prod[idx_begin:idx_end] = A @ v + return mat_vec_prod + + @abstractmethod + def diagonal(self, X): + pass + + @abstractmethod + def __str__(self): + pass + + @abstractmethod + def set_params(self, params): + pass + + +# Abstract RBF +class L_RBF(Kernel): + @abstractmethod + def __init__(self): + super(L_RBF, self).__init__() + + def eval(self, x, y): + return self.rbf(self.ep, distance_matrix(np.atleast_2d(x), np.atleast_2d(y))) + + def diagonal(self, X): + return np.ones(X.shape[0]) * self.rbf(self.ep, 0) + + def __str__(self): + return self.name + ' [gamma = %2.2e]' % self.ep + + def set_params(self, par): + self.ep = par + + def dd_diagonal(self, X): + # Evaluation of (L^x - shift) (L^y - shift) k(x,y) for x=y: Likely some constant + + return np.ones(X.shape[0]) * self.dd_eval(np.zeros((1, 1)), np.zeros((1, 1))).item() + + def dd_eval(self, x, y): + # Evaluation of (L^x - shift) (L^y - shift) k(x,y) + + array_dists = distance_matrix(np.atleast_2d(x), np.atleast_2d(y)) + + return self.rbf_utility(self.ep, array_dists) \ + - 2 * self.shift * self.d2_eval(x, y) - self.shift ** 2 * self.rbf(self.ep, array_dists) + + def d2_eval(self, x, y): + # Evaluation of (L^z - shift * Id) k(*,z)|_{x,y} + # Required exactly like this for "compute the nth basis", see also 14.12.21\A1 + + array_dists = distance_matrix(np.atleast_2d(x), np.atleast_2d(y)) + + array_result = - ((self.dim - 1) * self.rbf1_divided_by_r(self.ep, array_dists) + + self.rbf2(self.ep, array_dists)) \ + - self.shift * self.rbf(self.ep, array_dists) + + return array_result + + def d1_eval(self, x, y): + # Evaluation of (L - shift * Id) k(*,y)|_{x} + # Required exactly like this for "compute the nth basis", see also 14.12.21\A1 + + # Since the LaPlace is radial, the formula is the same as d2_eval + return self.d2_eval(x, y) + + def mixed_eval_L(self, x, ctrs, list_ind_interior): + # Kernel evaluation L^x k(x, y), whereby y1 := y[ind_interior, :] are centers in the interior, + # y2 := y[~ind_interior, :] are centers on the boundary. + # Therefore ddeval needs to be used with respect to y1, but deval with respect to y2. + + list_not_ind_interior = [idx for idx in range(x.shape[0]) if idx not in list_ind_interior] + + array_result = np.zeros((x.shape[0], ctrs.shape[0])) + + array_result[list_ind_interior, :] = self.dd_eval(x[list_ind_interior, :], ctrs) + array_result[list_not_ind_interior, :] = self.d1_eval(x[list_not_ind_interior, :], ctrs) + + return array_result + + def mixed_eval(self, x, ctrs, list_ind_interior): + # Kernel evaluation k(x, y), whereby y1 := y[ind_interior, :] are centers in the interior, + # y2 := y[~ind_interior, :] are centers on the boundary. + # Therefore deval needs to be used with respect to y1, but eval with respect to y2. + + list_not_ind_interior = [idx for idx in range(x.shape[0]) if idx not in list_ind_interior] + + array_result = np.zeros((x.shape[0], ctrs.shape[0])) + + array_result[list_ind_interior, :] = self.d2_eval(x[list_ind_interior, :], ctrs) + array_result[list_not_ind_interior, :] = self.eval(x[list_not_ind_interior, :], ctrs) + + return array_result + + +# Implementation of concrete RBFs +class Gaussian_laplace(L_RBF): + # # Gaussian kernel phi(r) = exp(-r^2), + # used in conjunction with L = -LaPlace - shift * Id, whereby shift is some constant + # (e.g. an eigenvalue of -LaPlace). + + def __init__(self, dim, shift=0, ep=1): + super().__init__() + self.dim = dim + self.shift = shift + self.ep = 1 # ToDo: Generalize this one!!! + self.name = 'gauss' + + # Implement not only the rbf, but also its derivatives + self.rbf = lambda ep, r: np.exp(-r**2) + + self.rbf1 = lambda ep, r: -2 * r * np.exp(-r**2) + self.rbf1_divided_by_r = lambda ep, r: -2 * np.exp(-r**2) + + self.rbf2 = lambda ep, r: (4 * r ** 2 - 2) * np.exp(-r**2) + + # Utility RBF for dd_eval: Double LaPlacian + self.rbf_utility = lambda ep, r: \ + np.exp(-r ** 2) * (16 * r ** 4 - (16 * self.dim + 32) * r ** 2 + (4 * self.dim ** 2 + 8 * self.dim)) + + +class wendland32_laplace(L_RBF): + # Wendland kernel 3, 2: phi(r) = (1-r)_+^6 * (35r**2 + 18r + 3) + # used in conjunction with L = -LaPlace - shift * Id, whereby shift is some constant + # (e.g. an eigenvalue of -LaPlace). + + # Only for dim <= 3! + + def __init__(self, dim, shift=0, ep=1): + super().__init__() + self.dim = dim + self.shift = shift + self.ep = 1 # ep ToDo: Implement general case! + self.name = 'Wendland 3, 2' + + # Implement not only the rbf, but also its derivatives + self.rbf = lambda ep, r: 1/3 * np.maximum(1-r, 0)**6 * (35 * r**2 + 18 * r + 3) + + self.rbf1 = lambda ep, r: -1/3 * np.maximum(1 - r, 0) ** 5 * 56 * r * (5 * r + 1) + self.rbf1_divided_by_r = lambda ep, r: -1/3 * np.maximum(1 - r, 0) ** 5 * 56 * (5 * r + 1) + + self.rbf2 = lambda ep, r: 1/3 * np.maximum(1 - r, 0) ** 4 * 56 * (35 * r ** 2 - 4 * r - 1) + + # Utility RBF for dd_eval: Double LaPlacian + self.rbf_utility = lambda ep, r: \ + 1/3 * (1680*self.dim**2 + 3360*self.dim + + r**4*(1680*self.dim**2 + 16800*self.dim + 40320) + + r**3*(-6720*self.dim**2 - 53760*self.dim - 100800) + + r**2*(10080*self.dim**2 + 60480*self.dim + 80640) + + r*(-6720*self.dim**2 - 26880*self.dim - 20160)) * (r < 1) + + +class wendland33_laplace(L_RBF): + # Wendland kernel 3, 3: phi(r) = (1-r)_+^8 * (32r**3+ 25r^2 + 8r + 1) + # used in conjunction with L = -LaPlace - shift * Id, whereby shift is some constant + # (e.g. an eigenvalue of -LaPlace). + + # Only for dim <= 3! + + def __init__(self, dim, shift=0, ep=1): + super().__init__() + self.dim = dim + self.shift = shift + self.ep = 1 # ep ToDo: Implement general case! + self.name = 'wendland 3, 3' + + # Implement not only the rbf, but also its derivatives + self.rbf = lambda ep, r: np.maximum(1 - r, 0) ** 8 * (32 * r ** 3 + 25 * r ** 2 + 8 * r + 1) + + self.rbf1 = lambda ep, r: - np.maximum(1 - r, 0) ** 7 * 22 * r * (16 * r ** 2 + 7 * r + 1) + self.rbf1_divided_by_r = lambda ep, r: - np.maximum(1 - r, 0) ** 7 * 22 * (16 * r ** 2 + 7 * r + 1) + + self.rbf2 = lambda ep, r: np.maximum(1 - r, 0) ** 6 * 22 * (160 * r ** 3 + 15 * r ** 2 - 6 * r - 1) + + # Utility RBF for dd_eval: Double LaPlacian + self.rbf_utility = lambda ep, r: \ + (528 * self.dim ** 2 + 1056 * self.dim + r ** 7 * (3168 * self.dim ** 2 + 50688 * self.dim + 199584) + + r ** 6 * (-18480 * self.dim ** 2 - 258720 * self.dim - 887040) + + r ** 5 * (44352 * self.dim ** 2 + 532224 * self.dim + 1552320) + + r ** 4 * (-55440 * self.dim ** 2 - 554400 * self.dim - 1330560) + + r ** 3 * (36960 * self.dim ** 2 + 295680 * self.dim + 554400) + + r ** 2 * (-11088 * self.dim ** 2 - 66528 * self.dim - 88704)) * (r < 1) + + + + +class quadrMatern_laplace(L_RBF): + # Quadratic matern kernel phi(r) = (3 + 3r + r^2)*exp(-r), + # used in conjunction with L = -LaPlace + + def __init__(self, dim, shift=0, ep=1): + super().__init__() + self.dim = dim + self.shift = shift + self.ep = 1 # ep ToDo: Implement general case! + self.name = 'quadratic Matern' + + # Implement not only the rbf, but also its derivatives + self.rbf = lambda ep, r: np.exp(-r) * (3 + 3 *r + r ** 2) + + self.rbf1 = lambda ep, r: np.exp(-r) * (-r) * (r+1) + self.rbf1_divided_by_r = lambda ep, r: np.exp(-r) * (-1) * (r+1) + + self.rbf2 = lambda ep, r: np.exp(-r) * (r ** 2 - r - 1) + + # Utility RBF for dd_eval: Double LaPlacian + self.rbf_utility = lambda ep, r: \ + np.exp(-r) * (r**2 - (2 * self.dim + 3) * r + self.dim ** 2 + 2 * self.dim) + + + + diff --git a/vkoga_PDE.py b/vkoga_PDE.py new file mode 100644 index 0000000000000000000000000000000000000000..534434682dacf791e1e9af8ef5611810d242dac8 --- /dev/null +++ b/vkoga_PDE.py @@ -0,0 +1,594 @@ +# First approach to develope code for greedy PDE. +# ToDo: Not sure whether vector valued output makes sense, but let's try to implement it .. + +from vkoga_pde.kernels_PDE import wendland32_laplace +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.utils.validation import check_X_y, check_array + + +# VKOGA implementation +class VKOGA_PDE(BaseEstimator): + + def __init__(self, kernel=wendland32_laplace(dim=2), kernel_par=1, + verbose=True, n_report=10, + greedy_type='p_greedy', reg_par=0, restr_par=0, + tol_f=1e-10, tol_p=1e-10, beta=None): + + # Set the verbosity on/off + self.verbose = verbose + + # Set the frequency of report + self.n_report = n_report + + # Set the params defining the method + self.kernel = kernel + self.kernel_par = kernel_par + self.reg_par = reg_par + self.restr_par = restr_par + + # Define the greedy method properly + if beta is None: + self.greedy_type = greedy_type + if self.greedy_type == 'f_greedy': + self.beta = 1.0 + elif self.greedy_type == 'p_greedy': + self.beta = 0.0 + else: + self.beta = float(beta) + if self.beta == 1.0: + self.greedy_type = 'f_greedy' + elif self.beta == 0.0: + self.greedy_type = 'p_greedy' + else: + self.greedy_type = 'beta' + + # ToDo: Did never think about the regularization parameter .. + assert reg_par == 0, 'reg_par > 0 might not be implemented correctly!' + + self.flag_val = None + + # Set the stopping values + self.tol_f = tol_f + self.tol_p = tol_p + + # Some further settings + self.ctrs_ = None # centers for the interior + self.ind_ctrs1 = [] # self.ctrs1_ = self.ctrs_[self.indI_ctrs1], i.e. interior + self.ind_ctrs2 = [] # self.ctrs2_ = self.ctrs_[self.indI_ctrs2], i.e. boundary + + self.Cut_ = None + self.c = None + + # Initialize the convergence history + self.train_hist = {} + self.train_hist['n'] = [] + self.train_hist['f'] = [] + self.train_hist['f1'] = [] + self.train_hist['f2'] = [] + self.train_hist['p'] = [] + self.train_hist['p1'] = [] + self.train_hist['p2'] = [] + self.train_hist['p selected'] = [] # list of selected power vals + self.train_hist['f val'] = [] + self.train_hist['p val'] = [] + + # Initialize variables for the maximal kernel diagonal value in the interior and the boundary + # ToDo: So far only f-greedy and P-greedy is implemented in a meaningful way, + # for all other \beta values the weighting of the power function (via the kernel + # diagonal values) is not implemented. + self.max_kernel_diag1 = None + self.max_kernel_diag2 = None + + def selection_rule(self, f1, f2, p1, p2): + # Generalization of selection rule: We have residual values and power function values + # for both the interior (f1, p1) as well as for the boundary (p1, p2). + + # ToDo: So far only f-greedy and P-greedy is implemented in a meaningful way, + # for all other \beta values the weighting of the power function (via the kernel + # diagonal values) is not implemented. + + if self.restr_par > 0: + # ToDo: Not sure whether this will really work, restr_idx might be empty + + p_ = max([np.max(p1), np.max(p2)]) + restr_idx1 = np.nonzero(p1 >= self.restr_par * p_)[0] + restr_idx2 = np.nonzero(p2 >= self.restr_par * p_)[0] + else: + restr_idx1 = np.arange(len(p1)) + restr_idx2 = np.arange(len(p2)) + + f1 = np.sum(f1 ** 2, axis=1) + f2 = np.sum(f2 ** 2, axis=1) + + # Case distinction required for fp_greedy + if self.greedy_type == 'fp_greedy': + array_weighted1 = f1[restr_idx1] / p1[restr_idx1] + idx1 = np.argmax(array_weighted1) + + if len(f2[restr_idx2]) != 0: # required especially in 1D + array_weighted2 = f2[restr_idx2] / p2[restr_idx2] + idx2 = np.argmax(array_weighted2) + + if array_weighted1[idx1] > array_weighted2[idx2]: + one_or_two = 1 + idx = restr_idx1[idx1] + else: + one_or_two = 2 + idx = restr_idx2[idx2] + else: + one_or_two = 1 + idx = restr_idx1[idx1] + elif self.greedy_type == 'p_greedy': + # extra implementation for P-greedy to address weighting of powerfunc (interior, boundary) + array_weighted1 = p1[restr_idx1] + idx1 = np.argmax(array_weighted1) + + if len(f2[restr_idx2]) != 0: # required especially in 1D + array_weighted2 = p2[restr_idx2] + idx2 = np.argmax(array_weighted2) + + # Pay attention at the weighting of the power functions + if array_weighted1[idx1] / self.max_kernel_diag1 \ + > array_weighted2[idx2] / self.max_kernel_diag2: + one_or_two = 1 + idx = restr_idx1[idx1] + else: + one_or_two = 2 + idx = restr_idx2[idx2] + else: + one_or_two = 1 + idx = restr_idx1[idx1] + + else: + array_weighted1 = f1[restr_idx1] ** self.beta * p1[restr_idx1] ** (1 - self.beta) + idx1 = np.argmax(array_weighted1) + + if len(f2[restr_idx2]) != 0: # required especially in 1D + array_weighted2 = f2[restr_idx2] ** self.beta * p2[restr_idx2] ** (1 - self.beta) + idx2 = np.argmax(array_weighted2) + + if array_weighted1[idx1] > array_weighted2[idx2]: + one_or_two = 1 + idx = restr_idx1[idx1] + else: + one_or_two = 2 + idx = restr_idx2[idx2] + else: + one_or_two = 1 + idx = restr_idx1[idx1] + + # ToDo: This implementation is quite laborious .. + if self.greedy_type == 'p_greedy': + f_max1, p_max1 = np.max(f1), np.max(p1) / self.max_kernel_diag1 + else: + f_max1, p_max1 = np.max(f1), np.max(p1) + + if len(f2[restr_idx2]) != 0: + if self.greedy_type == 'p_greedy': + f_max2, p_max2 = np.max(f2), np.max(p2) / self.max_kernel_diag2 + else: + f_max2, p_max2 = np.max(f2), np.max(p2) + else: + f_max2, p_max2 = 0, 0 + + return (one_or_two, idx), f_max1, f_max2, p_max1, p_max2 + + def fit(self, X1, y1, X2, y2, + X1_val=None, y1_val=None, X2_val=None, y2_val=None, maxIter=None): + # Points in X1 are considered as interior points, points in X2 as boundary points. + # They can coincide (partly), but for X1 we use the L-evaluation, for X2 we use the point evaluation. + + # Compute the maximal kernel diagonal value in the interior and the boundary + self.max_kernel_diag2 = np.max(self.kernel.diagonal(X2)) + self.max_kernel_diag1 = np.max(self.kernel.dd_diagonal(X1)) + + list_ctrs = [] # not that beautiful, but required to store the final centers + + # Check the datasets + X1, y1 = check_X_y(X1, y1, multi_output=True) + X2, y2 = check_X_y(X2, y2, multi_output=True) # ToDo: Check for consistency of X1, X2, X1_val, X2_val + + if len(y1.shape) == 1: + y1 = y1[:, None] + if len(y2.shape) == 1: + y2 = y2[:, None] + + # Check the validation datasets (if provided, otherwise set to None) + if X1_val is None or y1_val is None or X2_val is None or y2_val is None: + X1_val = None + y1_val = None + X2_val = None + y2_val = None + self.flag_val = False + else: + self.flag_val = True + X1_val, y1_val = check_X_y(X1_val, y2_val, multi_output=True) + X2_val, y2_val = check_X_y(X2_val, y2_val, multi_output=True) + # We will assume in the following that X_val and X are disjoint + + if len(y1_val.shape) == 1: + y1_val = y1_val[:, None] + if len(y2_val.shape) == 1: + y2_val = y2_val[:, None] + + # Check whether already fitted + if self.ctrs_ is None: + self.ctrs_ = np.zeros((0, X1.shape[1])) + self.Cut_ = np.zeros((0, 0)) + self.c = np.zeros((0, y1.shape[1])) + + # ToDo: If self.ctrs_ is not None, does this work? + + # Check whether "new X1" and previously chosen centers overlap + list_truly_new_X1 = [] + if self.ctrs_.shape[0] > 0: + for idx_x in range(X1.shape[0]): + if min(np.linalg.norm(self.ctrs_ - X1[idx_x, :], axis=1)) < 1e-10: + continue + else: + list_truly_new_X1.append(idx_x) + else: + list_truly_new_X1 = list(range(X1.shape[0])) + X1 = X1[list_truly_new_X1, :] + y1 = y1[list_truly_new_X1, :] + + # Check whether "new X2" and previously chosen centers overlap + list_truly_new_X2 = [] + if self.ctrs_.shape[0] > 0: + for idx_x in range(X2.shape[0]): + if min(np.linalg.norm(self.ctrs_ - X2[idx_x, :], axis=1)) < 1e-10: + continue + else: + list_truly_new_X2.append(idx_x) + else: + list_truly_new_X2 = list(range(X2.shape[0])) + + X2 = X2[list_truly_new_X2, :] + y2 = y2[list_truly_new_X2, :] + + # Initialize the residual and update the given y values by substracting the current model + y1 = y1 - self.predict_Ls(X1) + y2 = y2 - self.predict_s(X2) + + if self.flag_val: + y1_val = y1_val - self.predict_Ls(X1_val) + y2_val = y2_val - self.predict_s(X2_val) + + # Get the data dimension + N, q = y1.shape[0] + y2.shape[0], y1.shape[1] + N1, N2 = y1.shape[0], y2.shape[0] + if self.flag_val: + N_val = y1_val.shape[0] + y2_val.shape[0] + N1_val, N2_val = y1_val.shape[0], y2_val.shape[0] + + # Set maxIter_continue + if maxIter is None or maxIter > N: + self.maxIter = min([N, 100]) + else: + self.maxIter = maxIter + + # Check compatibility of restriction + if self.beta == 0.0: # in case of P-greedy + self.restr_par = 0 + if not self.reg_par == 0: + self.restr_par = 0 + + # Initialize list for the chosen and non-chosen indices - seperately for interior and boundary + indI1_, indI2_ = [], [] + notIndI1, notIndI2 = list(range(y1.shape[0])), list(range(y2.shape[0])) + c = np.zeros((self.maxIter, q)) + + # Compute the Newton basis values (related to the old centers) on the new X + # ToDo: I think restart training etc does not work properly, since it is not debugged propely!! + # Thus the following lines of codes may be irrelevant + Vx_new_X1_old_ctrs = self.kernel.mixed_eval(X1, self.ctrs_, self.ind_ctrs1) @ self.Cut_.transpose() + Vx_new_X2_old_ctrs = self.kernel.mixed_eval(X2, self.ctrs_, self.ind_ctrs1) @ self.Cut_.transpose() + if self.flag_val: + Vx_val_new_X1_old_ctrs = self.kernel.mixed_eval( + X1_val, self.ctrs_, self.ind_ctrs1) @ self.Cut_.transpose() + Vx_val_new_X2_old_ctrs = self.kernel.mixed_eval( + X2_val, self.ctrs_, self.ind_ctrs1) @ self.Cut_.transpose() + + # Compute the application of L to the Newton basis values (related to the old centers) on the new X + LVx_new_X1_old_ctrs = self.kernel.mixed_eval_L(X1, self.ctrs_, self.ind_ctrs1) @ self.Cut_.transpose() + LVx_new_X2_old_ctrs = self.kernel.mixed_eval_L(X2, self.ctrs_, self.ind_ctrs1) @ self.Cut_.transpose() + if self.flag_val: + LVx_val_new_X1_old_ctrs = self.kernel.mixed_eval_L( + X1_val, self.ctrs_, self.ind_ctrs1) @ self.Cut_.transpose() + LVx_val_new_X2_old_ctrs = self.kernel.mixed_eval_L( + X2_val, self.ctrs_, self.ind_ctrs1) @ self.Cut_.transpose() + + # Initialize arrays for the (L-evaluation of the) Newton basis values .. + # .. (related to the new centers) on the new X1, X2 + Vx1 = np.zeros((N1, self.maxIter)) + Vx2 = np.zeros((N2, self.maxIter)) + LVx1 = np.zeros((N1, self.maxIter)) + LVx2 = np.zeros((N2, self.maxIter)) + + if self.flag_val: + Vx1_val = np.zeros((N1_val, self.maxIter)) + Vx2_val = np.zeros((N2_val, self.maxIter)) + LVx1_val = np.zeros((N1_val, self.maxIter)) + LVx2_val = np.zeros((N2_val, self.maxIter)) + + # Compute the powervals on X1, X2 and X1_val, X2_val + p1 = self.kernel.dd_diagonal(X1) + self.reg_par + p1 = p1 - np.sum(LVx_new_X1_old_ctrs ** 2, axis=1) + + p2 = self.kernel.diagonal(X2) + self.reg_par + p2 = p2 - np.sum(Vx_new_X2_old_ctrs ** 2, axis=1) + + if self.flag_val: + p1_val = self.kernel.dd_diagonal(X1_val) + self.reg_par + p1_val = p1_val - np.sum(LVx_val_new_X1_old_ctrs ** 2, axis=1) + + p2_val = self.kernel.diagonal(X2_val) + self.reg_par + p2_val = p2_val - np.sum(Vx_val_new_X2_old_ctrs ** 2, axis=1) + + # Extend Cut_ matrix, i.e. continue to build on old self.Cut_ matrix + N_ctrs_so_far, N_ctrs_so_far1, N_ctrs_so_far2 = \ + self.Cut_.shape[0], len(self.ind_ctrs1), len(self.ind_ctrs2) + + Cut_ = np.zeros((N_ctrs_so_far + self.maxIter, N_ctrs_so_far + self.maxIter)) + Cut_[:N_ctrs_so_far, :N_ctrs_so_far] = self.Cut_ + + # Iterative selection of new points + self.print_message('begin') + + n1, n2 = 0, 0 # number of centers in interior or boundary + for n in range(self.maxIter): + # prepare + self.train_hist['n'].append(self.ctrs_.shape[0] + n + 1) + self.train_hist['f'].append([]) + self.train_hist['f1'].append([]) + self.train_hist['f2'].append([]) + self.train_hist['p'].append([]) + self.train_hist['p1'].append([]) + self.train_hist['p2'].append([]) + self.train_hist['p selected'].append([]) + if self.flag_val: + self.train_hist['p val'].append([]) + self.train_hist['f val'].append([]) + + # select the current index + (one_or_two, idx), self.train_hist['f1'][-1], self.train_hist['f2'][-1], \ + self.train_hist['p1'][-1], self.train_hist['p2'][-1] \ + = self.selection_rule(y1[notIndI1], y2[notIndI2], p1[notIndI1], p2[notIndI2]) + self.train_hist['f'][-1] = max(self.train_hist['f1'][-1], self.train_hist['f2'][-1]) + self.train_hist['p'][-1] = max(self.train_hist['p1'][-1], self.train_hist['p2'][-1]) + + # add the current index and store selected p value and store the selected center + if one_or_two == 1: + self.train_hist['p selected'][-1] = p1[notIndI1[idx]] + indI1_.append(notIndI1[idx]) + + self.ind_ctrs1.append(N_ctrs_so_far + n) + list_ctrs.append(X1[[notIndI1[idx]], :]) + + elif one_or_two == 2: + self.train_hist['p selected'][-1] = p2[notIndI2[idx]] + indI2_.append(notIndI2[idx]) + + self.ind_ctrs2.append(N_ctrs_so_far + n) + list_ctrs.append(X2[[notIndI2[idx]], :]) + + if self.flag_val: + self.train_hist['p val'][-1] = max([np.max(p1_val), np.max(p2_val)]) + self.train_hist['f val'][-1] = max( + [np.max(np.sum(y1_val ** 2, axis=1)), np.max(np.sum(y2_val ** 2, axis=1))]) + + # check if the tolerances are reacheded + if self.train_hist['f'][n] <= self.tol_f or self.train_hist['p'][n] <= self.tol_p: + n = n - 1 + if one_or_two == 1: + n1 = n1 - 1 + elif one_or_two == 2: + n2 = n2 - 1 + self.print_message('end') + break + + # compute the nth basis (including normalization), see also 14.12.21\A1 + if one_or_two == 1: + # Computations (this is simply expansion in Newton basis) .. + Vx1[notIndI1, n] = self.kernel.d2_eval(X1[notIndI1, :], X1[indI1_[n1], :])[:, 0] \ + - Vx_new_X1_old_ctrs[notIndI1, :] @ LVx_new_X1_old_ctrs[indI1_[n1], :].transpose() \ + - Vx1[notIndI1, :] @ LVx1[indI1_[n1], :].transpose() + Vx2[notIndI2, n] = self.kernel.d2_eval(X2[notIndI2, :], X1[indI1_[n1], :])[:, 0] \ + - Vx_new_X2_old_ctrs[notIndI2, :] @ LVx_new_X1_old_ctrs[indI1_[n1], :].transpose() \ + - Vx2[notIndI2, :] @ LVx1[indI1_[n1], :].transpose() + + LVx1[notIndI1, n] = self.kernel.dd_eval(X1[notIndI1, :], X1[indI1_[n1], :])[:, 0] \ + - LVx_new_X1_old_ctrs[notIndI1, :] @ LVx_new_X1_old_ctrs[indI1_[n1], :].transpose() \ + - LVx1[notIndI1, :] @ LVx1[indI1_[n1], :].transpose() + LVx2[notIndI2, n] = self.kernel.dd_eval(X2[notIndI2, :], X1[indI1_[n1], :])[:, 0] \ + - LVx_new_X2_old_ctrs[notIndI2, :] @ LVx_new_X1_old_ctrs[indI1_[n1], :].transpose() \ + - LVx2[notIndI2, :] @ LVx1[indI1_[n1], :].transpose() + + Vx1[indI1_[n1], n] += self.reg_par # ToDo: This is likely wrong/incomplete/.. + + # Normalizations .. + Vx1[notIndI1, n] = Vx1[notIndI1, n] / np.sqrt(p1[indI1_[n1]]) + Vx2[notIndI2, n] = Vx2[notIndI2, n] / np.sqrt(p1[indI1_[n1]]) + LVx1[notIndI1, n] = LVx1[notIndI1, n] / np.sqrt(p1[indI1_[n1]]) + LVx2[notIndI2, n] = LVx2[notIndI2, n] / np.sqrt(p1[indI1_[n1]]) + + # ToDo: Take care of the validation sets .. + # if self.flag_val: + # Vx_val[:, n] = self.kernel.eval(X_val, X[indI_[n], :])[:, 0]\ + # - Vx_val_new_X_old_ctrs[:, :] @ Vx_new_X_old_ctrs[indI_[n], :].transpose()\ + # - Vx_val[:, :n+1] @ Vx[indI_[n], 0:n+1].transpose() + # Vx_val[:, n] = Vx_val[:, n] / np.sqrt(p[indI_[n]]) + + elif one_or_two == 2: + # Computations (this is simply expansion in Newton basis) .. + Vx1[notIndI1, n] = self.kernel.eval(X1[notIndI1, :], X2[indI2_[n2], :])[:, 0] \ + - Vx_new_X1_old_ctrs[notIndI1, :] @ Vx_new_X2_old_ctrs[indI2_[n2], :].transpose() \ + - Vx1[notIndI1, :] @ Vx2[indI2_[n2], :].transpose() + Vx2[notIndI2, n] = self.kernel.eval(X2[notIndI2, :], X2[indI2_[n2], :])[:, 0] \ + - Vx_new_X2_old_ctrs[notIndI2, :] @ Vx_new_X2_old_ctrs[indI2_[n2], :].transpose() \ + - Vx2[notIndI2, :] @ Vx2[indI2_[n2], :].transpose() + + LVx1[notIndI1, n] = self.kernel.d1_eval(X1[notIndI1, :], X2[indI2_[n2], :])[:, 0] \ + - LVx_new_X1_old_ctrs[notIndI1, :] @ Vx_new_X2_old_ctrs[indI2_[n2], :].transpose() \ + - LVx1[notIndI1, :] @ Vx2[indI2_[n2], :].transpose() + LVx2[notIndI2, n] = self.kernel.d1_eval(X2[notIndI2, :], X2[indI2_[n2], :])[:, 0] \ + - LVx_new_X2_old_ctrs[notIndI2, :] @ Vx_new_X2_old_ctrs[indI2_[n2], :].transpose() \ + - LVx2[notIndI2, :] @ Vx2[indI2_[n2], :].transpose() + + Vx2[indI2_[n2], n] += self.reg_par # ToDo: This is likely wrong/incomplete/.. + + # Normalizations .. + Vx1[notIndI1, n] = Vx1[notIndI1, n] / np.sqrt(p2[indI2_[n2]]) + Vx2[notIndI2, n] = Vx2[notIndI2, n] / np.sqrt(p2[indI2_[n2]]) + LVx1[notIndI1, n] = LVx1[notIndI1, n] / np.sqrt(p2[indI2_[n2]]) + LVx2[notIndI2, n] = LVx2[notIndI2, n] / np.sqrt(p2[indI2_[n2]]) + + # ToDo: Take care of the validation sets + + # update the change of basis (see 14.12.21\B) + Cut_new_row = np.ones(N_ctrs_so_far + n + 1) # required only for the final one + + if one_or_two == 1: + Cut_new_row[:N_ctrs_so_far + n] = \ + -np.concatenate((LVx_new_X1_old_ctrs[indI1_[n1], :], LVx1[indI1_[n1], :n])) \ + @ Cut_[:N_ctrs_so_far + n, :N_ctrs_so_far + n] + + Cut_[N_ctrs_so_far + n, :N_ctrs_so_far + n + 1] = Cut_new_row / LVx1[indI1_[n1], n] + elif one_or_two == 2: + Cut_new_row[:N_ctrs_so_far + n] = \ + -np.concatenate((Vx_new_X2_old_ctrs[indI2_[n2], :], Vx2[indI2_[n2], :n])) \ + @ Cut_[:N_ctrs_so_far + n, :N_ctrs_so_far + n] + + Cut_[N_ctrs_so_far + n, :N_ctrs_so_far + n + 1] = Cut_new_row / Vx2[indI2_[n2], n] + + # compute the nth coefficient + if one_or_two == 1: + c[n] = y1[indI1_[n1]] / np.sqrt(p1[indI1_[n1]]) + elif one_or_two == 2: + c[n] = y2[indI2_[n2]] / np.sqrt(p2[indI2_[n2]]) + + # update the power functions: ToDo: For Gaussian in 1D we obtain negative power vals :( + p1[notIndI1] = p1[notIndI1] - LVx1[notIndI1, n] ** 2 + p2[notIndI2] = p2[notIndI2] - Vx2[notIndI2, n] ** 2 + + if self.flag_val: + p1_val = p1_val - LVx1_val[:, n] ** 2 + p2_val = p2_val - Vx2_val[:, n] ** 2 + + # update the residual + y1[notIndI1] = y1[notIndI1] - LVx1[notIndI1, n][:, None] * c[n] + y2[notIndI2] = y2[notIndI2] - Vx2[notIndI2, n][:, None] * c[n] + + if self.flag_val: + y1_val = y1_val - LVx1_val[:, n][:, None] * c[n] + y2_val = y2_val - Vx2_val[:, n][:, None] * c[n] + + # remove the nth index from the dictionary + if one_or_two == 1: + notIndI1.pop(idx) + n1 += 1 + elif one_or_two == 2: + notIndI2.pop(idx) + n2 += 1 + + # Report some data every now and then + if n % self.n_report == 0: + self.print_message('track') + + else: + self.print_message('end') + + # Define coefficients and centers + # ToDo: Check whether everything is clipped here correctly!! + self.c = np.concatenate((self.c, c[:n + 1])) + self.Cut_ = Cut_[:N_ctrs_so_far + n + 1, :N_ctrs_so_far + n + 1] + self.ind_ctrs1 = self.ind_ctrs1[:N_ctrs_so_far1 + n1 + 1] # ToDo: Check whether this is correct!!!! + self.ind_ctrs2 = self.ind_ctrs2[:N_ctrs_so_far2 + n2 + 1] + self.coef_ = self.Cut_.transpose() @ self.c + + self.ctrs_ = np.concatenate((self.ctrs_, + np.concatenate(list_ctrs[:n + 1], axis=0)), + axis=0) + + return self + + def predict_s(self, X): + # Prediction of s, which might/should approximate the true solution u + + X = check_array(X) + + # Compute prediction + if self.ctrs_.shape[0] > 0: + prediction = self.kernel.d2_eval(X, self.ctrs_[self.ind_ctrs1]) @ self.coef_[self.ind_ctrs1] \ + + self.kernel.eval(X, self.ctrs_[self.ind_ctrs2]) @ self.coef_[self.ind_ctrs2] + else: + prediction = np.zeros((X.shape[0], 1)) + + return prediction + + def predict_Ls(self, X): + # Prediction of Ls, i.e. application of L to the prediction + + # Validate the input + X = check_array(X) + + # Compute prediction + if self.ctrs_.shape[0] > 0: + prediction = self.kernel.dd_eval(X, self.ctrs_[self.ind_ctrs1]) @ self.coef_[self.ind_ctrs1] \ + + self.kernel.d1_eval(X, self.ctrs_[self.ind_ctrs2]) @ self.coef_[self.ind_ctrs2] + else: + prediction = np.zeros((X.shape[0], 1)) + + return prediction + + def print_message(self, when): + if self.verbose and when == 'begin': + print('') + print('*' * 30 + ' [VKOGA] ' + '*' * 30) + print('Training model with') + print(' |_ kernel : %s' % self.kernel) + print(' |_ regularization par. : %2.2e' % self.reg_par) + print(' |_ restriction par. : %2.2e' % self.restr_par) + print('') + + if self.verbose and when == 'end': + print('Training completed with') + print(' |_ selected points : %8d / %8d' % ( + self.train_hist['n'][-1], self.ctrs_.shape[0] + self.maxIter)) + if self.flag_val: + print(' |_ train, val residual : %2.2e / %2.2e, %2.2e' % + (self.train_hist['f'][-1], self.tol_f, self.train_hist['f val'][-1])) + print(' |_ train, val power fun: %2.2e / %2.2e, %2.2e' % + (self.train_hist['p'][-1], self.tol_p, self.train_hist['p val'][-1])) + else: + print(' |_ train residual : %2.2e / %2.2e' % (self.train_hist['f'][-1], self.tol_f)) + print(' |_ train power fun : %2.2e / %2.2e' % (self.train_hist['p'][-1], self.tol_p)) + + if self.verbose and when == 'track': + print('Training ongoing with') + print(' |_ selected points : %8d / %8d' % ( + self.train_hist['n'][-1], self.ctrs_.shape[0] + self.maxIter)) + if self.flag_val: + print(' |_ train, val residual : %2.2e / %2.2e, %2.2e' % + (self.train_hist['f'][-1], self.tol_f, self.train_hist['f val'][-1])) + print(' |_ train, val power fun: %2.2e / %2.2e, %2.2e' % + (self.train_hist['p'][-1], self.tol_p, self.train_hist['p val'][-1])) + else: + print(' |_ train residual : %2.2e / %2.2e' % (self.train_hist['f'][-1], self.tol_f)) + print(' |_ train power fun : %2.2e / %2.2e' % (self.train_hist['p'][-1], self.tol_p)) + + +# %% Utilities to +import pickle + + +def save_object(obj, filename): + with open(filename, 'wb') as output: + pickle.dump(obj, output, pickle.HIGHEST_PROTOCOL) + + +def load_object(filename): + with open(filename, 'rb') as input: + obj = pickle.load(input) + return obj