1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//! An easy-to-use, simple Particle Swarm Optimization (PSO) implementation in Rust.
//!
//! [![Crates.io](https://img.shields.io/crates/v/pso_rs?style=for-the-badge)](https://crates.io/crates/pso-rs)
//! [![docs.rs](https://img.shields.io/docsrs/pso-rs?style=for-the-badge)](https://docs.rs/pso-rs/latest/pso_rs/)
//! [![GitHub](https://img.shields.io/github/license/czonios/pso-rs?style=for-the-badge)](https://github.com/czonios/pso-rs/blob/master/LICENSE)
//! [![Website](https://img.shields.io/website?style=for-the-badge&url=https%3A%2F%2Fczonios.github.io%2Fpso-rs%2F)](https://czonios.github.io/pso-rs/)
//!
//! It uses the [`rand`](https://crates.io/crates/rand) crate for random initialization, and the [`rayon`](https://crates.io/crates/rayon) crate for parallel objective function computation. It also has a nice progress bar curtesy of the [`indicatif`](https://crates.io/crates/indicatif) crate. Below is a screenshot of PSO running, attempting to minimize the Lennard-Jones potential energy in a cluster of 20 molecules:
//!
//! ![Screenshot](https://raw.githubusercontent.com/czonios/pso-rs/master/screenshots/pbar.gif)
//!
//! The [examples](#examples) below can get you started.
//! In order to use it in your own optimization problem, you will need to define an objective function as it is defined in the [run](https://docs.rs/pso-rs/latest/pso_rs/fn.run.html) function, and a [`Config`](https://docs.rs/pso-rs/latest/pso_rs/model/struct.Config.html) object. See the [Notes](#notes) section for more tips.
//!
//! # Examples
//!
//! ## Run PSO
//!
//! ```rust
//! use pso_rs::*;
//!
//! // define objective function (d-dimensional Rosenbrock)
//! fn objective_function(
//!     p: &Particle,
//!     _flat_dim: usize,
//!     dimensions: &Vec<usize>
//! ) -> f64 {
//!     (0..dimensions[0] - 1).map(|i| {
//!         100.0 * ((p[i+1]-p[i]).powf(2.0)).powf(2.0)
//!             + (1.0-p[i]).powf(2.0)
//!     }).sum()
//! }
//!
//! // define a termination condition (optional)
//! fn terminate(f_best: f64) -> bool {
//!     f_best - (0.0) < 1e-4
//! }
//!
//! let config = Config {
//!     // dimension shape of each particle
//!     dimensions: vec![2],
//!     // problem bounds in each dimension
//!     bounds: vec![(-5.0, 10.0); 2],
//!     // maximum no. of objective function computations
//!     t_max: 10000,
//!     // leave the rest of the params as default
//!     ..Config::default()
//! };
//!
//! let pso = pso_rs::run(
//!     config,
//!     objective_function,
//!     Some(terminate)
//! ).unwrap();
//!     
//! let model = pso.model;
//! println!("Model: {:?} ", model.get_f_best());
//! ```
//!
//! ## Initialize PSO for later execution
//!
//! ```rust
//! use pso_rs::*;
//!
//! // define objective function (d-dimensional Rosenbrock)
//! fn objective_function(
//!     p: &Particle,
//!     _flat_dim: usize,
//!     dimensions: &Vec<usize>
//! ) -> f64 {
//!     (0..dimensions[0] - 1).map(|i| {
//!         100.0 * ((p[i+1]-p[i]).powf(2.0)).powf(2.0)
//!             + (1.0-p[i]).powf(2.0)
//!     }).sum()
//! }
//!
//!
//! let config = Config {
//!     dimensions: vec![2],
//!     bounds: vec![(-5.0, 10.0); 2],
//!     t_max: 10000,
//!     ..Config::default()
//! };
//!
//! let mut pso = pso_rs::init(
//!     config,
//!     objective_function
//! ).unwrap();
//!
//! // run PSO with no termination condition
//! pso.run(|_| false);
//!     
//! let model = pso.model;
//! println!("Found minimum: {:#?} ", model.get_f_best());
//! println!("Minimizer: {:#?}", model.get_x_best());
//! ```
//!
//! # Notes
//!
//! ## Performance
//!
//! This implementation uses a flat vector (`Vec<f64>`) to represent any d-dimensional problem (see the [Optimization Problem Dimensionality](#optimization-problem-dimensionality) section). This means that the vector has an O(1) access time, and can be cached for fast access, similarly to a static array.
//!
//! The computation of the objective function for each particle is performed in parallel, as it is computationally expensive for any non-trivial problem. In the future, complete swarms will be able to be run in parallel and optionally communicate their best found positions by passing messages.
//!
//! ## Optimization problem dimensionality
//!
//! Even though you can have particles of any shape and size, as long as each item is `f64`, `pso_rs` represents each particle as a flat vector: `Vec<f64>`.
//!
//! This means that, for example, in order to find clusters of 20 molecules in 3D space that minimize the [Lennard-Jones potential energy](https://en.wikipedia.org/wiki/Lennard-Jones_potential), you can define `dimensions` as (20, 3).
//! If you want, you can also create a custom `reshape` function, like this one for molecule clusters below:
//!
//! ```rust
//! use pso_rs::*;
//!
//! fn reshape(
//!     particle: &Particle,
//!     particle_dims: &Vec<usize>
//! ) -> Vec<Vec<f64>> {
//!     let mut reshaped_cluster = vec![];
//!     let mut i = 0;
//!     for _ in 0..particle_dims[0] {
//!         let mut reshaped_molecule = vec![];
//!         for _ in 0..particle_dims[1] {
//!             reshaped_molecule.push(particle[i]);
//!             i += 1;
//!         }
//!         reshaped_cluster.push(reshaped_molecule);
//!     }
//!     reshaped_cluster
//! }
//!
//! // used in the objective function
//! fn objective_function(
//!     p: &Particle,
//!     _flat_dim: usize,
//!     dimensions: &Vec<usize>
//! ) -> f64 {
//!     let _reshaped_particle = reshape(p, dimensions);
//!     /* Do stuff */
//!     0.0
//! }
//!
//! let config = Config {
//!     dimensions: vec![20, 3],
//!     bounds: vec![(-2.5, 2.5); 3],
//!     t_max: 1,
//!     ..Config::default()
//! };
//!
//! let pso = pso_rs::run(
//!     config,
//!     objective_function,
//!     None
//! ).unwrap();
//!
//! // somewhere in main(), after running PSO as in the example:
//! println!(
//!     "Best found minimizer: {:#?} ",
//!     reshape(&pso.model.get_x_best(),
//!         &pso.model.config.dimensions)
//! );
//! ```

pub mod model;
pub mod pso;

pub use model::*;

use model::Model;
use pso::PSO;
use std::error::Error;

/// Creates a model and runs the PSO method
///
/// # Panics
///
/// Panics if any particle coefficient becomes NaN (usually because of bad parameterization, e.g. c1 + c2 < 4)
pub fn run(
    config: Config,
    obj_f: fn(&Particle, usize, &Vec<usize>) -> f64,
    terminate_f: Option<fn(f64) -> bool>,
) -> Result<PSO, Box<dyn Error>> {
    assert_config(&config)?;
    let mut pso = init(config, obj_f).unwrap();
    let term_condition = match terminate_f {
        Some(terminate_f) => terminate_f,
        None => |_| false,
    };
    pso.run(term_condition);
    Ok(pso)
}

/// Initializes and returns a PSO instance without running the optimization process
///
/// Useful for initializing an instance for running at a later time
pub fn init(
    config: Config,
    obj_f: fn(&Particle, usize, &Vec<usize>) -> f64,
) -> Result<PSO, &'static str> {
    assert_config(&config)?;
    let model = Model::new(config, obj_f);
    let pso = PSO::new(model);
    Ok(pso)
}

fn assert_config(config: &Config) -> Result<(), &'static str> {
    if config.c1 + config.c2 < 4.0 {
        return Err("c1 + c2 must be greater than 4");
    }
    if config.dimensions.len() == 0 {
        return Err("dimensions must be set");
    }
    if config.bounds.len() != config.dimensions[config.dimensions.len() - 1] {
        return Err("bounds vector must have the same length as the last dimension of the model");
    }
    Ok(())
}

#[cfg(test)]
mod tests {

    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}