Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix cec2022 benchmarks #16

Merged
merged 2 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions opfunu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ def get_functions_based_ndim(ndim=None):
return functions


def get_all_named_functions():
return [cls for classname, cls in FUNC_DATABASE if classname not in EXCLUDES]


def get_all_cec_functions():
return [cls for classname, cls in CEC_DATABASE if classname not in EXCLUDES]
def get_functions(ndim, continuous=None, linear=None, convex=None, unimodal=None, separable=None,
differentiable=None, scalable=None, randomized_term=None, parametric=None, modality=None):
functions = [cls for classname, cls in FUNC_DATABASE if classname not in EXCLUDES]
Expand Down
2 changes: 1 addition & 1 deletion opfunu/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def check_solution(self, x):
The solution
"""
if not self.dim_changeable and (len(x) != self._ndim):
raise ValueError(f"The length of solution should has {self._ndim} variables!")
raise ValueError(f"The length of solution should have {self._ndim} variables!")

def get_paras(self):
"""
Expand Down
28 changes: 14 additions & 14 deletions opfunu/cec_based/cec2022.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def evaluate(self, x, *args):
self.n_fe += 1
self.check_solution(x, self.dim_max, self.dim_supported)
z = np.dot(self.f_matrix, 0.5*(x - self.f_shift)/100)
return operator.expanded_scaffer_f6_func(z) + self.f_bias
return operator.rotated_expanded_schaffer_func(z) + self.f_bias


class F42022(F12022):
Expand All @@ -134,7 +134,7 @@ def evaluate(self, x, *args):
self.n_fe += 1
self.check_solution(x, self.dim_max, self.dim_supported)
z = np.dot(self.f_matrix, 5.12*(x - self.f_shift)/100)
return operator.expanded_scaffer_f6_func(z) + self.f_bias
return operator.non_continuous_rastrigin_func(z) + self.f_bias


class F52022(F12022):
Expand Down Expand Up @@ -214,7 +214,7 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_6", f_matrix="M_6
self.n2 = int(np.ceil(self.p[1] * self.ndim)) + self.n1
self.idx1, self.idx2, self.idx3 = self.f_shuffle[:self.n1], self.f_shuffle[self.n1:self.n2], self.f_shuffle[self.n2:self.ndim]
self.g1 = operator.bent_cigar_func
self.g2 = operator.hgbat_func
self.g2 = operator.hgbat_shifted_func
self.g3 = operator.rastrigin_func
self.paras = {"f_shift": self.f_shift, "f_bias": self.f_bias, "f_matrix": self.f_matrix, "f_shuffle": self.f_shuffle}

Expand Down Expand Up @@ -281,7 +281,7 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_7", f_matrix="M_7
self.n5 = int(np.ceil(self.p[4] * self.ndim)) + self.n4
self.idx1, self.idx2, self.idx3 = self.f_shuffle[:self.n1], self.f_shuffle[self.n1:self.n2], self.f_shuffle[self.n2:self.n3]
self.idx4, self.idx5, self.idx6 = self.f_shuffle[self.n3:self.n4], self.f_shuffle[self.n4:self.n5], self.f_shuffle[self.n5:self.ndim]
self.g1 = operator.hgbat_func
self.g1 = operator.hgbat_shifted_func
self.g2 = operator.katsuura_func
self.g3 = operator.ackley_func
self.g4 = operator.rastrigin_func
Expand Down Expand Up @@ -353,8 +353,8 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_8", f_matrix="M_8
self.idx1, self.idx2, self.idx3 = self.f_shuffle[:self.n1], self.f_shuffle[self.n1:self.n2], self.f_shuffle[self.n2:self.n3]
self.idx4, self.idx5 = self.f_shuffle[self.n3:self.n4], self.f_shuffle[self.n4:self.ndim]
self.g1 = operator.katsuura_func
self.g2 = operator.happy_cat_func
self.g3 = operator.expanded_griewank_rosenbrock_func
self.g2 = operator.happy_cat_shifted_func
self.g3 = operator.grie_rosen_cec_func
self.g4 = operator.modified_schwefel_func
self.g5 = operator.ackley_func
self.paras = {"f_shift": self.f_shift, "f_bias": self.f_bias, "f_matrix": self.f_matrix, "f_shuffle": self.f_shuffle}
Expand Down Expand Up @@ -412,7 +412,7 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_9", f_matrix="M_9
self.f_global = f_bias
self.x_global = self.f_shift[0]
self.n_funcs = 5
self.xichmas = [10, 20, 30, 40, 50]
self.xichmas = [10, 20, 30, 40, 50] # aka delta in original CEC2022 logic
self.lamdas = [1, 1e-6, 1e-6, 1e-6, 1e-6]
self.bias = [0, 200, 300, 100, 400]
self.g0 = operator.rosenbrock_func
Expand Down Expand Up @@ -505,25 +505,25 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_10", f_matrix="M_
self.bias = [0, 200, 100]
self.g0 = operator.modified_schwefel_func
self.g1 = operator.rastrigin_func
self.g2 = operator.hgbat_func
self.g2 = operator.hgbat_shifted_func
self.paras = {"f_shift": self.f_shift, "f_bias": self.f_bias, "f_matrix": self.f_matrix}

def evaluate(self, x, *args):
self.n_fe += 1
self.check_solution(x, self.dim_max, self.dim_supported)

# 1. Rotated Schwefel's Function f12
z0 = np.dot(self.f_matrix[:self.ndim, :], 1000.*(x - self.f_shift[0])/100) + 1
z0 = np.dot(self.f_matrix[:self.ndim, :], (1000./100)*(x - self.f_shift[0]))
g0 = self.lamdas[0] * self.g0(z0) + self.bias[0]
w0 = operator.calculate_weight(x - self.f_shift[0], self.xichmas[0])

# 2. Rotated Rastrigin’s Function f4
z1 = np.dot(self.f_matrix[self.ndim:2*self.ndim, :], 5.12*(x - self.f_shift[0])/100)
z1 = np.dot(self.f_matrix[self.ndim:2*self.ndim, :], (5.12/100)*(x - self.f_shift[0]))
g1 = self.lamdas[1] * self.g1(z1) + self.bias[1]
w1 = operator.calculate_weight(x - self.f_shift[1], self.xichmas[1])

# 3. HGBat Function f7
z2 = np.dot(self.f_matrix[2*self.ndim:3*self.ndim, :], 5*(x - self.f_shift[0])/100)
z2 = np.dot(self.f_matrix[2*self.ndim:3*self.ndim, :], (5/100)*(x - self.f_shift[0]))
g2 = self.lamdas[2] * self.g2(z2) + self.bias[2]
w2 = operator.calculate_weight(x - self.f_shift[2], self.xichmas[2])

Expand Down Expand Up @@ -579,7 +579,7 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_11", f_matrix="M_
self.xichmas = [20, 20, 30, 30, 20]
self.lamdas = [1e-26, 10, 1e-6, 10, 5e-4]
self.bias = [0, 200, 300, 400, 200]
self.g0 = operator.expanded_scaffer_f6_func
self.g0 = operator.rotated_expanded_schaffer_func
firestrand marked this conversation as resolved.
Show resolved Hide resolved
self.g1 = operator.modified_schwefel_func
self.g2 = operator.griewank_func
self.g3 = operator.rosenbrock_func
Expand Down Expand Up @@ -667,12 +667,12 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_12", f_matrix="M_
self.xichmas = [10, 20, 30, 40, 50, 60]
self.lamdas = [10, 10, 2.5, 1e-26, 1e-6, 5e-4]
self.bias = [0, 300, 500, 100, 400, 200]
self.g0 = operator.hgbat_func
self.g0 = operator.hgbat_shifted_func
self.g1 = operator.rastrigin_func
self.g2 = operator.modified_schwefel_func
self.g3 = operator.bent_cigar_func
self.g4 = operator.elliptic_func
self.g5 = operator.expanded_scaffer_f6_func
self.g5 = operator.rotated_expanded_schaffer_func
self.paras = {"f_shift": self.f_shift, "f_bias": self.f_bias, "f_matrix": self.f_matrix}

def evaluate(self, x, *args):
Expand Down
79 changes: 64 additions & 15 deletions opfunu/utils/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,38 @@ def sphere_func(x):
x = np.array(x).ravel()
return np.sum(x ** 2)


def rotated_expanded_schaffer_func(x):
x = np.asarray(x).ravel()
x_pairs = np.column_stack((x, np.roll(x, -1)))
sum_sq = x_pairs[:, 0] ** 2 + x_pairs[:, 1] ** 2
# Calculate the Schaffer function for all pairs simultaneously
schaffer_values = (0.5 + (np.sin(np.sqrt(sum_sq)) ** 2 - 0.5) /
(1 + 0.001 * sum_sq) ** 2)
return np.sum(schaffer_values)

def rotated_expanded_scaffer_func(x):
x = np.array(x).ravel()
results = [scaffer_func([x[idx], x[idx + 1]]) for idx in range(0, len(x) - 1)]
return np.sum(results) + scaffer_func([x[-1], x[0]])


def grie_rosen_cec_func(x):
"""This is based on the CEC version which unrolls the griewank and rosenbrock functions for better performance"""
z = np.array(x).ravel()
z += 1.0 # This centers the optimal solution of rosenbrock to 0

tmp1 = (z[:-1] * z[:-1] - z[1:]) ** 2
tmp2 = (z[:-1] - 1.0) ** 2
temp = 100.0 * tmp1 + tmp2
f = np.sum(temp ** 2 / 4000.0 - np.cos(temp) + 1.0)
# Last calculation
tmp1 = (z[-1] * z[-1] - z[0]) ** 2
tmp2 = (z[-1] - 1.0) ** 2
temp = 100.0 * tmp1 + tmp2
f += (temp ** 2) / 4000.0 - np.cos(temp) + 1.0

return f

def f8f2_func(x):
x = np.array(x).ravel()
results = [griewank_func(rosenbrock_func([x[idx], x[idx + 1]])) for idx in range(0, len(x) - 1)]
Expand Down Expand Up @@ -232,30 +257,51 @@ def lunacek_bi_rastrigin_func(x, z, miu0=2.5, d=1.):
return result1 + 10 * (ndim - np.sum(np.cos(2 * np.pi * z)))


def calculate_weight(x, xichma=1.):
def calculate_weight(x, delta=1.):
ndim = len(x)
weight = 1
temp = np.sum(x ** 2)
if temp != 0:
weight = (1.0 / np.sqrt(temp)) * np.exp(-temp / (2 * ndim * xichma ** 2))
weight = np.sqrt(1.0 / temp) * np.exp(-temp / (2 * ndim * delta ** 2))
else:
weight = 1e99 # this is the INF definition in original CEC Calculate logic

return weight


def modified_schwefel_func(x):
x = np.array(x).ravel()
ndim = len(x)
z = x + 4.209687462275036e+002
return 418.9829 * ndim - np.sum(gz_func(z))
"""
This is a direct conversion of the CEC2021 C-Code for the Modified Schwefel F11 Function
"""
z = np.array(x).ravel() + 4.209687462275036e+002
nx = len(z)

mask1 = z > 500
mask2 = z < -500
mask3 = ~mask1 & ~mask2
fx = np.zeros(nx)
fx[mask1] -= (500.0 + np.fmod(np.abs(z[mask1]), 500)) * np.sin(np.sqrt(500.0 - np.fmod(np.abs(z[mask1]), 500))) - (
(z[mask1] - 500.0) / 100.) ** 2 / nx
fx[mask2] -= (-500.0 + np.fmod(np.abs(z[mask2]), 500)) * np.sin(np.sqrt(500.0 - np.fmod(np.abs(z[mask2]), 500))) - (
(z[mask2] + 500.0) / 100.) ** 2 / nx
fx[mask3] -= z[mask3] * np.sin(np.sqrt(np.abs(z[mask3])))

return np.sum(fx) + 4.189828872724338e+002 * nx



def happy_cat_func(x):
x = np.array(x).ravel()
ndim = len(x)
t1 = np.sum(x)
t2 = np.sum(x ** 2)
z = np.array(x).ravel()
ndim = len(z)
t1 = np.sum(z)
t2 = np.sum(z ** 2)
return np.abs(t2 - ndim) ** 0.25 + (0.5 * t2 + t1) / ndim + 0.5


def happy_cat_shifted_func(x):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@firestrand,

Could you please add a 'shifted' parameter to this type of function? Kindly avoid fixed the shifted value within the function. This will make the function more reusable.

return happy_cat_func(x - 1.0)



def hgbat_func(x):
x = np.array(x).ravel()
ndim = len(x)
Expand All @@ -264,20 +310,23 @@ def hgbat_func(x):
return np.abs(t2 ** 2 - t1 ** 2) ** 0.5 + (0.5 * t2 + t1) / ndim + 0.5


def hgbat_shifted_func(x):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@firestrand ,

This function too, you can add parameter for it.

Copy link
Contributor Author

@firestrand firestrand Sep 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please confirm you want parameterized after reading.
I specifically didn't go with a parameterized because I was thinking it would require either an if condition to check the parameter and shift or not, or always perform the shift just with a 0 for some cases if no shift is desired. While small this could introduce some extra processing. So having a shifted override function which calls the base unshifted seemed like the best performance vs clean approach.

That said if you feel going with a parameterized approach matches the codebase better I can go that direction, and if so prefer passing the shift value as 1, -1 etc. with a default of 0 instead of an if check.

Thoughts?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@firestrand,

Yes, please set up the parameter name that match with function name. Also set up the default value for it. You don't need to check if else condition here. Your function will be reused more in future.
Other sections looking good. I will accept this PR after you do it. Thanks

Copy link
Contributor Author

@firestrand firestrand Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thieu1995
Started to make the change and it doesn't seem right for the codebase.
Changing the signature to something like def happy_cat_func(x, shift=0.0) works fine for when the benchmark is either calling the shifted or unshifted directly. ie. happy_cat_func(x, shift=-1.0) or happy_cat_func(x). However with the composite benchmarks the code does something like self.g2 = operator.happy_cat_func then in the evaluate we will get self.g2(mz[self.n1:self.n2], shift=-1.0) which will be different from the other self.g1,g3 calls.

I considered a functional approach, which would be something like def happy_cat_func(shift=0.0): def inner(x): <existing logic> which would then transform the self.g2 = operator.happy_cat_func(-1.0) and the evaluate code would be the same. But then the functions that currently call the function directly would look like operator.happy_cat_func()(x) which would be different.

The last alternative is leaving things the way I have with a separate function definition for the shifted case so the signatures in the benchmark stay the same.

So advise option 1, 2, or 3 given this information.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@firestrand ,

I forgot to tell you when I defined self.g1 =..., self.g2=... in the init function. It is just for short code.
Now because you have parameter in the function, then you don't have to define g1, g2,... function.

Just write full function in evaluate().
For example, return operator.happy_cat_func((mz[self.n1:self.n2], shift=1.0) + operator.katsuura_func(mz[self.n1:self.n2], shift=-1.0,...) like that.

return hgbat_func(x - 1.0)



def zakharov_func(x):
x = np.array(x).ravel()
temp = np.sum(0.5 * x)
return np.sum(x ** 2) + temp ** 2 + temp ** 4


def levy_func(x):
x = np.array(x).ravel()
w = 1 + (x - 1) / 4
w = 1. + x / 4
firestrand marked this conversation as resolved.
Show resolved Hide resolved
t1 = np.sin(np.pi * w[0]) ** 2 + (w[-1] - 1) ** 2 * (1 + np.sin(2 * np.pi * w[-1]) ** 2)
t2 = np.sum((w[:-1] - 1) ** 2 * (1 + 10 * np.sin(np.pi * w[:-1] + 1) ** 2))
return t1 + t2


def schaffer_f7_func(x):
x = np.array(x).ravel()
ndim = len(x)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def readme():
long_description_content_type="text/markdown",
keywords=["optimization functions", "test functions", "benchmark functions", "mathematical functions",
"CEC competitions", "CEC-2008", "CEC-2009", "CEC-2010", "CEC-2011", "CEC-2012", "CEC-2013",
"CEC-2014", "CEC-2015", "CEC-2017", "CEC-2019", "CEC-2020", "CEC-2021", "CEC-2023", "soft computing",
"CEC-2014", "CEC-2015", "CEC-2017", "CEC-2019", "CEC-2020", "CEC-2021", "CEC-2022", "soft computing",
"Stochastic optimization", "Global optimization", "Convergence analysis", "Search space exploration",
"Local search", "Computational intelligence", "Performance analysis",
"Exploration versus exploitation", "Constrained optimization", "Simulations"],
Expand Down
12 changes: 12 additions & 0 deletions tests/cec_based/test_cec2022.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,15 @@ def test_F122022_results():
assert len(problem.lb) == ndim
assert problem.bounds.shape[0] == ndim
assert len(problem.x_global) == ndim


def test_all_optimal_results():
ndim = 10
known_failing = []
all_functions = [x for x in opfunu.get_all_cec_functions()
if x.__name__[-4:] == '2022' and x.__name__ not in known_failing]
for function in all_functions:
problem = function(ndim=ndim)
x = problem.x_global
result = problem.evaluate(x)
assert abs(result - problem.f_global) <= problem.epsilon, f'{function.__name__} Failed Optimal Test'
33 changes: 33 additions & 0 deletions tests/cec_based/test_cec_based.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import numpy as np
from opfunu import get_all_cec_functions


def test_whenNdimNone_thenDefaultNdimUsed():
allFunctions = get_all_cec_functions()
for f in allFunctions:
f_default = f()
assert f_default.ndim == f_default.dim_default, f'{f.__name__} failed to have ndim == dim_default'

def test_whenEvaulateWith_x_global_then_f_global():
# The following are broken or have incorrect or unknown correct , values.
known_failing = ['F72005', 'F72008', 'F142013', 'F152013', 'F212013', 'F222013', 'F232013',
'F242013', 'F252013', 'F262013', 'F272013', 'F282013', 'F102014', 'F112014',
'F132014', 'F142014', 'F172014', 'F182014', 'F192014', 'F202014', 'F212014',
'F222014', 'F232014', 'F242014', 'F252014', 'F262014', 'F272014', 'F282014',
'F292014', 'F302014', 'F42015', 'F62015', 'F72015', 'F102015', 'F112015',
'F122015', 'F132015', 'F142015', 'F152015', 'F82017', 'F92017', 'F102017',
'F112017', 'F122017', 'F142017', 'F152017', 'F162017', 'F172017', 'F182017',
'F192017', 'F202017', 'F212017', 'F222017', 'F232017', 'F242017', 'F252017',
'F262017', 'F272017', 'F282017', 'F292017', 'F12019', 'F22019', 'F32019',
'F72019', 'F92019', 'F52020', 'F62020', 'F72020', 'F82020', 'F92020',
'F102020', 'F52021', 'F62021', 'F72021', 'F82021', 'F92021', 'F102021']
all_functions = [x for x in get_all_cec_functions() if x.__name__ not in known_failing]
failing = []
for f in all_functions:
f_default = f()
x_global = f_default.x_global
if abs(f_default.evaluate(x_global) - f_default.f_global) >= f_default.epsilon:
failing.append(f.__name__)
assert len(failing) == 0, f'{failing} failed to have x_global result in f_global'


Loading