From 3ec391ff57788ef148b0b0a497b1f6504dd99d3b Mon Sep 17 00:00:00 2001 From: Duncan Tilley Date: Tue, 22 Nov 2022 12:16:52 +0200 Subject: [PATCH 1/4] Optimize all functions for 2D arrays --- cec2017/basic.py | 401 +++++++++++++++++++++-------------------- cec2017/composition.py | 265 ++++++++++----------------- cec2017/hybrid.py | 91 +++++++--- cec2017/simple.py | 74 +++++--- cec2017/transforms.py | 50 +++++ 5 files changed, 463 insertions(+), 418 deletions(-) diff --git a/cec2017/basic.py b/cec2017/basic.py index 3729352..f67adae 100644 --- a/cec2017/basic.py +++ b/cec2017/basic.py @@ -2,287 +2,298 @@ # Author: Duncan Tilley # Basic function definitions +from typing import Optional import numpy as np -def bent_cigar(x): - sm = 0.0 - for i in range(1, len(x)): - sm += x[i]*x[i] - sm *= 10e6 - return x[0]*x[0] + sm -def sum_diff_pow(x): - sm = 0.0 - for i in range(0, len(x)): - sm += (abs(x[i])) ** (i+1) - return sm +def bent_cigar(x: np.ndarray) -> np.ndarray: + sm = np.sum(x[:, 1:] * x[:, 1:], axis=1) + sm = sm * 10e6 + return x[:, 0]*x[:, 0] + sm -def zakharov(x): - sms = 0.0 - sm = 0.0 - for i in range(0, len(x)): - sms += x[i]*x[i] - # Note: the i+1 term is not in the CEC function definitions, but is - # in the code and in any definition you find online - sm += (i+1)*x[i] + +def sum_diff_pow(x: np.ndarray) -> np.ndarray: + i = np.expand_dims(np.arange(x.shape[1]) + 1, 0) + x_pow = np.power(np.abs(x), i) + return np.sum(x_pow, axis=1) + + +def zakharov(x: np.ndarray) -> np.ndarray: + # NOTE: the i+1 term is not in the CEC function definitions, but is in the + # code and in any definition you find online + i = np.expand_dims(np.arange(x.shape[1]) + 1, 0) + sm = np.sum(i * x, axis=1) + sms = np.sum(x * x, axis=1) sm = 0.5 * sm sm = sm * sm return sms + sm + (sm * sm) -def rosenbrock(x): + +def rosenbrock(x: np.ndarray) -> np.ndarray: x = 0.02048 * x + 1.0 - sm = 0 - for i in range(0, len(x)-1): - t1 = x[i]*x[i] - x[i+1] - t1 = 100*t1*t1 - t2 = x[i] - 1 - t2 = t2*t2 - sm += t1 + t2 - return sm + t1 = x[:, :-1] * x[:, :-1] - x[:, 1:] + t1 = 100 * t1 * t1 + t2 = x[:, :-1] - 1 + t2 = t2 * t2 + return np.sum(t1 + t2, axis=1) + -def rastrigin(x): - # Note: the 0.0512 shrinking is omitted in the problem definitions but is +def rastrigin(x: np.ndarray) -> np.ndarray: + # NOTE: the 0.0512 shrinking is omitted in the problem definitions but is # present in the provided code x = 0.0512 * x - tpi = 2.0 * np.pi - sm = 0.0 - cs = np.cos(tpi*x) - for i in range(0, len(x)): - sm += x[i]*x[i] - 10*cs[i] - return sm + 10*len(x) + cs = np.cos(2 * np.pi * x) + xs = x*x - 10*cs + 10 + return np.sum(xs, axis=1) -def expanded_schaffers_f6(x): - sm = 0.0 - for i in range(0, len(x)-1): - t = x[i]*x[i] + x[i+1]*x[i+1] - t1 = np.sin(np.sqrt(t)) - t1 = t1*t1 - 0.5 - t2 = 1 + 0.001*t - t2 = t2*t2 - sm += 0.5 + t1/t2 - return sm -def lunacek_bi_rastrigin(x, shift=None, rotation=None): +def expanded_schaffers_f6(x: np.ndarray) -> np.ndarray: + t = x[:, :-1]*x[:, :-1] + x[:, 1:]*x[:, 1:] + t1 = np.sin(np.sqrt(t)) + t1 = t1*t1 - 0.5 + t2 = 1 + 0.001*t + t2 = t2*t2 + return np.sum(0.5 + t1/t2, axis=1) + + +def lunacek_bi_rastrigin( + x: np.ndarray, + shift: Optional[np.ndarray] = None, + rotation: Optional[np.ndarray] = None, +) -> np.ndarray: # a special case; we need the shift vector and rotation matrix - nx = len(x) + nx = x.shape[1] if shift is None: - shift = np.zeros(nx) + shift = np.zeros((1, nx)) + else: + shift = np.expand_dims(shift, 0) # calculate the coefficients - mu0=2.5 - tmpx = np.zeros(nx) + mu0 = 2.5 s = 1 - 1 / (2 * ((nx+20)**0.5) - 8.2) mu1 = -((mu0*mu0-1)/s)**0.5 # shift and scale y = 0.1 * (x - shift) - for i in range(0, nx): - tmpx[i] = 2*y[i] - if shift[i] < 0.0: - tmpx[i] *= -1.0 + tmpx = 2 * y + tmpx[:, shift[0] < 0] *= -1 z = tmpx.copy() tmpx = tmpx + mu0 - t1=0.0 - t2=0.0 - for i in range(0, nx): - t = tmpx[i]-mu0 - t1 += t*t - t = tmpx[i]-mu1 - t2 += t*t - t2 *= s - t2 += nx + t1 = tmpx - mu0 + t1 = t1 * t1 + t1 = np.sum(t1, axis=1) + t2 = tmpx - mu1 + t2 = s * t2 * t2 + t2 = np.sum(t2, axis=1) + nx + + if rotation is None: + y = z + else: + y = np.matmul( + np.expand_dims(rotation, 0), + np.expand_dims(z, -1), + )[:, :, 0] - y = z if rotation is None else np.matmul(rotation, z) - - t = 0.0 y = np.cos(2.0*np.pi*y) - for i in range(0, nx): - t += y[i] + t = np.sum(y, axis=1) - r = t1 if t1 < t2 else t2 + r = t1 + r[t1 >= t2] = t2[t1 >= t2] return r + 10.0*(nx-t) -def non_cont_rastrigin(x, shift=None, rotation=None): + +def non_cont_rastrigin( + x: np.ndarray, + shift: Optional[np.ndarray] = None, + rotation: Optional[np.ndarray] = None, +) -> np.ndarray: # a special case; we need the shift vector and rotation matrix + nx = x.shape[1] if shift is None: - shift = np.zeros(x.shape) + shift = np.zeros((1, nx)) + else: + shift = np.expand_dims(shift, 0) + shifted = x - shift - nx = len(x) sm = 0.0 - for i in range(0, nx): - if abs(x[i]-shift[i]) > 0.5: - x[i] = shift[i] + np.floor(2*(x[i]-shift[i])+0.5)/2 - - z = 0.0512 * (x - shift) - z = z if rotation is None else np.matmul(rotation, z) - - for i in range(0, nx): - sm += (z[i]*z[i] - 10.0*np.cos(2.0*np.pi*z[i]) + 10.0) + x = x.copy() + mask = np.abs(shifted) > 0.5 + x[mask] = (shift + np.floor(2*shifted+0.5) * 0.5)[mask] + + # for i in range(0, nx): + # if abs(x[i]-shift[i]) > 0.5: + # x[i] = shift[i] + np.floor(2*(x[i]-shift[i])+0.5)/2 + + z = 0.0512 * shifted + if rotation is not None: + z = np.matmul( + np.expand_dims(rotation, 0), + np.expand_dims(z, -1), + )[:, :, 0] + + sm = z*z - 10*np.cos(2*np.pi*z) + 10 + sm = np.sum(sm, axis=1) + # for i in range(0, nx): + # sm += (z[i]*z[i] - 10.0*np.cos(2.0*np.pi*z[i]) + 10.0) return sm -def levy(x): - # Note: the function definitions state to scale by 5.12/100, but the code + +def levy(x: np.ndarray) -> np.ndarray: + # NOTE: the function definitions state to scale by 5.12/100, but the code # doesn't do this, and the example graph in the definitions correspond to # the version without scaling # x = 0.0512 * x - nx = len(x) w = 1.0 + 0.25*(x - 1.0) - term1 = (np.sin(np.pi*w[0]))**2 - term3 = ((w[nx-1] - 1)**2) * (1 + ((np.sin(2*np.pi*w[nx-1]))**2)) + term1 = (np.sin(np.pi*w[:, 0]))**2 + term3 = ((w[:, -1] - 1)**2) * (1 + ((np.sin(2*np.pi*w[:, -1]))**2)) sm = 0.0 - for i in range(0, nx-1): - wi = w[i] - newv = ((wi-1)**2) * (1 + 10*((np.sin(np.pi*wi+1))**2)) - sm += newv + wi = w[:, :-1] + newv = ((wi - 1)**2) * (1 + 10*((np.sin(np.pi*wi+1))**2)) + sm = np.sum(newv, axis=1) return term1 + sm + term3 -def modified_schwefel(x): - nx = len(x) + +def modified_schwefel(x: np.ndarray) -> np.ndarray: + nx = x.shape[1] x = 10.0 * x # scale to search range - sm = 0.0 - for i in range(0, nx): - z = x[i] + 420.9687462275036 - if z < -500: - zm = (abs(z) % 500) - 500 - t = z + 500 - t = t*t - sm += zm * np.sin(np.sqrt(abs(zm))) - t / (10000*nx) - elif z > 500: - zm = 500 - (z % 500) - t = z - 500 - t = t*t - sm += zm * np.sin(np.sqrt(abs(zm))) - t / (10000*nx) - else: - sm += z * np.sin(np.sqrt(abs(z))) - - return 418.9829*nx - sm - -def high_conditioned_elliptic(x): - factor = 6 / (len(x) - 1) - sm = 0.0 - for i in range(0, len(x)): - sm += x[i]*x[i] * 10**(i*factor) - return sm -def discus(x): - sm = 1e+6*x[0]*x[0] - for i in range(1, len(x)): - sm += x[i]*x[i] - return sm + z = x + 420.9687462275036 + mask1 = z < -500 + mask2 = z > 500 + sm = z * np.sin(np.sqrt(np.abs(z))) + + zm = np.mod(np.abs(z), 500) + zm[mask1] = (zm[mask1] - 500) + zm[mask2] = (500 - zm[mask2]) + t = z + 500 + t[mask2] = z[mask2] - 500 + t = t*t -def ackley(x): - smsq = 0.0 - smcs = 0.0 - cs = np.cos((2*np.pi)*x) - for i in range(0, len(x)): - smsq += x[i]*x[i] - smcs += cs[i] - inx = 1/len(x) + mask1_or_2 = np.logical_or(mask1, mask2) + sm[mask1_or_2] = (zm * np.sin(np.sqrt(np.abs(zm))) - t / (10_000*nx))[mask1_or_2] + return 418.9829*nx - np.sum(sm, axis=1) + + +def high_conditioned_elliptic(x: np.ndarray) -> np.ndarray: + factor = 6 / (x.shape[1] - 1) + i = np.expand_dims(np.arange(x.shape[1]), 0) + sm = x*x * 10**(i * factor) + return np.sum(sm, axis=1) + + +def discus(x: np.ndarray) -> np.ndarray: + sm0 = 1e+6*x[:, 0]*x[:, 0] + sm = np.sum(x[:, 1:]*x[:, 1:], axis=1) + return sm0 + sm + + +def ackley(x: np.ndarray) -> np.ndarray: + smsq = np.sum(x*x, axis=1) + smcs = np.sum(np.cos((2*np.pi)*x), axis=1) + inx = 1/x.shape[1] return -20*np.exp(-0.2*np.sqrt(inx*smsq)) - np.exp(inx*smcs) + 20 + np.e -def weierstrass(x): + +def weierstrass(x: np.ndarray) -> np.ndarray: x = 0.005 * x k = np.arange(start=0, stop=21, step=1) + k = np.expand_dims(np.expand_dims(k, 0), 0) ak = 0.5**k bk = np.pi * (3**k) - sm = 0.0 - for i in range(0, len(x)): - kcs = ak * np.cos(2*(x[i]+0.5)*bk) - ksm = 0.0 - for j in range(0, 21): - ksm += kcs[j] - sm += ksm + + kcs = ak * np.cos(2*(np.expand_dims(x, -1) + 0.5)*bk) # shape (M, nx, 21) + ksm = np.sum(kcs, axis=2) + sm = np.sum(ksm, axis=1) + kcs = ak * np.cos(bk) - ksm = 0.0 - for j in range(0, 21): - ksm += kcs[j] - return sm - len(x)*ksm + ksm = np.sum(kcs) + return sm - x.shape[1]*ksm -def griewank(x): + +def griewank(x: np.ndarray) -> np.ndarray: + nx = x.shape[1] x = 6.0 * x factor = 1/4000 - cs = np.cos(x / np.arange(start=1, stop=len(x)+1)) - sm = 0.0 - pd = 1.0 - for i in range(0, len(x)): - sm += factor*x[i]*x[i] - pd *= cs[i] + d = np.expand_dims(np.arange(start=1, stop=nx + 1), 0) + cs = np.cos(x / d) + sm = np.sum(factor*x*x, axis=1) + pd = np.prod(np.cos(x / d), axis=1) return sm - pd + 1 -def katsuura(x): + +def katsuura(x: np.ndarray) -> np.ndarray: x = 0.05 * x - nx = len(x) + nx = x.shape[1] pw = 10/(nx**1.2) prd = 1.0 tj = 2**np.arange(start=1, stop=33, step=1) - for i in range(0, nx): - tjx = tj*x[i] - t = np.abs(tjx - np.round(tjx)) / tj - tsm = 0.0 - for j in range(0, 32): - tsm += t[j] - prd *= (1+ (i+1)*tsm)**pw + tj = np.expand_dims(np.expand_dims(tj, 0), 0) + tjx = tj*np.expand_dims(x, -1) # shape (M, nx, 32) + t = np.abs(tjx - np.round(tjx)) / tj + tsm = np.sum(t, axis=2) + + i = np.arange(nx) + 1 + prd = np.prod((1 + i*tsm)**pw, axis=1) df = 10/(nx*nx) return df*prd - df -def happy_cat(x): + +def happy_cat(x: np.ndarray) -> np.ndarray: x = (0.05 * x) - 1 - nx = len(x) - sm = 0.0 - smsq = 0.0 - for i in range(0, nx): - sm += x[i] - smsq += x[i]*x[i] - return (abs(smsq - nx))**0.25 + (0.5*smsq + sm)/nx + 0.5 + nx = x.shape[1] + sm = np.sum(x, axis=1) + smsq = np.sum(x*x, axis=1) + return (np.abs(smsq - nx))**0.25 + (0.5*smsq + sm)/nx + 0.5 -def h_g_bat(x): + +def h_g_bat(x: np.ndarray) -> np.ndarray: x = (0.05 * x) - 1 - nx = len(x) - sm = 0.0 - smsq = 0.0 - for i in range(0, nx): - sm += x[i] - smsq += x[i]*x[i] - return (abs(smsq*smsq - sm*sm))**0.5 + (0.5*smsq + sm)/nx + 0.5 + nx = x.shape[1] + sm = np.sum(x, axis=1) + smsq = np.sum(x*x, axis=1) + return (np.abs(smsq*smsq - sm*sm))**0.5 + (0.5*smsq + sm)/nx + 0.5 + -def expanded_griewanks_plus_rosenbrock(x): +def expanded_griewanks_plus_rosenbrock(x: np.ndarray) -> np.ndarray: x = (0.05 * x) + 1 - sm = 0.0 - for i in range(0, len(x)-1): - tmp1 = x[i]*x[i]-x[i+1] - tmp2 = x[i] - 1.0 - temp = 100*tmp1*tmp1 + tmp2*tmp2 - sm += (temp*temp)/4000.0 - np.cos(temp) + 1 - tmp1 = x[-1]*x[-1] - x[0] - tmp2 = x[-1] - 1 - temp = 100.0*tmp1*tmp1 + tmp2*tmp2 - sm += (temp*temp)/4000.0 - np.cos(temp) + 1.0 - return sm + tmp1 = x[:, :-1]*x[:, :-1] - x[:, 1:] + tmp2 = x[:, :-1] - 1.0 + temp = 100*tmp1*tmp1 + tmp2*tmp2 + sm = (temp*temp)/4000 - np.cos(temp) + 1 + + tmp1 = x[:, -1:]*x[:, -1:] - x[:, 0:1] + tmp2 = x[:, -1:] - 1 + temp = 100*tmp1*tmp1 + tmp2*tmp2 + sm = sm + (temp*temp)/4000 - np.cos(temp) + 1 -def schaffers_f7(x): - nx = len(x) - # Note: the function definitions state to scale by 0.5/100, but the code + return np.sum(sm, axis=1) + + +def schaffers_f7(x: np.ndarray) -> np.ndarray: + nx = x.shape[1] + # NOTE: the function definitions state to scale by 0.5/100, but the code # doesn't do this, and the example graph in the definitions correspond to # the version without scaling # x = 0.005 * x sm = 0.0 - for i in range(0, nx-1): - si = (x[i]*x[i] + x[i+1]*x[i+1])**0.5 - tmp = np.sin(50.0*(si**0.2)) - # Note: the original code has this error here (tmp shouldn't be squared) - # that I'm keeping for consistency. - sm += (si**0.5) * (tmp*tmp + 1) + si = np.sqrt(x[:, :-1]*x[:, :-1] + x[:, 1:]*x[:, 1:]) + tmp = np.sin(50*(np.power(si, 0.2))) + # NOTE: the original code has this error here (tmp shouldn't be squared) + # that I'm keeping for consistency. + sm = np.sqrt(si) * (tmp*tmp + 1) + sm = np.sum(sm, axis=1) sm = (sm*sm) / (nx*nx - 2*nx + 1) return sm + all_functions = [ bent_cigar, sum_diff_pow, diff --git a/cec2017/composition.py b/cec2017/composition.py index 1e280dc..a8e99e3 100644 --- a/cec2017/composition.py +++ b/cec2017/composition.py @@ -8,17 +8,57 @@ import numpy as np + def _calc_w(x, sigma): - nx = len(x) - w = 0 - for i in range(0, nx): - w += x[i]*x[i] - if (w != 0): - w = ((1.0/w)**0.5) * np.exp(-w / (2.0*nx*sigma*sigma)) - else: - w = float('inf') + nx = x.shape[1] + w = np.sum(x*x, axis=1) + nzmask = w != 0 + w[nzmask] = ((1.0/w)**0.5) * np.exp(-w / (2.0*nx*sigma*sigma))[nzmask] + w[~nzmask] = float('inf') return w + +def _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases): + nv = x.shape[0] + nx = x.shape[1] + + N = len(funcs) + vals = np.zeros((nv, N)) + w = np.zeros((nv, N)) + for i in range(0, N): + x_shifted = x - np.expand_dims(shifts[i][:nx], 0) + x_t = transforms.shift_rotate(x, shifts[i][:nx], rotations[i]) + vals[:, i] = funcs[i](x_t) + w[:, i] = _calc_w(x_shifted, sigmas[i]) + w_sm = np.sum(w, axis=1) + + nz_mask = w_sm != 0.0 + w[nz_mask, :] /= w_sm[nz_mask, None] + w[~nz_mask, :] = 1/N + + return np.sum(w * (lambdas*vals + biases), axis=1) + + +def _compose_hybrids(x, rotations, shifts, shuffles, funcs, sigmas, offsets, biases): + nv = x.shape[0] + nx = x.shape[1] + + N = len(funcs) + vals = np.zeros((nv, N)) + w = np.zeros((nv, N)) + for i in range(0, N): + x_shifted = x - np.expand_dims(shifts[i][:nx], 0) + vals[:, i] = funcs[i](x, rotation=rotations[i], shift=shifts[i][:nx], shuffle=shuffles[i]) - offsets[i] + w[:, i] = _calc_w(x_shifted, sigmas[i]) + w_sm = np.sum(w, axis=1) + + nz_mask = w_sm != 0.0 + w[nz_mask, :] /= w_sm[nz_mask, None] + w[~nz_mask, :] = 1/N + + return np.sum(w * (vals + biases), axis=1) + + def f21(x, rotations=None, shifts=None): """ Composition Function 1 (N=3) @@ -31,32 +71,19 @@ def f21(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ - nx = len(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][0] if shifts is None: shifts = transforms.shifts_cf[0] - N = 3 funcs = [basic.rosenbrock, basic.high_conditioned_elliptic, basic.rastrigin] sigmas = np.array([10.0, 20.0, 30.0]) lambdas = np.array([1.0, 1.0e-6, 1.0]) biases = np.array([0.0, 100.0, 200.0]) - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](np.matmul(rotations[i], x_shifted)) - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] + return _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases) + 2100 - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) - - return np.sum(w * (lambdas*vals + biases)) + 2100 def f22(x, rotations=None, shifts=None): """ @@ -70,32 +97,21 @@ def f22(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][1] if shifts is None: shifts = transforms.shifts_cf[1] - N = 3 funcs = [basic.rastrigin, basic.griewank, basic.modified_schwefel] sigmas = np.array([10.0, 20.0, 30.0]) lambdas = np.array([1.0, 10.0, 1.0]) biases = np.array([0.0, 100.0, 200.0]) - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](np.matmul(rotations[i], x_shifted)) - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) + return _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases) + 2200 - return np.sum(w * (lambdas*vals + biases)) + 2200 def f23(x, rotations=None, shifts=None): """ @@ -109,32 +125,20 @@ def f23(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][2] if shifts is None: shifts = transforms.shifts_cf[2] - N = 4 funcs = [basic.rosenbrock, basic.ackley, basic.modified_schwefel, basic.rastrigin] sigmas = np.array([10.0, 20.0, 30.0, 40.0]) lambdas = np.array([1.0, 10.0, 1.0, 1.0]) biases = np.array([0.0, 100.0, 200.0, 300.0]) - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](np.matmul(rotations[i], x_shifted)) - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] + return _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases) + 2300 - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) - - return np.sum(w * (lambdas*vals + biases)) + 2300 def f24(x, rotations=None, shifts=None): """ @@ -148,32 +152,20 @@ def f24(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][3] if shifts is None: shifts = transforms.shifts_cf[3] - N = 4 funcs = [basic.ackley, basic.high_conditioned_elliptic, basic.griewank, basic.rastrigin] sigmas = np.array([10.0, 20.0, 30.0, 40.0]) lambdas = np.array([1.0, 1.0e-6, 10.0, 1.0]) biases = np.array([0.0, 100.0, 200.0, 300.0]) - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](np.matmul(rotations[i], x_shifted)) - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] + return _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases) + 2400 - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) - - return np.sum(w * (lambdas*vals + biases)) + 2400 def f25(x, rotations=None, shifts=None): """ @@ -187,32 +179,20 @@ def f25(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][4] if shifts is None: shifts = transforms.shifts_cf[4] - N = 5 funcs = [basic.rastrigin, basic.happy_cat, basic.ackley, basic.discus, basic.rosenbrock] sigmas = np.array([10.0, 20.0, 30.0, 40.0, 50.0]) lambdas = np.array([10.0, 1.0, 10.0, 1.0e-6, 1.0]) biases = np.array([0.0, 100.0, 200.0, 300.0, 400.0]) - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](np.matmul(rotations[i], x_shifted)) - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] - - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) + return _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases) + 2500 - return np.sum(w * (lambdas*vals + biases)) + 2500 def f26(x, rotations=None, shifts=None): """ @@ -226,34 +206,23 @@ def f26(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][5] if shifts is None: shifts = transforms.shifts_cf[5] - N = 5 funcs = [basic.expanded_schaffers_f6, basic.modified_schwefel, basic.griewank, basic.rosenbrock, basic.rastrigin] sigmas = np.array([10.0, 20.0, 20.0, 30.0, 40.0]) - # Note: the lambdas specified in the problem definitions (below) differ from what is used in the code + # NOTE: the lambdas specified in the problem definitions (below) differ from + # what is used in the code #lambdas = np.array([1.0e-26, 10.0, 1.0e-6, 10.0, 5.0e-4]) lambdas = np.array([5.0e-4, 1.0, 10.0, 1.0, 10.0]) biases = np.array([0.0, 100.0, 200.0, 300.0, 400.0]) - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](np.matmul(rotations[i], x_shifted)) - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] - - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) + return _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases) + 2600 - return np.sum(w * (lambdas*vals + biases)) + 2600 def f27(x, rotations=None, shifts=None): """ @@ -267,38 +236,27 @@ def f27(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][6] if shifts is None: shifts = transforms.shifts_cf[6] - N = 6 funcs = [ basic.h_g_bat, basic.rastrigin, basic.modified_schwefel, basic.bent_cigar, basic.high_conditioned_elliptic, - basic.expanded_schaffers_f6] + basic.expanded_schaffers_f6, + ] sigmas = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0]) lambdas = np.array([10.0, 10.0, 2.5, 1.0e-26, 1.0e-6, 5.0e-4]) biases = np.array([0.0, 100.0, 200.0, 300.0, 400.0, 500.0]) - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](np.matmul(rotations[i], x_shifted)) - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] - - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) + return _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases) + 2700 - return np.sum(w * (lambdas*vals + biases)) + 2700 def f28(x, rotations=None, shifts=None): """ @@ -312,38 +270,27 @@ def f28(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][7] if shifts is None: shifts = transforms.shifts_cf[7] - N = 6 funcs = [ basic.ackley, basic.griewank, basic.discus, basic.rosenbrock, basic.happy_cat, - basic.expanded_schaffers_f6] + basic.expanded_schaffers_f6, + ] sigmas = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0]) lambdas = np.array([10.0, 10.0, 1.0e-6, 1.0, 1.0, 5.0e-4]) biases = np.array([0.0, 100.0, 200.0, 300.0, 400.0, 500.0]) - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](np.matmul(rotations[i], x_shifted)) - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] + return _composition(x, rotations, shifts, funcs, sigmas, lambdas, biases) + 2800 - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) - - return np.sum(w * (lambdas*vals + biases)) + 2800 def f29(x, rotations=None, shifts=None, shuffles=None): """ @@ -359,7 +306,9 @@ def f29(x, rotations=None, shifts=None, shuffles=None): shuffles (array): Optional shuffle vectors (NxD). If None (default), the official permutation vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][8] if shifts is None: @@ -367,27 +316,13 @@ def f29(x, rotations=None, shifts=None, shuffles=None): if shuffles is None: shuffles = transforms.shuffles_cf[nx][0] - N = 3 funcs = [hybrid.f15, hybrid.f16, hybrid.f17] sigmas = np.array([10.0, 30.0, 50.0]) biases = np.array([0.0, 100.0, 200.0]) offsets = np.array([1500, 1600, 1700]) # subtract F* added at the end of the functions - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](x, rotation=rotations[i], shift=shifts[i][:nx], shuffle=shuffles[i]) - vals[i] -= offsets[i] - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) + return _compose_hybrids(x, rotations, shifts, shuffles, funcs, sigmas, offsets, biases) + 2900 - return np.sum(w * (vals + biases)) + 2900 def f30(x, rotations=None, shifts=None, shuffles=None): """ @@ -403,7 +338,9 @@ def f30(x, rotations=None, shifts=None, shuffles=None): shuffles (array): Optional shuffle vectors (NxD). If None (default), the official permutation vectors from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotations is None: rotations = transforms.rotations_cf[nx][9] if shifts is None: @@ -411,24 +348,8 @@ def f30(x, rotations=None, shifts=None, shuffles=None): if shuffles is None: shuffles = transforms.shuffles_cf[nx][1] - N = 3 funcs = [hybrid.f15, hybrid.f18, hybrid.f19] sigmas = np.array([10.0, 30.0, 50.0]) biases = np.array([0.0, 100.0, 200.0]) offsets = np.array([1500, 1800, 1900]) # subtract F* added at the end of the functions - vals = np.zeros(N) - w = np.zeros(N) - w_sm = 0.0 - for i in range(0, N): - x_shifted = x-shifts[i][:nx] - vals[i] = funcs[i](x, rotation=rotations[i], shift=shifts[i][:nx], shuffle=shuffles[i]) - vals[i] -= offsets[i] - w[i] = _calc_w(x_shifted, sigmas[i]) - w_sm += w[i] - - if (w_sm != 0.0): - w /= w_sm - else: - w = np.full(N, 1/N) - - return np.sum(w * (vals + biases)) + 3000 + return _compose_hybrids(x, rotations, shifts, shuffles, funcs, sigmas, offsets, biases) + 3000 diff --git a/cec2017/hybrid.py b/cec2017/hybrid.py index b42a226..ba54fbd 100644 --- a/cec2017/hybrid.py +++ b/cec2017/hybrid.py @@ -35,6 +35,7 @@ def _shuffle_and_partition(x, shuffle, partitions): parts.append(xs[end:]) return parts + def f11(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 1 (N=3) @@ -48,7 +49,9 @@ def f11(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][10] if shift is None: @@ -56,14 +59,15 @@ def f11(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][0] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.2, 0.4, 0.4]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.2, 0.4, 0.4]) y = basic.zakharov(x_parts[0]) y += basic.rosenbrock(x_parts[1]) y += basic.rastrigin(x_parts[2]) return y + 1100.0 + def f12(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 2 (N=3) @@ -77,7 +81,9 @@ def f12(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][11] if shift is None: @@ -85,14 +91,15 @@ def f12(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][1] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.3, 0.3, 0.4]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.3, 0.3, 0.4]) y = basic.high_conditioned_elliptic(x_parts[0]) y += basic.modified_schwefel(x_parts[1]) y += basic.bent_cigar(x_parts[2]) return y + 1200.0 + def f13(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 3 (N=3) @@ -106,7 +113,9 @@ def f13(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][12] if shift is None: @@ -114,14 +123,15 @@ def f13(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][2] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.3, 0.3, 0.4]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.3, 0.3, 0.4]) y = basic.bent_cigar(x_parts[0]) y += basic.rosenbrock(x_parts[1]) y += basic.lunacek_bi_rastrigin(x_parts[2]) return y + 1300.0 + def f14(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 4 (N=4) @@ -135,7 +145,9 @@ def f14(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][13] if shift is None: @@ -143,8 +155,8 @@ def f14(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][3] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.2, 0.4]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.2, 0.4]) y = basic.high_conditioned_elliptic(x_parts[0]) y += basic.ackley(x_parts[1]) @@ -152,6 +164,7 @@ def f14(x, rotation=None, shift=None, shuffle=None): y += basic.rastrigin(x_parts[3]) return y + 1400.0 + def f15(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 5 (N=4) @@ -165,7 +178,9 @@ def f15(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][14] if shift is None: @@ -173,8 +188,8 @@ def f15(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][4] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.3, 0.3]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.3, 0.3]) y = basic.bent_cigar(x_parts[0]) y += basic.h_g_bat(x_parts[1]) @@ -182,6 +197,7 @@ def f15(x, rotation=None, shift=None, shuffle=None): y += basic.rosenbrock(x_parts[3]) return y + 1500.0 + def f16(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 6 (N=4) @@ -195,7 +211,9 @@ def f16(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][15] if shift is None: @@ -203,8 +221,8 @@ def f16(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][5] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.3, 0.3]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.3, 0.3]) y = basic.expanded_schaffers_f6(x_parts[0]) y += basic.h_g_bat(x_parts[1]) @@ -212,6 +230,7 @@ def f16(x, rotation=None, shift=None, shuffle=None): y += basic.modified_schwefel(x_parts[3]) return y + 1600.0 + def f17(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 7 (N=5) @@ -225,7 +244,9 @@ def f17(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][16] if shift is None: @@ -233,8 +254,8 @@ def f17(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][6] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.1, 0.2, 0.2, 0.2, 0.3]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.1, 0.2, 0.2, 0.2, 0.3]) y = basic.katsuura(x_parts[0]) y += basic.ackley(x_parts[1]) @@ -243,6 +264,7 @@ def f17(x, rotation=None, shift=None, shuffle=None): y += basic.rastrigin(x_parts[4]) return y + 1700.0 + def f18(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 8 (N=5) @@ -256,7 +278,9 @@ def f18(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][17] if shift is None: @@ -264,8 +288,8 @@ def f18(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][7] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.2, 0.2, 0.2]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.2, 0.2, 0.2]) y = basic.high_conditioned_elliptic(x_parts[0]) y += basic.ackley(x_parts[1]) @@ -274,6 +298,7 @@ def f18(x, rotation=None, shift=None, shuffle=None): y += basic.discus(x_parts[4]) return y + 1800.0 + def f19(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 9 (N=5) @@ -287,7 +312,9 @@ def f19(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][18] if shift is None: @@ -295,8 +322,8 @@ def f19(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][8] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.2, 0.2, 0.2]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.2, 0.2, 0.2, 0.2, 0.2]) y = basic.bent_cigar(x_parts[0]) y += basic.rastrigin(x_parts[1]) @@ -305,6 +332,7 @@ def f19(x, rotation=None, shift=None, shuffle=None): y += basic.expanded_schaffers_f6(x_parts[4]) return y + 1900.0 + def f20(x, rotation=None, shift=None, shuffle=None): """ Hybrid Function 10 (N=6) @@ -318,7 +346,9 @@ def f20(x, rotation=None, shift=None, shuffle=None): shuffle (array): Optionbal shuffle vector. If None (default), the official permutation vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][19] if shift is None: @@ -326,8 +356,8 @@ def f20(x, rotation=None, shift=None, shuffle=None): if shuffle is None: shuffle = transforms.shuffles[nx][9] - x_transformed = np.matmul(rotation, x - shift) - x_parts = _shuffle_and_partition(x_transformed, shuffle, [0.1, 0.1, 0.2, 0.2, 0.2, 0.2]) + x_transformed = transforms.shift_rotate(x, shift, rotation) + x_parts = transforms.shuffle_and_partition(x_transformed, shuffle, [0.1, 0.1, 0.2, 0.2, 0.2, 0.2]) y = basic.happy_cat(x_parts[0]) y += basic.katsuura(x_parts[1]) @@ -337,6 +367,7 @@ def f20(x, rotation=None, shift=None, shuffle=None): y += basic.schaffers_f7(x_parts[5]) return y + 2000.0 + all_functions = [ f11, f12, diff --git a/cec2017/simple.py b/cec2017/simple.py index 34004fe..7c4792a 100644 --- a/cec2017/simple.py +++ b/cec2017/simple.py @@ -7,6 +7,7 @@ import numpy as np + def f1(x, rotation=None, shift=None): """ Shifted and Rotated Bent Cigar Function @@ -18,14 +19,18 @@ def f1(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][0] if shift is None: shift = transforms.shifts[0][:nx] - x_transformed = np.matmul(rotation, x - shift) + + x_transformed = transforms.shift_rotate(x, shift, rotation) return basic.bent_cigar(x_transformed) + 100.0 + def f2(x, rotation=None, shift=None): """ (Deprecated) Shifted and Rotated Sum of Different Power Function @@ -41,14 +46,17 @@ def f2(x, rotation=None, shift=None): f2.warned = True print('WARNING: f2 has been deprecated from the CEC 2017 benchmark suite') - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][1] if shift is None: shift = transforms.shifts[1][:nx] - x_transformed = np.matmul(rotation, x - shift) + x_transformed = transforms.shift_rotate(x, shift, rotation) return basic.sum_diff_pow(x_transformed) + 200.0 + def f3(x, rotation=None, shift=None): """ Shifted and Rotated Zakharov Function @@ -60,17 +68,20 @@ def f3(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][2] if shift is None: shift = transforms.shifts[2][:nx] - x_transformed = np.matmul(rotation, x - shift) + x_transformed = transforms.shift_rotate(x, shift, rotation) return basic.zakharov(x_transformed) + 300.0 + def f4(x, rotation=None, shift=None): """ - Shifted and Rotated Rosenbrock’s Function + Shifted and Rotated Rosenbrock's Function Args: x (array): Input vector of dimension 2, 10, 20, 30, 50 or 100. @@ -79,14 +90,17 @@ def f4(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][3] if shift is None: shift = transforms.shifts[3][:nx] - x_transformed = np.matmul(rotation, (x - shift)) + x_transformed = transforms.shift_rotate(x, shift, rotation) return basic.rosenbrock(x_transformed) + 400.0 + def f5(x, rotation=None, shift=None): """ Shifted and Rotated Rastrigin's Function @@ -98,17 +112,20 @@ def f5(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][4] if shift is None: shift = transforms.shifts[4][:nx] - x_transformed = np.matmul(rotation, (x - shift)) + x_transformed = transforms.shift_rotate(x, shift, rotation) return basic.rastrigin(x_transformed) + 500.0 + def f6(x, rotation=None, shift=None): """ - Shifted and Rotated Schaffer’s F7 Function + Shifted and Rotated Schaffer's F7 Function Args: x (array): Input vector of dimension 2, 10, 20, 30, 50 or 100. @@ -117,17 +134,20 @@ def f6(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][5] if shift is None: shift = transforms.shifts[5][:nx] - x_transformed = np.matmul(rotation, (x - shift)) + x_transformed = transforms.shift_rotate(x, shift, rotation) return basic.schaffers_f7(x_transformed) + 600.0 + def f7(x, rotation=None, shift=None): """ - Shifted and Rotated Lunacek Bi-Rastrigin’s Function + Shifted and Rotated Lunacek Bi-Rastrigin's Function Args: x (array): Input vector of dimension 2, 10, 20, 30, 50 or 100. @@ -136,7 +156,9 @@ def f7(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][6] if shift is None: @@ -144,6 +166,7 @@ def f7(x, rotation=None, shift=None): # pass the shift and rotation directly to the function return basic.lunacek_bi_rastrigin(x, shift, rotation) + 700.0 + def f8(x, rotation=None, shift=None): """ Shifted and Rotated Non-Continuous Rastrigin’s Function @@ -155,7 +178,9 @@ def f8(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][7] if shift is None: @@ -163,6 +188,7 @@ def f8(x, rotation=None, shift=None): # pass the shift and rotation directly to the function return basic.non_cont_rastrigin(x, shift, rotation) + 800.0 + def f9(x, rotation=None, shift=None): """ Shifted and Rotated Levy Function @@ -174,14 +200,17 @@ def f9(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][8] if shift is None: shift = transforms.shifts[8][:nx] - x_transformed = np.matmul(rotation, (x - shift)) + x_transformed = transforms.shift_rotate(x, shift, rotation) return basic.levy(x_transformed) + 900.0 + def f10(x, rotation=None, shift=None): """ Shifted and Rotated Schwefel’s Function @@ -193,14 +222,17 @@ def f10(x, rotation=None, shift=None): shift (array): Optional shift vector. If None (default), the official vector from the benchmark suite will be used. """ - nx = len(x) + x = np.array(x) + nx = x.shape[1] + if rotation is None: rotation = transforms.rotations[nx][9] if shift is None: shift = transforms.shifts[9][:nx] - x_transformed = np.matmul(rotation, (x - shift)) + x_transformed = transforms.shift_rotate(x, shift, rotation) return basic.modified_schwefel(x_transformed) + 1000.0 + all_functions = [ f1, f2, diff --git a/cec2017/transforms.py b/cec2017/transforms.py index 877eaf1..c2d40d0 100644 --- a/cec2017/transforms.py +++ b/cec2017/transforms.py @@ -58,3 +58,53 @@ 50: _pkl['shuffle_cf_D50'], 100: _pkl['shuffle_cf_D100'] } + + +def shift_rotate(x: np.ndarray, shift: np.ndarray, rotation: np.ndarray) -> np.ndarray: + """ + Apply the shift and rotation to vector x along its second axis. + + Args: + x (np.ndarray): + (M, N) array of M N-dimensional vectors. + shift (np.ndarray): + Array of size N providing the shift. + rotation (np.ndarray): + (N, N) array providing the rotation matrix. + + Returns: + (M, N) array of M shifted and rotated N-dimensional vectors. + """ + shifted = np.expand_dims(x - np.expand_dims(shift, 0), -1) + x_transformed = np.matmul(np.expand_dims(rotation, 0), shifted) + return x_transformed[:, :, 0] + + +def shuffle_and_partition(x, shuffle, partitions): + """ + First applies the given permutation, then splits x into partitions given + the percentages. + + Args: + x (array): Input vector. + shuffle (array): Shuffle vector. + partitions (list): List of percentages. Assumed to add up to 1.0. + + Returns: + (list of arrays): The partitions of x after shuffling. + """ + nx = x.shape[1] + + # shuffle + xs = np.zeros_like(x) + for i in range(0, nx): + xs[:, i] = x[:, shuffle[i]] + # and partition + parts = [] + start, end = 0, 0 + for p in partitions[:-1]: + end = start + int(np.ceil(p * nx)) + parts.append(xs[:, start:end]) + start = end + parts.append(xs[:, end:]) + return parts From 63c53c717523c924bfe8bd4cc8f3300b15132bf7 Mon Sep 17 00:00:00 2001 From: Duncan Tilley Date: Wed, 23 Nov 2022 09:01:14 +0200 Subject: [PATCH 2/4] Let plotting use multiple samples --- cec2017/utils.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cec2017/utils.py b/cec2017/utils.py index 00078bc..05bcd10 100644 --- a/cec2017/utils.py +++ b/cec2017/utils.py @@ -24,16 +24,19 @@ def surface_plot(function, domain=(-100,100), points=30, dimension=2, ax=None): # create points^2 tuples of (x,y) and populate z xys = np.linspace(domain[0], domain[1], points) xys = np.transpose([np.tile(xys, len(xys)), np.repeat(xys, len(xys))]) - zs = np.zeros(points*points) + # zs = np.zeros(points*points) if dimension > 2: # concatenate remaining zeros - tail = np.zeros(dimension - 2) - for i in range(0, xys.shape[0]): - zs[i] = function(np.concatenate([xys[i], tail])) + tail = np.zeros((xys.shape[0], dimension - 2)) + x = np.concatenate([xys, tail], axis=1) + zs = function(x) + # for i in range(0, xys.shape[0]): + # zs[i] = function(np.concatenate([xys[i], tail])) else: - for i in range(0, xys.shape[0]): - zs[i] = function(xys[i]) + zs = function(xys) + # for i in range(0, xys.shape[0]): + # zs[i] = function(xys[i]) # create the plot ax_in = ax From 78b863b8d4d9d6a7b0f7a5f6551063127aeae8b4 Mon Sep 17 00:00:00 2001 From: Duncan Tilley Date: Wed, 23 Nov 2022 09:34:39 +0200 Subject: [PATCH 3/4] Update documents --- LICENSE.txt | 2 +- README.md | 32 +++++++++++++++++++++++--------- example.py | 37 ++++++++++++++++++++++++++----------- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index de033de..101499c 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Duncan Tilley +Copyright (c) 2022 Duncan Tilley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a459204..69679fb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # CEC 2017 Python +> **⚠ 23 Nov 2022**: Breaking changes were made to all function signatures. See the changelog for details. + Python 3 module containing a native implementation of the CEC 2017 benchmark functions (single objective optimization). The implementation is adapted from Awad's original C implementation, available on [Suganthan's GitHub repo](https://github.com/P-N-Suganthan/CEC2017-BoundContrained), along with the problem definitions [1]. Although there are wrappers for the C code, this module is easier to use, natively supports numpy arrays and is (_much_) more readable than the C implementation. @@ -14,9 +16,9 @@ As per the problem definitions, functions are defined for 10, 30, 50 and 100 dim ## Features -- Native implementation of all CEC 2017 single objective functions -- Pre-defined rotations, shifts and shuffles for 2, 10, 20, 30, 50 and 100 dimensions -- Allows custom rotations, shifts and shuffles +- Native implementation of all CEC 2017 single objective functions optimized for multiple simultaneous evaluations +- Pre-defined rotations, shifts, and shuffles for 2, 10, 20, 30, 50 and 100 dimensions +- Allows custom rotations, shifts, and shuffles - Convenient surface plot utility - Easy access to basic functions f1 to f19 (e.g. Ackley, Discus, etc.) @@ -30,26 +32,38 @@ python3 setup.py install ## Usage -Below is a simple example of executing either a single function or all functions. See [example.py](example.py) for more advanced use cases. +Below is a simple example of executing either a single function or all functions. Note that each function takes a 2D array and returns a 1D array. See [example.py](example.py) for more advanced use cases. ```py # Using only f5: from cec2017.functions import f5 -x = np.random.uniform(-100, 100, size=50) +samples = 3 +dimension = 50 +x = np.random.uniform(-100, 100, size=(samples, dimension)) val = f5(x) -print('f5(x) = %.6f' %val) +for i in range(samples): + print(f"f5(x_{i}) = {val[i]:.6f}") # Using all functions: from cec2017.functions import all_functions for f in all_functions: - x = np.random.uniform(-100, 100, size=50) + x = np.random.uniform(-100, 100, size=(samples, dimension)) val = f(x) - print('%s(x) = %.6f' %( f.__name__, val )) + for i in range(samples): + print(f"{f.__name__}(x_{i}) = {val[i]:.6f}") ``` +## Changelog + +### 23 Nov 2022 +- All functions have been reimplemented using numpy vector operations instead of native python loops. +- **Breaking change:** Functions now expect a 2D input array of shape (m, D) where D is the dimensionality, and m is an arbitrary sample size. + +The above updates have allowed a substantial increase in performance when evaluating multiple parameter samples simultaneously, which previously would have had to be done as consecutive calls. See [PR 5](https://github.com/tilleyd/cec2017-py/pull/5) for more details. + ## License -Copyright © 2020 Duncan Tilley +Copyright © 2022 Duncan Tilley See the [license notice](LICENSE.txt) for full details. ## Issues diff --git a/example.py b/example.py index 21c8aee..a4794c6 100755 --- a/example.py +++ b/example.py @@ -1,30 +1,45 @@ #!/usr/bin/python3 import numpy as np -# Accepted dimensions are 2, 10, 20, 30, 50 or 100 -# (f11 - f20 and f29 - f30 not defined for D = 2) -D = 10 -# evaluate a specific function a few times +# evaluate a specific function a few times with one sample import cec2017.functions as functions + f = functions.f5 +dimension = 30 +for i in range(0, 10): + x = np.random.uniform(low=-100, high=100, size=dimension) + y = f([x])[0] + print(f"f5({x[0]:.2f}, {x[1]:.2f}, ...) = {y:.2f}") + +# or with a population (i.e. multiple samples) +f = functions.f3 +samples = 3 +dimension = 30 for i in range(0, 10): - x = np.random.uniform(low=-100, high=100, size=D) + x = np.random.uniform(low=-100, high=100, size=(samples, dimension)) y = f(x) - print('%s( %.1f, %.1f, ... ) = %.2f' %(f.__name__, x[0], x[1], y)) + for i in range(samples): + print(f"f5({x[i, 0]:.2f}, {x[i, 1]:.2f}, ...) = {y[i]:.2f}") # or evaluate each function once +samples = 3 +dimension = 50 for f in functions.all_functions: - x = np.random.uniform(low=-100, high=100, size=D) - y = f(x) - print('%s( %.1f, %.1f, ... ) = %.2f' %(f.__name__, x[0], x[1], y)) + x = np.random.uniform(-100, 100, size=(samples, dimension)) + val = f(x) + for i in range(samples): + print(f"{f.__name__}({x[i, 0]:.2f}, {x[i, 1]:.2f}, ...) = {y[i]:.2f}") # or all hybrid functions (or basic, simple or composite functions...) import cec2017.simple as simple # cec2017.basic cec2017.hybrid cec2017.composite +samples = 3 +dimension = 50 for f in simple.all_functions: # f1 to f10 - x = np.random.uniform(low=-100, high=100, size=D) + x = np.random.uniform(low=-100, high=100, size=(samples, dimension)) y = f(x) - print('%s( %.1f, %.1f, ... ) = %.2f' %(f.__name__, x[0], x[1], y)) + for i in range(samples): + print(f"{f.__name__}({x[i, 0]:.2f}, {x[i, 1]:.2f}, ...) = {y[i]:.2f}") # make a surface plot of f27 import cec2017.utils as utils From 2464a286054b7ac95fb404cc35a747f9b1aa01b9 Mon Sep 17 00:00:00 2001 From: Duncan Tilley Date: Wed, 23 Nov 2022 09:48:50 +0200 Subject: [PATCH 4/4] Add tests and small fix --- cec2017/composition.py | 15 ++++++++ tests/__init__.py | 0 tests/test_functions.py | 79 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_functions.py diff --git a/cec2017/composition.py b/cec2017/composition.py index a8e99e3..781c120 100644 --- a/cec2017/composition.py +++ b/cec2017/composition.py @@ -71,6 +71,7 @@ def f21(x, rotations=None, shifts=None): shifts (array): Optional shift vectors (NxD). If None (default), the official vectors from the benchmark suite will be used. """ + x = np.array(x) nx = x.shape[1] if rotations is None: @@ -353,3 +354,17 @@ def f30(x, rotations=None, shifts=None, shuffles=None): biases = np.array([0.0, 100.0, 200.0]) offsets = np.array([1500, 1800, 1900]) # subtract F* added at the end of the functions return _compose_hybrids(x, rotations, shifts, shuffles, funcs, sigmas, offsets, biases) + 3000 + + +all_functions = [ + f21, + f22, + f23, + f24, + f25, + f26, + f27, + f28, + f29, + f30, +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_functions.py b/tests/test_functions.py new file mode 100644 index 0000000..d51b095 --- /dev/null +++ b/tests/test_functions.py @@ -0,0 +1,79 @@ +import numpy as np + + +def test_simple_functions(): + from cec2017.simple import all_functions + + assert len(all_functions) == 10 + + M = 2 + dims = [2, 10, 20, 30, 50, 100] + + for i, f in enumerate(all_functions): + assert f.__name__ == f"f{1 + i}" + for d in dims: + x = np.zeros((M, d)) + y = f(x) + assert y.shape == (M,) + + pass + + +def test_hybrid_functions(): + from cec2017.hybrid import all_functions + + assert len(all_functions) == 10 + + M = 2 + dims = [10, 30, 50, 100] + + for i, f in enumerate(all_functions): + assert f.__name__ == f"f{11 + i}" + for d in dims: + x = np.zeros((M, d)) + y = f(x) + assert y.shape == (M,) + + +def test_composition_functions(): + from cec2017.composition import all_functions + + assert len(all_functions) == 10 + + M = 2 + dims = [10, 30, 50, 100] + + for i, f in enumerate(all_functions): + assert f.__name__ == f"f{21 + i}" + for d in dims: + x = np.zeros((M, d)) + y = f(x) + assert y.shape == (M,) + + +def test_all_functions(): + from cec2017.functions import all_functions + + assert len(all_functions) == 30 + + M = 2 + D = 10 + x = np.zeros((M, D)) + + for i, f in enumerate(all_functions): + assert f.__name__ == f"f{1 + i}" + y = f(x) + assert y.shape == (M,) + + +def test_list_inputs(): + from cec2017.functions import all_functions + + M = 2 + D = 10 + x = [0.0] * D + x = [x] * M + # functions should allow + for f in all_functions: + y = f(x) + assert y.shape == (M,)