def _initialize(self): # ! get the initial population - different ways are possible # provide a whole population object - (individuals might be already evaluated) if isinstance(self.sampling, Population): pop = self.sampling else: pop = Population(0, individual=self.individual) if isinstance(self.sampling, np.ndarray): pop = pop.new("X", self.sampling) else: pop = self.sampling.sample(self.problem, pop, self.pop_size, algorithm=self) # repair in case it is necessary if self.func_repair: pop = self.func_repair(self.problem, pop, algorithm=self) # in case the initial population was not evaluated if np.any(pop.collect(lambda ind: ind.F is None, as_numpy_array=True)): self.evaluator.eval(self.problem, pop, algorithm=self) # that call is a dummy survival to set attributes that are necessary for the mating selection if self.survival: pop = self.survival.do(self.problem, pop, self.pop_size, algorithm=self) return pop
def _next(self): # all the elements in the interval a, left, right, b = self.pop # the golden ratio (precomputed constant) R = self.R # if the left solution is better than the right if left.F[0] < right.F[0]: # make the right to be the new right bound and the left becomes the right a, b = a, right right = left # create a new left individual and evaluate left = Individual(X=b.X - R * (b.X - a.X)) self.evaluator.eval(self.problem, Population.create(left), algorithm=self) # if the right solution is better than the left else: # make the left to be the new left bound and the right becomes the left a, b = left, b left = right # create a new right individual and evaluate right = Individual(X=a.X + R * (b.X - a.X)) self.evaluator.eval(self.problem, Population.create(right), algorithm=self) # update the population with all the four individuals self.pop = Population.create(a, left, right, b)
def _next(self): # do the mating using the total population Hm = Population.merge(self.pop, self.da) self.off = self.mating.do(self.problem, Hm, n_offsprings=self.n_offsprings, algorithm=self) # if the mating could not generate any new offspring (duplicate elimination might make that happen) if len(self.off) == 0: self.termination.force_termination = True return # if not the desired number of offspring could be created elif len(self.off) < self.n_offsprings: if self.verbose: print( "WARNING: Mating could not produce the required number of (unique) offsprings!" ) # evaluate the offspring self.evaluator.eval(self.problem, self.off, algorithm=self) # merge the offsprings with the current population self.pop = Population.merge(self.pop, self.off) # the do survival selection self.pop, self.da = self.survival.do(self.problem, self.pop, self.da, self.pop_size, algorithm=self)
def _initialize(self): # ! get the initial population - different ways are possible # provide a whole population object - (individuals might be already evaluated) if isinstance(self.sampling, Population): pop = self.sampling else: pop = Population(0, individual=self.individual) if isinstance(self.sampling, np.ndarray): pop = pop.new("X", self.sampling) else: pop = self.sampling.do(self.problem, self.pop_size, pop=pop, algorithm=self) # repair all solutions that are not already evaluated if self.repair: I = [k for k in range(len(pop)) if pop[k].F is None] pop = self.repair.do(self.problem, pop[I], algorithm=self) # then evaluate using the objective function self.evaluator.eval(self.problem, pop, algorithm=self) # that call is a dummy survival to set attributes that are necessary for the mating selection if self.survival: pop = self.survival.do(self.problem, pop, len(pop), algorithm=self) self.pop = pop
def _step(self): pop = self.pop X = pop.get("X") F = pop.get("F") #Levy Flight best = self.opt G_X = best.get("X") step_size = self._get_global_step_size(X) _X = X + np.random.rand(*X.shape) * step_size * (G_X - X) _X = set_to_bounds_if_outside_by_problem(self.problem, _X) #Evaluate off = Population(len(pop)).set("X", _X) self.evaluator.eval(self.problem, off, algorithm=self) # replace the worse pop with better off per index # this method includes replacement with less constraints violation # which the original paper doesn't have ImprovementReplacement().do(self.problem, pop, off, inplace=True) #Local Random Walk dir_vec = self._get_local_directional_vector(X) _X = X + dir_vec _X = set_to_bounds_if_outside_by_problem(self.problem, _X) off = Population(len(pop)).set("X", _X) self.evaluator.eval(self.problem, off, algorithm=self) #append offspring to population and then sort for elitism (survival) self.pop = Population.merge(pop, off) self.pop = self.survival.do(self.problem, self.pop, self.pop_size, algorithm=self)
def _do(self, problem, pop, n_offsprings, parents=None, **kwargs): rnd = np.random.random(n_offsprings) n_neighbors = (rnd <= self.bias).sum() other = super()._do(problem, pop, n_offsprings - n_neighbors, parents, **kwargs) N = [] cand = TournamentSelection(comp_by_rank).do(pop, n_neighbors, n_parents=1)[:, 0] for k in cand: N.append(pop[k]) n_cand_neighbors = pop[k].get("neighbors") rnd = np.random.permutation( len(n_cand_neighbors))[:self.crossover.n_parents - 1] [N.append(e) for e in n_cand_neighbors[rnd]] parents = np.reshape(np.arange(len(N)), (-1, self.crossover.n_parents)) N = Population.create(*N) bias = super()._do(problem, N, n_neighbors, parents, **kwargs) return Population.merge(bias, other)
def test_restricted_mating_selection(self): np.random.seed(200) selection = RestrictedMating(func_comp=comp_by_cv_dom_then_random) problem = C3DTLZ4(n_var=12, n_obj=3) ca_x = np.loadtxt(path_to_test_resources('ctaea', 'c3dtlz4', 'case2', 'preCA.x')) CA = Population.create(ca_x) self.evaluator.eval(problem, CA) da_x = np.loadtxt(path_to_test_resources('ctaea', 'c3dtlz4', 'case2', 'preDA.x')) DA = Population.create(da_x) self.evaluator.eval(problem, DA) Hm = CA.merge(DA) n_pop = len(CA) _, rank = NonDominatedSorting().do(Hm.get('F'), return_rank=True) Pc = (rank[:n_pop] == 0).sum()/len(Hm) Pd = (rank[n_pop:] == 0).sum()/len(Hm) P = selection.do(Hm, len(CA)) assert P.shape == (91, 2) if Pc > Pd: assert (P[:, 0] < n_pop).all() else: assert (P[:, 0] >= n_pop).all() assert (P[:, 1] >= n_pop).any() assert (P[:, 1] < n_pop).any()
def test_association(self): problem = C1DTLZ3(n_var=12, n_obj=3) ca_x = np.loadtxt(path_to_test_resources('ctaea', 'c1dtlz3', 'case3', 'preCA.x')) CA = Population.create(ca_x) self.evaluator.eval(problem, CA) da_x = np.loadtxt(path_to_test_resources('ctaea', 'c1dtlz3', 'case3', 'preDA.x')) DA = Population.create(da_x) self.evaluator.eval(problem, DA) off_x = np.loadtxt(path_to_test_resources('ctaea', 'c1dtlz3', 'case3', 'offspring.x')) off = Population.create(off_x) self.evaluator.eval(problem, off) true_assoc = np.loadtxt(path_to_test_resources('ctaea', 'c1dtlz3', 'case3', 'feasible_rank0.txt')) true_niche = true_assoc[:, 1] true_id = true_assoc[:, 0] sorted_id = np.argsort(true_id) survival = CADASurvival(self.ref_dirs) mixed = CA.merge(off) survival.ideal_point = np.min(np.vstack((DA.get("F"), mixed.get("F"))), axis=0) fronts = NonDominatedSorting().do(mixed.get("F"), n_stop_if_ranked=len(self.ref_dirs)) I = np.concatenate(fronts) niche, _ = survival._associate(mixed[I]) sorted_I = np.argsort(I) assert (niche[sorted_I] == true_niche[sorted_id]).all()
def do(self, problem, n_samples, pop=Population(), **kwargs): """ Sample new points with problem information if necessary. Parameters ---------- problem : :class:`~pymoo.model.problem.Problem` The problem to which points should be sampled. (lower and upper bounds, discrete, binary, ...) n_samples : int Number of samples pop : :class:`~pymoo.model.population.Population` The sampling results are stored in a population. The template of the population can be changed. If 'none' simply a numpy array is returned. Returns ------- X : numpy.array Samples points in a two dimensional array """ val = self._do(problem, n_samples, **kwargs) if pop is None: return val return Population.new("X", val)
def _update(self): D = self.D ind = Individual(X=np.copy(D["X"]), F=np.copy(D["F"]), G=np.copy(-D["G"])) pop = Population.merge(self.pop, Population.create(ind)) set_cv(pop) self.pop = pop
def _initialize(self): pop = Population() if isinstance(self.sampling, np.ndarray): pop.X = self.sampling else: pop.X = self.sampling.sample(self.problem, self.pop_size) pop.F, pop.G = self.evaluator.eval(self.problem, pop.X) return pop
def crossover(crossover, a, b, c=None, xl=0, xu=1, type_var=np.double, **kwargs): n = a.shape[0] _pop = Population.merge(Population().new("X", a), Population().new("X", b)) _P = np.column_stack([np.arange(n), np.arange(n) + n]) if c is not None: _pop = Population.merge(_pop, Population().new("X", c)) _P = np.column_stack([_P, np.arange(n) + 2 * n]) problem = get_problem_func(a.shape[1], xl, xu, type_var)(**kwargs) return crossover.do(problem, _pop, _P, **kwargs).get("X")
def _each_iteration_samoo(self, X, **kwargs): self.archive = self.samoo_evaluator.eval(problem=self.samoo_problem, x=X, archive=self.archive) temp_pop = Population(0, individual=Individual()) temp_pop = temp_pop.new("X", self.archive['x'], "F", self.archive['f'], "CV", self.archive['cv'], "G", self.archive['g'], "feasible", self.archive['feasible_index']) self.func_eval = self.archive['x'].shape[0] return temp_pop
def test_preevaluated(self): evaluator = Evaluator() pop = Population().new("X", X) evaluator.eval(problem, pop) pop[range(30)].set("F", None) evaluator = Evaluator() evaluator.eval(problem, pop) np.testing.assert_allclose(F, pop.get("F")) self.assertTrue(evaluator.n_eval == 30)
def _next(self, pop): # do the iteration for the next generation - population objective is modified inplace # do the mating and evaluate the offsprings off = Population() off.X = self._mating(pop) off.F, off.G = self.evaluator.eval(self.problem, off.X) # do the survival selection with the merged population self.survival.do(pop, off, self.pop_size, out=self.D, **self.D) return off
def _solve(self, pop): # generate direction vectors by random sampling ref_dirs = np.random.random((self.batch_size, self.n_obj)) ref_dirs /= np.expand_dims(np.sum(ref_dirs, axis=1), 1) algo = MOEAD_algo(ref_dirs=ref_dirs, n_neighbors=len(ref_dirs), eliminate_duplicates=False) repair, crossover, mutation = algo.mating.repair, algo.mating.crossover, algo.mating.mutation if isinstance(algo.decomposition, str): decomp = algo.decomposition if decomp == 'auto': if self.n_obj <= 2: decomp = 'tchebi' else: decomp = 'pbi' decomposition = get_decomposition(decomp) else: decomposition = algo.decomposition ideal_point = np.min(pop.get('F'), axis=0) # find the optimal individual for each reference direction opt_pop = Population(0, individual=Individual()) for i in range(self.batch_size): N = algo.neighbors[i, :] FV = decomposition.do(pop.get("F"), weights=ref_dirs[i], ideal_point=ideal_point) opt_pop = Population.merge(opt_pop, pop[np.argmin(FV)]) all_off = Population(0, individual=Individual()) for i in np.random.permutation(self.batch_size): N = algo.neighbors[i, :] if self.batch_size > 1: if np.random.random() < algo.prob_neighbor_mating: parents = N[np.random.permutation(algo.n_neighbors)][:crossover.n_parents] else: parents = np.random.permutation(algo.pop_size)[:crossover.n_parents] # do recombination and create an offspring off = crossover.do(self.real_problem, opt_pop, parents[None, :]) else: off = opt_pop[N].copy() off = mutation.do(self.real_problem, off) off = off[np.random.randint(0, len(off))] # repair first in case it is necessary if repair: off = algo.repair.do(self.real_problem, off, algorithm=algo) all_off = Population.merge(all_off, off) return all_off
def _exploration_move(self, center, opt=None): if opt is None: opt = center def step(x, delta, k): # copy and add delta to the new point X = np.copy(x) # normalize the delta by the bounds if they are provided by the problem eps = delta[k] # if the problem has bounds normalize the delta if self.problem.has_bounds(): xl, xu = self.problem.bounds() eps *= (xu[k] - xl[k]) # now add to the current solution X[k] = X[k] + eps # repair if out of bounds if necessary X = set_to_bounds_if_outside_by_problem(self.problem, X) # return the new solution as individual mutant = pop_from_array_or_individual(X)[0] return mutant for k in range(self.problem.n_var): # create the the individual and evaluate it mutant = step(center.X, self.explr_delta, k) self.evaluator.eval(self.problem, mutant, algorithm=self) self.pop = Population.merge(self.pop, mutant) if is_better(mutant, opt): center, opt = mutant, mutant else: # inverse the sign of the delta self.explr_delta[k] = -self.explr_delta[k] # now try the other sign if there was no improvement mutant = step(center.X, self.explr_delta, k) self.evaluator.eval(self.problem, mutant, algorithm=self) self.pop = Population.merge(self.pop, mutant) if is_better(mutant, opt): center, opt = mutant, mutant return opt
def eval(self, problem, pop, **kwargs): """ This function is used to return the result of one valid evaluation. Parameters ---------- problem : class The problem which is used to be evaluated pop : np.array or Population object kwargs : dict Additional arguments which might be necessary for the problem to evaluate. """ is_individual = isinstance(pop, Individual) is_numpy_array = isinstance(pop, np.ndarray) and not isinstance(pop, Population) # make sure the object is a population if is_individual or is_numpy_array: pop = Population().create(pop) # find indices to be evaluated if self.skip_already_evaluated: I = [k for k in range(len(pop)) if pop[k].F is None] else: I = np.arange(len(pop)) # update the function evaluation counter self.n_eval += len(I) # actually evaluate all solutions using the function that can be overwritten if len(I) > 0: self._eval(problem, pop[I], **kwargs) # set the feasibility attribute if cv exists for ind in pop[I]: cv = ind.get("CV") if cv is not None: ind.set("feasible", cv <= 0) if is_individual: return pop[0] elif is_numpy_array: if len(pop) == 1: pop = pop[0] return tuple([pop.get(e) for e in self.evaluate_values_of]) else: return pop
def do(self, problem, pop, n_survive, return_indices=False, **kwargs): # make sure the population has at least one individual if len(pop) == 0: return pop # if the split should be done beforehand if self.filter_infeasible and problem.n_constr > 0: feasible, infeasible = split_by_feasibility( pop, sort_infeasbible_by_cv=True) # initialize the feasible and infeasible population feas_pop, infeas_pop = Population(), Population() # if there was no feasible solution was added at all - which means at least one infeasible if len(feasible) == 0: infeas_pop = self.cv_survival.do(problem, pop[infeasible], n_survive) # if there are feasible solutions in the population else: # if feasible solution do exist if len(feasible) > 0: feas_pop = self._do(problem, pop[feasible], min(len(feasible), n_survive), **kwargs) # calculate how many individuals are still remaining to be filled up with infeasible ones n_remaining = n_survive - len(feas_pop) # if infeasible solutions needs to be added if n_remaining > 0: infeas_pop = self.cv_survival.do(problem, pop[infeasible], n_remaining) survivors = Population.merge(feas_pop, infeas_pop) else: survivors = self._do(problem, pop, n_survive, **kwargs) if return_indices: H = {} for k, ind in enumerate(pop): H[ind] = k return [H[survivor] for survivor in survivors] else: return survivors
def _next(self): # the offspring population to finally evaluate and attach to the population off = Population() # find the potential optimal solution in the current population potential_optimal = self._potential_optimal() # for each of those solutions execute the division move for current in potential_optimal: # find the largest dimension the solution has not been evaluated yet nxl, nxu = norm_bounds(current, problem) k = np.argmax(nxu - nxl) # the delta value to be used to get left and right - this is one sixth of the range xl, xu = current.get("xl"), current.get("xu") delta = (xu[k] - xl[k]) / 6 # print(current.X, delta, k, xl, xu) # create the left individual left_x = np.copy(current.X) left_x[k] = xl[k] + delta left = Individual(X=left_x) # create the right individual right_x = np.copy(current.X) right_x[k] = xu[k] - delta right = Individual(X=right_x) # update the boundaries for all the points accordingly for ind in [current, left, right]: update_bounds(ind, xl, xu, k, delta) # create the offspring population, evaluate and attach to current population _off = Population.create(left, right) _off.set("depth", current.get("depth") + 1) off = Population.merge(off, _off) # evaluate the offsprings self.evaluator.eval(self.problem, off, algorithm=self) # print(off.get("X")) # add the offsprings to the population self.pop = Population.merge(self.pop, off)
def _step(self): selection, crossover, mutation = self.mating.selection, self.mating.crossover, self.mating.mutation # retrieve the current population pop = self.pop # get the vectors from the population F, CV, feasible = pop.get("F", "CV", "feasible") F = parameter_less(F, CV) # create offsprings and add it to the data of the algorithm P = selection.do(pop, self.pop_size, crossover.n_parents) if self.var_selection == "best": P[:, 0] = np.argmin(F[:, 0]) elif self.var_selection == "rand+best": P[np.random.random(len(pop)) < 0.3, 0] = np.argmin(F[:, 0]) # do the first crossover which is the actual DE operation off = crossover.do(self.problem, pop, P, algorithm=self) # then do the mutation (which is actually a crossover between old and new individual) _pop = Population.merge(self.pop, off) _P = np.column_stack( [np.arange(len(pop)), np.arange(len(pop)) + len(pop)]) off = mutation.do(self.problem, _pop, _P, algorithm=self)[:len(self.pop)] # bounds back if something is out of bounds off = BounceBackOutOfBoundsRepair().do(self.problem, off) return off
def _next(self): # make a step and create the offsprings self.off = self._step() # evaluate the offsprings self.evaluator.eval(self.problem, self.off, algorithm=self) survivors = [] for k in range(self.pop_size): parent, off = self.pop[k], self.off[k] rel = get_relation(parent, off) if rel == 0: survivors.extend([parent, off]) elif rel == -1: survivors.append(off) else: survivors.append(parent) survivors = Population.create(*survivors) if len(survivors) > self.pop_size: survivors = RankAndCrowdingSurvival().do(self.problem, survivors, self.pop_size) self.pop = survivors
def _initialize(self): # initialize the function parameters self.alpha, self.beta, self.gamma, self.delta = self.func_params( self.problem) # reset the restart history self.restart_history = [] # initialize the point if self.x0 is None: if not self.problem.has_bounds(): raise Exception( "Either provide an x0 or a problem with variable bounds!") # initialize randomly and make sure 5% is left for creating the initial simplex X = np.random.random(self.problem.n_var) self.x0 = denormalize(X, self.problem.xl, self.problem.xu) # parse the initial population from array or population object pop = pop_from_array_or_individual(self.x0) # if the simplex has not the correct number of points if len(pop) == 1: # the corresponding x values x0 = pop[0].X # if lower and upper bounds are given take 5% of the range and add if self.problem.has_bounds(): self.simplex_scaling = 0.05 * (self.problem.xu - self.problem.xl) # no bounds are given do it based on x0 - MATLAB procedure else: self.simplex_scaling = 0.05 * x0 # some value needs to be added if x0 is zero self.simplex_scaling[self.simplex_scaling == 0] = 0.00025 # initialize the simplex X = self.initialize_simplex(x0) # create a population object pop = pop.merge(Population().new("X", X)) # evaluate the values that are not already evaluated evaluate_if_not_done_yet(self.evaluator, self.problem, pop, algorithm=self) elif len(pop) != self.problem.n_var + 1: raise Exception( "Provided initial population has size of %s, but should have size of %s" % (len(pop), self.problem.n_var + 1)) # sort by its corresponding function values self.pop = pop[np.argsort(pop.get("F")[:, 0])] self.opt = self.pop[0]
def filter_optimum(pop, least_infeasible=False): # first only choose feasible solutions ret = pop[pop.get("feasible")[:, 0]] # if at least one feasible solution was found if len(ret) > 0: # then check the objective values F = ret.get("F") if F.shape[1] > 1: I = NonDominatedSorting().do(F, only_non_dominated_front=True) ret = ret[I] else: ret = ret[np.argmin(F)] # no feasible solution was found else: # if flag enable report the least infeasible if least_infeasible: ret = pop[np.argmin(pop.get("CV"))] # otherwise just return none else: ret = None if isinstance(ret, Individual): ret = Population().create(ret) return ret
def _initialize(self): super()._initialize() self.alpha, self.beta, self.gamma, self.delta = self.func_params( self.problem) # the corresponding x values of the provided or best found solution x0 = self.x0.X # if lower and upper bounds are given take 5% of the range and add if self.problem.has_bounds(): self.simplex_scaling = 0.05 * (self.problem.xu - self.problem.xl) # no bounds are given do it based on x0 - MATLAB procedure else: self.simplex_scaling = 0.05 * self.x0.X # some value needs to be added if x0 is zero self.simplex_scaling[self.simplex_scaling == 0] = 0.00025 # initialize the simplex simplex = pop_from_array_or_individual(self.initialize_simplex(x0)) self.evaluator.eval(self.problem, simplex, algorithm=self) # make the simplex the current the population and sort them by fitness pop = Population.merge(self.opt, simplex) self.pop = FitnessSurvival().do(self.problem, pop, len(pop))
def do(self, problem, pop, n_offsprings, **kwargs): X, F, G = pop.get("X", "F", "G") xl, xu = problem.bounds() # defining a params dict for the parameters to be sent to the API DATA = { 'X': numpy_to_json(X), 'F': numpy_to_json(F), 'xl': numpy_to_json(xl), 'xu': numpy_to_json(xu), } if problem.has_constraints(): DATA['G'] = numpy_to_json(G) DATA = {**DATA, 'n_infills': n_offsprings, **self.params} # sending get request and saving the response as response object r = requests.post(url=f"{self.url}/{self.endpoint}", json=json.dumps(DATA)) # extracting data in json format resp = r.json() if not resp["success"]: raise Exception(f"ERROR during remote call: {resp['error']}") X = np.array(resp["X"]) return Population.new(X=X)
def sample_by_bounds(clazz, n_samples, n_var, x_min, x_max, **kwargs): """ Convenience method if only the bounds are needed to create the sample points. Parameters ---------- clazz : class The sampling strategy to be used. n_var : int number of variables n_samples : int number of samples x_min : np.array lower bounds x_max : np.array upper bounds kwargs : dict additional arguments Returns ------- X : np.array Samples points in a two dimensional array """ class P(Problem): def __init__(self) -> None: self.n_var = n_var self.xl = np.full(n_var, x_min) self.xu = np.full(n_var, x_max) return clazz.sample(P(), Population(), n_samples, **kwargs).get("X")
def _next(self): if self.pop is None: X = next(self.es) else: F = self.pop.get("F")[:, 0].tolist() if not self.send_array_to_yield: F = F[0] try: X = self.es.send(F) except StopIteration: X = None self.termination.force_termination = True if X is not None: X = np.array(X) self.send_array_to_yield = X.ndim > 1 X = np.atleast_2d(X) # evaluate the population self.pop = Population().new("X", X) self.evaluator.eval(self.problem, self.pop, algorithm=self) # set infeasible individual's objective values to np.nan - then CMAES can handle it for ind in self.pop: if not ind.feasible[0]: ind.F[0] = np.nan
def test_survival(self): problem = DTLZ2(n_obj=3) for k in range(1, 11): print("TEST RVEA GEN", k) ref_dirs = np.loadtxt( path_to_test_resources('rvea', f"ref_dirs_{k}.txt")) F = np.loadtxt(path_to_test_resources('rvea', f"F_{k}.txt")) pop = Population.new(F=F) algorithm = RVEA(ref_dirs) algorithm.setup(problem, termination=('n_gen', 500)) algorithm.n_gen = k algorithm.pop = pop survival = APDSurvival(ref_dirs) survivors = survival.do(problem, algorithm.pop, len(pop), algorithm=algorithm, return_indices=True) apd = pop[survivors].get("apd") correct_apd = np.loadtxt( path_to_test_resources('rvea', f"apd_{k}.txt")) np.testing.assert_allclose(apd, correct_apd)
def do(self, problem, n_samples, pop=Population(), **kwargs): """ Sample new points with problem information if necessary. Parameters ---------- problem: class The problem to which points should be sampled. (lower and upper bounds, discrete, binary, ...) n_samples: int Number of samples kwargs: class Any additional data that might be necessary. e.g. constants of the algorithm, ... pop : Population The sampling results are stored in a population. The template of the population can be changed. If 'none' simply a numpy array is returned. Returns ------- X : np.array Samples points in a two dimensional array """ val = self._do(problem, n_samples, **kwargs) if pop is None: return val return pop.new("X", val)