Source code for ddmtolab.Algorithms.MTMO.MO_SBO

"""
Multi-Objective Symbiosis-Based Optimization (MO-SBO)

This module implements the MO-SBO algorithm for multi-objective many-task optimization
based on symbiotic relationships in biocoenosis. The algorithm adaptively controls
knowledge transfer rates by tracking six types of symbiotic interactions: mutualism,
commensalism, parasitism, competition, amensalism, and neutralism.

References
----------
    [1] R.-T. Liaw and C.-K. Ting. "Evolutionary Manytasking Optimization Based on Symbiosis in Biocoenosis." Proceedings of the AAAI Conference on Artificial Intelligence, 33(01): 4295-4303, 2019.

Notes
-----
The code is developed in accordance with the MATLAB-based MTO-platform framework.

Author: Jiangtao Shen
Email: j.shen5@exeter.ac.uk
Date: 2026.02.21
Version: 1.0
"""
import time
import numpy as np
from tqdm import tqdm
from ddmtolab.Methods.Algo_Methods.algo_utils import *


[docs] class MO_SBO: """ Multi-Objective Symbiosis-Based Optimization for many-task multi-objective optimization. The algorithm uses symbiotic relationships between tasks to adaptively control knowledge transfer rates. Six types of symbiotic interactions are tracked: - Mutualism (MIJ): Both tasks benefit (transferred solution ranks high in both) - Commensalism (OIJ): One benefits, other neutral - Parasitism (PIJ): One benefits, other harmed - Competition (CIJ): Both harmed - Amensalism (AIJ): One harmed, other neutral - Neutralism (NIJ): Both neutral Attributes ---------- algorithm_information : dict Dictionary containing algorithm capabilities and requirements """ algorithm_information = { 'n_tasks': '[2, K]', 'dims': 'unequal', 'objs': 'unequal', 'n_objs': '[2, M]', 'cons': 'unequal', 'n_cons': '[0, C]', 'expensive': 'False', 'knowledge_transfer': 'True', 'n': 'equal', 'max_nfes': 'equal' } @classmethod def get_algorithm_information(cls, print_info=True): return get_algorithm_information(cls, print_info)
[docs] def __init__(self, problem, n=None, max_nfes=None, benefit=0.25, harm=0.5, mu_c=20, mu_m=15, save_data=True, save_path='./Data', name='MO-SBO', disable_tqdm=True): """ Initialize MO-SBO algorithm. Parameters ---------- problem : MTOP Multi-task optimization problem instance n : int, optional Population size per task (default: 100) max_nfes : int, optional Maximum number of function evaluations per task (default: 10000) benefit : float, optional Beneficial factor threshold for symbiosis categorization (default: 0.25) harm : float, optional Harmful factor threshold for symbiosis categorization (default: 0.5) mu_c : float, optional Distribution index for simulated binary crossover (default: 20) mu_m : float, optional Distribution index for polynomial mutation (default: 15) save_data : bool, optional Whether to save optimization data (default: True) save_path : str, optional Path to save results (default: './Data') name : str, optional Name for the experiment (default: 'MO-SBO') disable_tqdm : bool, optional Whether to disable progress bar (default: True) """ self.problem = problem self.n = n if n is not None else 100 self.max_nfes = max_nfes if max_nfes is not None else 10000 self.benefit = benefit self.harm = harm self.mu_c = mu_c self.mu_m = mu_m self.save_data = save_data self.save_path = save_path self.name = name self.disable_tqdm = disable_tqdm
[docs] def optimize(self): """ Execute the MO-SBO algorithm. Returns ------- Results Optimization results containing decision variables, objectives, constraints, and runtime """ start_time = time.time() problem = self.problem n = self.n nt = problem.n_tasks dims = problem.dims max_dim = max(dims) max_nfes_per_task = par_list(self.max_nfes, nt) max_nfes = self.max_nfes * nt # Initialize population in native space and evaluate decs = initialization(problem, n) objs, cons = evaluation(problem, decs) nfes = n * nt # Convert to unified space (pad with random values for unused dimensions) uni_decs = [] for t in range(nt): padded = np.random.rand(n, max_dim) padded[:, :dims[t]] = decs[t] uni_decs.append(padded) all_decs, all_objs, all_cons = init_history(decs, objs, cons) # Symbiosis interaction counters (initialized to 1 to avoid division by zero) RIJ = 0.5 * np.ones((nt, nt)) # Transfer rates MIJ = np.ones((nt, nt)) # Mutualism NIJ = np.ones((nt, nt)) # Neutralism CIJ = np.ones((nt, nt)) # Competition OIJ = np.ones((nt, nt)) # Commensalism PIJ = np.ones((nt, nt)) # Parasitism AIJ = np.ones((nt, nt)) # Amensalism pbar = tqdm(total=max_nfes, initial=nfes, desc=f"{self.name}", disable=self.disable_tqdm) while nfes < max_nfes: # === Step 1: Generate offspring for each task (SBX + PM in unified space) === off_uni_decs = [] off_rank_o = [] off_belong_t = [] for t in range(nt): off_t = ga_generation(uni_decs[t], self.mu_c, self.mu_m) off_uni_decs.append(off_t) # Offspring inherits positional rank from parent (1-based) off_rank_o.append(np.arange(1, n + 1)) off_belong_t.append(np.full(n, t)) # === Step 2: Knowledge transfer based on symbiosis === for t in range(nt): # Find task with highest transfer rate (exclude self) rij_row = RIJ[t].copy() rij_row[t] = -np.inf transfer_task = int(np.argmax(rij_row)) if np.random.rand() < RIJ[t, transfer_task]: si = int(np.floor(n * RIJ[t, transfer_task])) if si > 0: ind1 = np.random.permutation(n)[:si] ind2 = np.random.permutation(n)[:si] # Replace offspring of task t with solutions from transfer_task off_uni_decs[t][ind1] = off_uni_decs[transfer_task][ind2] off_belong_t[t][ind1] = transfer_task # === Step 3: Evaluate, compute rankC, and selection for each task === all_rank_c = [None] * nt for t in range(nt): # Evaluate offspring (trim to native dimensions) off_native = off_uni_decs[t][:, :dims[t]] off_objs_t, off_cons_t = evaluation_single(problem, off_native, t) nfes += n pbar.update(n) # NSGA-II sort offspring to get rankC (1-based) all_rank_c[t] = _nsga2_rank(off_objs_t, off_cons_t) # Merge parents and offspring merged_uni = np.vstack([uni_decs[t], off_uni_decs[t]]) merged_objs = np.vstack([objs[t], off_objs_t]) merged_cons = np.vstack([cons[t], off_cons_t]) # NSGA-II selection: keep top n merged_rank = _nsga2_rank(merged_objs, merged_cons) select_idx = np.argsort(merged_rank)[:n] uni_decs[t] = merged_uni[select_idx] objs[t] = merged_objs[select_idx] cons[t] = merged_cons[select_idx] decs[t] = uni_decs[t][:, :dims[t]] # === Step 4: Update symbiosis counters === for t in range(nt): transferred_idx = np.where(off_belong_t[t] != t)[0] for k in transferred_idx: rc = all_rank_c[t][k] # Rank after evaluation on task t ro = off_rank_o[t][k] # Positional rank from parent population src = int(off_belong_t[t][k]) if rc < n * self.benefit: # Transferred solution performs well on task t if ro < n * self.benefit: MIJ[t, src] += 1 # Both benefit elif ro > n * (1 - self.harm): PIJ[t, src] += 1 # One benefits, other harmed else: OIJ[t, src] += 1 # One benefits, other neutral elif rc > n * (1 - self.harm): # Transferred solution performs poorly on task t if ro > n * (1 - self.harm): CIJ[t, src] += 1 # Both harmed else: # Transferred solution is neutral on task t if ro > n * (1 - self.harm): AIJ[t, src] += 1 # One harmed, other neutral elif n * self.benefit <= ro <= n * (1 - self.harm): NIJ[t, src] += 1 # Both neutral # === Step 5: Update transfer rates === RIJ = (MIJ + OIJ + PIJ) / (MIJ + OIJ + PIJ + AIJ + CIJ + NIJ) append_history(all_decs, decs, all_objs, objs, all_cons, cons) pbar.close() runtime = time.time() - start_time results = build_save_results( all_decs=all_decs, all_objs=all_objs, runtime=runtime, max_nfes=max_nfes_per_task, all_cons=all_cons, bounds=problem.bounds, save_path=self.save_path, filename=self.name, save_data=self.save_data ) return results
def _nsga2_rank(objs, cons): """ Compute NSGA-II rank for each individual. Parameters ---------- objs : np.ndarray Objective values, shape (pop_size, n_obj) cons : np.ndarray Constraint values, shape (pop_size, n_con) Returns ------- rank : np.ndarray 1-based rank for each individual (lower is better), shape (pop_size,) """ pop_size = objs.shape[0] if cons is not None and cons.size > 0: front_no, _ = nd_sort(objs, cons, pop_size) else: front_no, _ = nd_sort(objs, pop_size) crowd_dis = crowding_distance(objs, front_no) sorted_indices = np.lexsort((-crowd_dis, front_no)) rank = np.empty(pop_size, dtype=int) rank[sorted_indices] = np.arange(1, pop_size + 1) return rank