diff --git a/src/components/mapping/mod.rs b/src/components/mapping/mod.rs index 550b180..4732ff4 100644 --- a/src/components/mapping/mod.rs +++ b/src/components/mapping/mod.rs @@ -22,8 +22,8 @@ //! using [`ValueOf>>`] as input lens and //! [`ValueOf`] as output lens: //! -//! [`InertiaWeight`]: crate::components::swarm::InertiaWeight -//! [`ParticleVelocitiesUpdate`]: crate::components::swarm::ParticleVelocitiesUpdate +//! [`InertiaWeight`]: crate::components::swarm::pso::InertiaWeight +//! [`ParticleVelocitiesUpdate`]: crate::components::swarm::pso::ParticleVelocitiesUpdate //! [`ValueOf>>`]: crate::lens::ValueOf //! [`ValueOf`]: crate::lens::ValueOf //! @@ -40,7 +40,7 @@ //! 0.4, //! 0.9, //! ValueOf::>>::new(), -//! ValueOf::>::new(), +//! ValueOf::>::new(), //! ) //! # } //! ``` diff --git a/src/components/replacement/bh.rs b/src/components/replacement/bh.rs new file mode 100644 index 0000000..f4132dd --- /dev/null +++ b/src/components/replacement/bh.rs @@ -0,0 +1,61 @@ +//! Replacement components for the Black Hole algorithm (BH). + +use rand::Rng; +use serde::{Deserialize, Serialize}; + +use crate::{ + problems::LimitedVectorProblem, utils::squared_euclidean, Component, ExecResult, + SingleObjectiveProblem, State, +}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct EventHorizon; + +impl EventHorizon { + pub fn new

() -> Box> + where + P: LimitedVectorProblem, + P: SingleObjectiveProblem, + { + Box::new(Self) + } +} + +impl

Component

for EventHorizon +where + P: LimitedVectorProblem, + P: SingleObjectiveProblem, +{ + fn execute(&self, problem: &P, state: &mut State

) -> ExecResult<()> { + let mut populations = state.populations_mut(); + let offspring = populations.current_mut(); + + let f_bh = state.best_objective_value().unwrap().value(); + + let fitness_sum = offspring.iter().map(|x| x.objective().value()).sum::(); + let radius = f_bh / fitness_sum; + + let (index, best) = offspring + .iter() + .enumerate() + .min_by_key(|(_u, i)| i.objective()) + .unwrap(); + let distances = offspring + .iter() + .map(|o| squared_euclidean(o.solution(), best.solution()).sqrt()) + .collect::>(); + + for (u, i) in offspring.iter_mut().enumerate() { + // do not replace best individual + if distances[u] < radius && u != index { + let rand: Vec = problem + .domain() + .iter() + .map(|d| state.random_mut().gen_range(d.clone())) + .collect(); + *i.solution_mut() = rand; + } + } + Ok(()) + } +} diff --git a/src/components/replacement/mod.rs b/src/components/replacement/mod.rs index da5716f..8bea95c 100644 --- a/src/components/replacement/mod.rs +++ b/src/components/replacement/mod.rs @@ -8,6 +8,7 @@ use crate::{ Individual, Problem, State, }; +pub mod bh; pub mod common; pub mod sa; diff --git a/src/components/replacement/sa.rs b/src/components/replacement/sa.rs index 1ac486e..5e86de3 100644 --- a/src/components/replacement/sa.rs +++ b/src/components/replacement/sa.rs @@ -75,7 +75,6 @@ impl Component

for ExponentialAnnealingAcceptance } else { populations.pop(); } - Ok(()) } } diff --git a/src/components/swarm/bh.rs b/src/components/swarm/bh.rs new file mode 100644 index 0000000..a4ecd45 --- /dev/null +++ b/src/components/swarm/bh.rs @@ -0,0 +1,85 @@ +use rand::distributions::{Distribution, Uniform}; +use serde::Serialize; + +use crate::{ + component::ExecResult, + components::Component, + identifier::{Global, Identifier, PhantomId}, + population::{AsSolutionsMut, BestIndividual}, + problems::LimitedVectorProblem, + SingleObjectiveProblem, State, +}; + +/// Updates the positions in the black hole algorithm. +/// +/// Originally proposed for, and used as operator in [`bh`]. +/// The operator is similar to the [`ParticleVelocitiesUpdate`] in [`pso`]. +/// Specifically, they are identical when for [`pso`]: +/// inertia weight = 0, +/// c_1 = 0, +/// c_2 = 1, +/// v_max = 1 +/// +/// [`bh`]: crate::heuristics::bh +/// [`ParticleVelocitiesUpdate`]: crate::components::swarm::pso::ParticleVelocitiesUpdate` +/// [`pso`]: crate::heuristics::pso +#[derive(Clone, Serialize)] +pub struct BlackHoleParticlesUpdate { + id: PhantomId, +} + +impl BlackHoleParticlesUpdate { + pub fn from_params() -> Self { + Self { + id: PhantomId::default(), + } + } + + pub fn new_with_id

() -> Box> + where + P: SingleObjectiveProblem + LimitedVectorProblem, + { + Box::new(Self::from_params()) + } +} + +impl BlackHoleParticlesUpdate { + pub fn new

() -> Box> + where + P: SingleObjectiveProblem + LimitedVectorProblem, + { + Self::new_with_id() + } +} + +impl Component

for BlackHoleParticlesUpdate +where + P: SingleObjectiveProblem + LimitedVectorProblem, + I: Identifier, +{ + fn init(&self, _problem: &P, _state: &mut State

) -> ExecResult<()> { + Ok(()) + } + + fn execute(&self, _problem: &P, state: &mut State

) -> ExecResult<()> { + let distribution = Uniform::new(0.0, 1.0); + + // Get necessary state like global best `xg` + let best = state.populations().current().best_individual().cloned(); + let best_ind = best.unwrap(); + let xg = best_ind.solution(); + let mut population = state.populations_mut(); + let xs = population.current_mut().as_solutions_mut(); + + // Perform the update step. + for x in xs { + for i in 0..x.len() { + // Calculate change in position + let pos = distribution.sample(&mut *state.random_mut()) * (xg[i] - x[i]); + // Add value to particle position + x[i] += pos; + } + } + Ok(()) + } +} diff --git a/src/components/swarm/fa.rs b/src/components/swarm/fa.rs new file mode 100644 index 0000000..f64e90d --- /dev/null +++ b/src/components/swarm/fa.rs @@ -0,0 +1,137 @@ +use std::array::from_mut; + +use better_any::{Tid, TidAble}; +use derive_more::{Deref, DerefMut}; +use itertools::izip; +use rand::Rng; +use serde::Serialize; + +use crate::{ + component::ExecResult, + components::Component, + identifier::{Global, Identifier, PhantomId}, + problems::LimitedVectorProblem, + state::{common, common::Evaluator}, + CustomState, State, +}; + +/// Updates the and firefly positions. +/// +/// Originally proposed for, and used as operator in [`fa`]. +/// +/// Uses the [`RandomizationParameter`]. +/// +/// [`fa`]: crate::heuristics::fa +#[derive(Clone, Serialize)] +pub struct FireflyPositionsUpdate { + pub alpha: f64, + pub beta: f64, + pub gamma: f64, + id: PhantomId, +} + +impl FireflyPositionsUpdate { + pub fn from_params(alpha: f64, beta: f64, gamma: f64) -> Self { + Self { + alpha, + beta, + gamma, + id: PhantomId::default(), + } + } + + pub fn new_with_id

(alpha: f64, beta: f64, gamma: f64) -> Box> + where + P: LimitedVectorProblem, + { + Box::new(Self::from_params(alpha, beta, gamma)) + } +} + +impl FireflyPositionsUpdate { + pub fn new

(alpha: f64, beta: f64, gamma: f64) -> Box> + where + P: LimitedVectorProblem, + { + Self::new_with_id(alpha, beta, gamma) + } +} + +impl Component

for FireflyPositionsUpdate +where + P: LimitedVectorProblem, + I: Identifier, +{ + fn init(&self, _problem: &P, state: &mut State

) -> ExecResult<()> { + state.insert(RandomizationParameter(self.alpha)); + Ok(()) + } + + fn execute(&self, problem: &P, state: &mut State

) -> ExecResult<()> { + // Prepare parameters + let &Self { beta, gamma, .. } = self; + let a = state.get_value::(); + + // Get population from state + let mut individuals = state.populations_mut().pop(); + + // scale for adapting to problem domain + let scales = problem + .domain() + .iter() + .map(|p| (p.end - p.start).abs()) + .collect::>(); + + // Perform the update step. + for i in 0..individuals.len() { + for j in 0..individuals.len() { + // if individual j is "more attractive" (i.e. has lower fitness), move towards j + if individuals[i].objective() > individuals[j].objective() { + // draw random values from uniform distribution between 0 and 1 + // according to paper: also possible to use normal distribution, depending on problem + let rands: Vec = (0..problem.dimension()) + .map(|_| state.random_mut().gen_range(0.0..1.0)) + .collect(); + let mut current = individuals[i].clone(); + izip!( + current.solution_mut(), + individuals[j].solution(), + &scales, + rands + ) + .map(|(xi, xj, scale, rand)| { + let pos = beta * (-gamma * (*xi - xj).powf(2.0)).exp() * (xj - *xi) + + a * (rand - 0.5) * scale; + (xi, pos) + }) + .for_each(|(xi, pos)| *xi += pos); + individuals[i] = current; + + state.holding::>( + |evaluator: &mut Evaluator, state| { + evaluator.as_inner_mut().evaluate( + problem, + state, + from_mut(&mut individuals[i]), + ); + Ok(()) + }, + )?; + *state.borrow_value_mut::() += 1; + } + } + } + state.populations_mut().push(individuals); + Ok(()) + } +} + +/// The randomization parameter used to update the firefly positions. +#[derive(Deref, DerefMut, Tid)] +pub struct RandomizationParameter( + #[deref] + #[deref_mut] + pub f64, +); + +impl CustomState<'_> for RandomizationParameter {} diff --git a/src/components/swarm/mod.rs b/src/components/swarm/mod.rs new file mode 100644 index 0000000..16e6e0a --- /dev/null +++ b/src/components/swarm/mod.rs @@ -0,0 +1,3 @@ +pub mod bh; +pub mod fa; +pub mod pso; diff --git a/src/components/swarm.rs b/src/components/swarm/pso.rs similarity index 98% rename from src/components/swarm.rs rename to src/components/swarm/pso.rs index 3e5fc97..6e62af4 100644 --- a/src/components/swarm.rs +++ b/src/components/swarm/pso.rs @@ -125,9 +125,9 @@ pub struct ParticleVelocitiesUpdate { impl ParticleVelocitiesUpdate { pub fn from_params(weight: f64, c_1: f64, c_2: f64, v_max: f64) -> ExecResult { - ensure!(weight > 0., "`weight` must be > 0, but was {}", weight); - ensure!(c_1 > 0., "`c_1` must be > 0, but was {}", c_1); - ensure!(c_2 > 0., "`c_2` must be > 0, but was {}", c_2); + ensure!(weight >= 0., "`weight` must be >= 0, but was {}", weight); + ensure!(c_1 >= 0., "`c_1` must be >= 0, but was {}", c_1); + ensure!(c_2 >= 0., "`c_2` must be >= 0, but was {}", c_2); ensure!(v_max > 0., "`v_max` must be > 0, but was {}", v_max); Ok(Self { weight, diff --git a/src/heuristics/bh.rs b/src/heuristics/bh.rs new file mode 100644 index 0000000..1061b60 --- /dev/null +++ b/src/heuristics/bh.rs @@ -0,0 +1,85 @@ +//! Black Hole Algorithm (BH). +//! +//! # References +//! +//! \[1\] Abdolreza Hatamlou. 2013. +//! Black hole: A new heuristic optimization approach for data clustering. +//! In Information Sciences 222, 175–184. +//! DOI: + +use crate::{ + component::ExecResult, + components::{boundary, initialization, replacement, swarm}, + conditions::Condition, + configuration::Configuration, + identifier::{Global, Identifier}, + logging::Logger, + problems::{LimitedVectorProblem, SingleObjectiveProblem}, + Component, +}; + +/// Parameters for [`real_bh`]. +pub struct RealProblemParameters { + pub num_particles: u32, +} + +/// An example single-objective BH algorithm operating on a real search space. +/// +/// Uses the [`bh`] component internally. +pub fn real_bh

( + params: RealProblemParameters, + condition: Box>, +) -> ExecResult> +where + P: SingleObjectiveProblem + LimitedVectorProblem, +{ + let RealProblemParameters { num_particles } = params; + + Ok(Configuration::builder() + .do_(initialization::RandomSpread::new(num_particles)) + .evaluate() + .update_best_individual() + .do_(bh::( + Parameters { + particle_update: swarm::bh::BlackHoleParticlesUpdate::new(), + constraints: boundary::Saturation::new(), + replacement: replacement::bh::EventHorizon::new(), + }, + condition, + )) + .build()) +} + +/// Basic building blocks of [`bh`]. +pub struct Parameters

{ + pub particle_update: Box>, + pub constraints: Box>, + pub replacement: Box>, +} + +/// A generic single-objective Black Hole algorithm (BH) template. +pub fn bh(params: Parameters

, condition: Box>) -> Box> +where + P: SingleObjectiveProblem, + I: Identifier, +{ + let Parameters { + particle_update, + constraints, + replacement, + } = params; + + Configuration::builder() + .while_(condition, |builder| { + builder + .do_(particle_update) + .do_(constraints) + .evaluate_with::() + .update_best_individual() + .do_(replacement) + .evaluate_with::() + .update_best_individual() + .do_(Logger::new()) + }) + .build_component() +} diff --git a/src/heuristics/fa.rs b/src/heuristics/fa.rs new file mode 100644 index 0000000..a68371d --- /dev/null +++ b/src/heuristics/fa.rs @@ -0,0 +1,98 @@ +//! Firefly Algorithm (FA). +//! +//! # References +//! +//! \[1\] Xin-She Yang. 2009. +//! Firefly algorithms for Multimodal Optimization. +//! In Watanabe, O.; Zeugmann, T. (eds) Stochastic Algorithms: Foundations and Applications. +//! SAGA 2009. Lecture Notes in Computer Science, vol 5792. Springer, Berlin, Heidelberg. +//! DOI: + +use crate::{ + component::ExecResult, + components::{boundary, initialization, mapping, swarm}, + conditions::Condition, + configuration::Configuration, + identifier::{Global, Identifier}, + lens::ValueOf, + logging::Logger, + problems::{LimitedVectorProblem, SingleObjectiveProblem}, + Component, +}; + +/// Parameters for [`real_fa`]. +pub struct RealProblemParameters { + pub pop_size: u32, + pub alpha: f64, + pub beta: f64, + pub gamma: f64, + pub delta: f64, +} + +/// An example single-objective FA operating on a real search space. +/// +/// Uses the [`fa`] component internally. +pub fn real_fa

( + params: RealProblemParameters, + condition: Box>, +) -> ExecResult> +where + P: SingleObjectiveProblem + LimitedVectorProblem, +{ + let RealProblemParameters { + pop_size, + alpha, + beta, + gamma, + delta, + } = params; + + Ok(Configuration::builder() + .do_(initialization::RandomSpread::new(pop_size)) + .evaluate() + .update_best_individual() + .do_(fa::( + Parameters { + firefly_update: swarm::fa::FireflyPositionsUpdate::new(alpha, beta, gamma), + constraints: boundary::Saturation::new(), + alpha_update: Box::from(mapping::sa::GeometricCooling::new( + delta, + ValueOf::::new(), + )), + }, + condition, + )) + .build()) +} + +/// Basic building blocks of [`fa`]. +pub struct Parameters

{ + pub firefly_update: Box>, + pub constraints: Box>, + pub alpha_update: Box>, +} + +/// A generic single-objective Firefly Algorithm (FA) template. +pub fn fa(params: Parameters

, condition: Box>) -> Box> +where + P: SingleObjectiveProblem, + I: Identifier, +{ + let Parameters { + firefly_update, + constraints, + alpha_update, + } = params; + + Configuration::builder() + .while_(condition, |builder| { + builder + .do_(firefly_update) + .do_(constraints) + .evaluate_with::() + .update_best_individual() + .do_(alpha_update) + .do_(Logger::new()) + }) + .build_component() +} diff --git a/src/heuristics/mod.rs b/src/heuristics/mod.rs index b498efa..f4c078a 100644 --- a/src/heuristics/mod.rs +++ b/src/heuristics/mod.rs @@ -8,9 +8,11 @@ //! structure and adapt it to one's needs. pub mod aco; +pub mod bh; pub mod cro; pub mod de; pub mod es; +pub mod fa; pub mod ga; pub mod ils; pub mod iwo; diff --git a/src/heuristics/pso.rs b/src/heuristics/pso.rs index 70c1bd2..fdec30c 100644 --- a/src/heuristics/pso.rs +++ b/src/heuristics/pso.rs @@ -62,8 +62,8 @@ where .update_best_individual() .do_(pso::( Parameters { - particle_init: swarm::ParticleSwarmInit::new(v_max)?, - particle_update: swarm::ParticleVelocitiesUpdate::new( + particle_init: swarm::pso::ParticleSwarmInit::new(v_max)?, + particle_update: swarm::pso::ParticleVelocitiesUpdate::new( start_weight, c_one, c_two, @@ -75,9 +75,10 @@ where start_weight, end_weight, ValueOf::>>::new(), - ValueOf::>::new(), + ValueOf::>::new( + ), )), - state_update: swarm::ParticleSwarmUpdate::new(), + state_update: swarm::pso::ParticleSwarmUpdate::new(), }, condition, )) diff --git a/src/utils.rs b/src/utils.rs index 5a6d91d..d5a2c7f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -44,3 +44,8 @@ impl serde::Serialize for SerializablePhantom { serializer.serialize_unit_struct(std::any::type_name::()) } } + +/// Calculates squared Euclidean distance between two vectors. +pub fn squared_euclidean(a: &[f64], b: &Vec) -> f64 { + a.iter().zip(b).map(|(p, q)| (p - q).powi(2)).sum::() +}