diff --git a/opfunu/__init__.py b/opfunu/__init__.py index 7c3b7a8..36da8de 100644 --- a/opfunu/__init__.py +++ b/opfunu/__init__.py @@ -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] diff --git a/opfunu/benchmark.py b/opfunu/benchmark.py index 0f0fd8b..5b790ec 100644 --- a/opfunu/benchmark.py +++ b/opfunu/benchmark.py @@ -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): """ diff --git a/opfunu/cec_based/cec2022.py b/opfunu/cec_based/cec2022.py index 27938b5..997b59f 100644 --- a/opfunu/cec_based/cec2022.py +++ b/opfunu/cec_based/cec2022.py @@ -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): @@ -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): @@ -160,7 +160,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.levy_func(z) + self.f_bias + return operator.levy_func(z, shift=1.0) + self.f_bias class F62022(CecBenchmark): @@ -213,9 +213,6 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_6", f_matrix="M_6 self.n1 = int(np.ceil(self.p[0] * self.ndim)) 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.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} def evaluate(self, x, *args): @@ -224,7 +221,9 @@ def evaluate(self, x, *args): z = x - self.f_shift z1 = np.concatenate((z[self.idx1], z[self.idx2], z[self.idx3])) mz = np.dot(self.f_matrix, z1) - return self.g1(mz[:self.n1]) + self.g2(mz[self.n1:self.n2]) + self.g3(mz[self.n2:]) + self.f_bias + return (operator.bent_cigar_func(mz[:self.n1]) + + operator.hgbat_func(mz[self.n1:self.n2], shift=-1.0) + + operator.rastrigin_func(mz[self.n2:]) + self.f_bias) class F72022(CecBenchmark): @@ -281,12 +280,6 @@ 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.g2 = operator.katsuura_func - self.g3 = operator.ackley_func - self.g4 = operator.rastrigin_func - self.g5 = operator.modified_schwefel_func - self.g6 = operator.schaffer_f7_func self.paras = {"f_shift": self.f_shift, "f_bias": self.f_bias, "f_matrix": self.f_matrix, "f_shuffle": self.f_shuffle} def evaluate(self, x, *args): @@ -295,8 +288,12 @@ def evaluate(self, x, *args): z = x - self.f_shift z1 = np.concatenate((z[self.idx1], z[self.idx2], z[self.idx3], z[self.idx4], z[self.idx5], z[self.idx6])) mz = np.dot(self.f_matrix, z1) - return self.g1(mz[:self.n1]) + self.g2(mz[self.n1:self.n2]) + self.g3(mz[self.n2:self.n3]) + \ - self.g4(mz[self.n3:self.n4]) + self.g5(mz[self.n4:self.n5]) + self.g6(mz[self.n5:self.ndim]) + self.f_bias + return (operator.hgbat_func(mz[:self.n1], shift=-1.0) + + operator.katsuura_func(mz[self.n1:self.n2]) + + operator.ackley_func(mz[self.n2:self.n3]) + + operator.rastrigin_func(mz[self.n3:self.n4]) + + operator.modified_schwefel_func(mz[self.n4:self.n5]) + + operator.schaffer_f7_func(mz[self.n5:self.ndim]) + self.f_bias) class F82022(CecBenchmark): @@ -352,11 +349,6 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_8", f_matrix="M_8 self.n4 = int(np.ceil(self.p[3] * self.ndim)) + self.n3 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.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} def evaluate(self, x, *args): @@ -365,8 +357,11 @@ def evaluate(self, x, *args): z = x - self.f_shift z1 = np.concatenate((z[self.idx1], z[self.idx2], z[self.idx3], z[self.idx4], z[self.idx5])) mz = np.dot(self.f_matrix, z1) - return self.g1(mz[:self.n1]) + self.g2(mz[self.n1:self.n2]) + self.g3(mz[self.n2:self.n3]) + \ - self.g4(mz[self.n3:self.n4]) + self.g5(mz[self.n4:self.ndim]) + self.f_bias + return (operator.katsuura_func(mz[:self.n1]) + + operator.happy_cat_func(mz[self.n1:self.n2], shift=-1.0) + + operator.grie_rosen_cec_func(mz[self.n2:self.n3]) + + operator.modified_schwefel_func(mz[self.n3:self.n4]) + + operator.ackley_func(mz[self.n4:self.ndim]) + self.f_bias) class F92022(CecBenchmark): @@ -412,7 +407,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 @@ -503,9 +498,6 @@ def __init__(self, ndim=None, bounds=None, f_shift="shift_data_10", f_matrix="M_ self.xichmas = [20, 10, 10] self.lamdas = [1, 1, 1] self.bias = [0, 200, 100] - self.g0 = operator.modified_schwefel_func - self.g1 = operator.rastrigin_func - self.g2 = operator.hgbat_func self.paras = {"f_shift": self.f_shift, "f_bias": self.f_bias, "f_matrix": self.f_matrix} def evaluate(self, x, *args): @@ -513,18 +505,18 @@ def evaluate(self, x, *args): 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 - g0 = self.lamdas[0] * self.g0(z0) + self.bias[0] + z0 = np.dot(self.f_matrix[:self.ndim, :], (1000./100)*(x - self.f_shift[0])) + g0 = self.lamdas[0] * operator.modified_schwefel_func(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) - g1 = self.lamdas[1] * self.g1(z1) + self.bias[1] + z1 = np.dot(self.f_matrix[self.ndim:2*self.ndim, :], (5.12/100)*(x - self.f_shift[0])) + g1 = self.lamdas[1] * operator.rastrigin_func(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) - g2 = self.lamdas[2] * self.g2(z2) + self.bias[2] + z2 = np.dot(self.f_matrix[2*self.ndim:3*self.ndim, :], (5/100)*(x - self.f_shift[0])) + g2 = self.lamdas[2] * operator.hgbat_func(z2, shift=-1.0) + self.bias[2] w2 = operator.calculate_weight(x - self.f_shift[2], self.xichmas[2]) ws = np.array([w0, w1, w2]) @@ -579,7 +571,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 self.g1 = operator.modified_schwefel_func self.g2 = operator.griewank_func self.g3 = operator.rosenbrock_func @@ -667,12 +659,6 @@ 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.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.paras = {"f_shift": self.f_shift, "f_bias": self.f_bias, "f_matrix": self.f_matrix} def evaluate(self, x, *args): @@ -681,32 +667,32 @@ def evaluate(self, x, *args): # 1. HGBat Function f7 z0 = np.dot(self.f_matrix[:self.ndim, :], 5.*(x - self.f_shift[0])/100) - g0 = self.lamdas[0] * self.g0(z0) + self.bias[0] + g0 = self.lamdas[0] * operator.hgbat_func(z0, shift=-1.0) + self.bias[0] w0 = operator.calculate_weight(x - self.f_shift[0], self.xichmas[0]) # 2. Rastrigin’s Function f4 z1 = np.dot(self.f_matrix[self.ndim:2*self.ndim, :], 5.12*(x - self.f_shift[0])/100) - g1 = self.lamdas[1] * self.g1(z1) + self.bias[1] + g1 = self.lamdas[1] * operator.rastrigin_func(z1) + self.bias[1] w1 = operator.calculate_weight(x - self.f_shift[1], self.xichmas[1]) # 3. Modified Schwefel's Function f12 z2 = np.dot(self.f_matrix[2*self.ndim:3*self.ndim, :], 1000.*(x - self.f_shift[0])/100) - g2 = self.lamdas[2] * self.g2(z2) + self.bias[2] + g2 = self.lamdas[2] * operator.modified_schwefel_func(z2) + self.bias[2] w2 = operator.calculate_weight(x - self.f_shift[2], self.xichmas[2]) # 4. Bent Cigar Function f6 z3 = np.dot(self.f_matrix[3 * self.ndim:4 * self.ndim, :], x - self.f_shift[0]) - g3 = self.lamdas[3] * self.g3(z3) + self.bias[3] + g3 = self.lamdas[3] * operator.bent_cigar_func(z3) + self.bias[3] w3 = operator.calculate_weight(x - self.f_shift[3], self.xichmas[3]) # 5. High Conditioned Elliptic Function f8 z4 = np.dot(self.f_matrix[4 * self.ndim:5 * self.ndim, :], x - self.f_shift[0]) - g4 = self.lamdas[4] * self.g4(z4) + self.bias[4] + g4 = self.lamdas[4] * operator.elliptic_func(z4) + self.bias[4] w4 = operator.calculate_weight(x - self.f_shift[4], self.xichmas[4]) # 6. Expanded Schaffer’s F6 Function f3 z5 = np.dot(self.f_matrix[5 * self.ndim:6 * self.ndim, :], x - self.f_shift[0]) - g5 = self.lamdas[5] * self.g5(z5) + self.bias[5] + g5 = self.lamdas[5] * operator.rotated_expanded_schaffer_func(z5) + self.bias[5] w5 = operator.calculate_weight(x - self.f_shift[5], self.xichmas[5]) ws = np.array([w0, w1, w2, w3, w4, w5]) diff --git a/opfunu/utils/operator.py b/opfunu/utils/operator.py index 1099854..6b0cdd5 100644 --- a/opfunu/utils/operator.py +++ b/opfunu/utils/operator.py @@ -67,12 +67,40 @@ def sphere_func(x): 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)] @@ -200,14 +228,6 @@ def gz_func(x): conditions = [x < -500, (-500 <= x) & (x <= 500), x > 500] choices = [t2, t3, t1] y = np.select(conditions, choices, default=np.nan) - # y = x.copy() - # for idx in range(0, ndim): - # if x[idx] > 500: - # y[idx] = (500 - np.mod(x[idx], 500)) * np.sin(np.sqrt(np.abs(500 - np.mod(x[idx], 500)))) - (x[idx] - 500)**2/(10000*ndim) - # elif x[idx] < -500: - # y[idx] = (np.mod(x[idx], 500) - 500) * np.sin(np.sqrt(np.abs(np.mod(np.abs(x[idx]), 500) - 500))) - (x[idx]+500)**2/(10000*ndim) - # else: - # y[idx] = x[idx]*np.sin(np.abs(x[idx])**0.5) return y @@ -232,32 +252,47 @@ 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)) - - -def happy_cat_func(x): - x = np.array(x).ravel() - ndim = len(x) - t1 = np.sum(x) - t2 = np.sum(x ** 2) + """ + 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, shift=0.0): + z = np.array(x).ravel() + shift + 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 hgbat_func(x): - x = np.array(x).ravel() +def hgbat_func(x, shift=0.0): + x = np.array(x).ravel() + shift ndim = len(x) t1 = np.sum(x) t2 = np.sum(x ** 2) @@ -270,9 +305,9 @@ def zakharov_func(x): return np.sum(x ** 2) + temp ** 2 + temp ** 4 -def levy_func(x): - x = np.array(x).ravel() - w = 1 + (x - 1) / 4 +def levy_func(x, shift=0.0): + x = np.array(x).ravel() + shift + w = 1. + (x - 1.) / 4 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 diff --git a/setup.py b/setup.py index 2f06683..3b6bb1c 100644 --- a/setup.py +++ b/setup.py @@ -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"], diff --git a/tests/cec_based/test_cec2022.py b/tests/cec_based/test_cec2022.py index 6cd0f95..dcb094c 100644 --- a/tests/cec_based/test_cec2022.py +++ b/tests/cec_based/test_cec2022.py @@ -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' diff --git a/tests/cec_based/test_cec_based.py b/tests/cec_based/test_cec_based.py new file mode 100644 index 0000000..3b42aad --- /dev/null +++ b/tests/cec_based/test_cec_based.py @@ -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' + +