From b589bf66c6cdf7cc5da1fca03ce63abb0e3537e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9C=E4=B8=96=E6=A9=8B=20Du=20Shiqiao?= Date: Mon, 12 Aug 2024 11:01:26 +0900 Subject: [PATCH 1/4] feat: use anyhow for error handling (#32) --- Cargo.toml | 1 + README.md | 5 +++-- examples/quadratic_model.rs | 8 +++----- examples/tsp_model.rs | 7 ++----- src/model.rs | 7 ++----- src/tests.rs | 8 ++------ 6 files changed, 13 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c5bcbd0..caae8e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ rand = "0.8.5" ordered-float = "4.2.2" rayon = "1.10.0" auto_impl = "1.2.0" +anyhow = "1.0.86" [target.'cfg(target_family = "wasm")'.dependencies] web-time = "1.1.0" diff --git a/README.md b/README.md index 339d53a..15f4c6e 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ All of the algorithms are parallelized with Rayon. You need to implement your own model that implements `OptModel` trait. Actual optimization is handled by each algorithm functions. Here is a simple example to optimize a quadratic function with Hill Climbing algorithm. ```rust -use std::{error::Error, time::Duration}; +use std::{time::Duration}; +use anyhow::Result as AnyResult; use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; use localsearch::{ optim::{HillClimbingOptimizer, LocalSearchOptimizer}, @@ -52,7 +53,7 @@ impl OptModel for QuadraticModel { fn generate_random_solution( &self, rng: &mut R, - ) -> Result> { + ) -> AnyResult { let solution = self.dist.sample_iter(rng).take(self.k).collect::>(); Ok(solution) } diff --git a/examples/quadratic_model.rs b/examples/quadratic_model.rs index 2236b26..046e684 100644 --- a/examples/quadratic_model.rs +++ b/examples/quadratic_model.rs @@ -1,5 +1,6 @@ -use std::{error::Error, time::Duration}; +use std::time::Duration; +use anyhow::Result as AnyResult; use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; use localsearch::{ optim::{HillClimbingOptimizer, LocalSearchOptimizer}, @@ -30,10 +31,7 @@ impl OptModel for QuadraticModel { type SolutionType = SolutionType; type TransitionType = (); type ScoreType = ScoreType; - fn generate_random_solution( - &self, - rng: &mut R, - ) -> Result> { + fn generate_random_solution(&self, rng: &mut R) -> AnyResult { let solution = self.dist.sample_iter(rng).take(self.k).collect::>(); Ok(solution) } diff --git a/examples/tsp_model.rs b/examples/tsp_model.rs index 6847834..a70b09d 100644 --- a/examples/tsp_model.rs +++ b/examples/tsp_model.rs @@ -1,12 +1,12 @@ use core::time::Duration; use std::{ collections::{HashMap, HashSet}, - error::Error, fs::File, io::{self, BufRead}, path::Path, }; +use anyhow::Result as AnyResult; use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; use localsearch::{ optim::{ @@ -94,10 +94,7 @@ impl OptModel for TSPModel { type SolutionType = SolutionType; type TransitionType = TransitionType; type ScoreType = ScoreType; - fn generate_random_solution( - &self, - rng: &mut R, - ) -> Result> { + fn generate_random_solution(&self, rng: &mut R) -> AnyResult { let mut cities = self .distance_matrix .keys() diff --git a/src/model.rs b/src/model.rs index 108c575..ddd5843 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use anyhow::Result as AnyResult; use auto_impl::auto_impl; @@ -13,10 +13,7 @@ pub trait OptModel: Sync + Send { type TransitionType: Clone + Sync + Send; /// Randomly generate a solution - fn generate_random_solution( - &self, - rng: &mut R, - ) -> Result>; + fn generate_random_solution(&self, rng: &mut R) -> AnyResult; /// Generate a new trial solution from current solution fn generate_trial_solution( diff --git a/src/tests.rs b/src/tests.rs index b9406c7..6b99e2f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,5 +1,4 @@ -use std::error::Error; - +use anyhow::Result as AnyResult; use ordered_float::NotNan; use rand::{distributions::Uniform, prelude::Distribution}; @@ -27,10 +26,7 @@ impl OptModel for QuadraticModel { type SolutionType = SolutionType; type TransitionType = TransitionType; type ScoreType = NotNan; - fn generate_random_solution( - &self, - rng: &mut R, - ) -> Result> { + fn generate_random_solution(&self, rng: &mut R) -> AnyResult { let solution = self.dist.sample_iter(rng).take(self.k).collect::>(); Ok(solution) } From 4747f60ac6d20adc6f4bbec5e2182aa28cd324a6 Mon Sep 17 00:00:00 2001 From: Du Shiqiao Date: Mon, 12 Aug 2024 11:02:31 +0900 Subject: [PATCH 2/4] chore: use std instead of core --- examples/tsp_model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tsp_model.rs b/examples/tsp_model.rs index a70b09d..e6bdd8b 100644 --- a/examples/tsp_model.rs +++ b/examples/tsp_model.rs @@ -1,9 +1,9 @@ -use core::time::Duration; use std::{ collections::{HashMap, HashSet}, fs::File, io::{self, BufRead}, path::Path, + time::Duration, }; use anyhow::Result as AnyResult; From 8d500bbcd17afee5eb14aaee24ad9ae9cab8655f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9C=E4=B8=96=E6=A9=8B=20Du=20Shiqiao?= Date: Mon, 12 Aug 2024 11:46:37 +0900 Subject: [PATCH 3/4] feat: improved OptModel and Optimizer interface (#33) * feat: change OptModel and Optimizer interface * doc: update README * doc: Add initial_score and time_limit parameters to optimizers' docstring --- README.md | 40 +++---- examples/quadratic_model.rs | 38 ++++--- examples/tsp_model.rs | 148 ++++++++++++++------------ src/model.rs | 28 +++-- src/optim/base.rs | 24 +++-- src/optim/epsilon_greedy.rs | 4 + src/optim/generic.rs | 9 +- src/optim/hill_climbing.rs | 4 + src/optim/logistic_annealing.rs | 4 + src/optim/relative_annealing.rs | 4 + src/optim/simulated_annealing.rs | 9 +- src/optim/tabu_search.rs | 9 +- src/tests.rs | 36 ++++--- src/tests/test_epsilon_greedy.rs | 18 ++-- src/tests/test_hill_climbing.rs | 18 ++-- src/tests/test_logistic_annealing.rs | 18 ++-- src/tests/test_relative_annealing.rs | 18 ++-- src/tests/test_simulated_annealing.rs | 18 ++-- src/tests/test_tabu_search.rs | 18 ++-- 19 files changed, 270 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index 15f4c6e..e541ee0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ All of the algorithms are parallelized with Rayon. You need to implement your own model that implements `OptModel` trait. Actual optimization is handled by each algorithm functions. Here is a simple example to optimize a quadratic function with Hill Climbing algorithm. ```rust -use std::{time::Duration}; +use std::time::Duration; use anyhow::Result as AnyResult; use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; @@ -28,6 +28,9 @@ use localsearch::{ use ordered_float::NotNan; use rand::{self, distributions::Uniform, prelude::Distribution}; +type SolutionType = Vec; +type ScoreType = NotNan; + #[derive(Clone)] struct QuadraticModel { k: usize, @@ -41,10 +44,15 @@ impl QuadraticModel { let dist = Uniform::new(low, high); Self { k, centers, dist } } -} -type SolutionType = Vec; -type ScoreType = NotNan; + fn evaluate_solution(&self, solution: &SolutionType) -> NotNan { + let score = (0..self.k) + .into_iter() + .map(|i| (solution[i] - self.centers[i]).powf(2.0)) + .sum(); + NotNan::new(score).unwrap() + } +} impl OptModel for QuadraticModel { type SolutionType = SolutionType; @@ -53,32 +61,25 @@ impl OptModel for QuadraticModel { fn generate_random_solution( &self, rng: &mut R, - ) -> AnyResult { + ) -> AnyResult<(Self::SolutionType, Self::ScoreType)> { let solution = self.dist.sample_iter(rng).take(self.k).collect::>(); - Ok(solution) + let score = self.evaluate_solution(&solution); + Ok((solution, score)) } fn generate_trial_solution( &self, - current_solution: &Self::SolutionType, + current_solution: Self::SolutionType, + _current_score: Self::ScoreType, rng: &mut R, - _current_score: Option>, ) -> (Self::SolutionType, Self::TransitionType, NotNan) { let k = rng.gen_range(0..self.k); let v = self.dist.sample(rng); - let mut new_solution = current_solution.clone(); + let mut new_solution = current_solution; new_solution[k] = v; let score = self.evaluate_solution(&new_solution); (new_solution, (), score) } - - fn evaluate_solution(&self, solution: &Self::SolutionType) -> NotNan { - let score = (0..self.k) - .into_iter() - .map(|i| (solution[i] - self.centers[i]).powf(2.0)) - .sum(); - NotNan::new(score).unwrap() - } } fn create_pbar(n_iter: u64) -> ProgressBar { @@ -99,7 +100,7 @@ fn main() { println!("running Hill Climbing optimizer"); let n_iter = 10000; - let time_limit = Duration::from_secs(60); + let time_limit = Duration::from_secs_f32(1.0); let patiance = 1000; let n_trials = 50; let opt = HillClimbingOptimizer::new(patiance, n_trials); @@ -111,9 +112,8 @@ fn main() { let res = opt.run(&model, None, n_iter, time_limit, Some(&callback), ()); pb.finish(); - dbg!(res); + dbg!(res.unwrap()); } - ``` In addition you can also add `preprocess_initial_solution` and `postprocess_final_solution` to your model. diff --git a/examples/quadratic_model.rs b/examples/quadratic_model.rs index 046e684..3779a03 100644 --- a/examples/quadratic_model.rs +++ b/examples/quadratic_model.rs @@ -9,6 +9,9 @@ use localsearch::{ use ordered_float::NotNan; use rand::{self, distributions::Uniform, prelude::Distribution}; +type SolutionType = Vec; +type ScoreType = NotNan; + #[derive(Clone)] struct QuadraticModel { k: usize, @@ -22,41 +25,42 @@ impl QuadraticModel { let dist = Uniform::new(low, high); Self { k, centers, dist } } -} -type SolutionType = Vec; -type ScoreType = NotNan; + fn evaluate_solution(&self, solution: &SolutionType) -> NotNan { + let score = (0..self.k) + .into_iter() + .map(|i| (solution[i] - self.centers[i]).powf(2.0)) + .sum(); + NotNan::new(score).unwrap() + } +} impl OptModel for QuadraticModel { type SolutionType = SolutionType; type TransitionType = (); type ScoreType = ScoreType; - fn generate_random_solution(&self, rng: &mut R) -> AnyResult { + fn generate_random_solution( + &self, + rng: &mut R, + ) -> AnyResult<(Self::SolutionType, Self::ScoreType)> { let solution = self.dist.sample_iter(rng).take(self.k).collect::>(); - Ok(solution) + let score = self.evaluate_solution(&solution); + Ok((solution, score)) } fn generate_trial_solution( &self, - current_solution: &Self::SolutionType, + current_solution: Self::SolutionType, + _current_score: Self::ScoreType, rng: &mut R, - _current_score: Option>, ) -> (Self::SolutionType, Self::TransitionType, NotNan) { let k = rng.gen_range(0..self.k); let v = self.dist.sample(rng); - let mut new_solution = current_solution.clone(); + let mut new_solution = current_solution; new_solution[k] = v; let score = self.evaluate_solution(&new_solution); (new_solution, (), score) } - - fn evaluate_solution(&self, solution: &Self::SolutionType) -> NotNan { - let score = (0..self.k) - .into_iter() - .map(|i| (solution[i] - self.centers[i]).powf(2.0)) - .sum(); - NotNan::new(score).unwrap() - } } fn create_pbar(n_iter: u64) -> ProgressBar { @@ -89,5 +93,5 @@ fn main() { let res = opt.run(&model, None, n_iter, time_limit, Some(&callback), ()); pb.finish(); - dbg!(res); + dbg!(res.unwrap()); } diff --git a/examples/tsp_model.rs b/examples/tsp_model.rs index e6bdd8b..2a38cf8 100644 --- a/examples/tsp_model.rs +++ b/examples/tsp_model.rs @@ -28,6 +28,10 @@ fn min_sorted(c1: usize, c2: usize) -> (usize, usize) { } type Edge = (usize, usize); +type SolutionType = Vec; +// remvoed edges and inserted edges +type TransitionType = ([Edge; 2], [Edge; 2]); +type ScoreType = NotNan; #[derive(Clone, Debug)] struct TSPModel { @@ -72,6 +76,16 @@ impl TSPModel { self.distance_matrix[&key] } } + + fn evaluate_solution(&self, solution: &SolutionType) -> ScoreType { + let score = (0..solution.len() - 1) + .map(|i| { + let key = min_sorted(solution[i], solution[i + 1]); + self.get_distance(&key, true) + }) + .sum(); + NotNan::new(score).unwrap() + } } fn select_two_indides(lb: usize, ub: usize, rng: &mut R) -> (usize, usize) { @@ -85,16 +99,14 @@ fn select_two_indides(lb: usize, ub: usize, rng: &mut R) -> (usize min_sorted(n1, n2) } -type SolutionType = Vec; -// remvoed edges and inserted edges -type TransitionType = ([Edge; 2], [Edge; 2]); -type ScoreType = NotNan; - impl OptModel for TSPModel { type SolutionType = SolutionType; type TransitionType = TransitionType; type ScoreType = ScoreType; - fn generate_random_solution(&self, rng: &mut R) -> AnyResult { + fn generate_random_solution( + &self, + rng: &mut R, + ) -> AnyResult<(self::SolutionType, self::ScoreType)> { let mut cities = self .distance_matrix .keys() @@ -113,15 +125,17 @@ impl OptModel for TSPModel { // append start city to the last cities.push(self.start); - Ok(cities) + let score = self.evaluate_solution(&cities); + + Ok((cities, score)) } fn generate_trial_solution( &self, - current_solution: &SolutionType, + current_solution: Self::SolutionType, + current_score: Self::ScoreType, rng: &mut R, - current_score: Option>, - ) -> (SolutionType, TransitionType, NotNan) { + ) -> (Self::SolutionType, Self::TransitionType, Self::ScoreType) { let (ind1, ind2) = select_two_indides(1, current_solution.len() - 1, rng); let mut new_solution = current_solution.clone(); @@ -140,31 +154,17 @@ impl OptModel for TSPModel { ]; // calculate new score - let new_score = match current_score { - Some(s) => { - s - self.get_distance(&removed_edges[0], true) - - self.get_distance(&removed_edges[1], true) - + self.get_distance(&inserted_edges[0], true) - + self.get_distance(&inserted_edges[1], true) - } - None => self.evaluate_solution(&new_solution), - }; + let new_score = current_score + - self.get_distance(&removed_edges[0], true) + - self.get_distance(&removed_edges[1], true) + + self.get_distance(&inserted_edges[0], true) + + self.get_distance(&inserted_edges[1], true); // create transition let trans = (removed_edges, inserted_edges); (new_solution, trans, new_score) } - - fn evaluate_solution(&self, solution: &SolutionType) -> NotNan { - let score = (0..solution.len() - 1) - .map(|i| { - let key = min_sorted(solution[i], solution[i + 1]); - self.get_distance(&key, true) - }) - .sum(); - NotNan::new(score).unwrap() - } } #[derive(Debug)] @@ -261,14 +261,16 @@ fn main() { println!("run hill climbing"); let optimizer = HillClimbingOptimizer::new(1000, 200); - let (final_solution, final_score, _) = optimizer.run( - &tsp_model, - initial_solution.clone(), - n_iter, - time_limit, - Some(&callback), - (), - ); + let (final_solution, final_score, _) = optimizer + .run( + &tsp_model, + initial_solution.clone(), + n_iter, + time_limit, + Some(&callback), + (), + ) + .unwrap(); println!( "final score = {}, num of cities {}", final_score, @@ -280,14 +282,16 @@ fn main() { println!("run tabu search"); let tabu_list = DequeTabuList::new(20); let optimizer = TabuSearchOptimizer::new(patience, 200, 10); - let (final_solution, final_score, _) = optimizer.run( - &tsp_model, - initial_solution.clone(), - n_iter, - time_limit, - Some(&callback), - tabu_list, - ); + let (final_solution, final_score, _) = optimizer + .run( + &tsp_model, + initial_solution.clone(), + n_iter, + time_limit, + Some(&callback), + tabu_list, + ) + .unwrap(); println!( "final score = {}, num of cities {}", final_score, @@ -298,14 +302,16 @@ fn main() { println!("run annealing"); let optimizer = SimulatedAnnealingOptimizer::new(patience, 200); - let (final_solution, final_score, _) = optimizer.run( - &tsp_model, - initial_solution.clone(), - n_iter, - time_limit, - Some(&callback), - (200.0, 50.0), - ); + let (final_solution, final_score, _) = optimizer + .run( + &tsp_model, + initial_solution.clone(), + n_iter, + time_limit, + Some(&callback), + (200.0, 50.0), + ) + .unwrap(); println!( "final score = {}, num of cities {}", final_score, @@ -316,14 +322,16 @@ fn main() { println!("run epsilon greedy"); let optimizer = EpsilonGreedyOptimizer::new(patience, 200, 10, 0.3); - let (final_solution, final_score, _) = optimizer.run( - &tsp_model, - initial_solution.clone(), - n_iter, - time_limit, - Some(&callback), - (), - ); + let (final_solution, final_score, _) = optimizer + .run( + &tsp_model, + initial_solution.clone(), + n_iter, + time_limit, + Some(&callback), + (), + ) + .unwrap(); println!( "final score = {}, num of cities {}", final_score, @@ -334,14 +342,16 @@ fn main() { println!("run relative annealing"); let optimizer = RelativeAnnealingOptimizer::new(patience, 200, 10, 1e1); - let (final_solution, final_score, _) = optimizer.run( - &tsp_model, - initial_solution, - n_iter, - time_limit, - Some(&callback), - (), - ); + let (final_solution, final_score, _) = optimizer + .run( + &tsp_model, + initial_solution, + n_iter, + time_limit, + Some(&callback), + (), + ) + .unwrap(); println!( "final score = {}, num of cities {}", final_score, diff --git a/src/model.rs b/src/model.rs index ddd5843..98c4d42 100644 --- a/src/model.rs +++ b/src/model.rs @@ -13,26 +13,34 @@ pub trait OptModel: Sync + Send { type TransitionType: Clone + Sync + Send; /// Randomly generate a solution - fn generate_random_solution(&self, rng: &mut R) -> AnyResult; + fn generate_random_solution( + &self, + rng: &mut R, + ) -> AnyResult<(Self::SolutionType, Self::ScoreType)>; /// Generate a new trial solution from current solution fn generate_trial_solution( &self, - current_solution: &Self::SolutionType, + current_solution: Self::SolutionType, + current_score: Self::ScoreType, rng: &mut R, - current_score: Option, ) -> (Self::SolutionType, Self::TransitionType, Self::ScoreType); - /// Evaluate the given solution - fn evaluate_solution(&self, solution: &Self::SolutionType) -> Self::ScoreType; - /// Preprocess the solution - fn preprocess_solution(&self, solution: Self::SolutionType) -> Self::SolutionType { - solution + fn preprocess_solution( + &self, + current_solution: Self::SolutionType, + current_score: Self::ScoreType, + ) -> AnyResult<(Self::SolutionType, Self::ScoreType)> { + Ok((current_solution, current_score)) } /// Postprocess the solution - fn postprocess_solution(&self, solution: Self::SolutionType) -> Self::SolutionType { - solution + fn postprocess_solution( + &self, + current_solution: Self::SolutionType, + current_score: Self::ScoreType, + ) -> (Self::SolutionType, Self::ScoreType) { + (current_solution, current_score) } } diff --git a/src/optim/base.rs b/src/optim/base.rs index bf9739a..df32a97 100644 --- a/src/optim/base.rs +++ b/src/optim/base.rs @@ -1,3 +1,4 @@ +use anyhow::Result as AnyResult; use auto_impl::auto_impl; use crate::{callback::OptCallbackFn, Duration, OptModel}; @@ -15,6 +16,7 @@ pub trait LocalSearchOptimizer { &self, model: &M, initial_solution: M::SolutionType, + initial_score: M::ScoreType, n_iter: usize, time_limit: Duration, callback: Option<&F>, @@ -28,31 +30,39 @@ pub trait LocalSearchOptimizer { fn run( &self, model: &M, - initial_solution: Option, + initial_solution_and_score: Option<(M::SolutionType, M::ScoreType)>, n_iter: usize, time_limit: Duration, callback: Option<&F>, extra_in: Self::ExtraIn, - ) -> (M::SolutionType, M::ScoreType, Self::ExtraOut) + ) -> AnyResult<(M::SolutionType, M::ScoreType, Self::ExtraOut)> where M: OptModel, F: OptCallbackFn, { - let initial_solution = model.preprocess_solution(initial_solution.unwrap_or_else(|| { - let mut rng = rand::thread_rng(); - model.generate_random_solution(&mut rng).unwrap() - })); + let (initial_solution, initial_score) = match initial_solution_and_score { + Some((solution, score)) => (solution, score), + None => { + let mut rng = rand::thread_rng(); + model.generate_random_solution(&mut rng)? + } + }; + + let (initial_solution, initial_score) = + model.preprocess_solution(initial_solution, initial_score)?; let (solution, score, extra) = self.optimize( model, initial_solution, + initial_score, n_iter, time_limit, callback, extra_in, ); - (model.postprocess_solution(solution), score, extra) + let (solution, score) = model.postprocess_solution(solution, score); + Ok((solution, score, extra)) } } diff --git a/src/optim/epsilon_greedy.rs b/src/optim/epsilon_greedy.rs index 96d66f4..2541fca 100644 --- a/src/optim/epsilon_greedy.rs +++ b/src/optim/epsilon_greedy.rs @@ -45,13 +45,16 @@ impl LocalSearchOptimizer for EpsilonGreedyOptimizer { /// /// - `model` : the model to optimize /// - `initial_solution` : the initial solution to start optimization. If None, a random solution will be generated. + /// - `initial_score` : the initial score of the initial solution /// - `n_iter`: maximum iterations + /// - `time_limit`: maximum iteration time /// - `callback` : callback function that will be invoked at the end of each iteration /// - `_extra_in` : not used fn optimize( &self, model: &M, initial_solution: M::SolutionType, + initial_score: M::ScoreType, n_iter: usize, time_limit: Duration, callback: Option<&F>, @@ -70,6 +73,7 @@ impl LocalSearchOptimizer for EpsilonGreedyOptimizer { optimizer.optimize( model, initial_solution, + initial_score, n_iter, time_limit, callback, diff --git a/src/optim/generic.rs b/src/optim/generic.rs index a71fe6e..ec2b8d8 100644 --- a/src/optim/generic.rs +++ b/src/optim/generic.rs @@ -61,13 +61,16 @@ where /// /// - `model` : the model to optimize /// - `initial_solution` : the initial solution to start optimization. If None, a random solution will be generated. + /// - `initial_score` : the initial score of the initial solution /// - `n_iter`: maximum iterations + /// - `time_limit`: maximum iteration time /// - `callback` : callback function that will be invoked at the end of each iteration /// - `_extra_in` : not used fn optimize( &self, model: &M, initial_solution: M::SolutionType, + initial_score: M::ScoreType, n_iter: usize, time_limit: Duration, callback: Option<&F>, @@ -79,7 +82,7 @@ where let start_time = Instant::now(); let mut rng = rand::thread_rng(); let mut current_solution = initial_solution; - let mut current_score = model.evaluate_solution(¤t_solution); + let mut current_score = initial_score; let best_solution = Rc::new(RefCell::new(current_solution.clone())); let mut best_score = current_score; let mut accepted_counter = 0; @@ -95,9 +98,9 @@ where .map(|_| { let mut rng = rand::thread_rng(); let (solution, _, score) = model.generate_trial_solution( - ¤t_solution, + current_solution.clone(), + current_score, &mut rng, - Some(current_score), ); (solution, score) }) diff --git a/src/optim/hill_climbing.rs b/src/optim/hill_climbing.rs index 8483a3c..ea1dd33 100644 --- a/src/optim/hill_climbing.rs +++ b/src/optim/hill_climbing.rs @@ -26,13 +26,16 @@ impl LocalSearchOptimizer for HillClimbingOptimizer { /// /// - `model` : the model to optimize /// - `initial_solution` : the initial solution to start optimization. If None, a random solution will be generated. + /// - `initial_score` : the initial score of the initial solution /// - `n_iter`: maximum iterations + /// - `time_limit`: maximum iteration time /// - `callback` : callback function that will be invoked at the end of each iteration /// - `_extra_in` : not used fn optimize( &self, model: &M, initial_solution: M::SolutionType, + initial_score: M::ScoreType, n_iter: usize, time_limit: Duration, callback: Option<&F>, @@ -45,6 +48,7 @@ impl LocalSearchOptimizer for HillClimbingOptimizer { optimizer.optimize( model, initial_solution, + initial_score, n_iter, time_limit, callback, diff --git a/src/optim/logistic_annealing.rs b/src/optim/logistic_annealing.rs index e54e93b..52f374b 100644 --- a/src/optim/logistic_annealing.rs +++ b/src/optim/logistic_annealing.rs @@ -50,13 +50,16 @@ impl>> LocalSearchOptimizer for LogisticA /// /// - `model` : the model to optimize /// - `initial_solution` : the initial solution to start optimization. If None, a random solution will be generated. + /// - `initial_score` : the initial score of the initial solution /// - `n_iter`: maximum iterations + /// - `time_limit`: maximum iteration time /// - `callback` : callback function that will be invoked at the end of each iteration /// - `_extra_in` : not used fn optimize( &self, model: &M, initial_solution: M::SolutionType, + initial_score: M::ScoreType, n_iter: usize, time_limit: Duration, callback: Option<&F>, @@ -75,6 +78,7 @@ impl>> LocalSearchOptimizer for LogisticA optimizer.optimize( model, initial_solution, + initial_score, n_iter, time_limit, callback, diff --git a/src/optim/relative_annealing.rs b/src/optim/relative_annealing.rs index abe0e9e..19b346b 100644 --- a/src/optim/relative_annealing.rs +++ b/src/optim/relative_annealing.rs @@ -51,13 +51,16 @@ impl>> LocalSearchOptimizer for RelativeA /// /// - `model` : the model to optimize /// - `initial_solution` : the initial solution to start optimization. If None, a random solution will be generated. + /// - `initial_score` : the initial score of the initial solution /// - `n_iter`: maximum iterations + /// - `time_limit`: maximum iteration time /// - `callback` : callback function that will be invoked at the end of each iteration /// - `_extra_in` : not used fn optimize( &self, model: &M, initial_solution: M::SolutionType, + initial_score: M::ScoreType, n_iter: usize, time_limit: Duration, callback: Option<&F>, @@ -76,6 +79,7 @@ impl>> LocalSearchOptimizer for RelativeA optimizer.optimize( model, initial_solution, + initial_score, n_iter, time_limit, callback, diff --git a/src/optim/simulated_annealing.rs b/src/optim/simulated_annealing.rs index 36fff02..044845d 100644 --- a/src/optim/simulated_annealing.rs +++ b/src/optim/simulated_annealing.rs @@ -38,13 +38,16 @@ impl>> LocalSearchOptimizer for Simulated /// /// - `model` : the model to optimize /// - `initial_solution` : the initial solution to start optimization. If None, a random solution will be generated. + /// - `initial_score` : the initial score of the initial solution /// - `n_iter`: maximum iterations + /// - `time_limit`: maximum iteration time /// - `callback` : callback function that will be invoked at the end of each iteration /// - `max_min_temperatures` : (max_temperature, min_temperature) fn optimize( &self, model: &M, initial_solution: M::SolutionType, + initial_score: M::ScoreType, n_iter: usize, time_limit: Duration, callback: Option<&F>, @@ -57,7 +60,7 @@ impl>> LocalSearchOptimizer for Simulated let (max_temperature, min_temperature) = max_min_temperatures; let mut rng = rand::thread_rng(); let mut current_solution = initial_solution; - let mut current_score = model.evaluate_solution(¤t_solution); + let mut current_score = initial_score; let best_solution = Rc::new(RefCell::new(current_solution.clone())); let mut best_score = current_score; let mut accepted_counter = 0; @@ -75,9 +78,9 @@ impl>> LocalSearchOptimizer for Simulated .map(|_| { let mut rng = rand::thread_rng(); let (solution, _, score) = model.generate_trial_solution( - ¤t_solution, + current_solution.clone(), + current_score, &mut rng, - Some(current_score), ); (solution, score) }) diff --git a/src/optim/tabu_search.rs b/src/optim/tabu_search.rs index e5c9579..4c26c3d 100644 --- a/src/optim/tabu_search.rs +++ b/src/optim/tabu_search.rs @@ -83,13 +83,16 @@ impl> Loca /// /// - `model` : the model to optimize /// - `initial_solution` : the initial solution to start optimization. If None, a random solution will be generated. + /// - `initial_score` : the initial score of the initial solution /// - `n_iter`: maximum iterations + /// - `time_limit`: maximum iteration time /// - `callback` : callback function that will be invoked at the end of each iteration /// - `tabu_list` : initial tabu list fn optimize( &self, model: &M, initial_solution: M::SolutionType, + initial_score: M::ScoreType, n_iter: usize, time_limit: Duration, callback: Option<&F>, @@ -100,7 +103,7 @@ impl> Loca { let start_time = Instant::now(); let mut current_solution = initial_solution; - let mut current_score = model.evaluate_solution(¤t_solution); + let mut current_score = initial_score; let best_solution = Rc::new(RefCell::new(current_solution.clone())); let mut best_score = current_score; let mut counter = 0; @@ -117,9 +120,9 @@ impl> Loca .map(|_| { let mut rng = rand::thread_rng(); let (solution, transitions, score) = model.generate_trial_solution( - ¤t_solution, + current_solution.clone(), + current_score, &mut rng, - Some(current_score), ); (solution, transitions, score) }) diff --git a/src/tests.rs b/src/tests.rs index 6b99e2f..fa7f3b9 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,6 +4,10 @@ use rand::{distributions::Uniform, prelude::Distribution}; use crate::OptModel; +type SolutionType = Vec; +type TransitionType = (usize, f64, f64); +type ScoreType = NotNan; + #[derive(Clone)] struct QuadraticModel { k: usize, @@ -17,25 +21,35 @@ impl QuadraticModel { let dist = Uniform::new(low, high); Self { k, centers, dist } } -} -type SolutionType = Vec; -type TransitionType = (usize, f64, f64); + fn evaluate_solution(&self, solution: &SolutionType) -> ScoreType { + let score = (0..self.k) + .into_iter() + .map(|i| (solution[i] - self.centers[i]).powf(2.0)) + .sum(); + NotNan::new(score).unwrap() + } +} impl OptModel for QuadraticModel { type SolutionType = SolutionType; type TransitionType = TransitionType; type ScoreType = NotNan; - fn generate_random_solution(&self, rng: &mut R) -> AnyResult { + + fn generate_random_solution( + &self, + rng: &mut R, + ) -> AnyResult<(Self::SolutionType, Self::ScoreType)> { let solution = self.dist.sample_iter(rng).take(self.k).collect::>(); - Ok(solution) + let score = self.evaluate_solution(&solution); + Ok((solution, score)) } fn generate_trial_solution( &self, - current_solution: &Self::SolutionType, + current_solution: Self::SolutionType, + _current_score: Self::ScoreType, rng: &mut R, - _current_score: Option>, ) -> (Self::SolutionType, Self::TransitionType, NotNan) { let k = rng.gen_range(0..self.k); let v = self.dist.sample(rng); @@ -44,14 +58,6 @@ impl OptModel for QuadraticModel { let score = self.evaluate_solution(&new_solution); (new_solution, (k, current_solution[k], v), score) } - - fn evaluate_solution(&self, solution: &Self::SolutionType) -> NotNan { - let score = (0..self.k) - .into_iter() - .map(|i| (solution[i] - self.centers[i]).powf(2.0)) - .sum(); - NotNan::new(score).unwrap() - } } mod test_epsilon_greedy; diff --git a/src/tests/test_epsilon_greedy.rs b/src/tests/test_epsilon_greedy.rs index ef0fcb7..35fc146 100644 --- a/src/tests/test_epsilon_greedy.rs +++ b/src/tests/test_epsilon_greedy.rs @@ -11,14 +11,16 @@ fn test() { let model = QuadraticModel::new(3, vec![2.0, 0.0, -3.5], (-10.0, 10.0)); let opt = EpsilonGreedyOptimizer::new(1000, 10, 200, 0.1); let null_closure = None::<&fn(_)>; - let (final_solution, final_score, _) = opt.run( - &model, - None, - 10000, - Duration::from_secs(10), - null_closure, - (), - ); + let (final_solution, final_score, _) = opt + .run( + &model, + None, + 10000, + Duration::from_secs(10), + null_closure, + (), + ) + .unwrap(); assert_abs_diff_eq!(2.0, final_solution[0], epsilon = 0.05); assert_abs_diff_eq!(0.0, final_solution[1], epsilon = 0.05); assert_abs_diff_eq!(-3.5, final_solution[2], epsilon = 0.05); diff --git a/src/tests/test_hill_climbing.rs b/src/tests/test_hill_climbing.rs index e75f70e..a1b1b37 100644 --- a/src/tests/test_hill_climbing.rs +++ b/src/tests/test_hill_climbing.rs @@ -11,14 +11,16 @@ fn test() { let model = QuadraticModel::new(3, vec![2.0, 0.0, -3.5], (-10.0, 10.0)); let opt = HillClimbingOptimizer::new(1000, 10); let null_closure = None::<&fn(_)>; - let (final_solution, final_score, _) = opt.run( - &model, - None, - 10000, - Duration::from_secs(10), - null_closure, - (), - ); + let (final_solution, final_score, _) = opt + .run( + &model, + None, + 10000, + Duration::from_secs(10), + null_closure, + (), + ) + .unwrap(); assert_abs_diff_eq!(2.0, final_solution[0], epsilon = 0.05); assert_abs_diff_eq!(0.0, final_solution[1], epsilon = 0.05); assert_abs_diff_eq!(-3.5, final_solution[2], epsilon = 0.05); diff --git a/src/tests/test_logistic_annealing.rs b/src/tests/test_logistic_annealing.rs index 9a73388..8df0ea7 100644 --- a/src/tests/test_logistic_annealing.rs +++ b/src/tests/test_logistic_annealing.rs @@ -11,14 +11,16 @@ fn test() { let model = QuadraticModel::new(3, vec![2.0, 0.0, -3.5], (-10.0, 10.0)); let opt = LogisticAnnealingOptimizer::new(5000, 10, 200, 1e1); let null_closure = None::<&fn(_)>; - let (final_solution, final_score, _) = opt.run( - &model, - None, - 10000, - Duration::from_secs(10), - null_closure, - (), - ); + let (final_solution, final_score, _) = opt + .run( + &model, + None, + 10000, + Duration::from_secs(10), + null_closure, + (), + ) + .unwrap(); assert_abs_diff_eq!(2.0, final_solution[0], epsilon = 0.05); assert_abs_diff_eq!(0.0, final_solution[1], epsilon = 0.05); assert_abs_diff_eq!(-3.5, final_solution[2], epsilon = 0.05); diff --git a/src/tests/test_relative_annealing.rs b/src/tests/test_relative_annealing.rs index ee5e46b..683fd9b 100644 --- a/src/tests/test_relative_annealing.rs +++ b/src/tests/test_relative_annealing.rs @@ -11,14 +11,16 @@ fn test() { let model = QuadraticModel::new(3, vec![2.0, 0.0, -3.5], (-10.0, 10.0)); let opt = RelativeAnnealingOptimizer::new(5000, 10, 200, 1e1); let null_closure = None::<&fn(_)>; - let (final_solution, final_score, _) = opt.run( - &model, - None, - 10000, - Duration::from_secs(10), - null_closure, - (), - ); + let (final_solution, final_score, _) = opt + .run( + &model, + None, + 10000, + Duration::from_secs(10), + null_closure, + (), + ) + .unwrap(); assert_abs_diff_eq!(2.0, final_solution[0], epsilon = 0.05); assert_abs_diff_eq!(0.0, final_solution[1], epsilon = 0.05); assert_abs_diff_eq!(-3.5, final_solution[2], epsilon = 0.05); diff --git a/src/tests/test_simulated_annealing.rs b/src/tests/test_simulated_annealing.rs index d0ac024..e629361 100644 --- a/src/tests/test_simulated_annealing.rs +++ b/src/tests/test_simulated_annealing.rs @@ -11,14 +11,16 @@ fn test() { let model = QuadraticModel::new(3, vec![2.0, 0.0, -3.5], (-10.0, 10.0)); let opt = SimulatedAnnealingOptimizer::new(10000, 10); let null_closure = None::<&fn(_)>; - let (final_solution, final_score, _) = opt.run( - &model, - None, - 5000, - Duration::from_secs(10), - null_closure, - (1.0, 0.1), - ); + let (final_solution, final_score, _) = opt + .run( + &model, + None, + 5000, + Duration::from_secs(10), + null_closure, + (1.0, 0.1), + ) + .unwrap(); assert_abs_diff_eq!(2.0, final_solution[0], epsilon = 0.05); assert_abs_diff_eq!(0.0, final_solution[1], epsilon = 0.05); assert_abs_diff_eq!(-3.5, final_solution[2], epsilon = 0.05); diff --git a/src/tests/test_tabu_search.rs b/src/tests/test_tabu_search.rs index e45e49f..ef8d918 100644 --- a/src/tests/test_tabu_search.rs +++ b/src/tests/test_tabu_search.rs @@ -42,14 +42,16 @@ fn test() { let opt = TabuSearchOptimizer::new(1000, 25, 5); let tabu_list = MyTabuList::new(10); let null_closure = None::<&fn(_)>; - let (final_solution, final_score, _) = opt.run( - &model, - None, - 10000, - Duration::from_secs(10), - null_closure, - tabu_list, - ); + let (final_solution, final_score, _) = opt + .run( + &model, + None, + 10000, + Duration::from_secs(10), + null_closure, + tabu_list, + ) + .unwrap(); assert_abs_diff_eq!(2.0, final_solution[0], epsilon = 0.1); assert_abs_diff_eq!(0.0, final_solution[1], epsilon = 0.1); assert_abs_diff_eq!(-3.5, final_solution[2], epsilon = 0.1); From eea59aa884e1b6c8aca691436862523d1ad13525 Mon Sep 17 00:00:00 2001 From: Du Shiqiao Date: Mon, 12 Aug 2024 11:55:48 +0900 Subject: [PATCH 4/4] Bump version to 0.14.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index caae8e3..a5fbca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ categories = ["algorithms"] repository = "https://github.com/lucidfrontier45/localsearch" license-file = "LICENSE" readme = "README.md" -version = "0.13.1" +version = "0.14.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html