diff --git a/examples/run_TSeedA_example.py b/examples/run_TSeedA_example.py new file mode 100644 index 00000000..dd6a67a8 --- /dev/null +++ b/examples/run_TSeedA_example.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# Tree-Seed Algorithm (TSeedA) Example +# Reference: Kiran, M. S. (2015). TSA: Tree-seed algorithm for continuous optimization. +# Expert Systems with Applications, 42(19), 6686-6698. DOI: 10.1016/j.eswa.2015.04.055 + +import numpy as np +from mealpy import FloatVar, TSeedA + + +def sphere_function(solution): + """Sphere function - simple unimodal test function""" + return np.sum(solution ** 2) + + +def rastrigin_function(solution): + """Rastrigin function - multimodal test function""" + n = len(solution) + return 10 * n + np.sum(solution ** 2 - 10 * np.cos(2 * np.pi * solution)) + + +def ackley_function(solution): + """Ackley function - multimodal test function""" + n = len(solution) + sum1 = np.sum(solution ** 2) + sum2 = np.sum(np.cos(2 * np.pi * solution)) + return -20 * np.exp(-0.2 * np.sqrt(sum1 / n)) - np.exp(sum2 / n) + 20 + np.e + + +if __name__ == "__main__": + # Define the problem + problem_dict = { + "bounds": FloatVar(lb=(-10.,) * 30, ub=(10.,) * 30, name="delta"), + "minmax": "min", + "obj_func": sphere_function + } + + # Test with different ST values + st_values = [0.1, 0.3, 0.5] + + print("=" * 60) + print("Tree-Seed Algorithm (TSA) Benchmark") + print("=" * 60) + + for st in st_values: + print(f"\n--- ST = {st} ---") + model = TSeedA.OriginalTSeedA(epoch=500, pop_size=50, st=st) + g_best = model.solve(problem_dict) + print(f"Best Fitness: {g_best.target.fitness:.6e}") + print(f"Solution (first 5 dims): {g_best.solution[:5]}") + + # Test on Rastrigin function + print("\n" + "=" * 60) + print("Testing on Rastrigin function (ST=0.1)...") + problem_dict["obj_func"] = rastrigin_function + + model = TSeedA.OriginalTSeedA(epoch=500, pop_size=50, st=0.1) + g_best = model.solve(problem_dict) + print(f"Best Fitness: {g_best.target.fitness:.6e}") + + # Test on Ackley function + print("\n" + "=" * 60) + print("Testing on Ackley function (ST=0.1)...") + problem_dict["obj_func"] = ackley_function + problem_dict["bounds"] = FloatVar(lb=(-32.,) * 30, ub=(32.,) * 30, name="delta") + + model = TSeedA.OriginalTSeedA(epoch=500, pop_size=50, st=0.1) + g_best = model.solve(problem_dict) + print(f"Best Fitness: {g_best.target.fitness:.6e}") + + print("\n" + "=" * 60) + print("Tree-Seed Algorithm Example completed successfully!") + print("=" * 60) diff --git a/mealpy/__init__.py b/mealpy/__init__.py index b5ba005f..28e65d0b 100644 --- a/mealpy/__init__.py +++ b/mealpy/__init__.py @@ -31,7 +31,7 @@ import sys import inspect -from .bio_based import (BBO, BBOA, BMO, EOA, IWO, SBO, SMA, SOA, SOS, TPO, TSA, VCS, WHO, BCO, EAO, SFOA) +from .bio_based import (BBO, BBOA, BMO, EOA, IWO, SBO, SMA, SOA, SOS, TPO, TSA, TSeedA, VCS, WHO, BCO, EAO, SFOA) from .evolutionary_based import (CRO, DE, EP, ES, FPA, GA, MA, SHADE) from .human_based import (BRO, BSO, CA, CHIO, FBIO, GSKA, HBO, HCO, ICA, LCO, QSA, SARO, SPBO, SSDO, TLO, TOA, WarSO, AFT, CDDO) diff --git a/mealpy/bio_based/TSeedA.py b/mealpy/bio_based/TSeedA.py new file mode 100644 index 00000000..ecc7430b --- /dev/null +++ b/mealpy/bio_based/TSeedA.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +import numpy as np +from mealpy.optimizer import Optimizer + + +class OriginalTSeedA(Optimizer): + """ + Tree-Seed Algorithm (TSA) for continuous optimization. + + Paper: + Kiran, M. S. (2015). TSA: Tree-seed algorithm for continuous optimization. + Expert Systems with Applications, 42(19), 6686-6698. DOI: 10.1016/j.eswa.2015.04.055 + + Parameters: + epoch (int): maximum number of iterations, default = 10000 + pop_size (int): population size, default = 100 + st (float): search tendency in (0, 1), default = 0.1 + + Equations: + For each seed S_ij: + if rand_j < st: S_ij = T_ij + a_ij * (B_j - T_rj) (Eq. 3) + else: S_ij = T_ij + a_ij * (T_ij - T_rj) (Eq. 4) + where T is the current tree, B is the global best tree so far, and r != i. + + Examples + ~~~~~~~~ + >>> import numpy as np + >>> from mealpy import FloatVar, TSeedA + >>> + >>> def objective_function(solution): + >>> return np.sum(solution**2) + >>> + >>> problem_dict = { + >>> "bounds": FloatVar(lb=(-10.,) * 30, ub=(10.,) * 30, name="delta"), + >>> "minmax": "min", + >>> "obj_func": objective_function + >>> } + >>> + >>> model = TSeedA.OriginalTSeedA(epoch=1000, pop_size=50, st=0.1) + >>> g_best = model.solve(problem_dict) + >>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}") + + References + ~~~~~~~~~~ + [1] Kiran, M. S. (2015). TSA: Tree-seed algorithm for continuous optimization. + Expert Systems with Applications, 42(19), 6686-6698. DOI: 10.1016/j.eswa.2015.04.055 + + BibTeX + ~~~~~~ + @article{Kiran2015TSA, + title={TSA: Tree-seed algorithm for continuous optimization}, + author={Kiran, Mustafa Servet}, + journal={Expert Systems with Applications}, + volume={42}, + number={19}, + pages={6686--6698}, + year={2015}, + doi={10.1016/j.eswa.2015.04.055} + } + """ + + def __init__(self, epoch: int = 10000, pop_size: int = 100, st: float = 0.1, **kwargs: object) -> None: + """ + Args: + epoch: maximum number of iterations, default = 10000 + pop_size: population size, default = 100 + st: search tendency in (0, 1), default = 0.1 + """ + super().__init__(**kwargs) + self.epoch = self.validator.check_int("epoch", epoch, [1, 100000]) + self.pop_size = self.validator.check_int("pop_size", pop_size, [5, 10000]) + self.st = self.validator.check_float("st", st, (0, 1.0)) + self.set_parameters(["epoch", "pop_size", "st"]) + self.sort_flag = False + + def _get_random_tree_index(self, current_idx: int) -> int: + r_idx = int(self.generator.integers(0, self.pop_size - 1)) + if r_idx >= current_idx: + r_idx += 1 + return r_idx + + def evolve(self, epoch: int) -> None: + """ + The main operations (equations) of algorithm. Inherit from Optimizer class + """ + n_dims = self.problem.n_dims + lb = self.problem.lb + ub = self.problem.ub + + # Calculate number of seeds range + low = max(1, int(np.ceil(self.pop_size * 0.1))) + high = max(1, int(np.ceil(self.pop_size * 0.25))) + if high < low: + high = low + + for i in range(self.pop_size): + # Determine number of seeds for this tree (Eq 1) + # ns = int(np.fix(low + (high - low) * self.generator.random())) + 1 + n_seeds = int(np.fix(low + (high - low) * self.generator.random())) + 1 + if n_seeds > high: + n_seeds = high + + # Find best solution in current population to use in Eq 3 + best_idx = np.argmin([agent.target.fitness for agent in self.pop]) + best_params = self.pop[best_idx].solution.copy() + + seeds_pop = [] + seeds_fitness = [] + + for j in range(n_seeds): + # Select a neighbor (komsu) different from i + neighbor_idx = int(np.fix(self.generator.random() * self.pop_size)) + while i == neighbor_idx: + neighbor_idx = int(np.fix(self.generator.random() * self.pop_size)) + + # Create a new seed from the current tree + # Note: In original Matlab "seeds(j,:) = trees(j,:)" is used initially, + # but we use trees(i,:) as the base because we are generating seeds for tree i. + # However, following the exact logic of the Matlab code where it iterates j for seeds: + seed = self.pop[i].solution.copy() + + # Evolve seed params (Eq 3 or Eq 4) + for d in range(n_dims): + if self.generator.random() < self.st: + # Eq 3: Use global best + alpha = (self.generator.random() - 0.5) * 2 + seed[d] = self.pop[i].solution[d] + alpha * (best_params[d] - self.pop[neighbor_idx].solution[d]) + else: + # Eq 4: Use neighbor + alpha = (self.generator.random() - 0.5) * 2 + seed[d] = self.pop[i].solution[d] + alpha * (self.pop[i].solution[d] - self.pop[neighbor_idx].solution[d]) + + # Boundary check + if seed[d] > ub[d]: + seed[d] = ub[d] + if seed[d] < lb[d]: + seed[d] = lb[d] + + # Create agent for seed + agent = self.generate_empty_agent(seed) + if self.mode not in self.AVAILABLE_MODES: + agent.target = self.get_target(seed) + seeds_pop.append(agent) + seeds_fitness.append(agent.target.fitness if agent.target else float('inf')) + + # Update part + if self.mode in self.AVAILABLE_MODES: + seeds_pop = self.update_target_for_population(seeds_pop) + seeds_fitness = [agent.target.fitness for agent in seeds_pop] + + # Find the best seed (mintohum) + best_seed_idx = int(np.argmin(seeds_fitness)) + best_seed_fitness = seeds_fitness[best_seed_idx] + + # If best seed is better than current tree, replace tree + if best_seed_fitness < self.pop[i].target.fitness: + self.pop[i].update(solution=seeds_pop[best_seed_idx].solution.copy(), + target=seeds_pop[best_seed_idx].target.copy()) + diff --git a/tests/bio_based/test_TSeedA.py b/tests/bio_based/test_TSeedA.py new file mode 100644 index 00000000..782eb645 --- /dev/null +++ b/tests/bio_based/test_TSeedA.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +from mealpy import FloatVar, TSeedA, Optimizer +import numpy as np +import pytest + + +@pytest.fixture(scope="module") # scope: Call only 1 time at the beginning +def problem(): + def objective_function(solution): + return np.sum(solution ** 2) + + problem = { + "obj_func": objective_function, + "bounds": FloatVar(lb=[-10, -10, -10, -10, -10], ub=[10, 10, 10, 10, 10]), + "minmax": "min", + } + return problem + + +def test_OriginalTSeedA_results(problem): + epoch = 10 + pop_size = 50 + st = 0.1 + model = TSeedA.OriginalTSeedA(epoch, pop_size, st) + g_best = model.solve(problem) + assert isinstance(model, Optimizer) + assert isinstance(g_best.solution, np.ndarray) + assert len(g_best.solution) == len(model.problem.lb) + + +@pytest.mark.parametrize("problem, epoch, system_code", + [ + (problem, None, 0), + (problem, "hello", 0), + (problem, -10, 0), + (problem, [10], 0), + (problem, (0, 9), 0), + (problem, 0, 0), + (problem, float("inf"), 0), + ]) +def test_epoch_TSeedA(problem, epoch, system_code): + pop_size = 50 + st = 0.1 + algorithms = [TSeedA.OriginalTSeedA] + for algorithm in algorithms: + with pytest.raises(ValueError) as e: + algorithm(epoch, pop_size, st) + assert e.type == ValueError + + +@pytest.mark.parametrize("problem, pop_size, system_code", + [ + (problem, None, 0), + (problem, "hello", 0), + (problem, -10, 0), + (problem, [10], 0), + (problem, (0, 9), 0), + (problem, 0, 0), + (problem, float("inf"), 0), + ]) +def test_pop_size_TSeedA(problem, pop_size, system_code): + epoch = 10 + st = 0.1 + algorithms = [TSeedA.OriginalTSeedA] + for algorithm in algorithms: + with pytest.raises(ValueError) as e: + algorithm(epoch, pop_size, st) + assert e.type == ValueError + + +@pytest.mark.parametrize("problem, st, system_code", + [ + (problem, None, 0), + (problem, "hello", 0), + (problem, -1.0, 0), + (problem, [10], 0), + (problem, (0, 9), 0), + (problem, 0, 0), + (problem, 1, 0), + (problem, 1.1, 0), + (problem, -0.01, 0), + ]) +def test_st_TSeedA(problem, st, system_code): + epoch = 10 + pop_size = 50 + algorithms = [TSeedA.OriginalTSeedA] + for algorithm in algorithms: + with pytest.raises(ValueError) as e: + algorithm(epoch, pop_size, st=st) + assert e.type == ValueError