# Author: Mirko Fischer
# Date: 12.08.2024
# Version: 0.1
# License: MIT license
import numpy as np
import pandas as pd
from itertools import combinations_with_replacement
from sklearn.preprocessing import PolynomialFeatures
# All model classes within this module should implement a function called evaluate
# The evaluate function should take as arguments only the features (called X) and a noise parameter (noise)
[docs]
class inv_sphere:
"""
N-dimensional inverted sphere function. This is similar to the sphere function multiplied by -1
so that the function has a global maximum instead of a minimum.
The inverted sphere function is given as:
$$
f(x) = -\sum_{i=1}^{d}x_i^2
$$
We can also add some noise $\epsilon$:
$$
f(x) = -\sum_{i=1}^{d}x_i^2 + \epsilon
$$
Parameters:
-----------
d : int
Number of dimensions.
random_state : int, optional
Random state for reproducibility. The default value is 1.
Attributes:
-----------
n_features : int
Number of features.
rng : RandomNumberGenerator
Random Number Generator from numpy.
"""
def __init__(self, d=1, random_state=None):
self.n_features = d
self.rng = np.random.RandomState(seed=random_state)
[docs]
def evaluate(self, X, noise=0):
"""
Evaluation function.
Parameters:
-----------
X : nd_array
Input values (coordinates) for every dimension.
noise : float, optional
Additional noise to add to the model.
Returns:
--------
The function values for each data point.
"""
y = np.sum(np.power(X, 2), axis=-1)
n = self.rng.normal(loc=0, scale=noise, size=y.shape)
y = y + n
return -y
[docs]
class inv_rastrigin:
"""
N-dimensional inverted rastrigin function. This is similar to the rastrigin function multiplied by -1
so that the function has a global maximum instead of a minimum.
The inverted rastrigin function is given as:
$$
f(X) = - h \cdot n - \sum_{i=1}^{n} [x_i^2 - h \cos(2 \pi x_i)]
$$
We can also add some noise $\epsilon$:
$$
f(X) = - h \cdot n - \sum_{i=1}^{n} [x_i^2 - h \cos(2 \pi x_i)]+ \epsilon
$$
Parameters:
-----------
d : int
Number of dimensions.
random_state : int, optional
Random state for reproducibility. The default value is 1.
Attributes:
-----------
n_features : int
Number of features.
h : float, optional
Parameter for the Rastrigin function. The default value is 10.
rng : RandomNumberGenerator
Random Number Generator from numpy.
"""
def __init__(self, d=2, h=10, random_state=None):
self.n_features = d
self.h = h
self.rng = np.random.RandomState(seed=random_state)
[docs]
def evaluate(self, X, noise=0):
"""
Evaluation function.
Parameters:
-----------
X : nd_array
Input values (coordinates) for every dimension.
noise : float, optional
Additional noise to add to the model.
Returns:
--------
The function values for each data point.
"""
if self.n_features == 1:
X = X.reshape(
self.n_features,
*[
int(X.shape[0] ** (1 / self.n_features))
for _ in range(self.n_features)
]
)
y = self.h * self.n_features + np.sum(
[(x**2 - self.h * np.cos(2 * np.pi * x)) for x in X], axis=0
)
else:
y = self.h * self.n_features + np.sum(
[(x**2 - self.h * np.cos(2 * np.pi * x)) for x in X], axis=-1
)
n = self.rng.normal(loc=0, scale=noise, size=y.shape)
y = y + n
return -y
[docs]
class inv_rosenbrock:
"""
N-dimensional inverted rastrigin function, defined for at least two dimensions. This is similar to the rastrigin function multiplied by -1
so that the function has a global maximum instead of a minimum.
The inverted rastrigin function is given as:
$$
f(X) = - \sum_{i=1}^{n} [100 (x_{i+1} - x_i^2)^2 + (1-x_i)^2 ]
$$
We can also add some noise $\epsilon$:
$$
f(X) = - \sum_{i=1}^{n} [100 (x_{i+1} - x_i^2)^2 + (1-x_i)^2 ] + \epsilon
$$
Parameters:
-----------
d : int
Number of dimensions.
random_state : int, optional
Random state for reproducibility. The default value is 1.
Attributes:
-----------
n_features : int
Number of features.
rng : RandomNumberGenerator
Random Number Generator from numpy.
"""
def __init__(self, d=2, random_state=None):
if d < 2:
raise Exception("Rosenbrock not defined for dimension lower than 2.")
self.n_features = d
self.rng = np.random.RandomState(seed=random_state)
[docs]
def evaluate(self, X, noise=0):
"""
Evaluation function.
Parameters:
-----------
X : nd_array
Input values (coordinates) for every dimension.
noise : float, optional
Additional noise to add to the model.
Returns:
--------
The function values for each data point.
"""
N = self.n_features
y = 0
for i in range(N - 1):
y += 100 * (X[:, i + 1] - X[:, i] ** 2) ** 2 + (1 - X[:, i]) ** 2
n = self.rng.normal(loc=0, scale=noise, size=y.shape)
y = y + n
return -y
[docs]
class inv_alos:
"""
1-dimensional inverted ALOS ( Agglomeration of Locally Optimized Surrogate) function, taken from https://doi.org/10.48550/arXiv.2303.01560.
This is similar to the ALOS function multiplied by -1
so that the function has a global maximum instead of a minimum.
The inverted ALOS function is given as:
$$
f(X) = - \sin[ 30 (x-0.9)^4 ] \cos[ 2 (x-0.9) ] + (x-0.9)/2
$$
We can also add some noise $\epsilon$:
$$
f(X) = - \sin[ 30 (x-0.9)^4 ] \cos[ 2 (x-0.9) ] + (x-0.9)/2 + \epsilon
$$
Parameters:
-----------
d : int
Number of dimensions.
random_state : int, optional
Random state for reproducibility. The default value is 1.
Attributes:
-----------
n_features : int
Number of features.
rng : RandomNumberGenerator
Random Number Generator from numpy.
"""
def __init__(self, d=1, random_state=None):
self.n_features = d
self.rng = np.random.RandomState(seed=random_state)
[docs]
def evaluate(self, X, noise=0):
"""
Evaluation function.
Parameters:
-----------
X : nd_array
Input values (coordinates) for every dimension.
noise : float, optional
Additional noise to add to the model.
Returns:
--------
The function values for each data point.
"""
if self.n_features == 1:
X = X.flatten()
y = np.sin(30 * (X - 0.9) ** 4) * np.cos(2 * (X - 0.9)) + (X - 0.9) / 2
n = self.rng.normal(loc=0, scale=noise, size=y.shape)
y = y + n
return -y
[docs]
class PrefitModel:
"""
Use a prefit sklearn model as true model.
Parameters:
-----------
model : sklearn-model
A prefitted sklearn-model which is used as a model for the true data.
n_features : int
Number of features.
scaler : sklearn Scaler, optional
Scaler that is used to preprocess data for the model. A prefitted scaler should be used.
The default value is ``None``.
random_state : int, optional
Random state for reproducibility. The default value is 1.
fixed_features : dict
Dictionary with features from the prefit modle that are kept fixed and are not changed.
The key is the number of the fixed feature and the value the fixed value.
The default value is ``None``.
verbose : bool
Print debug information. The default value is ``False``.
Attributes:
-----------
model : sklearn-model
A prefitted sklearn-model which is used as a model for the true data.
scaler : sklearn Scaler, optional
Scaler that is used to preprocess data for the model. A prefitted scaler should be used.
The default value is ``None``.
n_features : int
Number of features.
rng : RandomNumberGenerator
Random Number Generator from numpy.
fixed_features : dict
Dictionary with features from the prefit modle that are kept fixed and are not changed.
The key is the number of the fixed feature and the value the fixed value.
The default value is ``None``.
verbose : bool
Print debug information. The default value is ``False``.
"""
def __init__(
self,
model,
n_features,
scaler=None,
fixed_features=None,
random_state=None,
verbose=False,
):
self.model = model
self.scaler = scaler
self.rng = np.random.RandomState(seed=random_state)
self.n_features = n_features
self.fixed_features = fixed_features
self.verbose = verbose
[docs]
def evaluate(self, grid, noise=0):
"""
Evaluation function.
Parameters:
-----------
grid : nd_array
Input values (coordinates) for every dimension.
noise : float, optional
Additional noise to add to the model.
Returns:
--------
The function values for each data point.
"""
if isinstance(self.fixed_features, dict):
if self.verbose:
print(
"Using fixed values for features {}".format(
list(self.fixed_features.keys())
)
)
print("The previous shape of the grid is {}".format(grid.shape))
total_features = grid.shape[1] + len(self.fixed_features)
keys = np.asarray(list(self.fixed_features.keys()), dtype=int)
grid_new = np.zeros((grid.shape[0], total_features))
k = 0
for i in range(total_features):
if i in keys:
grid_new[:, i] = self.fixed_features[str(i)]
else:
grid_new[:, i] = grid[:, k]
k += 1
if self.verbose:
print("The new shape of the grid is {}".format(grid_new.shape))
else:
grid_new = grid
if self.scaler != None:
grid = self.scaler.transform(grid_new)
y = self.model.predict(grid)
n = self.rng.normal(loc=0, scale=noise, size=len(y))
y = y + n
return y
[docs]
class PolyModel:
"""
Polynomial Model.
"""
def __init__(
self,
weights,
features="xy",
polynomial_degree=2,
scaler=None,
random_state=None,
):
self.n_features = len(features)
self.features = features
self.polynomial_degree = polynomial_degree
self.weights = self._create_feature_df(weights, features, polynomial_degree)
self.rng = np.random.RandomState(seed=random_state)
self.scaler = scaler
# print('The follwing weights have been assigned:')
# display(self.weights)
def _create_feature_df(self, coeffs, features, polynomial_degree):
final_combinations = []
for i in range(polynomial_degree + 1):
combi = list(combinations_with_replacement(features, i))
for c in combi:
c_str = ""
for cc in c:
c_str = c_str + cc
if c_str == "":
c_str = "const"
final_combinations.append(c_str)
coeffs = pd.DataFrame([coeffs], columns=final_combinations)
return coeffs
def _internal_evaluation(self, grid):
poly = PolynomialFeatures(degree=self.polynomial_degree)
poly_features = poly.fit_transform(grid.transpose())
if self.scaler != None:
poly_features = self.scaler.transform(poly_features)
y = (
np.sum(poly_features * np.array(self.weights.iloc[0]), axis=1)
+ self.weights.iloc[0]["const"]
)
return y
[docs]
def evaluate_on_grid(self, n_data, lim=(-2, 2)):
x = [np.linspace(*lim, n_data) for i in range(self.n_features)]
grid = np.array(np.meshgrid(*x))
grid = grid.reshape(self.n_features, n_data**self.n_features)
# y = np.zeros(n_data**self.n_features)
y = self._internal_evaluation(grid)
return grid.transpose(), y
[docs]
def evaluate(self, grid, noise=0):
grid = np.array(grid).transpose()
# n_data = len(grid)
# y = np.zeros(n_data)
y = self._internal_evaluation(grid)
n = self.rng.normal(loc=0, scale=noise, size=len(y))
y = y + n
return y
[docs]
class ArrheniusModel:
"""
An Arrhenius type model with the form:
.. math:: S = S_0 - S_1 (\\beta-\\beta_0) - S_2 (\\beta-\\beta_0)^2
where :math:`S_0` corresponds to the value of :math:`S` at the onset temperature
:math:`T_0`, :math:`S_1` is an activation energy and :math:`S_2` corresponds to deviations from Arrhenius behaviour.
:math:`\\beta` is the inverse temperature. A model for each of the :math:`S_i` is needed.
"""
def __init__(
self,
S0,
S1,
S2,
temperature=(1000 / 293.15),
beta_0=(1000 / 333.15),
random_state=None,
):
self.model_S0 = S0
self.model_S1 = S1
self.model_S2 = S2
self.n_features = S0.n_features
self.rng = np.random.RandomState(seed=random_state)
self.temperature = temperature
self.beta_0 = beta_0
self.delta_beta = temperature - beta_0
self.weights = {"S0": S0.weights, "S1": S1.weights, "S2": S2.weights}
[docs]
def evaluate(self, grid, noise):
S0_res = self.model_S0.evaluate(grid, noise)
S1_res = self.model_S0.evaluate(grid, noise)
S2_res = self.model_S0.evaluate(grid, noise)
sigma = S0_res - self.delta_beta * S1_res - self.delta_beta**2 * S2_res
return sigma
[docs]
class PoolModel:
"""
A model where data points are taken directly from a pool of already known data.
"""
def __init__(self, features, objective):
self.n_features = np.array(features).shape[1]
self.features = np.array(features)
self.objective = np.array(objective).flatten()
[docs]
def evaluate(self, grid, **kwargs):
grid = np.array(grid)
if len(grid.shape) == 1:
grid = grid.reshape(1, -1)
idx = []
for i in range(len(grid)):
index = np.where(
np.isclose(np.sum(self.features, axis=1), np.sum(grid[i]), atol=1e-10)
)[0][0]
idx.append(index)
idx = np.array(idx)
return self.objective[idx]