Source code for mealpy.swarm_based.NWOA

#!/usr/bin/env python
# -------------------------------------------------------------------------------
# Created By: Sadik on 01/03/2026
# Github: https://github.com/Sadik-Ahmet
# -------------------------------------------------------------------------------
# Updated By: Thieu on 11/07/2026
# Github: https://github.com/thieu1995
# -------------------------------------------------------------------------------

"""
Provides context and a disclaimer regarding the 'Narwhal Optimization Algorithm'.

.. danger::
   There are two distinct papers proposing algorithms under the same name.
   Both have been published in journals with low academic impact; therefore,
   the mathematical soundness and experimental results are highly questionable.
   Users are strongly advised to exercise extreme caution and perform rigorous
   validation before applying these to critical optimization tasks.

The two identified versions are:

1. 'Narwhal Optimizer: A Novel Nature-Inspired Metaheuristic Algorithm' (May 2024)
    - Acronym: NO
    - Note: Lacks significant or novel update operators.
    - DOI: https://doi.org/10.34028/iajit/21/3/6

2. 'Narwhal Optimizer: A Nature-Inspired Optimization Algorithm for Solving Complex Optimization Problems' (September 2025)
    - Acronym: NWOA
    - Note: Performance results reported in the paper may not be replicable or statistically valid.
    - DOI: https://doi.org/10.32604/cmc.2025.066797

Danger
------
   Neither implementation offers a robust contribution to the metaheuristic field.
   It is recommended to utilize established, peer-reviewed optimization frameworks instead.
"""

import numpy as np
from mealpy.optimizer import Optimizer
from mealpy.utils.opt_info import OptInfo


[docs]class OriginalNWOA(Optimizer): """ The original version of: Narwhal Optimization Algorithm (NWOA) Parameters ---------- epoch : int Maximum number of iterations, in range [1, 100000]. Default is 10000. pop_size : int Number of population size, in range [5, 10000]. Default is 100. amplitude : float Wave amplitude, in range [-100.0, 100.0]. Default is 1.0. delta_decay : float Decay constant, in range (0.0, 1.0). Default is 0.01. lamda_decay : float Energy decay rate, in range (0.0, 1.0). Default is 0.001. References ---------- 1. Masadeh, R., Almomani, O., Zaqebah, A., Masadeh, S., Alshqurat, K., Sharieh, A., & Alsharman, N. (2025). Narwhal Optimizer: A Nature-Inspired Optimization Algorithm for Solving Complex Optimization Problems. Computers, Materials & Continua, 85(2). https://doi.org/10.32604/cmc.2025.066797 Examples -------- >>> import numpy as np >>> from mealpy import FloatVar, NWOA >>> >>> 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 = NWOA.OriginalNWOA(epoch=1000, pop_size=50, amplitude=2.0, delta_decay=0.01, lamda_decay=0.001) >>> g_best = model.solve(problem_dict) >>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}") >>> print(f"Solution: {model.g_best.solution}, Fitness: {model.g_best.target.fitness}") """ OPT_INFO = OptInfo(name="Narwhal Optimization Algorithm", year=2025, difficulty="hard", kind="original") def __init__(self, epoch: int = 10000, pop_size: int = 100, amplitude: float = 1.0, delta_decay: float = 0.01, lamda_decay: float = 0.001, **kwargs: object) -> None: """ Args: epoch (int): maximum number of iterations, default = 10000 pop_size (int): number of population size, default = 100 amplitude (float): Wave amplitude, default = 1.0 delta_decay (float): Decay constant, default = 0.01 lamda_decay (float): Energy decay rate, default = 0.001 """ 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.amplitude = self.validator.check_float("amplitude", amplitude, [-100., 100.]) self.delta_decay = self.validator.check_float("delta_decay", delta_decay, (0, 1)) self.lamda_decay = self.validator.check_float("lamda_decay", lamda_decay, (0, 1)) self.set_parameters(["epoch", "pop_size", "amplitude", "delta_decay", "lamda_decay"]) self.sort_flag = False
[docs] def initialize_variables(self): """Initialize algorithm-specific variables""" self.prey_energy = 1.0 # Initial prey energy
[docs] @staticmethod def cosine_similarity(agent_pos: np.ndarray, best_pos: np.ndarray) -> float: """ Calculate cosine similarity distance (Eq. 3 from paper) Args: agent_pos: Current agent position best_pos: Best solution position Returns: float: Cosine similarity distance """ dot_product = np.dot(agent_pos, best_pos) norm_agent = np.linalg.norm(agent_pos) norm_best = np.linalg.norm(best_pos) if norm_agent == 0 or norm_best == 0: return 1.0 return 1 - dot_product / (norm_agent * norm_best)
[docs] def wave_strength(self, agent_pos: np.ndarray, t: int) -> float: """ Calculate wave strength using sonar wave propagation (Eq. 4 from paper) Args: agent_pos: Current agent position t: Current iteration Returns: float: Wave strength value """ h_i = self.cosine_similarity(agent_pos, self.g_best.solution) strength = self.amplitude * abs(np.sin(2*np.pi * h_i - 2*np.pi * t)) * np.exp(-self.delta_decay * t) return strength
[docs] def get_exploration_ratio(self, fitness_improvement: float) -> float: """ Dynamic exploration ratio (Eq. 17 from paper) Args: fitness_improvement: Improvement in fitness from previous iteration Returns: float: Exploration ratio """ if fitness_improvement < 0.01: return 0.7 # Slow improvement, prefer exploration elif self.prey_energy < 0.3: return 0.3 # Low energy, shift to exploitation else: return 0.7 # Default: prefer exploration
[docs] def evolve(self, epoch): """ The main operations (equations) of algorithm. Inherit from Optimizer class Args: epoch (int): The current iteration """ # Update parameters a = 2.0 - (2 * epoch / self.epoch) # Exploration decay factor (Eq. 16) energy = self.prey_energy * np.exp(-self.lamda_decay * epoch) # (Eq. 8 and 9 from paper) self.prey_energy = max(energy, 0) # Calculate fitness improvement if epoch <= 2: fitness_improvement = 1.0 else: fitness_improvement = abs(self.history.list_global_best_fit[-2] - self.history.list_global_best_fit[-1]) exploration_ratio = self.get_exploration_ratio(fitness_improvement) pop_new = [] for idx in range(0, self.pop_size): r1 = self.generator.random() # Exploration or Exploitation phase wave_str = self.wave_strength(self.pop[idx].solution, epoch) if r1 < exploration_ratio: # Exploration phase (Eq. 5, 6, 7) A_exploration = 2 * a * self.generator.random() - a pos_new = self.pop[idx].solution + A_exploration * (self.g_best.solution - self.pop[idx].solution) + wave_str * self.generator.random() else: # Exploitation phase (Eq. 10-15) r2 = self.generator.random() AA = a * r1 - a CC = 2 * r2 # Calculate suction force distance = np.linalg.norm(self.g_best.solution - self.pop[idx].solution) suction_force = self.prey_energy * self.prey_energy / (1 + distance) pos_new = self.pop[idx].solution - AA * (self.g_best.solution - self.pop[idx].solution) + CC * suction_force * wave_str * self.generator.random() # Boundary handling pos_new = self.correct_solution(pos_new) agent = self.generate_empty_agent(pos_new) pop_new.append(agent) # Update fitness in single mode if self.mode not in self.AVAILABLE_MODES: agent.target = self.get_target(pos_new) self.pop[idx] = self.get_better_agent(agent, self.pop[idx], self.problem.minmax) # Update fitness in parallel modes if self.mode in self.AVAILABLE_MODES: pop_new = self.update_target_for_population(pop_new) self.pop = self.greedy_selection_population(self.pop, pop_new, self.problem.minmax)
[docs]class OriginalNO(Optimizer): """ The original version of: Narwhal Optimization (NO) Parameters ---------- epoch : int Maximum number of iterations, in range [1, 100000]. Default is 10000. pop_size : int Number of population size, in range [5, 10000]. Default is 100. alpha : float Signal intensity control factor, in range [-100.0, 100.0]. Default is 2.0. sigma0 : float Initial standard deviation for signal propagation, in range [-100.0, 100.0]. Default is 2.0. References ---------- 1. Medjahed, Seyyid Ahmed, and Fatima Boukhatem. "Narwhal Optimizer: A Novel Nature-Inspired Metaheuristic Algorithm". Int. Arab J. Inf. Technol. 21.3 (2024): 418-426. https://doi.org/10.34028/iajit/21/3/6 Examples -------- >>> import numpy as np >>> from mealpy import FloatVar, NWOA >>> >>> 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 = NWOA.OriginalNO(epoch=1000, pop_size=50, alpha=2.0, sigma0=2.0) >>> g_best = model.solve(problem_dict) >>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}") >>> print(f"Solution: {model.g_best.solution}, Fitness: {model.g_best.target.fitness}") """ OPT_INFO = OptInfo(name="Narwhal Optimization", year=2024, difficulty="easy", kind="original") def __init__(self, epoch: int = 10000, pop_size: int = 100, alpha=2.0, sigma0=2.0, **kwargs: object) -> None: """ Args: epoch (int): maximum number of iterations, default = 10000 pop_size (int): number of population size, default = 100 alpha (float): Signal intensity control factor. Default=2.0 sigma0 (float): Initial standard deviation for signal propagation. Default=2.0 """ 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.alpha = self.validator.check_float("alpha", alpha, [-100., 100.]) self.sigma0 = self.validator.check_float("sigma0", sigma0, [-100., 100.]) self.set_parameters(["epoch", "pop_size", "alpha", "sigma0"]) self.sort_flag = False
[docs] def evolve(self, epoch): """ The main operations (equations) of algorithm. Inherit from Optimizer class Args: epoch (int): The current iteration """ # Update standard deviation sigma linearly over iterations sigma_t = self.sigma0 * (1.0 - epoch / self.epoch) # Loop through each search agent (narwhal) pop_new = [] for idx in range(self.pop_size): # Calculate Euclidean distance between the current narwhal and the prey dist = np.linalg.norm(self.pop[idx].solution - self.g_best.solution) # Calculate Signal Emission SE = 0.1 / (1.0 + self.alpha * dist) # Calculate Signal Propagation if sigma_t > self.EPSILON: PR = np.exp(-(dist ** 2) / (2 * sigma_t ** 2)) else: PR = 0.0 SP = SE * PR # Calculate the step delta r1 = self.generator.random() beta = r1 - (1.0 / (sigma_t + 1.0)) delta = beta * np.abs(SP * self.g_best.solution - self.pop[idx].solution) # Update the position of the current narwhal pos_new = self.pop[idx].solution + delta # Apply boundary constraints pos_new = self.correct_solution(pos_new) agent = self.generate_empty_agent(pos_new) pop_new.append(agent) # Update fitness in single mode if self.mode not in self.AVAILABLE_MODES: agent.target = self.get_target(pos_new) self.pop[idx] = self.get_better_agent(agent, self.pop[idx], self.problem.minmax) # Update fitness in parallel modes if self.mode in self.AVAILABLE_MODES: pop_new = self.update_target_for_population(pop_new) self.pop = self.greedy_selection_population(self.pop, pop_new, self.problem.minmax)